basePen.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  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, Identity
  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. all instances of a sub-class to raise a :class:`MissingComponentError`
  150. exception by default.
  151. """
  152. skipMissingComponents = True
  153. # alias error for convenience
  154. MissingComponentError = MissingComponentError
  155. def __init__(
  156. self,
  157. glyphSet,
  158. *args,
  159. skipMissingComponents=None,
  160. reverseFlipped=False,
  161. **kwargs,
  162. ):
  163. """Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
  164. as components are looked up by their name.
  165. If the optional 'reverseFlipped' argument is True, components whose transformation
  166. matrix has a negative determinant will be decomposed with a reversed path direction
  167. to compensate for the flip.
  168. The optional 'skipMissingComponents' argument can be set to True/False to
  169. override the homonymous class attribute for a given pen instance.
  170. """
  171. super(DecomposingPen, self).__init__(*args, **kwargs)
  172. self.glyphSet = glyphSet
  173. self.skipMissingComponents = (
  174. self.__class__.skipMissingComponents
  175. if skipMissingComponents is None
  176. else skipMissingComponents
  177. )
  178. self.reverseFlipped = reverseFlipped
  179. def addComponent(self, glyphName, transformation):
  180. """Transform the points of the base glyph and draw it onto self."""
  181. from fontTools.pens.transformPen import TransformPen
  182. try:
  183. glyph = self.glyphSet[glyphName]
  184. except KeyError:
  185. if not self.skipMissingComponents:
  186. raise MissingComponentError(glyphName)
  187. self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName)
  188. else:
  189. pen = self
  190. if transformation != Identity:
  191. pen = TransformPen(pen, transformation)
  192. if self.reverseFlipped:
  193. # if the transformation has a negative determinant, it will
  194. # reverse the contour direction of the component
  195. a, b, c, d = transformation[:4]
  196. det = a * d - b * c
  197. if det < 0:
  198. from fontTools.pens.reverseContourPen import ReverseContourPen
  199. pen = ReverseContourPen(pen)
  200. glyph.draw(pen)
  201. def addVarComponent(self, glyphName, transformation, location):
  202. # GlyphSet decomposes for us
  203. raise AttributeError
  204. class BasePen(DecomposingPen):
  205. """Base class for drawing pens. You must override _moveTo, _lineTo and
  206. _curveToOne. You may additionally override _closePath, _endPath,
  207. addComponent, addVarComponent, and/or _qCurveToOne. You should not
  208. override any other methods.
  209. """
  210. def __init__(self, glyphSet=None):
  211. super(BasePen, self).__init__(glyphSet)
  212. self.__currentPoint = None
  213. # must override
  214. def _moveTo(self, pt):
  215. raise NotImplementedError
  216. def _lineTo(self, pt):
  217. raise NotImplementedError
  218. def _curveToOne(self, pt1, pt2, pt3):
  219. raise NotImplementedError
  220. # may override
  221. def _closePath(self):
  222. pass
  223. def _endPath(self):
  224. pass
  225. def _qCurveToOne(self, pt1, pt2):
  226. """This method implements the basic quadratic curve type. The
  227. default implementation delegates the work to the cubic curve
  228. function. Optionally override with a native implementation.
  229. """
  230. pt0x, pt0y = self.__currentPoint
  231. pt1x, pt1y = pt1
  232. pt2x, pt2y = pt2
  233. mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
  234. mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
  235. mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
  236. mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
  237. self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
  238. # don't override
  239. def _getCurrentPoint(self):
  240. """Return the current point. This is not part of the public
  241. interface, yet is useful for subclasses.
  242. """
  243. return self.__currentPoint
  244. def closePath(self):
  245. self._closePath()
  246. self.__currentPoint = None
  247. def endPath(self):
  248. self._endPath()
  249. self.__currentPoint = None
  250. def moveTo(self, pt):
  251. self._moveTo(pt)
  252. self.__currentPoint = pt
  253. def lineTo(self, pt):
  254. self._lineTo(pt)
  255. self.__currentPoint = pt
  256. def curveTo(self, *points):
  257. n = len(points) - 1 # 'n' is the number of control points
  258. assert n >= 0
  259. if n == 2:
  260. # The common case, we have exactly two BCP's, so this is a standard
  261. # cubic bezier. Even though decomposeSuperBezierSegment() handles
  262. # this case just fine, we special-case it anyway since it's so
  263. # common.
  264. self._curveToOne(*points)
  265. self.__currentPoint = points[-1]
  266. elif n > 2:
  267. # n is the number of control points; split curve into n-1 cubic
  268. # bezier segments. The algorithm used here is inspired by NURB
  269. # splines and the TrueType "implied point" principle, and ensures
  270. # the smoothest possible connection between two curve segments,
  271. # with no disruption in the curvature. It is practical since it
  272. # allows one to construct multiple bezier segments with a much
  273. # smaller amount of points.
  274. _curveToOne = self._curveToOne
  275. for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
  276. _curveToOne(pt1, pt2, pt3)
  277. self.__currentPoint = pt3
  278. elif n == 1:
  279. self.qCurveTo(*points)
  280. elif n == 0:
  281. self.lineTo(points[0])
  282. else:
  283. raise AssertionError("can't get there from here")
  284. def qCurveTo(self, *points):
  285. n = len(points) - 1 # 'n' is the number of control points
  286. assert n >= 0
  287. if points[-1] is None:
  288. # Special case for TrueType quadratics: it is possible to
  289. # define a contour with NO on-curve points. BasePen supports
  290. # this by allowing the final argument (the expected on-curve
  291. # point) to be None. We simulate the feature by making the implied
  292. # on-curve point between the last and the first off-curve points
  293. # explicit.
  294. x, y = points[-2] # last off-curve point
  295. nx, ny = points[0] # first off-curve point
  296. impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
  297. self.__currentPoint = impliedStartPoint
  298. self._moveTo(impliedStartPoint)
  299. points = points[:-1] + (impliedStartPoint,)
  300. if n > 0:
  301. # Split the string of points into discrete quadratic curve
  302. # segments. Between any two consecutive off-curve points
  303. # there's an implied on-curve point exactly in the middle.
  304. # This is where the segment splits.
  305. _qCurveToOne = self._qCurveToOne
  306. for pt1, pt2 in decomposeQuadraticSegment(points):
  307. _qCurveToOne(pt1, pt2)
  308. self.__currentPoint = pt2
  309. else:
  310. self.lineTo(points[0])
  311. def decomposeSuperBezierSegment(points):
  312. """Split the SuperBezier described by 'points' into a list of regular
  313. bezier segments. The 'points' argument must be a sequence with length
  314. 3 or greater, containing (x, y) coordinates. The last point is the
  315. destination on-curve point, the rest of the points are off-curve points.
  316. The start point should not be supplied.
  317. This function returns a list of (pt1, pt2, pt3) tuples, which each
  318. specify a regular curveto-style bezier segment.
  319. """
  320. n = len(points) - 1
  321. assert n > 1
  322. bezierSegments = []
  323. pt1, pt2, pt3 = points[0], None, None
  324. for i in range(2, n + 1):
  325. # calculate points in between control points.
  326. nDivisions = min(i, 3, n - i + 2)
  327. for j in range(1, nDivisions):
  328. factor = j / nDivisions
  329. temp1 = points[i - 1]
  330. temp2 = points[i - 2]
  331. temp = (
  332. temp2[0] + factor * (temp1[0] - temp2[0]),
  333. temp2[1] + factor * (temp1[1] - temp2[1]),
  334. )
  335. if pt2 is None:
  336. pt2 = temp
  337. else:
  338. pt3 = (0.5 * (pt2[0] + temp[0]), 0.5 * (pt2[1] + temp[1]))
  339. bezierSegments.append((pt1, pt2, pt3))
  340. pt1, pt2, pt3 = temp, None, None
  341. bezierSegments.append((pt1, points[-2], points[-1]))
  342. return bezierSegments
  343. def decomposeQuadraticSegment(points):
  344. """Split the quadratic curve segment described by 'points' into a list
  345. of "atomic" quadratic segments. The 'points' argument must be a sequence
  346. with length 2 or greater, containing (x, y) coordinates. The last point
  347. is the destination on-curve point, the rest of the points are off-curve
  348. points. The start point should not be supplied.
  349. This function returns a list of (pt1, pt2) tuples, which each specify a
  350. plain quadratic bezier segment.
  351. """
  352. n = len(points) - 1
  353. assert n > 0
  354. quadSegments = []
  355. for i in range(n - 1):
  356. x, y = points[i]
  357. nx, ny = points[i + 1]
  358. impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
  359. quadSegments.append((points[i], impliedPt))
  360. quadSegments.append((points[-2], points[-1]))
  361. return quadSegments
  362. class _TestPen(BasePen):
  363. """Test class that prints PostScript to stdout."""
  364. def _moveTo(self, pt):
  365. print("%s %s moveto" % (pt[0], pt[1]))
  366. def _lineTo(self, pt):
  367. print("%s %s lineto" % (pt[0], pt[1]))
  368. def _curveToOne(self, bcp1, bcp2, pt):
  369. print(
  370. "%s %s %s %s %s %s curveto"
  371. % (bcp1[0], bcp1[1], bcp2[0], bcp2[1], pt[0], pt[1])
  372. )
  373. def _closePath(self):
  374. print("closepath")
  375. if __name__ == "__main__":
  376. pen = _TestPen(None)
  377. pen.moveTo((0, 0))
  378. pen.lineTo((0, 100))
  379. pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
  380. pen.closePath()
  381. pen = _TestPen(None)
  382. # testing the "no on-curve point" scenario
  383. pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
  384. pen.closePath()