recordingPen.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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
  4. __all__ = [
  5. "replayRecording",
  6. "RecordingPen",
  7. "DecomposingRecordingPen",
  8. "RecordingPointPen",
  9. "lerpRecordings",
  10. ]
  11. def replayRecording(recording, pen):
  12. """Replay a recording, as produced by RecordingPen or DecomposingRecordingPen,
  13. to a pen.
  14. Note that recording does not have to be produced by those pens.
  15. It can be any iterable of tuples of method name and tuple-of-arguments.
  16. Likewise, pen can be any objects receiving those method calls.
  17. """
  18. for operator, operands in recording:
  19. getattr(pen, operator)(*operands)
  20. class RecordingPen(AbstractPen):
  21. """Pen recording operations that can be accessed or replayed.
  22. The recording can be accessed as pen.value; or replayed using
  23. pen.replay(otherPen).
  24. :Example:
  25. from fontTools.ttLib import TTFont
  26. from fontTools.pens.recordingPen import RecordingPen
  27. glyph_name = 'dollar'
  28. font_path = 'MyFont.otf'
  29. font = TTFont(font_path)
  30. glyphset = font.getGlyphSet()
  31. glyph = glyphset[glyph_name]
  32. pen = RecordingPen()
  33. glyph.draw(pen)
  34. print(pen.value)
  35. """
  36. def __init__(self):
  37. self.value = []
  38. def moveTo(self, p0):
  39. self.value.append(("moveTo", (p0,)))
  40. def lineTo(self, p1):
  41. self.value.append(("lineTo", (p1,)))
  42. def qCurveTo(self, *points):
  43. self.value.append(("qCurveTo", points))
  44. def curveTo(self, *points):
  45. self.value.append(("curveTo", points))
  46. def closePath(self):
  47. self.value.append(("closePath", ()))
  48. def endPath(self):
  49. self.value.append(("endPath", ()))
  50. def addComponent(self, glyphName, transformation):
  51. self.value.append(("addComponent", (glyphName, transformation)))
  52. def addVarComponent(self, glyphName, transformation, location):
  53. self.value.append(("addVarComponent", (glyphName, transformation, location)))
  54. def replay(self, pen):
  55. replayRecording(self.value, pen)
  56. draw = replay
  57. class DecomposingRecordingPen(DecomposingPen, RecordingPen):
  58. """Same as RecordingPen, except that it doesn't keep components
  59. as references, but draws them decomposed as regular contours.
  60. The constructor takes a single 'glyphSet' positional argument,
  61. a dictionary of glyph objects (i.e. with a 'draw' method) keyed
  62. by thir name::
  63. >>> class SimpleGlyph(object):
  64. ... def draw(self, pen):
  65. ... pen.moveTo((0, 0))
  66. ... pen.curveTo((1, 1), (2, 2), (3, 3))
  67. ... pen.closePath()
  68. >>> class CompositeGlyph(object):
  69. ... def draw(self, pen):
  70. ... pen.addComponent('a', (1, 0, 0, 1, -1, 1))
  71. >>> glyphSet = {'a': SimpleGlyph(), 'b': CompositeGlyph()}
  72. >>> for name, glyph in sorted(glyphSet.items()):
  73. ... pen = DecomposingRecordingPen(glyphSet)
  74. ... glyph.draw(pen)
  75. ... print("{}: {}".format(name, pen.value))
  76. a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())]
  77. b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())]
  78. """
  79. # raises KeyError if base glyph is not found in glyphSet
  80. skipMissingComponents = False
  81. class RecordingPointPen(AbstractPointPen):
  82. """PointPen recording operations that can be accessed or replayed.
  83. The recording can be accessed as pen.value; or replayed using
  84. pointPen.replay(otherPointPen).
  85. :Example:
  86. from defcon import Font
  87. from fontTools.pens.recordingPen import RecordingPointPen
  88. glyph_name = 'a'
  89. font_path = 'MyFont.ufo'
  90. font = Font(font_path)
  91. glyph = font[glyph_name]
  92. pen = RecordingPointPen()
  93. glyph.drawPoints(pen)
  94. print(pen.value)
  95. new_glyph = font.newGlyph('b')
  96. pen.replay(new_glyph.getPointPen())
  97. """
  98. def __init__(self):
  99. self.value = []
  100. def beginPath(self, identifier=None, **kwargs):
  101. if identifier is not None:
  102. kwargs["identifier"] = identifier
  103. self.value.append(("beginPath", (), kwargs))
  104. def endPath(self):
  105. self.value.append(("endPath", (), {}))
  106. def addPoint(
  107. self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
  108. ):
  109. if identifier is not None:
  110. kwargs["identifier"] = identifier
  111. self.value.append(("addPoint", (pt, segmentType, smooth, name), kwargs))
  112. def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
  113. if identifier is not None:
  114. kwargs["identifier"] = identifier
  115. self.value.append(("addComponent", (baseGlyphName, transformation), kwargs))
  116. def addVarComponent(
  117. self, baseGlyphName, transformation, location, identifier=None, **kwargs
  118. ):
  119. if identifier is not None:
  120. kwargs["identifier"] = identifier
  121. self.value.append(
  122. ("addVarComponent", (baseGlyphName, transformation, location), kwargs)
  123. )
  124. def replay(self, pointPen):
  125. for operator, args, kwargs in self.value:
  126. getattr(pointPen, operator)(*args, **kwargs)
  127. drawPoints = replay
  128. def lerpRecordings(recording1, recording2, factor=0.5):
  129. """Linearly interpolate between two recordings. The recordings
  130. must be decomposed, i.e. they must not contain any components.
  131. Factor is typically between 0 and 1. 0 means the first recording,
  132. 1 means the second recording, and 0.5 means the average of the
  133. two recordings. Other values are possible, and can be useful to
  134. extrapolate. Defaults to 0.5.
  135. Returns a generator with the new recording.
  136. """
  137. if len(recording1) != len(recording2):
  138. raise ValueError(
  139. "Mismatched lengths: %d and %d" % (len(recording1), len(recording2))
  140. )
  141. for (op1, args1), (op2, args2) in zip(recording1, recording2):
  142. if op1 != op2:
  143. raise ValueError("Mismatched operations: %s, %s" % (op1, op2))
  144. if op1 == "addComponent":
  145. raise ValueError("Cannot interpolate components")
  146. else:
  147. mid_args = [
  148. (x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
  149. for (x1, y1), (x2, y2) in zip(args1, args2)
  150. ]
  151. yield (op1, mid_args)
  152. if __name__ == "__main__":
  153. pen = RecordingPen()
  154. pen.moveTo((0, 0))
  155. pen.lineTo((0, 100))
  156. pen.curveTo((50, 75), (60, 50), (50, 25))
  157. pen.closePath()
  158. from pprint import pprint
  159. pprint(pen.value)