cu2quPen.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. # Copyright 2016 Google Inc. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import operator
  15. from fontTools.cu2qu import curve_to_quadratic, curves_to_quadratic
  16. from fontTools.pens.basePen import decomposeSuperBezierSegment
  17. from fontTools.pens.filterPen import FilterPen
  18. from fontTools.pens.reverseContourPen import ReverseContourPen
  19. from fontTools.pens.pointPen import BasePointToSegmentPen
  20. from fontTools.pens.pointPen import ReverseContourPointPen
  21. class Cu2QuPen(FilterPen):
  22. """A filter pen to convert cubic bezier curves to quadratic b-splines
  23. using the FontTools SegmentPen protocol.
  24. Args:
  25. other_pen: another SegmentPen used to draw the transformed outline.
  26. max_err: maximum approximation error in font units. For optimal results,
  27. if you know the UPEM of the font, we recommend setting this to a
  28. value equal, or close to UPEM / 1000.
  29. reverse_direction: flip the contours' direction but keep starting point.
  30. stats: a dictionary counting the point numbers of quadratic segments.
  31. all_quadratic: if True (default), only quadratic b-splines are generated.
  32. if False, quadratic curves or cubic curves are generated depending
  33. on which one is more economical.
  34. """
  35. def __init__(
  36. self,
  37. other_pen,
  38. max_err,
  39. reverse_direction=False,
  40. stats=None,
  41. all_quadratic=True,
  42. ):
  43. if reverse_direction:
  44. other_pen = ReverseContourPen(other_pen)
  45. super().__init__(other_pen)
  46. self.max_err = max_err
  47. self.stats = stats
  48. self.all_quadratic = all_quadratic
  49. def _convert_curve(self, pt1, pt2, pt3):
  50. curve = (self.current_pt, pt1, pt2, pt3)
  51. result = curve_to_quadratic(curve, self.max_err, self.all_quadratic)
  52. if self.stats is not None:
  53. n = str(len(result) - 2)
  54. self.stats[n] = self.stats.get(n, 0) + 1
  55. if self.all_quadratic:
  56. self.qCurveTo(*result[1:])
  57. else:
  58. if len(result) == 3:
  59. self.qCurveTo(*result[1:])
  60. else:
  61. assert len(result) == 4
  62. super().curveTo(*result[1:])
  63. def curveTo(self, *points):
  64. n = len(points)
  65. if n == 3:
  66. # this is the most common case, so we special-case it
  67. self._convert_curve(*points)
  68. elif n > 3:
  69. for segment in decomposeSuperBezierSegment(points):
  70. self._convert_curve(*segment)
  71. else:
  72. self.qCurveTo(*points)
  73. class Cu2QuPointPen(BasePointToSegmentPen):
  74. """A filter pen to convert cubic bezier curves to quadratic b-splines
  75. using the FontTools PointPen protocol.
  76. Args:
  77. other_point_pen: another PointPen used to draw the transformed outline.
  78. max_err: maximum approximation error in font units. For optimal results,
  79. if you know the UPEM of the font, we recommend setting this to a
  80. value equal, or close to UPEM / 1000.
  81. reverse_direction: reverse the winding direction of all contours.
  82. stats: a dictionary counting the point numbers of quadratic segments.
  83. all_quadratic: if True (default), only quadratic b-splines are generated.
  84. if False, quadratic curves or cubic curves are generated depending
  85. on which one is more economical.
  86. """
  87. __points_required = {
  88. "move": (1, operator.eq),
  89. "line": (1, operator.eq),
  90. "qcurve": (2, operator.ge),
  91. "curve": (3, operator.eq),
  92. }
  93. def __init__(
  94. self,
  95. other_point_pen,
  96. max_err,
  97. reverse_direction=False,
  98. stats=None,
  99. all_quadratic=True,
  100. ):
  101. BasePointToSegmentPen.__init__(self)
  102. if reverse_direction:
  103. self.pen = ReverseContourPointPen(other_point_pen)
  104. else:
  105. self.pen = other_point_pen
  106. self.max_err = max_err
  107. self.stats = stats
  108. self.all_quadratic = all_quadratic
  109. def _flushContour(self, segments):
  110. assert len(segments) >= 1
  111. closed = segments[0][0] != "move"
  112. new_segments = []
  113. prev_points = segments[-1][1]
  114. prev_on_curve = prev_points[-1][0]
  115. for segment_type, points in segments:
  116. if segment_type == "curve":
  117. for sub_points in self._split_super_bezier_segments(points):
  118. on_curve, smooth, name, kwargs = sub_points[-1]
  119. bcp1, bcp2 = sub_points[0][0], sub_points[1][0]
  120. cubic = [prev_on_curve, bcp1, bcp2, on_curve]
  121. quad = curve_to_quadratic(cubic, self.max_err, self.all_quadratic)
  122. if self.stats is not None:
  123. n = str(len(quad) - 2)
  124. self.stats[n] = self.stats.get(n, 0) + 1
  125. new_points = [(pt, False, None, {}) for pt in quad[1:-1]]
  126. new_points.append((on_curve, smooth, name, kwargs))
  127. if self.all_quadratic or len(new_points) == 2:
  128. new_segments.append(["qcurve", new_points])
  129. else:
  130. new_segments.append(["curve", new_points])
  131. prev_on_curve = sub_points[-1][0]
  132. else:
  133. new_segments.append([segment_type, points])
  134. prev_on_curve = points[-1][0]
  135. if closed:
  136. # the BasePointToSegmentPen.endPath method that calls _flushContour
  137. # rotates the point list of closed contours so that they end with
  138. # the first on-curve point. We restore the original starting point.
  139. new_segments = new_segments[-1:] + new_segments[:-1]
  140. self._drawPoints(new_segments)
  141. def _split_super_bezier_segments(self, points):
  142. sub_segments = []
  143. # n is the number of control points
  144. n = len(points) - 1
  145. if n == 2:
  146. # a simple bezier curve segment
  147. sub_segments.append(points)
  148. elif n > 2:
  149. # a "super" bezier; decompose it
  150. on_curve, smooth, name, kwargs = points[-1]
  151. num_sub_segments = n - 1
  152. for i, sub_points in enumerate(
  153. decomposeSuperBezierSegment([pt for pt, _, _, _ in points])
  154. ):
  155. new_segment = []
  156. for point in sub_points[:-1]:
  157. new_segment.append((point, False, None, {}))
  158. if i == (num_sub_segments - 1):
  159. # the last on-curve keeps its original attributes
  160. new_segment.append((on_curve, smooth, name, kwargs))
  161. else:
  162. # on-curves of sub-segments are always "smooth"
  163. new_segment.append((sub_points[-1], True, None, {}))
  164. sub_segments.append(new_segment)
  165. else:
  166. raise AssertionError("expected 2 control points, found: %d" % n)
  167. return sub_segments
  168. def _drawPoints(self, segments):
  169. pen = self.pen
  170. pen.beginPath()
  171. last_offcurves = []
  172. points_required = self.__points_required
  173. for i, (segment_type, points) in enumerate(segments):
  174. if segment_type in points_required:
  175. n, op = points_required[segment_type]
  176. assert op(len(points), n), (
  177. f"illegal {segment_type!r} segment point count: "
  178. f"expected {n}, got {len(points)}"
  179. )
  180. offcurves = points[:-1]
  181. if i == 0:
  182. # any off-curve points preceding the first on-curve
  183. # will be appended at the end of the contour
  184. last_offcurves = offcurves
  185. else:
  186. for pt, smooth, name, kwargs in offcurves:
  187. pen.addPoint(pt, None, smooth, name, **kwargs)
  188. pt, smooth, name, kwargs = points[-1]
  189. if pt is None:
  190. assert segment_type == "qcurve"
  191. # special quadratic contour with no on-curve points:
  192. # we need to skip the "None" point. See also the Pen
  193. # protocol's qCurveTo() method and fontTools.pens.basePen
  194. pass
  195. else:
  196. pen.addPoint(pt, segment_type, smooth, name, **kwargs)
  197. else:
  198. raise AssertionError("unexpected segment type: %r" % segment_type)
  199. for pt, smooth, name, kwargs in last_offcurves:
  200. pen.addPoint(pt, None, smooth, name, **kwargs)
  201. pen.endPath()
  202. def addComponent(self, baseGlyphName, transformation):
  203. assert self.currentPath is None
  204. self.pen.addComponent(baseGlyphName, transformation)
  205. class Cu2QuMultiPen:
  206. """A filter multi-pen to convert cubic bezier curves to quadratic b-splines
  207. in a interpolation-compatible manner, using the FontTools SegmentPen protocol.
  208. Args:
  209. other_pens: list of SegmentPens used to draw the transformed outlines.
  210. max_err: maximum approximation error in font units. For optimal results,
  211. if you know the UPEM of the font, we recommend setting this to a
  212. value equal, or close to UPEM / 1000.
  213. reverse_direction: flip the contours' direction but keep starting point.
  214. This pen does not follow the normal SegmentPen protocol. Instead, its
  215. moveTo/lineTo/qCurveTo/curveTo methods take a list of tuples that are
  216. arguments that would normally be passed to a SegmentPen, one item for
  217. each of the pens in other_pens.
  218. """
  219. # TODO Simplify like 3e8ebcdce592fe8a59ca4c3a294cc9724351e1ce
  220. # Remove start_pts and _add_moveTO
  221. def __init__(self, other_pens, max_err, reverse_direction=False):
  222. if reverse_direction:
  223. other_pens = [
  224. ReverseContourPen(pen, outputImpliedClosingLine=True)
  225. for pen in other_pens
  226. ]
  227. self.pens = other_pens
  228. self.max_err = max_err
  229. self.start_pts = None
  230. self.current_pts = None
  231. def _check_contour_is_open(self):
  232. if self.current_pts is None:
  233. raise AssertionError("moveTo is required")
  234. def _check_contour_is_closed(self):
  235. if self.current_pts is not None:
  236. raise AssertionError("closePath or endPath is required")
  237. def _add_moveTo(self):
  238. if self.start_pts is not None:
  239. for pt, pen in zip(self.start_pts, self.pens):
  240. pen.moveTo(*pt)
  241. self.start_pts = None
  242. def moveTo(self, pts):
  243. self._check_contour_is_closed()
  244. self.start_pts = self.current_pts = pts
  245. self._add_moveTo()
  246. def lineTo(self, pts):
  247. self._check_contour_is_open()
  248. self._add_moveTo()
  249. for pt, pen in zip(pts, self.pens):
  250. pen.lineTo(*pt)
  251. self.current_pts = pts
  252. def qCurveTo(self, pointsList):
  253. self._check_contour_is_open()
  254. if len(pointsList[0]) == 1:
  255. self.lineTo([(points[0],) for points in pointsList])
  256. return
  257. self._add_moveTo()
  258. current_pts = []
  259. for points, pen in zip(pointsList, self.pens):
  260. pen.qCurveTo(*points)
  261. current_pts.append((points[-1],))
  262. self.current_pts = current_pts
  263. def _curves_to_quadratic(self, pointsList):
  264. curves = []
  265. for current_pt, points in zip(self.current_pts, pointsList):
  266. curves.append(current_pt + points)
  267. quadratics = curves_to_quadratic(curves, [self.max_err] * len(curves))
  268. pointsList = []
  269. for quadratic in quadratics:
  270. pointsList.append(quadratic[1:])
  271. self.qCurveTo(pointsList)
  272. def curveTo(self, pointsList):
  273. self._check_contour_is_open()
  274. self._curves_to_quadratic(pointsList)
  275. def closePath(self):
  276. self._check_contour_is_open()
  277. if self.start_pts is None:
  278. for pen in self.pens:
  279. pen.closePath()
  280. self.current_pts = self.start_pts = None
  281. def endPath(self):
  282. self._check_contour_is_open()
  283. if self.start_pts is None:
  284. for pen in self.pens:
  285. pen.endPath()
  286. self.current_pts = self.start_pts = None
  287. def addComponent(self, glyphName, transformations):
  288. self._check_contour_is_closed()
  289. for trans, pen in zip(transformations, self.pens):
  290. pen.addComponent(glyphName, trans)