From 646e26603db8026baea7b5acb0bbf5a7cf4f8c3d Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 28 Nov 2019 15:29:03 +0000 Subject: [PATCH 1/5] filterPen: add FilterPointPen, like FilterPen but for point pens the base class simply passes through contours/components unchanged --- Lib/fontTools/pens/filterPen.py | 43 +++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/pens/filterPen.py b/Lib/fontTools/pens/filterPen.py index 0ebb9c23b..7539efb5c 100644 --- a/Lib/fontTools/pens/filterPen.py +++ b/Lib/fontTools/pens/filterPen.py @@ -1,12 +1,13 @@ from fontTools.misc.py23 import * from fontTools.pens.basePen import AbstractPen +from fontTools.pens.pointPen import AbstractPointPen from fontTools.pens.recordingPen import RecordingPen class _PassThruComponentsMixin(object): - def addComponent(self, glyphName, transformation): - self._outPen.addComponent(glyphName, transformation) + def addComponent(self, glyphName, transformation, **kwargs): + self._outPen.addComponent(glyphName, transformation, **kwargs) class FilterPen(_PassThruComponentsMixin, AbstractPen): @@ -118,3 +119,41 @@ class ContourFilterPen(_PassThruComponentsMixin, RecordingPen): Otherwise, the return value is drawn with the output pen. """ return # or return contour + + +class FilterPointPen(_PassThruComponentsMixin, AbstractPointPen): + """ Baseclass for point pens that apply some transformation to the + coordinates they receive and pass them to another point pen. + + You can override any of its methods. The default implementation does + nothing, but passes the commands unmodified to the other pen. + + >>> from fontTools.pens.recordingPen import RecordingPointPen + >>> rec = RecordingPointPen() + >>> pen = FilterPointPen(rec) + >>> v = iter(rec.value) + >>> pen.beginPath(identifier="abc") + >>> next(v) + ('beginPath', (), {'identifier': 'abc'}) + >>> pen.addPoint((1, 2), "line", False) + >>> next(v) + ('addPoint', ((1, 2), 'line', False, None), {}) + >>> pen.addComponent("a", (2, 0, 0, 2, 10, -10), identifier="0001") + >>> next(v) + ('addComponent', ('a', (2, 0, 0, 2, 10, -10)), {'identifier': '0001'}) + >>> pen.endPath() + >>> next(v) + ('endPath', (), {}) + """ + + def __init__(self, outPointPen): + self._outPen = outPointPen + + def beginPath(self, **kwargs): + self._outPen.beginPath(**kwargs) + + def endPath(self): + self._outPen.endPath() + + def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) From 0fef59fd9e2990f631d462c28c5fa9efb608e422 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 28 Nov 2019 15:30:08 +0000 Subject: [PATCH 2/5] transformPen: add TransformPointPen, like TransformPen but for point pens --- Lib/fontTools/pens/transformPen.py | 47 +++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/pens/transformPen.py b/Lib/fontTools/pens/transformPen.py index 8d33a0865..6619ba739 100644 --- a/Lib/fontTools/pens/transformPen.py +++ b/Lib/fontTools/pens/transformPen.py @@ -1,5 +1,5 @@ from fontTools.misc.py23 import * -from fontTools.pens.filterPen import FilterPen +from fontTools.pens.filterPen import FilterPen, FilterPointPen __all__ = ["TransformPen"] @@ -55,6 +55,51 @@ class TransformPen(FilterPen): self._outPen.addComponent(glyphName, transformation) +class TransformPointPen(FilterPointPen): + """PointPen that transforms all coordinates using a Affine transformation, + and passes them to another PointPen. + + >>> from fontTools.pens.recordingPen import RecordingPointPen + >>> rec = RecordingPointPen() + >>> pen = TransformPointPen(rec, (2, 0, 0, 2, -10, 5)) + >>> v = iter(rec.value) + >>> pen.beginPath(identifier="contour-0") + >>> next(v) + ('beginPath', (), {'identifier': 'contour-0'}) + >>> pen.addPoint((100, 100), "line") + >>> next(v) + ('addPoint', ((190, 205), 'line', False, None), {}) + >>> pen.endPath() + >>> next(v) + ('endPath', (), {}) + >>> pen.addComponent("a", (1, 0, 0, 1, -10, 5), identifier="component-0") + >>> next(v) + ('addComponent', ('a', ), {'identifier': 'component-0'}) + """ + + def __init__(self, outPointPen, transformation): + """The 'outPointPen' argument is another point pen object. + It will receive the transformed coordinates. + The 'transformation' argument can either be a six-tuple, or a + fontTools.misc.transform.Transform object. + """ + super().__init__(outPointPen) + if not hasattr(transformation, "transformPoint"): + from fontTools.misc.transform import Transform + transformation = Transform(*transformation) + self._transformation = transformation + self._transformPoint = transformation.transformPoint + + def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + self._outPen.addPoint( + self._transformPoint(pt), segmentType, smooth, name, **kwargs + ) + + def addComponent(self, baseGlyphName, transformation, **kwargs): + transformation = self._transformation.transform(transformation) + self._outPen.addComponent(baseGlyphName, transformation, **kwargs) + + if __name__ == "__main__": from fontTools.pens.basePen import _TestPen pen = TransformPen(_TestPen(None), (2, 0, 0.5, 2, -10, 0)) From b885a852ed8b90d972ad0d7efa6b1a8e0d647b97 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 28 Nov 2019 15:31:04 +0000 Subject: [PATCH 3/5] recordingPen: add RecordingPointPen, like RecordingPen but for point pens --- Lib/fontTools/pens/recordingPen.py | 53 +++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/pens/recordingPen.py b/Lib/fontTools/pens/recordingPen.py index dbbcc916c..b25011d6d 100644 --- a/Lib/fontTools/pens/recordingPen.py +++ b/Lib/fontTools/pens/recordingPen.py @@ -1,9 +1,15 @@ """Pen recording operations that can be accessed or replayed.""" from fontTools.misc.py23 import * from fontTools.pens.basePen import AbstractPen, DecomposingPen +from fontTools.pens.pointPen import AbstractPointPen -__all__ = ["replayRecording", "RecordingPen", "DecomposingRecordingPen"] +__all__ = [ + "replayRecording", + "RecordingPen", + "DecomposingRecordingPen", + "RecordingPointPen", +] def replayRecording(recording, pen): @@ -89,6 +95,51 @@ class DecomposingRecordingPen(DecomposingPen, RecordingPen): skipMissingComponents = False +class RecordingPointPen(AbstractPointPen): + """PointPen recording operations that can be accessed or replayed. + + The recording can be accessed as pen.value; or replayed using + pointPen.replay(otherPointPen). + + Usage example: + ============== + from defcon import Font + from fontTools.pens.recordingPen import RecordingPointPen + + glyph_name = 'a' + font_path = 'MyFont.ufo' + + font = Font(font_path) + glyph = font[glyph_name] + + pen = RecordingPointPen() + glyph.drawPoints(pen) + print(pen.value) + + new_glyph = font.newGlyph('b') + pen.replay(new_glyph.getPointPen()) + """ + + def __init__(self): + self.value = [] + + def beginPath(self, **kwargs): + self.value.append(("beginPath", (), kwargs)) + + def endPath(self): + self.value.append(("endPath", (), {})) + + def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs)) + + def addComponent(self, baseGlyphName, transformation, **kwargs): + self.value.append(("addComponent", (baseGlyphName, transformation), kwargs)) + + def replay(self, pointPen): + for operator, args, kwargs in self.value: + getattr(pointPen, operator)(*args, **kwargs) + + if __name__ == "__main__": from fontTools.pens.basePen import _TestPen pen = RecordingPen() From ae5212f76b0da0bfa707d57e58fd12a22b63a8bf Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 28 Nov 2019 15:32:55 +0000 Subject: [PATCH 4/5] add tests for RecordingPointPen --- Tests/pens/recordingPen_test.py | 52 +++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/Tests/pens/recordingPen_test.py b/Tests/pens/recordingPen_test.py index 971536d7a..6977b93b0 100644 --- a/Tests/pens/recordingPen_test.py +++ b/Tests/pens/recordingPen_test.py @@ -1,19 +1,29 @@ -from fontTools.misc.py23 import * -from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen +from fontTools.pens.recordingPen import ( + RecordingPen, + DecomposingRecordingPen, + RecordingPointPen, +) import pytest class _TestGlyph(object): - def draw(self, pen): pen.moveTo((0.0, 0.0)) pen.lineTo((0.0, 100.0)) pen.curveTo((50.0, 75.0), (60.0, 50.0), (50.0, 0.0)) pen.closePath() + def drawPoints(self, pen): + pen.beginPath(identifier="abc") + pen.addPoint((0.0, 0.0), "line", False, "start", identifier="0000") + pen.addPoint((0.0, 100.0), "line", False, None, identifier="0001") + pen.addPoint((50.0, 75.0), None, False, None, identifier="0002") + pen.addPoint((60.0, 50.0), None, False, None, identifier="0003") + pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004") + pen.endPath() + class RecordingPenTest(object): - def test_addComponent(self): pen = RecordingPen() pen.addComponent("a", (2, 0, 0, 3, -10, 5)) @@ -21,18 +31,42 @@ class RecordingPenTest(object): class DecomposingRecordingPenTest(object): - def test_addComponent_decomposed(self): pen = DecomposingRecordingPen({"a": _TestGlyph()}) pen.addComponent("a", (2, 0, 0, 3, -10, 5)) assert pen.value == [ - ('moveTo', ((-10.0, 5.0),)), - ('lineTo', ((-10.0, 305.0),)), - ('curveTo', ((90.0, 230.0), (110.0, 155.0), (90.0, 5.0),)), - ('closePath', ())] + ("moveTo", ((-10.0, 5.0),)), + ("lineTo", ((-10.0, 305.0),)), + ("curveTo", ((90.0, 230.0), (110.0, 155.0), (90.0, 5.0))), + ("closePath", ()), + ] def test_addComponent_missing_raises(self): pen = DecomposingRecordingPen(dict()) with pytest.raises(KeyError) as excinfo: pen.addComponent("a", (1, 0, 0, 1, 0, 0)) assert excinfo.value.args[0] == "a" + + +class RecordingPointPenTest: + def test_record_and_replay(self): + pen = RecordingPointPen() + glyph = _TestGlyph() + glyph.drawPoints(pen) + pen.addComponent("a", (2, 0, 0, 2, -10, 5)) + + assert pen.value == [ + ("beginPath", (), {"identifier": "abc"}), + ("addPoint", ((0.0, 0.0), "line", False, "start"), {"identifier": "0000"}), + ("addPoint", ((0.0, 100.0), "line", False, None), {"identifier": "0001"}), + ("addPoint", ((50.0, 75.0), None, False, None), {"identifier": "0002"}), + ("addPoint", ((60.0, 50.0), None, False, None), {"identifier": "0003"}), + ("addPoint", ((50.0, 0.0), "curve", True, "last"), {"identifier": "0004"}), + ("endPath", (), {}), + ("addComponent", ("a", (2, 0, 0, 2, -10, 5)), {}), + ] + + pen2 = RecordingPointPen() + pen.replay(pen2) + + assert pen2.value == pen.value From d9b6067447c1ca875931e1f9cc80b42d9da532c1 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 28 Nov 2019 15:36:03 +0000 Subject: [PATCH 5/5] roundingPen: Add RoundingPointPen, like RoundingPen but for point pens --- Lib/fontTools/pens/roundingPen.py | 58 ++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/pens/roundingPen.py b/Lib/fontTools/pens/roundingPen.py index 6199e37c7..c032cad1f 100644 --- a/Lib/fontTools/pens/roundingPen.py +++ b/Lib/fontTools/pens/roundingPen.py @@ -1,6 +1,9 @@ from fontTools.misc.fixedTools import otRound from fontTools.misc.transform import Transform -from fontTools.pens.filterPen import FilterPen +from fontTools.pens.filterPen import FilterPen, FilterPointPen + + +__all__ = ["RoundingPen", "RoundingPointPen"] class RoundingPen(FilterPen): @@ -54,3 +57,56 @@ class RoundingPen(FilterPen): self.roundFunc(transformation[5]), ), ) + + +class RoundingPointPen(FilterPointPen): + """ + Filter point pen that rounds point coordinates and component XY offsets to integer. + + >>> from fontTools.pens.recordingPen import RecordingPointPen + >>> 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)) + >>> 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)), {}), + ... ] + True + """ + + def __init__(self, outPen, roundFunc=otRound): + super().__init__(outPen) + self.roundFunc = roundFunc + + def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + self._outPen.addPoint( + (self.roundFunc(pt[0]), self.roundFunc(pt[1])), + segmentType=segmentType, + smooth=smooth, + name=name, + **kwargs, + ) + + def addComponent(self, baseGlyphName, transformation, **kwargs): + self._outPen.addComponent( + baseGlyphName, + Transform( + *transformation[:4], + self.roundFunc(transformation[4]), + self.roundFunc(transformation[5]), + ), + **kwargs, + )