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 16from fontTools.qu2cu import quadratic_to_curves 17from fontTools.pens.filterPen import ContourFilterPen 18from fontTools.pens.reverseContourPen import ReverseContourPen 19import math 20 21 22class Qu2CuPen(ContourFilterPen): 23 """A filter pen to convert quadratic bezier splines to cubic curves 24 using the FontTools SegmentPen protocol. 25 26 Args: 27 28 other_pen: another SegmentPen used to draw the transformed outline. 29 max_err: maximum approximation error in font units. For optimal results, 30 if you know the UPEM of the font, we recommend setting this to a 31 value equal, or close to UPEM / 1000. 32 reverse_direction: flip the contours' direction but keep starting point. 33 stats: a dictionary counting the point numbers of cubic segments. 34 """ 35 36 def __init__( 37 self, 38 other_pen, 39 max_err, 40 all_cubic=False, 41 reverse_direction=False, 42 stats=None, 43 ): 44 if reverse_direction: 45 other_pen = ReverseContourPen(other_pen) 46 super().__init__(other_pen) 47 self.all_cubic = all_cubic 48 self.max_err = max_err 49 self.stats = stats 50 51 def _quadratics_to_curve(self, q): 52 curves = quadratic_to_curves(q, self.max_err, all_cubic=self.all_cubic) 53 if self.stats is not None: 54 for curve in curves: 55 n = str(len(curve) - 2) 56 self.stats[n] = self.stats.get(n, 0) + 1 57 for curve in curves: 58 if len(curve) == 4: 59 yield ("curveTo", curve[1:]) 60 else: 61 yield ("qCurveTo", curve[1:]) 62 63 def filterContour(self, contour): 64 quadratics = [] 65 currentPt = None 66 newContour = [] 67 for op, args in contour: 68 if op == "qCurveTo" and ( 69 self.all_cubic or (len(args) > 2 and args[-1] is not None) 70 ): 71 if args[-1] is None: 72 raise NotImplementedError( 73 "oncurve-less contours with all_cubic not implemented" 74 ) 75 quadratics.append((currentPt,) + args) 76 else: 77 if quadratics: 78 newContour.extend(self._quadratics_to_curve(quadratics)) 79 quadratics = [] 80 newContour.append((op, args)) 81 currentPt = args[-1] if args else None 82 if quadratics: 83 newContour.extend(self._quadratics_to_curve(quadratics)) 84 85 if not self.all_cubic: 86 # Add back implicit oncurve points 87 contour = newContour 88 newContour = [] 89 for op, args in contour: 90 if op == "qCurveTo" and newContour and newContour[-1][0] == "qCurveTo": 91 pt0 = newContour[-1][1][-2] 92 pt1 = newContour[-1][1][-1] 93 pt2 = args[0] 94 if ( 95 pt1 is not None 96 and math.isclose(pt2[0] - pt1[0], pt1[0] - pt0[0]) 97 and math.isclose(pt2[1] - pt1[1], pt1[1] - pt0[1]) 98 ): 99 newArgs = newContour[-1][1][:-1] + args 100 newContour[-1] = (op, newArgs) 101 continue 102 103 newContour.append((op, args)) 104 105 return newContour 106