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