xref: /aosp_15_r20/external/fonttools/Lib/fontTools/pens/recordingPen.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""Pen recording operations that can be accessed or replayed."""
2
3from fontTools.pens.basePen import AbstractPen, DecomposingPen
4from fontTools.pens.pointPen import AbstractPointPen
5
6
7__all__ = [
8    "replayRecording",
9    "RecordingPen",
10    "DecomposingRecordingPen",
11    "RecordingPointPen",
12    "lerpRecordings",
13]
14
15
16def replayRecording(recording, pen):
17    """Replay a recording, as produced by RecordingPen or DecomposingRecordingPen,
18    to a pen.
19
20    Note that recording does not have to be produced by those pens.
21    It can be any iterable of tuples of method name and tuple-of-arguments.
22    Likewise, pen can be any objects receiving those method calls.
23    """
24    for operator, operands in recording:
25        getattr(pen, operator)(*operands)
26
27
28class RecordingPen(AbstractPen):
29    """Pen recording operations that can be accessed or replayed.
30
31    The recording can be accessed as pen.value; or replayed using
32    pen.replay(otherPen).
33
34    :Example:
35
36            from fontTools.ttLib import TTFont
37            from fontTools.pens.recordingPen import RecordingPen
38
39            glyph_name = 'dollar'
40            font_path = 'MyFont.otf'
41
42            font = TTFont(font_path)
43            glyphset = font.getGlyphSet()
44            glyph = glyphset[glyph_name]
45
46            pen = RecordingPen()
47            glyph.draw(pen)
48            print(pen.value)
49    """
50
51    def __init__(self):
52        self.value = []
53
54    def moveTo(self, p0):
55        self.value.append(("moveTo", (p0,)))
56
57    def lineTo(self, p1):
58        self.value.append(("lineTo", (p1,)))
59
60    def qCurveTo(self, *points):
61        self.value.append(("qCurveTo", points))
62
63    def curveTo(self, *points):
64        self.value.append(("curveTo", points))
65
66    def closePath(self):
67        self.value.append(("closePath", ()))
68
69    def endPath(self):
70        self.value.append(("endPath", ()))
71
72    def addComponent(self, glyphName, transformation):
73        self.value.append(("addComponent", (glyphName, transformation)))
74
75    def addVarComponent(self, glyphName, transformation, location):
76        self.value.append(("addVarComponent", (glyphName, transformation, location)))
77
78    def replay(self, pen):
79        replayRecording(self.value, pen)
80
81    draw = replay
82
83
84class DecomposingRecordingPen(DecomposingPen, RecordingPen):
85    """Same as RecordingPen, except that it doesn't keep components
86    as references, but draws them decomposed as regular contours.
87
88    The constructor takes a single 'glyphSet' positional argument,
89    a dictionary of glyph objects (i.e. with a 'draw' method) keyed
90    by thir name::
91
92            >>> class SimpleGlyph(object):
93            ...     def draw(self, pen):
94            ...         pen.moveTo((0, 0))
95            ...         pen.curveTo((1, 1), (2, 2), (3, 3))
96            ...         pen.closePath()
97            >>> class CompositeGlyph(object):
98            ...     def draw(self, pen):
99            ...         pen.addComponent('a', (1, 0, 0, 1, -1, 1))
100            >>> glyphSet = {'a': SimpleGlyph(), 'b': CompositeGlyph()}
101            >>> for name, glyph in sorted(glyphSet.items()):
102            ...     pen = DecomposingRecordingPen(glyphSet)
103            ...     glyph.draw(pen)
104            ...     print("{}: {}".format(name, pen.value))
105            a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
106            b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
107    """
108
109    # raises KeyError if base glyph is not found in glyphSet
110    skipMissingComponents = False
111
112
113class RecordingPointPen(AbstractPointPen):
114    """PointPen recording operations that can be accessed or replayed.
115
116    The recording can be accessed as pen.value; or replayed using
117    pointPen.replay(otherPointPen).
118
119    :Example:
120
121            from defcon import Font
122            from fontTools.pens.recordingPen import RecordingPointPen
123
124            glyph_name = 'a'
125            font_path = 'MyFont.ufo'
126
127            font = Font(font_path)
128            glyph = font[glyph_name]
129
130            pen = RecordingPointPen()
131            glyph.drawPoints(pen)
132            print(pen.value)
133
134            new_glyph = font.newGlyph('b')
135            pen.replay(new_glyph.getPointPen())
136    """
137
138    def __init__(self):
139        self.value = []
140
141    def beginPath(self, identifier=None, **kwargs):
142        if identifier is not None:
143            kwargs["identifier"] = identifier
144        self.value.append(("beginPath", (), kwargs))
145
146    def endPath(self):
147        self.value.append(("endPath", (), {}))
148
149    def addPoint(
150        self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
151    ):
152        if identifier is not None:
153            kwargs["identifier"] = identifier
154        self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs))
155
156    def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
157        if identifier is not None:
158            kwargs["identifier"] = identifier
159        self.value.append(("addComponent", (baseGlyphName, transformation), kwargs))
160
161    def addVarComponent(
162        self, baseGlyphName, transformation, location, identifier=None, **kwargs
163    ):
164        if identifier is not None:
165            kwargs["identifier"] = identifier
166        self.value.append(
167            ("addVarComponent", (baseGlyphName, transformation, location), kwargs)
168        )
169
170    def replay(self, pointPen):
171        for operator, args, kwargs in self.value:
172            getattr(pointPen, operator)(*args, **kwargs)
173
174    drawPoints = replay
175
176
177def lerpRecordings(recording1, recording2, factor=0.5):
178    """Linearly interpolate between two recordings. The recordings
179    must be decomposed, i.e. they must not contain any components.
180
181    Factor is typically between 0 and 1. 0 means the first recording,
182    1 means the second recording, and 0.5 means the average of the
183    two recordings. Other values are possible, and can be useful to
184    extrapolate. Defaults to 0.5.
185
186    Returns a generator with the new recording.
187    """
188    if len(recording1) != len(recording2):
189        raise ValueError(
190            "Mismatched lengths: %d and %d" % (len(recording1), len(recording2))
191        )
192    for (op1, args1), (op2, args2) in zip(recording1, recording2):
193        if op1 != op2:
194            raise ValueError("Mismatched operations: %s, %s" % (op1, op2))
195        if op1 == "addComponent":
196            raise ValueError("Cannot interpolate components")
197        else:
198            mid_args = [
199                (x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
200                for (x1, y1), (x2, y2) in zip(args1, args2)
201            ]
202        yield (op1, mid_args)
203
204
205if __name__ == "__main__":
206    pen = RecordingPen()
207    pen.moveTo((0, 0))
208    pen.lineTo((0, 100))
209    pen.curveTo((50, 75), (60, 50), (50, 25))
210    pen.closePath()
211    from pprint import pprint
212
213    pprint(pen.value)
214