ttGlyphPen.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. from array import array
  2. from typing import Any, Callable, Dict, Optional, Tuple
  3. from fontTools.misc.fixedTools import MAX_F2DOT14, floatToFixedToFloat
  4. from fontTools.misc.loggingTools import LogMixin
  5. from fontTools.pens.pointPen import AbstractPointPen
  6. from fontTools.misc.roundTools import otRound
  7. from fontTools.pens.basePen import LoggingPen, PenError
  8. from fontTools.pens.transformPen import TransformPen, TransformPointPen
  9. from fontTools.ttLib.tables import ttProgram
  10. from fontTools.ttLib.tables._g_l_y_f import flagOnCurve, flagCubic
  11. from fontTools.ttLib.tables._g_l_y_f import Glyph
  12. from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
  13. from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
  14. from fontTools.ttLib.tables._g_l_y_f import dropImpliedOnCurvePoints
  15. import math
  16. __all__ = ["TTGlyphPen", "TTGlyphPointPen"]
  17. class _TTGlyphBasePen:
  18. def __init__(
  19. self,
  20. glyphSet: Optional[Dict[str, Any]],
  21. handleOverflowingTransforms: bool = True,
  22. ) -> None:
  23. """
  24. Construct a new pen.
  25. Args:
  26. glyphSet (Dict[str, Any]): A glyphset object, used to resolve components.
  27. handleOverflowingTransforms (bool): See below.
  28. If ``handleOverflowingTransforms`` is True, the components' transform values
  29. are checked that they don't overflow the limits of a F2Dot14 number:
  30. -2.0 <= v < +2.0. If any transform value exceeds these, the composite
  31. glyph is decomposed.
  32. An exception to this rule is done for values that are very close to +2.0
  33. (both for consistency with the -2.0 case, and for the relative frequency
  34. these occur in real fonts). When almost +2.0 values occur (and all other
  35. values are within the range -2.0 <= x <= +2.0), they are clamped to the
  36. maximum positive value that can still be encoded as an F2Dot14: i.e.
  37. 1.99993896484375.
  38. If False, no check is done and all components are translated unmodified
  39. into the glyf table, followed by an inevitable ``struct.error`` once an
  40. attempt is made to compile them.
  41. If both contours and components are present in a glyph, the components
  42. are decomposed.
  43. """
  44. self.glyphSet = glyphSet
  45. self.handleOverflowingTransforms = handleOverflowingTransforms
  46. self.init()
  47. def _decompose(
  48. self,
  49. glyphName: str,
  50. transformation: Tuple[float, float, float, float, float, float],
  51. ):
  52. tpen = self.transformPen(self, transformation)
  53. getattr(self.glyphSet[glyphName], self.drawMethod)(tpen)
  54. def _isClosed(self):
  55. """
  56. Check if the current path is closed.
  57. """
  58. raise NotImplementedError
  59. def init(self) -> None:
  60. self.points = []
  61. self.endPts = []
  62. self.types = []
  63. self.components = []
  64. def addComponent(
  65. self,
  66. baseGlyphName: str,
  67. transformation: Tuple[float, float, float, float, float, float],
  68. identifier: Optional[str] = None,
  69. **kwargs: Any,
  70. ) -> None:
  71. """
  72. Add a sub glyph.
  73. """
  74. self.components.append((baseGlyphName, transformation))
  75. def _buildComponents(self, componentFlags):
  76. if self.handleOverflowingTransforms:
  77. # we can't encode transform values > 2 or < -2 in F2Dot14,
  78. # so we must decompose the glyph if any transform exceeds these
  79. overflowing = any(
  80. s > 2 or s < -2
  81. for (glyphName, transformation) in self.components
  82. for s in transformation[:4]
  83. )
  84. components = []
  85. for glyphName, transformation in self.components:
  86. if glyphName not in self.glyphSet:
  87. self.log.warning(f"skipped non-existing component '{glyphName}'")
  88. continue
  89. if self.points or (self.handleOverflowingTransforms and overflowing):
  90. # can't have both coordinates and components, so decompose
  91. self._decompose(glyphName, transformation)
  92. continue
  93. component = GlyphComponent()
  94. component.glyphName = glyphName
  95. component.x, component.y = (otRound(v) for v in transformation[4:])
  96. # quantize floats to F2Dot14 so we get same values as when decompiled
  97. # from a binary glyf table
  98. transformation = tuple(
  99. floatToFixedToFloat(v, 14) for v in transformation[:4]
  100. )
  101. if transformation != (1, 0, 0, 1):
  102. if self.handleOverflowingTransforms and any(
  103. MAX_F2DOT14 < s <= 2 for s in transformation
  104. ):
  105. # clamp values ~= +2.0 so we can keep the component
  106. transformation = tuple(
  107. MAX_F2DOT14 if MAX_F2DOT14 < s <= 2 else s
  108. for s in transformation
  109. )
  110. component.transform = (transformation[:2], transformation[2:])
  111. component.flags = componentFlags
  112. components.append(component)
  113. return components
  114. def glyph(
  115. self,
  116. componentFlags: int = 0x04,
  117. dropImpliedOnCurves: bool = False,
  118. *,
  119. round: Callable[[float], int] = otRound,
  120. ) -> Glyph:
  121. """
  122. Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
  123. Args:
  124. componentFlags: Flags to use for component glyphs. (default: 0x04)
  125. dropImpliedOnCurves: Whether to remove implied-oncurve points. (default: False)
  126. """
  127. if not self._isClosed():
  128. raise PenError("Didn't close last contour.")
  129. components = self._buildComponents(componentFlags)
  130. glyph = Glyph()
  131. glyph.coordinates = GlyphCoordinates(self.points)
  132. glyph.endPtsOfContours = self.endPts
  133. glyph.flags = array("B", self.types)
  134. self.init()
  135. if components:
  136. # If both components and contours were present, they have by now
  137. # been decomposed by _buildComponents.
  138. glyph.components = components
  139. glyph.numberOfContours = -1
  140. else:
  141. glyph.numberOfContours = len(glyph.endPtsOfContours)
  142. glyph.program = ttProgram.Program()
  143. glyph.program.fromBytecode(b"")
  144. if dropImpliedOnCurves:
  145. dropImpliedOnCurvePoints(glyph)
  146. glyph.coordinates.toInt(round=round)
  147. return glyph
  148. class TTGlyphPen(_TTGlyphBasePen, LoggingPen):
  149. """
  150. Pen used for drawing to a TrueType glyph.
  151. This pen can be used to construct or modify glyphs in a TrueType format
  152. font. After using the pen to draw, use the ``.glyph()`` method to retrieve
  153. a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
  154. """
  155. drawMethod = "draw"
  156. transformPen = TransformPen
  157. def __init__(
  158. self,
  159. glyphSet: Optional[Dict[str, Any]] = None,
  160. handleOverflowingTransforms: bool = True,
  161. outputImpliedClosingLine: bool = False,
  162. ) -> None:
  163. super().__init__(glyphSet, handleOverflowingTransforms)
  164. self.outputImpliedClosingLine = outputImpliedClosingLine
  165. def _addPoint(self, pt: Tuple[float, float], tp: int) -> None:
  166. self.points.append(pt)
  167. self.types.append(tp)
  168. def _popPoint(self) -> None:
  169. self.points.pop()
  170. self.types.pop()
  171. def _isClosed(self) -> bool:
  172. return (not self.points) or (
  173. self.endPts and self.endPts[-1] == len(self.points) - 1
  174. )
  175. def lineTo(self, pt: Tuple[float, float]) -> None:
  176. self._addPoint(pt, flagOnCurve)
  177. def moveTo(self, pt: Tuple[float, float]) -> None:
  178. if not self._isClosed():
  179. raise PenError('"move"-type point must begin a new contour.')
  180. self._addPoint(pt, flagOnCurve)
  181. def curveTo(self, *points) -> None:
  182. assert len(points) % 2 == 1
  183. for pt in points[:-1]:
  184. self._addPoint(pt, flagCubic)
  185. # last point is None if there are no on-curve points
  186. if points[-1] is not None:
  187. self._addPoint(points[-1], 1)
  188. def qCurveTo(self, *points) -> None:
  189. assert len(points) >= 1
  190. for pt in points[:-1]:
  191. self._addPoint(pt, 0)
  192. # last point is None if there are no on-curve points
  193. if points[-1] is not None:
  194. self._addPoint(points[-1], 1)
  195. def closePath(self) -> None:
  196. endPt = len(self.points) - 1
  197. # ignore anchors (one-point paths)
  198. if endPt == 0 or (self.endPts and endPt == self.endPts[-1] + 1):
  199. self._popPoint()
  200. return
  201. if not self.outputImpliedClosingLine:
  202. # if first and last point on this path are the same, remove last
  203. startPt = 0
  204. if self.endPts:
  205. startPt = self.endPts[-1] + 1
  206. if self.points[startPt] == self.points[endPt]:
  207. self._popPoint()
  208. endPt -= 1
  209. self.endPts.append(endPt)
  210. def endPath(self) -> None:
  211. # TrueType contours are always "closed"
  212. self.closePath()
  213. class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen):
  214. """
  215. Point pen used for drawing to a TrueType glyph.
  216. This pen can be used to construct or modify glyphs in a TrueType format
  217. font. After using the pen to draw, use the ``.glyph()`` method to retrieve
  218. a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
  219. """
  220. drawMethod = "drawPoints"
  221. transformPen = TransformPointPen
  222. def init(self) -> None:
  223. super().init()
  224. self._currentContourStartIndex = None
  225. def _isClosed(self) -> bool:
  226. return self._currentContourStartIndex is None
  227. def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
  228. """
  229. Start a new sub path.
  230. """
  231. if not self._isClosed():
  232. raise PenError("Didn't close previous contour.")
  233. self._currentContourStartIndex = len(self.points)
  234. def endPath(self) -> None:
  235. """
  236. End the current sub path.
  237. """
  238. # TrueType contours are always "closed"
  239. if self._isClosed():
  240. raise PenError("Contour is already closed.")
  241. if self._currentContourStartIndex == len(self.points):
  242. # ignore empty contours
  243. self._currentContourStartIndex = None
  244. return
  245. contourStart = self.endPts[-1] + 1 if self.endPts else 0
  246. self.endPts.append(len(self.points) - 1)
  247. self._currentContourStartIndex = None
  248. # Resolve types for any cubic segments
  249. flags = self.types
  250. for i in range(contourStart, len(flags)):
  251. if flags[i] == "curve":
  252. j = i - 1
  253. if j < contourStart:
  254. j = len(flags) - 1
  255. while flags[j] == 0:
  256. flags[j] = flagCubic
  257. j -= 1
  258. flags[i] = flagOnCurve
  259. def addPoint(
  260. self,
  261. pt: Tuple[float, float],
  262. segmentType: Optional[str] = None,
  263. smooth: bool = False,
  264. name: Optional[str] = None,
  265. identifier: Optional[str] = None,
  266. **kwargs: Any,
  267. ) -> None:
  268. """
  269. Add a point to the current sub path.
  270. """
  271. if self._isClosed():
  272. raise PenError("Can't add a point to a closed contour.")
  273. if segmentType is None:
  274. self.types.append(0)
  275. elif segmentType in ("line", "move"):
  276. self.types.append(flagOnCurve)
  277. elif segmentType == "qcurve":
  278. self.types.append(flagOnCurve)
  279. elif segmentType == "curve":
  280. self.types.append("curve")
  281. else:
  282. raise AssertionError(segmentType)
  283. self.points.append(pt)