diff --git a/Lib/fontTools/pens/hashPointPen.py b/Lib/fontTools/pens/hashPointPen.py index b82468ec9..f15dcabbf 100644 --- a/Lib/fontTools/pens/hashPointPen.py +++ b/Lib/fontTools/pens/hashPointPen.py @@ -31,6 +31,20 @@ class HashPointPen(AbstractPointPen): > # The hash values are identical, the outline has not changed. > # Compile the hinting code ... > pass + + If you want to compare a glyph from a source format which supports floating point + coordinates and transformations against a glyph from a format which has restrictions + on the precision of floats, e.g. UFO vs. TTF, you must use an appropriate rounding + function to make the values comparable. For TTF fonts with composites, this + construct can be used to make the transform values conform to F2Dot14: + + > ttf_hash_pen = HashPointPen(ttf_glyph_width, ttFont.getGlyphSet()) + > ttf_round_pen = RoundingPointPen(ttf_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14)) + > ufo_hash_pen = HashPointPen(ufo_glyph.width, ufo) + > ttf_glyph.drawPoints(ttf_round_pen, ttFont["glyf"]) + > ufo_round_pen = RoundingPointPen(ufo_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14)) + > ufo_glyph.drawPoints(ufo_round_pen) + > assert ttf_hash_pen.hash == ufo_hash_pen.hash """ def __init__(self, glyphWidth=0, glyphSet=None): diff --git a/Lib/fontTools/pens/roundingPen.py b/Lib/fontTools/pens/roundingPen.py index 2a7c476c3..176bcc7a5 100644 --- a/Lib/fontTools/pens/roundingPen.py +++ b/Lib/fontTools/pens/roundingPen.py @@ -1,4 +1,4 @@ -from fontTools.misc.roundTools import otRound +from fontTools.misc.roundTools import noRound, otRound from fontTools.misc.transform import Transform from fontTools.pens.filterPen import FilterPen, FilterPointPen @@ -8,7 +8,9 @@ __all__ = ["RoundingPen", "RoundingPointPen"] class RoundingPen(FilterPen): """ - Filter pen that rounds point coordinates and component XY offsets to integer. + Filter pen that rounds point coordinates and component XY offsets to integer. For + rounding the component transform values, a separate round function can be passed to + the pen. >>> from fontTools.pens.recordingPen import RecordingPen >>> recpen = RecordingPen() @@ -28,9 +30,10 @@ class RoundingPen(FilterPen): True """ - def __init__(self, outPen, roundFunc=otRound): + def __init__(self, outPen, roundFunc=otRound, transformRoundFunc=noRound): super().__init__(outPen) self.roundFunc = roundFunc + self.transformRoundFunc = transformRoundFunc def moveTo(self, pt): self._outPen.moveTo((self.roundFunc(pt[0]), self.roundFunc(pt[1]))) @@ -49,12 +52,16 @@ class RoundingPen(FilterPen): ) def addComponent(self, glyphName, transformation): + xx, xy, yx, yy, dx, dy = transformation self._outPen.addComponent( glyphName, Transform( - *transformation[:4], - self.roundFunc(transformation[4]), - self.roundFunc(transformation[5]), + self.transformRoundFunc(xx), + self.transformRoundFunc(xy), + self.transformRoundFunc(yx), + self.transformRoundFunc(yy), + self.roundFunc(dx), + self.roundFunc(dy), ), ) @@ -62,6 +69,8 @@ class RoundingPen(FilterPen): class RoundingPointPen(FilterPointPen): """ Filter point pen that rounds point coordinates and component XY offsets to integer. + For rounding the component scale values, a separate round function can be passed to + the pen. >>> from fontTools.pens.recordingPen import RecordingPointPen >>> recpen = RecordingPointPen() @@ -87,26 +96,35 @@ class RoundingPointPen(FilterPointPen): True """ - def __init__(self, outPen, roundFunc=otRound): + def __init__(self, outPen, roundFunc=otRound, transformRoundFunc=noRound): super().__init__(outPen) self.roundFunc = roundFunc + self.transformRoundFunc = transformRoundFunc - def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + def addPoint( + self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs + ): self._outPen.addPoint( (self.roundFunc(pt[0]), self.roundFunc(pt[1])), segmentType=segmentType, smooth=smooth, name=name, + identifier=identifier, **kwargs, ) - def addComponent(self, baseGlyphName, transformation, **kwargs): + def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs): + xx, xy, yx, yy, dx, dy = transformation self._outPen.addComponent( - baseGlyphName, - Transform( - *transformation[:4], - self.roundFunc(transformation[4]), - self.roundFunc(transformation[5]), + baseGlyphName=baseGlyphName, + transformation=Transform( + self.transformRoundFunc(xx), + self.transformRoundFunc(xy), + self.transformRoundFunc(yx), + self.transformRoundFunc(yy), + self.roundFunc(dx), + self.roundFunc(dy), ), + identifier=identifier, **kwargs, ) diff --git a/Tests/pens/roundingPen_test.py b/Tests/pens/roundingPen_test.py new file mode 100644 index 000000000..3c1f00e4c --- /dev/null +++ b/Tests/pens/roundingPen_test.py @@ -0,0 +1,69 @@ +from fontTools.misc.fixedTools import floatToFixedToFloat +from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen +from fontTools.pens.roundingPen import RoundingPen, RoundingPointPen +from functools import partial + + +tt_scale_round = partial(floatToFixedToFloat, precisionBits=14) + + +class RoundingPenTest(object): + def test_general(self): + recpen = RecordingPen() + roundpen = RoundingPen(recpen) + roundpen.moveTo((0.4, 0.6)) + roundpen.lineTo((1.6, 2.5)) + roundpen.qCurveTo((2.4, 4.6), (3.3, 5.7), (4.9, 6.1)) + roundpen.curveTo((6.4, 8.6), (7.3, 9.7), (8.9, 10.1)) + roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5)) + assert recpen.value == [ + ("moveTo", ((0, 1),)), + ("lineTo", ((2, 3),)), + ("qCurveTo", ((2, 5), (3, 6), (5, 6))), + ("curveTo", ((6, 9), (7, 10), (9, 10))), + ("addComponent", ("a", (1.5, 0, 0, 1.5, 11, -10))), + ] + + def test_transform_round(self): + recpen = RecordingPen() + roundpen = RoundingPen(recpen, transformRoundFunc=tt_scale_round) + # The 0.913 is equal to 91.3% scale in a source editor + roundpen.addComponent("a", (0.9130000305, 0, 0, -1, 10.5, -10.5)) + # The value should compare equal to its F2Dot14 representation + assert recpen.value == [ + ("addComponent", ("a", (0.91302490234375, 0, 0, -1, 11, -10))), + ] + + +class RoundingPointPenTest(object): + def test_general(self): + recpen = RecordingPointPen() + roundpen = RoundingPointPen(recpen) + roundpen.beginPath() + roundpen.addPoint((0.4, 0.6), "line") + roundpen.addPoint((1.6, 2.5), "line") + roundpen.addPoint((2.4, 4.6)) + roundpen.addPoint((3.3, 5.7)) + roundpen.addPoint((4.9, 6.1), "qcurve") + roundpen.endPath() + roundpen.addComponent("a", (1.5, 0, 0, 1.5, 10.5, -10.5)) + assert recpen.value == [ + ("beginPath", (), {}), + ("addPoint", ((0, 1), "line", False, None), {}), + ("addPoint", ((2, 3), "line", False, None), {}), + ("addPoint", ((2, 5), None, False, None), {}), + ("addPoint", ((3, 6), None, False, None), {}), + ("addPoint", ((5, 6), "qcurve", False, None), {}), + ("endPath", (), {}), + ("addComponent", ("a", (1.5, 0, 0, 1.5, 11, -10)), {}), + ] + + def test_transform_round(self): + recpen = RecordingPointPen() + roundpen = RoundingPointPen(recpen, transformRoundFunc=tt_scale_round) + # The 0.913 is equal to 91.3% scale in a source editor + roundpen.addComponent("a", (0.913, 0, 0, -1, 10.5, -10.5)) + # The value should compare equal to its F2Dot14 representation + assert recpen.value == [ + ("addComponent", ("a", (0.91302490234375, 0, 0, -1, 11, -10)), {}), + ]