diff --git a/Lib/fontTools/pens/transformPen.py b/Lib/fontTools/pens/transformPen.py index 2dcf83b1a..93d19191a 100644 --- a/Lib/fontTools/pens/transformPen.py +++ b/Lib/fontTools/pens/transformPen.py @@ -1,7 +1,7 @@ from fontTools.pens.filterPen import FilterPen, FilterPointPen -__all__ = ["TransformPen"] +__all__ = ["TransformPen", "TransformPointPen"] class TransformPen(FilterPen): diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py index e7841efcb..86a0e5551 100644 --- a/Lib/fontTools/pens/ttGlyphPen.py +++ b/Lib/fontTools/pens/ttGlyphPen.py @@ -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. - """ - - def __init__(self, glyphSet, handleOverflowingTransforms=True): - """Construct a new pen. +class _TTGlyphBasePen: + def __init__( + self, glyphSet: Dict[str, Any], handleOverflowingTransforms: bool = True + ) -> None: + """ + 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) diff --git a/Tests/pens/ttGlyphPen_test.py b/Tests/pens/ttGlyphPen_test.py index 53db025ce..97637f379 100644 --- a/Tests/pens/ttGlyphPen_test.py +++ b/Tests/pens/ttGlyphPen_test.py @@ -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()