recordingPen.py 12 KB

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