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