1"""Pen recording operations that can be accessed or replayed."""
2
3from fontTools.pens.basePen import AbstractPen, DecomposingPen
4from fontTools.pens.pointPen import AbstractPointPen, DecomposingPointPen
5
6
7__all__ = [
8 "replayRecording",
9 "RecordingPen",
10 "DecomposingRecordingPen",
11 "DecomposingRecordingPointPen",
12 "RecordingPointPen",
13 "lerpRecordings",
14]
15
16
17def replayRecording(recording, pen):
18 """Replay a recording, as produced by RecordingPen or DecomposingRecordingPen,
19 to a pen.
20
21 Note that recording does not have to be produced by those pens.
22 It can be any iterable of tuples of method name and tuple-of-arguments.
23 Likewise, pen can be any objects receiving those method calls.
24 """
25 for operator, operands in recording:
26 getattr(pen, operator)(*operands)
27
28
29class RecordingPen(AbstractPen):
30 """Pen recording operations that can be accessed or replayed.
31
32 The recording can be accessed as pen.value; or replayed using
33 pen.replay(otherPen).
34
35 :Example:
36 .. code-block::
37
38 from fontTools.ttLib import TTFont
39 from fontTools.pens.recordingPen import RecordingPen
40
41 glyph_name = 'dollar'
42 font_path = 'MyFont.otf'
43
44 font = TTFont(font_path)
45 glyphset = font.getGlyphSet()
46 glyph = glyphset[glyph_name]
47
48 pen = RecordingPen()
49 glyph.draw(pen)
50 print(pen.value)
51 """
52
53 def __init__(self):
54 self.value = []
55
56 def moveTo(self, p0):
57 self.value.append(("moveTo", (p0,)))
58
59 def lineTo(self, p1):
60 self.value.append(("lineTo", (p1,)))
61
62 def qCurveTo(self, *points):
63 self.value.append(("qCurveTo", points))
64
65 def curveTo(self, *points):
66 self.value.append(("curveTo", points))
67
68 def closePath(self):
69 self.value.append(("closePath", ()))
70
71 def endPath(self):
72 self.value.append(("endPath", ()))
73
74 def addComponent(self, glyphName, transformation):
75 self.value.append(("addComponent", (glyphName, transformation)))
76
77 def addVarComponent(self, glyphName, transformation, location):
78 self.value.append(("addVarComponent", (glyphName, transformation, location)))
79
80 def replay(self, pen):
81 replayRecording(self.value, pen)
82
83 draw = replay
84
85
86class DecomposingRecordingPen(DecomposingPen, RecordingPen):
87 """Same as RecordingPen, except that it doesn't keep components
88 as references, but draws them decomposed as regular contours.
89
90 The constructor takes a required 'glyphSet' positional argument,
91 a dictionary of glyph objects (i.e. with a 'draw' method) keyed
92 by thir name; other arguments are forwarded to the DecomposingPen's
93 constructor::
94
95 >>> class SimpleGlyph(object):
96 ... def draw(self, pen):
97 ... pen.moveTo((0, 0))
98 ... pen.curveTo((1, 1), (2, 2), (3, 3))
99 ... pen.closePath()
100 >>> class CompositeGlyph(object):
101 ... def draw(self, pen):
102 ... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
103 >>> class MissingComponent(object):
104 ... def draw(self, pen):
105 ... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0))
106 >>> class FlippedComponent(object):
107 ... def draw(self, pen):
108 ... pen.addComponent('a', (-1, 0, 0, 1, 0, 0))
109 >>> glyphSet = {
110 ... 'a': SimpleGlyph(),
111 ... 'b': CompositeGlyph(),
112 ... 'c': MissingComponent(),
113 ... 'd': FlippedComponent(),
114 ... }
115 >>> for name, glyph in sorted(glyphSet.items()):
116 ... pen = DecomposingRecordingPen(glyphSet)
117 ... try:
118 ... glyph.draw(pen)
119 ... except pen.MissingComponentError:
120 ... pass
121 ... print("{}: {}".format(name, pen.value))
122 a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
123 b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
124 c: []
125 d: [('moveTo', ((0, 0),)), ('curveTo', ((-1, 1), (-2, 2), (-3, 3))), ('closePath', ())]
126
127 >>> for name, glyph in sorted(glyphSet.items()):
128 ... pen = DecomposingRecordingPen(
129 ... glyphSet, skipMissingComponents=True, reverseFlipped=True,
130 ... )
131 ... glyph.draw(pen)
132 ... print("{}: {}".format(name, pen.value))
133 a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
134 b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
135 c: []
136 d: [('moveTo', ((0, 0),)), ('lineTo', ((-3, 3),)), ('curveTo', ((-2, 2), (-1, 1), (0, 0))), ('closePath', ())]
137 """
138
139 # raises MissingComponentError(KeyError) if base glyph is not found in glyphSet
140 skipMissingComponents = False
141
142
143class RecordingPointPen(AbstractPointPen):
144 """PointPen recording operations that can be accessed or replayed.
145
146 The recording can be accessed as pen.value; or replayed using
147 pointPen.replay(otherPointPen).
148
149 :Example:
150 .. code-block::
151
152 from defcon import Font
153 from fontTools.pens.recordingPen import RecordingPointPen
154
155 glyph_name = 'a'
156 font_path = 'MyFont.ufo'
157
158 font = Font(font_path)
159 glyph = font[glyph_name]
160
161 pen = RecordingPointPen()
162 glyph.drawPoints(pen)
163 print(pen.value)
164
165 new_glyph = font.newGlyph('b')
166 pen.replay(new_glyph.getPointPen())
167 """
168
169 def __init__(self):
170 self.value = []
171
172 def beginPath(self, identifier=None, **kwargs):
173 if identifier is not None:
174 kwargs["identifier"] = identifier
175 self.value.append(("beginPath", (), kwargs))
176
177 def endPath(self):
178 self.value.append(("endPath", (), {}))
179
180 def addPoint(
181 self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
182 ):
183 if identifier is not None:
184 kwargs["identifier"] = identifier
185 self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs))
186
187 def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
188 if identifier is not None:
189 kwargs["identifier"] = identifier
190 self.value.append(("addComponent", (baseGlyphName, transformation), kwargs))
191
192 def addVarComponent(
193 self, baseGlyphName, transformation, location, identifier=None, **kwargs
194 ):
195 if identifier is not None:
196 kwargs["identifier"] = identifier
197 self.value.append(
198 ("addVarComponent", (baseGlyphName, transformation, location), kwargs)
199 )
200
201 def replay(self, pointPen):
202 for operator, args, kwargs in self.value:
203 getattr(pointPen, operator)(*args, **kwargs)
204
205 drawPoints = replay
206
207
208class DecomposingRecordingPointPen(DecomposingPointPen, RecordingPointPen):
209 """Same as RecordingPointPen, except that it doesn't keep components
210 as references, but draws them decomposed as regular contours.
211
212 The constructor takes a required 'glyphSet' positional argument,
213 a dictionary of pointPen-drawable glyph objects (i.e. with a 'drawPoints' method)
214 keyed by thir name; other arguments are forwarded to the DecomposingPointPen's
215 constructor::
216
217 >>> from pprint import pprint
218 >>> class SimpleGlyph(object):
219 ... def drawPoints(self, pen):
220 ... pen.beginPath()
221 ... pen.addPoint((0, 0), "line")
222 ... pen.addPoint((1, 1))
223 ... pen.addPoint((2, 2))
224 ... pen.addPoint((3, 3), "curve")
225 ... pen.endPath()
226 >>> class CompositeGlyph(object):
227 ... def drawPoints(self, pen):
228 ... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
229 >>> class MissingComponent(object):
230 ... def drawPoints(self, pen):
231 ... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0))
232 >>> class FlippedComponent(object):
233 ... def drawPoints(self, pen):
234 ... pen.addComponent('a', (-1, 0, 0, 1, 0, 0))
235 >>> glyphSet = {
236 ... 'a': SimpleGlyph(),
237 ... 'b': CompositeGlyph(),
238 ... 'c': MissingComponent(),
239 ... 'd': FlippedComponent(),
240 ... }
241 >>> for name, glyph in sorted(glyphSet.items()):
242 ... pen = DecomposingRecordingPointPen(glyphSet)
243 ... try:
244 ... glyph.drawPoints(pen)
245 ... except pen.MissingComponentError:
246 ... pass
247 ... pprint({name: pen.value})
248 {'a': [('beginPath', (), {}),
249 ('addPoint', ((0, 0), 'line', False, None), {}),
250 ('addPoint', ((1, 1), None, False, None), {}),
251 ('addPoint', ((2, 2), None, False, None), {}),
252 ('addPoint', ((3, 3), 'curve', False, None), {}),
253 ('endPath', (), {})]}
254 {'b': [('beginPath', (), {}),
255 ('addPoint', ((-1, 1), 'line', False, None), {}),
256 ('addPoint', ((0, 2), None, False, None), {}),
257 ('addPoint', ((1, 3), None, False, None), {}),
258 ('addPoint', ((2, 4), 'curve', False, None), {}),
259 ('endPath', (), {})]}
260 {'c': []}
261 {'d': [('beginPath', (), {}),
262 ('addPoint', ((0, 0), 'line', False, None), {}),
263 ('addPoint', ((-1, 1), None, False, None), {}),
264 ('addPoint', ((-2, 2), None, False, None), {}),
265 ('addPoint', ((-3, 3), 'curve', False, None), {}),
266 ('endPath', (), {})]}
267
268 >>> for name, glyph in sorted(glyphSet.items()):
269 ... pen = DecomposingRecordingPointPen(
270 ... glyphSet, skipMissingComponents=True, reverseFlipped=True,
271 ... )
272 ... glyph.drawPoints(pen)
273 ... pprint({name: pen.value})
274 {'a': [('beginPath', (), {}),
275 ('addPoint', ((0, 0), 'line', False, None), {}),
276 ('addPoint', ((1, 1), None, False, None), {}),
277 ('addPoint', ((2, 2), None, False, None), {}),
278 ('addPoint', ((3, 3), 'curve', False, None), {}),
279 ('endPath', (), {})]}
280 {'b': [('beginPath', (), {}),
281 ('addPoint', ((-1, 1), 'line', False, None), {}),
282 ('addPoint', ((0, 2), None, False, None), {}),
283 ('addPoint', ((1, 3), None, False, None), {}),
284 ('addPoint', ((2, 4), 'curve', False, None), {}),
285 ('endPath', (), {})]}
286 {'c': []}
287 {'d': [('beginPath', (), {}),
288 ('addPoint', ((0, 0), 'curve', False, None), {}),
289 ('addPoint', ((-3, 3), 'line', False, None), {}),
290 ('addPoint', ((-2, 2), None, False, None), {}),
291 ('addPoint', ((-1, 1), None, False, None), {}),
292 ('endPath', (), {})]}
293 """
294
295 # raises MissingComponentError(KeyError) if base glyph is not found in glyphSet
296 skipMissingComponents = False
297
298
299def lerpRecordings(recording1, recording2, factor=0.5):
300 """Linearly interpolate between two recordings. The recordings
301 must be decomposed, i.e. they must not contain any components.
302
303 Factor is typically between 0 and 1. 0 means the first recording,
304 1 means the second recording, and 0.5 means the average of the
305 two recordings. Other values are possible, and can be useful to
306 extrapolate. Defaults to 0.5.
307
308 Returns a generator with the new recording.
309 """
310 if len(recording1) != len(recording2):
311 raise ValueError(
312 "Mismatched lengths: %d and %d" % (len(recording1), len(recording2))
313 )
314 for (op1, args1), (op2, args2) in zip(recording1, recording2):
315 if op1 != op2:
316 raise ValueError("Mismatched operations: %s, %s" % (op1, op2))
317 if op1 == "addComponent":
318 raise ValueError("Cannot interpolate components")
319 else:
320 mid_args = [
321 (x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
322 for (x1, y1), (x2, y2) in zip(args1, args2)
323 ]
324 yield (op1, mid_args)
325
326
327if __name__ == "__main__":
328 pen = RecordingPen()
329 pen.moveTo((0, 0))
330 pen.lineTo((0, 100))
331 pen.curveTo((50, 75), (60, 50), (50, 25))
332 pen.closePath()
333 from pprint import pprint
334
335 pprint(pen.value)