Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fontTools/pens/recordingPen.py: 42%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

76 statements  

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)