Add transformRoundFunc parameter to RoundingPens (#3426)

* Add optional transformRoundFunc to RoundingPen and RoundingPointPen
* Add tests
* Add doc about comparing UFO to TTF glyphs
* Use floatToFixedToFloat for example with rounding
This commit is contained in:
Jens Kutilek 2024-01-23 18:59:09 +01:00 committed by GitHub
parent 306f40a2ae
commit 7cdac78423
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 115 additions and 14 deletions

View File

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

View File

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

View File

@ -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)), {}),
]