qu2cuPen.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. # Copyright 2016 Google Inc. All Rights Reserved.
  2. # Copyright 2023 Behdad Esfahbod. All Rights Reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from fontTools.qu2cu import quadratic_to_curves
  16. from fontTools.pens.filterPen import ContourFilterPen
  17. from fontTools.pens.reverseContourPen import ReverseContourPen
  18. import math
  19. class Qu2CuPen(ContourFilterPen):
  20. """A filter pen to convert quadratic bezier splines to cubic curves
  21. using the FontTools SegmentPen protocol.
  22. Args:
  23. other_pen: another SegmentPen used to draw the transformed outline.
  24. max_err: maximum approximation error in font units. For optimal results,
  25. if you know the UPEM of the font, we recommend setting this to a
  26. value equal, or close to UPEM / 1000.
  27. reverse_direction: flip the contours' direction but keep starting point.
  28. stats: a dictionary counting the point numbers of cubic segments.
  29. """
  30. def __init__(
  31. self,
  32. other_pen,
  33. max_err,
  34. all_cubic=False,
  35. reverse_direction=False,
  36. stats=None,
  37. ):
  38. if reverse_direction:
  39. other_pen = ReverseContourPen(other_pen)
  40. super().__init__(other_pen)
  41. self.all_cubic = all_cubic
  42. self.max_err = max_err
  43. self.stats = stats
  44. def _quadratics_to_curve(self, q):
  45. curves = quadratic_to_curves(q, self.max_err, all_cubic=self.all_cubic)
  46. if self.stats is not None:
  47. for curve in curves:
  48. n = str(len(curve) - 2)
  49. self.stats[n] = self.stats.get(n, 0) + 1
  50. for curve in curves:
  51. if len(curve) == 4:
  52. yield ("curveTo", curve[1:])
  53. else:
  54. yield ("qCurveTo", curve[1:])
  55. def filterContour(self, contour):
  56. quadratics = []
  57. currentPt = None
  58. newContour = []
  59. for op, args in contour:
  60. if op == "qCurveTo" and (
  61. self.all_cubic or (len(args) > 2 and args[-1] is not None)
  62. ):
  63. if args[-1] is None:
  64. raise NotImplementedError(
  65. "oncurve-less contours with all_cubic not implemented"
  66. )
  67. quadratics.append((currentPt,) + args)
  68. else:
  69. if quadratics:
  70. newContour.extend(self._quadratics_to_curve(quadratics))
  71. quadratics = []
  72. newContour.append((op, args))
  73. currentPt = args[-1] if args else None
  74. if quadratics:
  75. newContour.extend(self._quadratics_to_curve(quadratics))
  76. if not self.all_cubic:
  77. # Add back implicit oncurve points
  78. contour = newContour
  79. newContour = []
  80. for op, args in contour:
  81. if op == "qCurveTo" and newContour and newContour[-1][0] == "qCurveTo":
  82. pt0 = newContour[-1][1][-2]
  83. pt1 = newContour[-1][1][-1]
  84. pt2 = args[0]
  85. if (
  86. pt1 is not None
  87. and math.isclose(pt2[0] - pt1[0], pt1[0] - pt0[0])
  88. and math.isclose(pt2[1] - pt1[1], pt1[1] - pt0[1])
  89. ):
  90. newArgs = newContour[-1][1][:-1] + args
  91. newContour[-1] = (op, newArgs)
  92. continue
  93. newContour.append((op, args))
  94. return newContour