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

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

75 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 

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)