Add TTGlyphPointPen (#2205)
* Add TTGlyphPointPen * Format code with black * Implement TTGlyphPen and TTGlyphPointPen with common base class * Use PenError instead of assert * Add note about decomposing mixed composites to the docstring
This commit is contained in:
parent
edd97bcdc4
commit
0cb2bea386
@ -1,7 +1,7 @@
|
||||
from fontTools.pens.filterPen import FilterPen, FilterPointPen
|
||||
|
||||
|
||||
__all__ = ["TransformPen"]
|
||||
__all__ = ["TransformPen", "TransformPointPen"]
|
||||
|
||||
|
||||
class TransformPen(FilterPen):
|
||||
|
@ -1,30 +1,29 @@
|
||||
from array import array
|
||||
from fontTools.misc.fixedTools import MAX_F2DOT14, otRound, floatToFixedToFloat
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from fontTools.misc.fixedTools import MAX_F2DOT14, floatToFixedToFloat
|
||||
from fontTools.misc.loggingTools import LogMixin
|
||||
from fontTools.pens.pointPen import AbstractPointPen
|
||||
from fontTools.misc.roundTools import otRound
|
||||
from fontTools.pens.basePen import LoggingPen
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.pens.basePen import LoggingPen, PenError
|
||||
from fontTools.pens.transformPen import TransformPen, TransformPointPen
|
||||
from fontTools.ttLib.tables import ttProgram
|
||||
from fontTools.ttLib.tables._g_l_y_f import Glyph
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
||||
|
||||
|
||||
__all__ = ["TTGlyphPen"]
|
||||
__all__ = ["TTGlyphPen", "TTGlyphPointPen"]
|
||||
|
||||
|
||||
class TTGlyphPen(LoggingPen):
|
||||
"""Pen used for drawing to a TrueType glyph.
|
||||
|
||||
This pen can be used to construct or modify glyphs in a TrueType format
|
||||
font. After using the pen to draw, use the ``.glyph()`` method to retrieve
|
||||
a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
|
||||
class _TTGlyphBasePen:
|
||||
def __init__(
|
||||
self, glyphSet: Dict[str, Any], handleOverflowingTransforms: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet, handleOverflowingTransforms=True):
|
||||
"""Construct a new pen.
|
||||
Construct a new pen.
|
||||
|
||||
Args:
|
||||
glyphSet (ttLib._TTGlyphSet): A glyphset object, used to resolve components.
|
||||
glyphSet (Dict[str, Any]): A glyphset object, used to resolve components.
|
||||
handleOverflowingTransforms (bool): See below.
|
||||
|
||||
If ``handleOverflowingTransforms`` is True, the components' transform values
|
||||
@ -42,41 +41,155 @@ class TTGlyphPen(LoggingPen):
|
||||
If False, no check is done and all components are translated unmodified
|
||||
into the glyf table, followed by an inevitable ``struct.error`` once an
|
||||
attempt is made to compile them.
|
||||
|
||||
If both contours and components are present in a glyph, the components
|
||||
are decomposed.
|
||||
"""
|
||||
self.glyphSet = glyphSet
|
||||
self.handleOverflowingTransforms = handleOverflowingTransforms
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
def _decompose(
|
||||
self,
|
||||
glyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float],
|
||||
):
|
||||
tpen = self.transformPen(self, transformation)
|
||||
getattr(self.glyphSet[glyphName], self.drawMethod)(tpen)
|
||||
|
||||
def _isClosed(self):
|
||||
"""
|
||||
Check if the current path is closed.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def init(self) -> None:
|
||||
self.points = []
|
||||
self.endPts = []
|
||||
self.types = []
|
||||
self.components = []
|
||||
|
||||
def _addPoint(self, pt, onCurve):
|
||||
def addComponent(
|
||||
self,
|
||||
baseGlyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float],
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""
|
||||
Add a sub glyph.
|
||||
"""
|
||||
self.components.append((baseGlyphName, transformation))
|
||||
|
||||
def _buildComponents(self, componentFlags):
|
||||
if self.handleOverflowingTransforms:
|
||||
# we can't encode transform values > 2 or < -2 in F2Dot14,
|
||||
# so we must decompose the glyph if any transform exceeds these
|
||||
overflowing = any(
|
||||
s > 2 or s < -2
|
||||
for (glyphName, transformation) in self.components
|
||||
for s in transformation[:4]
|
||||
)
|
||||
components = []
|
||||
for glyphName, transformation in self.components:
|
||||
if glyphName not in self.glyphSet:
|
||||
self.log.warning(
|
||||
f"skipped non-existing component '{glyphName}'"
|
||||
)
|
||||
continue
|
||||
if self.points or (
|
||||
self.handleOverflowingTransforms and overflowing
|
||||
):
|
||||
# can't have both coordinates and components, so decompose
|
||||
self._decompose(glyphName, transformation)
|
||||
continue
|
||||
|
||||
component = GlyphComponent()
|
||||
component.glyphName = glyphName
|
||||
component.x, component.y = (otRound(v) for v in transformation[4:])
|
||||
# quantize floats to F2Dot14 so we get same values as when decompiled
|
||||
# from a binary glyf table
|
||||
transformation = tuple(
|
||||
floatToFixedToFloat(v, 14) for v in transformation[:4]
|
||||
)
|
||||
if transformation != (1, 0, 0, 1):
|
||||
if self.handleOverflowingTransforms and any(
|
||||
MAX_F2DOT14 < s <= 2 for s in transformation
|
||||
):
|
||||
# clamp values ~= +2.0 so we can keep the component
|
||||
transformation = tuple(
|
||||
MAX_F2DOT14 if MAX_F2DOT14 < s <= 2 else s
|
||||
for s in transformation
|
||||
)
|
||||
component.transform = (transformation[:2], transformation[2:])
|
||||
component.flags = componentFlags
|
||||
components.append(component)
|
||||
return components
|
||||
|
||||
def glyph(self, componentFlags: int = 0x4) -> Glyph:
|
||||
"""
|
||||
Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
|
||||
"""
|
||||
if not self._isClosed():
|
||||
raise PenError("Didn't close last contour.")
|
||||
components = self._buildComponents(componentFlags)
|
||||
|
||||
glyph = Glyph()
|
||||
glyph.coordinates = GlyphCoordinates(self.points)
|
||||
glyph.coordinates.toInt()
|
||||
glyph.endPtsOfContours = self.endPts
|
||||
glyph.flags = array("B", self.types)
|
||||
self.init()
|
||||
|
||||
if components:
|
||||
# If both components and contours were present, they have by now
|
||||
# been decomposed by _buildComponents.
|
||||
glyph.components = components
|
||||
glyph.numberOfContours = -1
|
||||
else:
|
||||
glyph.numberOfContours = len(glyph.endPtsOfContours)
|
||||
glyph.program = ttProgram.Program()
|
||||
glyph.program.fromBytecode(b"")
|
||||
|
||||
return glyph
|
||||
|
||||
|
||||
class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
|
||||
"""
|
||||
Pen used for drawing to a TrueType glyph.
|
||||
|
||||
This pen can be used to construct or modify glyphs in a TrueType format
|
||||
font. After using the pen to draw, use the ``.glyph()`` method to retrieve
|
||||
a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
|
||||
"""
|
||||
drawMethod = "draw"
|
||||
transformPen = TransformPen
|
||||
|
||||
def _addPoint(self, pt: Tuple[float, float], onCurve: int) -> None:
|
||||
self.points.append(pt)
|
||||
self.types.append(onCurve)
|
||||
|
||||
def _popPoint(self):
|
||||
def _popPoint(self) -> None:
|
||||
self.points.pop()
|
||||
self.types.pop()
|
||||
|
||||
def _isClosed(self):
|
||||
return (
|
||||
(not self.points) or
|
||||
(self.endPts and self.endPts[-1] == len(self.points) - 1))
|
||||
def _isClosed(self) -> bool:
|
||||
return (not self.points) or (
|
||||
self.endPts and self.endPts[-1] == len(self.points) - 1
|
||||
)
|
||||
|
||||
def lineTo(self, pt):
|
||||
def lineTo(self, pt: Tuple[float, float]) -> None:
|
||||
self._addPoint(pt, 1)
|
||||
|
||||
def moveTo(self, pt):
|
||||
assert self._isClosed(), '"move"-type point must begin a new contour.'
|
||||
def moveTo(self, pt: Tuple[float, float]) -> None:
|
||||
if not self._isClosed():
|
||||
raise PenError('"move"-type point must begin a new contour.')
|
||||
self._addPoint(pt, 1)
|
||||
|
||||
def curveTo(self, *points):
|
||||
def curveTo(self, *points) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
def qCurveTo(self, *points) -> None:
|
||||
assert len(points) >= 1
|
||||
for pt in points[:-1]:
|
||||
self._addPoint(pt, 0)
|
||||
@ -85,7 +198,7 @@ class TTGlyphPen(LoggingPen):
|
||||
if points[-1] is not None:
|
||||
self._addPoint(points[-1], 1)
|
||||
|
||||
def closePath(self):
|
||||
def closePath(self) -> None:
|
||||
endPt = len(self.points) - 1
|
||||
|
||||
# ignore anchors (one-point paths)
|
||||
@ -103,72 +216,71 @@ class TTGlyphPen(LoggingPen):
|
||||
|
||||
self.endPts.append(endPt)
|
||||
|
||||
def endPath(self):
|
||||
def endPath(self) -> None:
|
||||
# TrueType contours are always "closed"
|
||||
self.closePath()
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
self.components.append((glyphName, transformation))
|
||||
|
||||
def _buildComponents(self, componentFlags):
|
||||
if self.handleOverflowingTransforms:
|
||||
# we can't encode transform values > 2 or < -2 in F2Dot14,
|
||||
# so we must decompose the glyph if any transform exceeds these
|
||||
overflowing = any(s > 2 or s < -2
|
||||
for (glyphName, transformation) in self.components
|
||||
for s in transformation[:4])
|
||||
components = []
|
||||
for glyphName, transformation in self.components:
|
||||
if glyphName not in self.glyphSet:
|
||||
self.log.warning(
|
||||
"skipped non-existing component '%s'", glyphName
|
||||
)
|
||||
continue
|
||||
if (self.points or
|
||||
(self.handleOverflowingTransforms and overflowing)):
|
||||
# can't have both coordinates and components, so decompose
|
||||
tpen = TransformPen(self, transformation)
|
||||
self.glyphSet[glyphName].draw(tpen)
|
||||
continue
|
||||
class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen):
|
||||
"""
|
||||
Point pen used for drawing to a TrueType glyph.
|
||||
|
||||
component = GlyphComponent()
|
||||
component.glyphName = glyphName
|
||||
component.x, component.y = (otRound(v) for v in transformation[4:])
|
||||
# quantize floats to F2Dot14 so we get same values as when decompiled
|
||||
# from a binary glyf table
|
||||
transformation = tuple(
|
||||
floatToFixedToFloat(v, 14) for v in transformation[:4]
|
||||
)
|
||||
if transformation != (1, 0, 0, 1):
|
||||
if (self.handleOverflowingTransforms and
|
||||
any(MAX_F2DOT14 < s <= 2 for s in transformation)):
|
||||
# clamp values ~= +2.0 so we can keep the component
|
||||
transformation = tuple(MAX_F2DOT14 if MAX_F2DOT14 < s <= 2
|
||||
else s for s in transformation)
|
||||
component.transform = (transformation[:2], transformation[2:])
|
||||
component.flags = componentFlags
|
||||
components.append(component)
|
||||
return components
|
||||
This pen can be used to construct or modify glyphs in a TrueType format
|
||||
font. After using the pen to draw, use the ``.glyph()`` method to retrieve
|
||||
a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
|
||||
"""
|
||||
drawMethod = "drawPoints"
|
||||
transformPen = TransformPointPen
|
||||
|
||||
def glyph(self, componentFlags=0x4):
|
||||
"""Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph."""
|
||||
assert self._isClosed(), "Didn't close last contour."
|
||||
def init(self) -> None:
|
||||
super().init()
|
||||
self._currentContourStartIndex = None
|
||||
|
||||
components = self._buildComponents(componentFlags)
|
||||
def _isClosed(self) -> bool:
|
||||
return self._currentContourStartIndex is None
|
||||
|
||||
glyph = Glyph()
|
||||
glyph.coordinates = GlyphCoordinates(self.points)
|
||||
glyph.coordinates.toInt()
|
||||
glyph.endPtsOfContours = self.endPts
|
||||
glyph.flags = array("B", self.types)
|
||||
self.init()
|
||||
def beginPath(
|
||||
self, identifier: Optional[str] = None, **kwargs: Any
|
||||
) -> None:
|
||||
"""
|
||||
Start a new sub path.
|
||||
"""
|
||||
if not self._isClosed():
|
||||
raise PenError("Didn't close previous contour.")
|
||||
self._currentContourStartIndex = len(self.points)
|
||||
|
||||
if components:
|
||||
glyph.components = components
|
||||
glyph.numberOfContours = -1
|
||||
def endPath(self) -> None:
|
||||
"""
|
||||
End the current sub path.
|
||||
"""
|
||||
# TrueType contours are always "closed"
|
||||
if self._isClosed():
|
||||
raise PenError("Contour is already closed.")
|
||||
if self._currentContourStartIndex == len(self.points):
|
||||
raise PenError("Tried to end an empty contour.")
|
||||
self.endPts.append(len(self.points) - 1)
|
||||
self._currentContourStartIndex = None
|
||||
|
||||
def addPoint(
|
||||
self,
|
||||
pt: Tuple[float, float],
|
||||
segmentType: Optional[str] = None,
|
||||
smooth: bool = False,
|
||||
name: Optional[str] = None,
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""
|
||||
Add a point to the current sub path.
|
||||
"""
|
||||
if self._isClosed():
|
||||
raise PenError("Can't add a point to a closed contour.")
|
||||
if segmentType is None:
|
||||
self.types.append(0) # offcurve
|
||||
elif segmentType in ("qcurve", "line"):
|
||||
self.types.append(1) # oncurve
|
||||
else:
|
||||
glyph.numberOfContours = len(glyph.endPtsOfContours)
|
||||
glyph.program = ttProgram.Program()
|
||||
glyph.program.fromBytecode(b"")
|
||||
# cubic curves are not supported
|
||||
raise NotImplementedError
|
||||
|
||||
return glyph
|
||||
self.points.append(pt)
|
||||
|
@ -1,56 +1,63 @@
|
||||
import os
|
||||
import unittest
|
||||
import pytest
|
||||
import struct
|
||||
|
||||
from fontTools import ttLib
|
||||
from fontTools.misc.testTools import TestCase
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen, MAX_F2DOT14
|
||||
from fontTools.pens.basePen import PenError
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen, TTGlyphPointPen, MAX_F2DOT14
|
||||
|
||||
|
||||
class TTGlyphPenTest(TestCase):
|
||||
|
||||
class TTGlyphPenTestBase:
|
||||
def runEndToEnd(self, filename):
|
||||
font = ttLib.TTFont()
|
||||
ttx_path = os.path.join(
|
||||
os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
|
||||
'..', 'ttLib', 'data', filename)
|
||||
"..",
|
||||
"ttLib",
|
||||
"data",
|
||||
filename,
|
||||
)
|
||||
font.importXML(ttx_path)
|
||||
|
||||
glyphSet = font.getGlyphSet()
|
||||
glyfTable = font['glyf']
|
||||
pen = TTGlyphPen(font.getGlyphSet())
|
||||
glyfTable = font["glyf"]
|
||||
pen = self.penClass(font.getGlyphSet())
|
||||
|
||||
for name in font.getGlyphOrder():
|
||||
oldGlyph = glyphSet[name]
|
||||
oldGlyph.draw(pen)
|
||||
getattr(oldGlyph, self.drawMethod)(pen)
|
||||
oldGlyph = oldGlyph._glyph
|
||||
newGlyph = pen.glyph()
|
||||
|
||||
if hasattr(oldGlyph, 'program'):
|
||||
if hasattr(oldGlyph, "program"):
|
||||
newGlyph.program = oldGlyph.program
|
||||
|
||||
self.assertEqual(
|
||||
oldGlyph.compile(glyfTable), newGlyph.compile(glyfTable))
|
||||
assert oldGlyph.compile(glyfTable) == newGlyph.compile(glyfTable)
|
||||
|
||||
def test_e2e_linesAndSimpleComponents(self):
|
||||
self.runEndToEnd('TestTTF-Regular.ttx')
|
||||
self.runEndToEnd("TestTTF-Regular.ttx")
|
||||
|
||||
def test_e2e_curvesAndComponentTransforms(self):
|
||||
self.runEndToEnd('TestTTFComplex-Regular.ttx')
|
||||
self.runEndToEnd("TestTTFComplex-Regular.ttx")
|
||||
|
||||
|
||||
class TTGlyphPenTest(TTGlyphPenTestBase):
|
||||
penClass = TTGlyphPen
|
||||
drawMethod = "draw"
|
||||
|
||||
def test_moveTo_errorWithinContour(self):
|
||||
pen = TTGlyphPen(None)
|
||||
pen.moveTo((0, 0))
|
||||
with self.assertRaises(AssertionError):
|
||||
with pytest.raises(PenError):
|
||||
pen.moveTo((1, 0))
|
||||
|
||||
def test_closePath_ignoresAnchors(self):
|
||||
pen = TTGlyphPen(None)
|
||||
pen.moveTo((0, 0))
|
||||
pen.closePath()
|
||||
self.assertFalse(pen.points)
|
||||
self.assertFalse(pen.types)
|
||||
self.assertFalse(pen.endPts)
|
||||
assert not pen.points
|
||||
assert not pen.types
|
||||
assert not pen.endPts
|
||||
|
||||
def test_endPath_sameAsClosePath(self):
|
||||
pen = TTGlyphPen(None)
|
||||
@ -67,16 +74,16 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.endPath()
|
||||
endPathGlyph = pen.glyph()
|
||||
|
||||
self.assertEqual(closePathGlyph, endPathGlyph)
|
||||
assert closePathGlyph == endPathGlyph
|
||||
|
||||
def test_glyph_errorOnUnendedContour(self):
|
||||
pen = TTGlyphPen(None)
|
||||
pen.moveTo((0, 0))
|
||||
with self.assertRaises(AssertionError):
|
||||
with pytest.raises(PenError):
|
||||
pen.glyph()
|
||||
|
||||
def test_glyph_decomposes(self):
|
||||
componentName = 'a'
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPen(glyphSet)
|
||||
|
||||
@ -104,7 +111,7 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.closePath()
|
||||
plainGlyph = pen.glyph()
|
||||
|
||||
self.assertEqual(plainGlyph, compositeGlyph)
|
||||
assert plainGlyph == compositeGlyph
|
||||
|
||||
def test_remove_extra_move_points(self):
|
||||
pen = TTGlyphPen(None)
|
||||
@ -112,8 +119,8 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.lineTo((100, 0))
|
||||
pen.qCurveTo((100, 50), (50, 100), (0, 0))
|
||||
pen.closePath()
|
||||
self.assertEqual(len(pen.points), 4)
|
||||
self.assertEqual(pen.points[0], (0, 0))
|
||||
assert len(pen.points) == 4
|
||||
assert pen.points[0] == (0, 0)
|
||||
|
||||
def test_keep_move_point(self):
|
||||
pen = TTGlyphPen(None)
|
||||
@ -122,8 +129,8 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.qCurveTo((100, 50), (50, 100), (30, 30))
|
||||
# when last and move pts are different, closePath() implies a lineTo
|
||||
pen.closePath()
|
||||
self.assertEqual(len(pen.points), 5)
|
||||
self.assertEqual(pen.points[0], (0, 0))
|
||||
assert len(pen.points) == 5
|
||||
assert pen.points[0] == (0, 0)
|
||||
|
||||
def test_keep_duplicate_end_point(self):
|
||||
pen = TTGlyphPen(None)
|
||||
@ -132,11 +139,11 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.qCurveTo((100, 50), (50, 100), (0, 0))
|
||||
pen.lineTo((0, 0)) # the duplicate point is not removed
|
||||
pen.closePath()
|
||||
self.assertEqual(len(pen.points), 5)
|
||||
self.assertEqual(pen.points[0], (0, 0))
|
||||
assert len(pen.points) == 5
|
||||
assert pen.points[0] == (0, 0)
|
||||
|
||||
def test_within_range_component_transform(self):
|
||||
componentName = 'a'
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPen(glyphSet)
|
||||
|
||||
@ -154,10 +161,10 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0))
|
||||
expectedGlyph = pen.glyph()
|
||||
|
||||
self.assertEqual(expectedGlyph, compositeGlyph)
|
||||
assert expectedGlyph == compositeGlyph
|
||||
|
||||
def test_clamp_to_almost_2_component_transform(self):
|
||||
componentName = 'a'
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPen(glyphSet)
|
||||
|
||||
@ -182,10 +189,10 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0))
|
||||
expectedGlyph = pen.glyph()
|
||||
|
||||
self.assertEqual(expectedGlyph, compositeGlyph)
|
||||
assert expectedGlyph == compositeGlyph
|
||||
|
||||
def test_out_of_range_transform_decomposed(self):
|
||||
componentName = 'a'
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPen(glyphSet)
|
||||
|
||||
@ -214,10 +221,10 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.closePath()
|
||||
expectedGlyph = pen.glyph()
|
||||
|
||||
self.assertEqual(expectedGlyph, compositeGlyph)
|
||||
assert expectedGlyph == compositeGlyph
|
||||
|
||||
def test_no_handle_overflowing_transform(self):
|
||||
componentName = 'a'
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPen(glyphSet, handleOverflowingTransforms=False)
|
||||
|
||||
@ -231,14 +238,13 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.addComponent(componentName, (3, 0, 0, 1, 0, 0))
|
||||
compositeGlyph = pen.glyph()
|
||||
|
||||
self.assertEqual(compositeGlyph.components[0].transform,
|
||||
((3, 0), (0, 1)))
|
||||
assert compositeGlyph.components[0].transform == ((3, 0), (0, 1))
|
||||
|
||||
with self.assertRaises(struct.error):
|
||||
compositeGlyph.compile({'a': baseGlyph})
|
||||
with pytest.raises(struct.error):
|
||||
compositeGlyph.compile({"a": baseGlyph})
|
||||
|
||||
def assertGlyphBoundsEqual(self, glyph, bounds):
|
||||
self.assertEqual((glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax), bounds)
|
||||
assert (glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax) == bounds
|
||||
|
||||
def test_round_float_coordinates_and_component_offsets(self):
|
||||
glyphSet = {}
|
||||
@ -253,7 +259,7 @@ class TTGlyphPenTest(TestCase):
|
||||
simpleGlyph.recalcBounds(glyphSet)
|
||||
self.assertGlyphBoundsEqual(simpleGlyph, (0, 0, 368, 1))
|
||||
|
||||
componentName = 'a'
|
||||
componentName = "a"
|
||||
glyphSet[componentName] = simpleGlyph
|
||||
|
||||
pen.addComponent(componentName, (1, 0, 0, 1, -86.4, 0))
|
||||
@ -271,7 +277,7 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.lineTo((-55, 745))
|
||||
pen.lineTo((-231, 745))
|
||||
pen.closePath()
|
||||
glyphSet["gravecomb"] = gravecomb = pen.glyph()
|
||||
glyphSet["gravecomb"] = pen.glyph()
|
||||
|
||||
pen = TTGlyphPen(glyphSet)
|
||||
pen.moveTo((-278, 939))
|
||||
@ -279,7 +285,7 @@ class TTGlyphPenTest(TestCase):
|
||||
pen.lineTo((8, 745))
|
||||
pen.lineTo((-278, 745))
|
||||
pen.closePath()
|
||||
glyphSet["circumflexcomb"] = circumflexcomb = pen.glyph()
|
||||
glyphSet["circumflexcomb"] = pen.glyph()
|
||||
|
||||
pen = TTGlyphPen(glyphSet)
|
||||
pen.addComponent("circumflexcomb", (1, 0, 0, 1, 0, 0))
|
||||
@ -290,6 +296,260 @@ class TTGlyphPenTest(TestCase):
|
||||
self.assertGlyphBoundsEqual(uni0302_uni0300, (-278, 745, 148, 1025))
|
||||
|
||||
|
||||
class TTGlyphPointPenTest(TTGlyphPenTestBase):
|
||||
penClass = TTGlyphPointPen
|
||||
drawMethod = "drawPoints"
|
||||
|
||||
def test_glyph_simple(self):
|
||||
pen = TTGlyphPointPen(None)
|
||||
pen.beginPath()
|
||||
pen.addPoint((50, 0), "line")
|
||||
pen.addPoint((450, 0), "line")
|
||||
pen.addPoint((450, 700), "line")
|
||||
pen.addPoint((50, 700), "line")
|
||||
pen.endPath()
|
||||
glyph = pen.glyph()
|
||||
assert glyph.numberOfContours == 1
|
||||
assert glyph.endPtsOfContours == [3]
|
||||
|
||||
def test_addPoint_errorOnCurve(self):
|
||||
pen = TTGlyphPointPen(None)
|
||||
pen.beginPath()
|
||||
with pytest.raises(NotImplementedError):
|
||||
pen.addPoint((0, 0), "curve")
|
||||
|
||||
def test_beginPath_beginPathOnOpenPath(self):
|
||||
pen = TTGlyphPointPen(None)
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0))
|
||||
with pytest.raises(PenError):
|
||||
pen.beginPath()
|
||||
|
||||
def test_glyph_errorOnUnendedContour(self):
|
||||
pen = TTGlyphPointPen(None)
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0))
|
||||
with pytest.raises(PenError):
|
||||
pen.glyph()
|
||||
|
||||
def test_glyph_errorOnEmptyContour(self):
|
||||
pen = TTGlyphPointPen(None)
|
||||
pen.beginPath()
|
||||
with pytest.raises(PenError):
|
||||
pen.endPath()
|
||||
|
||||
def test_glyph_decomposes(self):
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPointPen(glyphSet)
|
||||
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, 1), "line")
|
||||
pen.addPoint((1, 0), "line")
|
||||
pen.endPath()
|
||||
glyphSet[componentName] = _TestGlyph(pen.glyph())
|
||||
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, 1), "line")
|
||||
pen.addPoint((1, 0), "line")
|
||||
pen.endPath()
|
||||
pen.addComponent(componentName, (1, 0, 0, 1, 2, 0))
|
||||
pen.addComponent("missing", (1, 0, 0, 1, 0, 0)) # skipped
|
||||
compositeGlyph = pen.glyph()
|
||||
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, 1), "line")
|
||||
pen.addPoint((1, 0), "line")
|
||||
pen.endPath()
|
||||
pen.beginPath()
|
||||
pen.addPoint((2, 0), "line")
|
||||
pen.addPoint((2, 1), "line")
|
||||
pen.addPoint((3, 0), "line")
|
||||
pen.endPath()
|
||||
plainGlyph = pen.glyph()
|
||||
|
||||
assert plainGlyph == compositeGlyph
|
||||
|
||||
def test_keep_duplicate_end_point(self):
|
||||
pen = TTGlyphPointPen(None)
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((100, 0), "line")
|
||||
pen.addPoint((100, 50))
|
||||
pen.addPoint((50, 100))
|
||||
pen.addPoint((0, 0), "qcurve")
|
||||
pen.addPoint((0, 0), "line") # the duplicate point is not removed
|
||||
pen.endPath()
|
||||
assert len(pen.points) == 6
|
||||
assert pen.points[0] == (0, 0)
|
||||
|
||||
def test_within_range_component_transform(self):
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPointPen(glyphSet)
|
||||
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, 1), "line")
|
||||
pen.addPoint((1, 0), "line")
|
||||
pen.endPath()
|
||||
glyphSet[componentName] = _TestGlyph(pen.glyph())
|
||||
|
||||
pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0))
|
||||
pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0))
|
||||
compositeGlyph = pen.glyph()
|
||||
|
||||
pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0))
|
||||
pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0))
|
||||
expectedGlyph = pen.glyph()
|
||||
|
||||
assert expectedGlyph == compositeGlyph
|
||||
|
||||
def test_clamp_to_almost_2_component_transform(self):
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPointPen(glyphSet)
|
||||
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, 1), "line")
|
||||
pen.addPoint((1, 0), "line")
|
||||
pen.endPath()
|
||||
glyphSet[componentName] = _TestGlyph(pen.glyph())
|
||||
|
||||
pen.addComponent(componentName, (1.99999, 0, 0, 1, 0, 0))
|
||||
pen.addComponent(componentName, (1, 2, 0, 1, 0, 0))
|
||||
pen.addComponent(componentName, (1, 0, 2, 1, 0, 0))
|
||||
pen.addComponent(componentName, (1, 0, 0, 2, 0, 0))
|
||||
pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0))
|
||||
compositeGlyph = pen.glyph()
|
||||
|
||||
almost2 = MAX_F2DOT14 # 0b1.11111111111111
|
||||
pen.addComponent(componentName, (almost2, 0, 0, 1, 0, 0))
|
||||
pen.addComponent(componentName, (1, almost2, 0, 1, 0, 0))
|
||||
pen.addComponent(componentName, (1, 0, almost2, 1, 0, 0))
|
||||
pen.addComponent(componentName, (1, 0, 0, almost2, 0, 0))
|
||||
pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0))
|
||||
expectedGlyph = pen.glyph()
|
||||
|
||||
assert expectedGlyph == compositeGlyph
|
||||
|
||||
def test_out_of_range_transform_decomposed(self):
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPointPen(glyphSet)
|
||||
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, 1), "line")
|
||||
pen.addPoint((1, 0), "line")
|
||||
pen.endPath()
|
||||
glyphSet[componentName] = _TestGlyph(pen.glyph())
|
||||
|
||||
pen.addComponent(componentName, (3, 0, 0, 2, 0, 0))
|
||||
pen.addComponent(componentName, (1, 0, 0, 1, -1, 2))
|
||||
pen.addComponent(componentName, (2, 0, 0, -3, 0, 0))
|
||||
compositeGlyph = pen.glyph()
|
||||
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, 2), "line")
|
||||
pen.addPoint((3, 0), "line")
|
||||
pen.endPath()
|
||||
pen.beginPath()
|
||||
pen.addPoint((-1, 2), "line")
|
||||
pen.addPoint((-1, 3), "line")
|
||||
pen.addPoint((0, 2), "line")
|
||||
pen.endPath()
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, -3), "line")
|
||||
pen.addPoint((2, 0), "line")
|
||||
pen.endPath()
|
||||
expectedGlyph = pen.glyph()
|
||||
|
||||
assert expectedGlyph == compositeGlyph
|
||||
|
||||
def test_no_handle_overflowing_transform(self):
|
||||
componentName = "a"
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPointPen(glyphSet, handleOverflowingTransforms=False)
|
||||
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, 1), "line")
|
||||
pen.addPoint((1, 0), "line")
|
||||
pen.endPath()
|
||||
baseGlyph = pen.glyph()
|
||||
glyphSet[componentName] = _TestGlyph(baseGlyph)
|
||||
|
||||
pen.addComponent(componentName, (3, 0, 0, 1, 0, 0))
|
||||
compositeGlyph = pen.glyph()
|
||||
|
||||
assert compositeGlyph.components[0].transform == ((3, 0), (0, 1))
|
||||
|
||||
with pytest.raises(struct.error):
|
||||
compositeGlyph.compile({"a": baseGlyph})
|
||||
|
||||
def assertGlyphBoundsEqual(self, glyph, bounds):
|
||||
assert (glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax) == bounds
|
||||
|
||||
def test_round_float_coordinates_and_component_offsets(self):
|
||||
glyphSet = {}
|
||||
pen = TTGlyphPointPen(glyphSet)
|
||||
|
||||
pen.beginPath()
|
||||
pen.addPoint((0, 0), "line")
|
||||
pen.addPoint((0, 1), "line")
|
||||
pen.addPoint((367.6, 0), "line")
|
||||
pen.endPath()
|
||||
simpleGlyph = pen.glyph()
|
||||
|
||||
simpleGlyph.recalcBounds(glyphSet)
|
||||
self.assertGlyphBoundsEqual(simpleGlyph, (0, 0, 368, 1))
|
||||
|
||||
componentName = "a"
|
||||
glyphSet[componentName] = simpleGlyph
|
||||
|
||||
pen.addComponent(componentName, (1, 0, 0, 1, -86.4, 0))
|
||||
compositeGlyph = pen.glyph()
|
||||
|
||||
compositeGlyph.recalcBounds(glyphSet)
|
||||
self.assertGlyphBoundsEqual(compositeGlyph, (-86, 0, 282, 1))
|
||||
|
||||
def test_scaled_component_bounds(self):
|
||||
glyphSet = {}
|
||||
|
||||
pen = TTGlyphPointPen(glyphSet)
|
||||
pen.beginPath()
|
||||
pen.addPoint((-231, 939), "line")
|
||||
pen.addPoint((-55, 939), "line")
|
||||
pen.addPoint((-55, 745), "line")
|
||||
pen.addPoint((-231, 745), "line")
|
||||
pen.endPath()
|
||||
glyphSet["gravecomb"] = pen.glyph()
|
||||
|
||||
pen = TTGlyphPointPen(glyphSet)
|
||||
pen.beginPath()
|
||||
pen.addPoint((-278, 939), "line")
|
||||
pen.addPoint((8, 939), "line")
|
||||
pen.addPoint((8, 745), "line")
|
||||
pen.addPoint((-278, 745), "line")
|
||||
pen.endPath()
|
||||
glyphSet["circumflexcomb"] = pen.glyph()
|
||||
|
||||
pen = TTGlyphPointPen(glyphSet)
|
||||
pen.addComponent("circumflexcomb", (1, 0, 0, 1, 0, 0))
|
||||
pen.addComponent("gravecomb", (0.9, 0, 0, 0.9, 198, 180))
|
||||
glyphSet["uni0302_uni0300"] = uni0302_uni0300 = pen.glyph()
|
||||
|
||||
uni0302_uni0300.recalcBounds(glyphSet)
|
||||
self.assertGlyphBoundsEqual(uni0302_uni0300, (-278, 745, 148, 1025))
|
||||
|
||||
|
||||
class _TestGlyph(object):
|
||||
def __init__(self, glyph):
|
||||
self.coordinates = glyph.coordinates
|
||||
@ -300,7 +560,8 @@ class _TestGlyph(object):
|
||||
pen.lineTo(point)
|
||||
pen.closePath()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(unittest.main())
|
||||
def drawPoints(self, pen):
|
||||
pen.beginPath()
|
||||
for point in self.coordinates:
|
||||
pen.addPoint(point, "line")
|
||||
pen.endPath()
|
||||
|
Loading…
x
Reference in New Issue
Block a user