basePen.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. """fontTools.pens.basePen.py -- Tools and base classes to build pen objects.
  2. The Pen Protocol
  3. A Pen is a kind of object that standardizes the way how to "draw" outlines:
  4. it is a middle man between an outline and a drawing. In other words:
  5. it is an abstraction for drawing outlines, making sure that outline objects
  6. don't need to know the details about how and where they're being drawn, and
  7. that drawings don't need to know the details of how outlines are stored.
  8. The most basic pattern is this::
  9. outline.draw(pen) # 'outline' draws itself onto 'pen'
  10. Pens can be used to render outlines to the screen, but also to construct
  11. new outlines. Eg. an outline object can be both a drawable object (it has a
  12. draw() method) as well as a pen itself: you *build* an outline using pen
  13. methods.
  14. The AbstractPen class defines the Pen protocol. It implements almost
  15. nothing (only no-op closePath() and endPath() methods), but is useful
  16. for documentation purposes. Subclassing it basically tells the reader:
  17. "this class implements the Pen protocol.". An examples of an AbstractPen
  18. subclass is :py:class:`fontTools.pens.transformPen.TransformPen`.
  19. The BasePen class is a base implementation useful for pens that actually
  20. draw (for example a pen renders outlines using a native graphics engine).
  21. BasePen contains a lot of base functionality, making it very easy to build
  22. a pen that fully conforms to the pen protocol. Note that if you subclass
  23. BasePen, you *don't* override moveTo(), lineTo(), etc., but _moveTo(),
  24. _lineTo(), etc. See the BasePen doc string for details. Examples of
  25. BasePen subclasses are fontTools.pens.boundsPen.BoundsPen and
  26. fontTools.pens.cocoaPen.CocoaPen.
  27. Coordinates are usually expressed as (x, y) tuples, but generally any
  28. sequence of length 2 will do.
  29. """
  30. from typing import Tuple, Dict
  31. from fontTools.misc.loggingTools import LogMixin
  32. from fontTools.misc.transform import DecomposedTransform
  33. __all__ = [
  34. "AbstractPen",
  35. "NullPen",
  36. "BasePen",
  37. "PenError",
  38. "decomposeSuperBezierSegment",
  39. "decomposeQuadraticSegment",
  40. ]
  41. class PenError(Exception):
  42. """Represents an error during penning."""
  43. class OpenContourError(PenError):
  44. pass
  45. class AbstractPen:
  46. def moveTo(self, pt: Tuple[float, float]) -> None:
  47. """Begin a new sub path, set the current point to 'pt'. You must
  48. end each sub path with a call to pen.closePath() or pen.endPath().
  49. """
  50. raise NotImplementedError
  51. def lineTo(self, pt: Tuple[float, float]) -> None:
  52. """Draw a straight line from the current point to 'pt'."""
  53. raise NotImplementedError
  54. def curveTo(self, *points: Tuple[float, float]) -> None:
  55. """Draw a cubic bezier with an arbitrary number of control points.
  56. The last point specified is on-curve, all others are off-curve
  57. (control) points. If the number of control points is > 2, the
  58. segment is split into multiple bezier segments. This works
  59. like this:
  60. Let n be the number of control points (which is the number of
  61. arguments to this call minus 1). If n==2, a plain vanilla cubic
  62. bezier is drawn. If n==1, we fall back to a quadratic segment and
  63. if n==0 we draw a straight line. It gets interesting when n>2:
  64. n-1 PostScript-style cubic segments will be drawn as if it were
  65. one curve. See decomposeSuperBezierSegment().
  66. The conversion algorithm used for n>2 is inspired by NURB
  67. splines, and is conceptually equivalent to the TrueType "implied
  68. points" principle. See also decomposeQuadraticSegment().
  69. """
  70. raise NotImplementedError
  71. def qCurveTo(self, *points: Tuple[float, float]) -> None:
  72. """Draw a whole string of quadratic curve segments.
  73. The last point specified is on-curve, all others are off-curve
  74. points.
  75. This method implements TrueType-style curves, breaking up curves
  76. using 'implied points': between each two consequtive off-curve points,
  77. there is one implied point exactly in the middle between them. See
  78. also decomposeQuadraticSegment().
  79. The last argument (normally the on-curve point) may be None.
  80. This is to support contours that have NO on-curve points (a rarely
  81. seen feature of TrueType outlines).
  82. """
  83. raise NotImplementedError
  84. def closePath(self) -> None:
  85. """Close the current sub path. You must call either pen.closePath()
  86. or pen.endPath() after each sub path.
  87. """
  88. pass
  89. def endPath(self) -> None:
  90. """End the current sub path, but don't close it. You must call
  91. either pen.closePath() or pen.endPath() after each sub path.
  92. """
  93. pass
  94. def addComponent(
  95. self,
  96. glyphName: str,
  97. transformation: Tuple[float, float, float, float, float, float],
  98. ) -> None:
  99. """Add a sub glyph. The 'transformation' argument must be a 6-tuple
  100. containing an affine transformation, or a Transform object from the
  101. fontTools.misc.transform module. More precisely: it should be a
  102. sequence containing 6 numbers.
  103. """
  104. raise NotImplementedError
  105. def addVarComponent(
  106. self,
  107. glyphName: str,
  108. transformation: DecomposedTransform,
  109. location: Dict[str, float],
  110. ) -> None:
  111. """Add a VarComponent sub glyph. The 'transformation' argument
  112. must be a DecomposedTransform from the fontTools.misc.transform module,
  113. and the 'location' argument must be a dictionary mapping axis tags
  114. to their locations.
  115. """
  116. # GlyphSet decomposes for us
  117. raise AttributeError
  118. class NullPen(AbstractPen):
  119. """A pen that does nothing."""
  120. def moveTo(self, pt):
  121. pass
  122. def lineTo(self, pt):
  123. pass
  124. def curveTo(self, *points):
  125. pass
  126. def qCurveTo(self, *points):
  127. pass
  128. def closePath(self):
  129. pass
  130. def endPath(self):
  131. pass
  132. def addComponent(self, glyphName, transformation):
  133. pass
  134. def addVarComponent(self, glyphName, transformation, location):
  135. pass
  136. class LoggingPen(LogMixin, AbstractPen):
  137. """A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)"""
  138. pass
  139. class MissingComponentError(KeyError):
  140. """Indicates a component pointing to a non-existent glyph in the glyphset."""
  141. class DecomposingPen(LoggingPen):
  142. """Implements a 'addComponent' method that decomposes components
  143. (i.e. draws them onto self as simple contours).
  144. It can also be used as a mixin class (e.g. see ContourRecordingPen).
  145. You must override moveTo, lineTo, curveTo and qCurveTo. You may
  146. additionally override closePath, endPath and addComponent.
  147. By default a warning message is logged when a base glyph is missing;
  148. set the class variable ``skipMissingComponents`` to False if you want
  149. to raise a :class:`MissingComponentError` exception.
  150. """
  151. skipMissingComponents = True
  152. def __init__(self, glyphSet):
  153. """Takes a single 'glyphSet' argument (dict), in which the glyphs
  154. that are referenced as components are looked up by their name.
  155. """
  156. super(DecomposingPen, self).__init__()
  157. self.glyphSet = glyphSet
  158. def addComponent(self, glyphName, transformation):
  159. """Transform the points of the base glyph and draw it onto self."""
  160. from fontTools.pens.transformPen import TransformPen
  161. try:
  162. glyph = self.glyphSet[glyphName]
  163. except KeyError:
  164. if not self.skipMissingComponents:
  165. raise MissingComponentError(glyphName)
  166. self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName)
  167. else:
  168. tPen = TransformPen(self, transformation)
  169. glyph.draw(tPen)
  170. def addVarComponent(self, glyphName, transformation, location):
  171. # GlyphSet decomposes for us
  172. raise AttributeError
  173. class BasePen(DecomposingPen):
  174. """Base class for drawing pens. You must override _moveTo, _lineTo and
  175. _curveToOne. You may additionally override _closePath, _endPath,
  176. addComponent, addVarComponent, and/or _qCurveToOne. You should not
  177. override any other methods.
  178. """
  179. def __init__(self, glyphSet=None):
  180. super(BasePen, self).__init__(glyphSet)
  181. self.__currentPoint = None
  182. # must override
  183. def _moveTo(self, pt):
  184. raise NotImplementedError
  185. def _lineTo(self, pt):
  186. raise NotImplementedError
  187. def _curveToOne(self, pt1, pt2, pt3):
  188. raise NotImplementedError
  189. # may override
  190. def _closePath(self):
  191. pass
  192. def _endPath(self):
  193. pass
  194. def _qCurveToOne(self, pt1, pt2):
  195. """This method implements the basic quadratic curve type. The
  196. default implementation delegates the work to the cubic curve
  197. function. Optionally override with a native implementation.
  198. """
  199. pt0x, pt0y = self.__currentPoint
  200. pt1x, pt1y = pt1
  201. pt2x, pt2y = pt2
  202. mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
  203. mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
  204. mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
  205. mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
  206. self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
  207. # don't override
  208. def _getCurrentPoint(self):
  209. """Return the current point. This is not part of the public
  210. interface, yet is useful for subclasses.
  211. """
  212. return self.__currentPoint
  213. def closePath(self):
  214. self._closePath()
  215. self.__currentPoint = None
  216. def endPath(self):
  217. self._endPath()
  218. self.__currentPoint = None
  219. def moveTo(self, pt):
  220. self._moveTo(pt)
  221. self.__currentPoint = pt
  222. def lineTo(self, pt):
  223. self._lineTo(pt)
  224. self.__currentPoint = pt
  225. def curveTo(self, *points):
  226. n = len(points) - 1 # 'n' is the number of control points
  227. assert n >= 0
  228. if n == 2:
  229. # The common case, we have exactly two BCP's, so this is a standard
  230. # cubic bezier. Even though decomposeSuperBezierSegment() handles
  231. # this case just fine, we special-case it anyway since it's so
  232. # common.
  233. self._curveToOne(*points)
  234. self.__currentPoint = points[-1]
  235. elif n > 2:
  236. # n is the number of control points; split curve into n-1 cubic
  237. # bezier segments. The algorithm used here is inspired by NURB
  238. # splines and the TrueType "implied point" principle, and ensures
  239. # the smoothest possible connection between two curve segments,
  240. # with no disruption in the curvature. It is practical since it
  241. # allows one to construct multiple bezier segments with a much
  242. # smaller amount of points.
  243. _curveToOne = self._curveToOne
  244. for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
  245. _curveToOne(pt1, pt2, pt3)
  246. self.__currentPoint = pt3
  247. elif n == 1:
  248. self.qCurveTo(*points)
  249. elif n == 0:
  250. self.lineTo(points[0])
  251. else:
  252. raise AssertionError("can't get there from here")
  253. def qCurveTo(self, *points):
  254. n = len(points) - 1 # 'n' is the number of control points
  255. assert n >= 0
  256. if points[-1] is None:
  257. # Special case for TrueType quadratics: it is possible to
  258. # define a contour with NO on-curve points. BasePen supports
  259. # this by allowing the final argument (the expected on-curve
  260. # point) to be None. We simulate the feature by making the implied
  261. # on-curve point between the last and the first off-curve points
  262. # explicit.
  263. x, y = points[-2] # last off-curve point
  264. nx, ny = points[0] # first off-curve point
  265. impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
  266. self.__currentPoint = impliedStartPoint
  267. self._moveTo(impliedStartPoint)
  268. points = points[:-1] + (impliedStartPoint,)
  269. if n > 0:
  270. # Split the string of points into discrete quadratic curve
  271. # segments. Between any two consecutive off-curve points
  272. # there's an implied on-curve point exactly in the middle.
  273. # This is where the segment splits.
  274. _qCurveToOne = self._qCurveToOne
  275. for pt1, pt2 in decomposeQuadraticSegment(points):
  276. _qCurveToOne(pt1, pt2)
  277. self.__currentPoint = pt2
  278. else:
  279. self.lineTo(points[0])
  280. def decomposeSuperBezierSegment(points):
  281. """Split the SuperBezier described by 'points' into a list of regular
  282. bezier segments. The 'points' argument must be a sequence with length
  283. 3 or greater, containing (x, y) coordinates. The last point is the
  284. destination on-curve point, the rest of the points are off-curve points.
  285. The start point should not be supplied.
  286. This function returns a list of (pt1, pt2, pt3) tuples, which each
  287. specify a regular curveto-style bezier segment.
  288. """
  289. n = len(points) - 1
  290. assert n > 1
  291. bezierSegments = []
  292. pt1, pt2, pt3 = points[0], None, None
  293. for i in range(2, n + 1):
  294. # calculate points in between control points.
  295. nDivisions = min(i, 3, n - i + 2)
  296. for j in range(1, nDivisions):
  297. factor = j / nDivisions
  298. temp1 = points[i - 1]
  299. temp2 = points[i - 2]
  300. temp = (
  301. temp2[0] + factor * (temp1[0] - temp2[0]),
  302. temp2[1] + factor * (temp1[1] - temp2[1]),
  303. )
  304. if pt2 is None:
  305. pt2 = temp
  306. else:
  307. pt3 = (0.5 * (pt2[0] + temp[0]), 0.5 * (pt2[1] + temp[1]))
  308. bezierSegments.append((pt1, pt2, pt3))
  309. pt1, pt2, pt3 = temp, None, None
  310. bezierSegments.append((pt1, points[-2], points[-1]))
  311. return bezierSegments
  312. def decomposeQuadraticSegment(points):
  313. """Split the quadratic curve segment described by 'points' into a list
  314. of "atomic" quadratic segments. The 'points' argument must be a sequence
  315. with length 2 or greater, containing (x, y) coordinates. The last point
  316. is the destination on-curve point, the rest of the points are off-curve
  317. points. The start point should not be supplied.
  318. This function returns a list of (pt1, pt2) tuples, which each specify a
  319. plain quadratic bezier segment.
  320. """
  321. n = len(points) - 1
  322. assert n > 0
  323. quadSegments = []
  324. for i in range(n - 1):
  325. x, y = points[i]
  326. nx, ny = points[i + 1]
  327. impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
  328. quadSegments.append((points[i], impliedPt))
  329. quadSegments.append((points[-2], points[-1]))
  330. return quadSegments
  331. class _TestPen(BasePen):
  332. """Test class that prints PostScript to stdout."""
  333. def _moveTo(self, pt):
  334. print("%s %s moveto" % (pt[0], pt[1]))
  335. def _lineTo(self, pt):
  336. print("%s %s lineto" % (pt[0], pt[1]))
  337. def _curveToOne(self, bcp1, bcp2, pt):
  338. print(
  339. "%s %s %s %s %s %s curveto"
  340. % (bcp1[0], bcp1[1], bcp2[0], bcp2[1], pt[0], pt[1])
  341. )
  342. def _closePath(self):
  343. print("closepath")
  344. if __name__ == "__main__":
  345. pen = _TestPen(None)
  346. pen.moveTo((0, 0))
  347. pen.lineTo((0, 100))
  348. pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
  349. pen.closePath()
  350. pen = _TestPen(None)
  351. # testing the "no on-curve point" scenario
  352. pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
  353. pen.closePath()