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:
Jens Kutilek 2021-05-17 14:09:36 +02:00 committed by GitHub
parent edd97bcdc4
commit 0cb2bea386
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 507 additions and 134 deletions

View File

@ -1,7 +1,7 @@
from fontTools.pens.filterPen import FilterPen, FilterPointPen
__all__ = ["TransformPen"]
__all__ = ["TransformPen", "TransformPointPen"]
class TransformPen(FilterPen):

View File

@ -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)

View File

@ -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()