xref: /aosp_15_r20/external/fonttools/Lib/fontTools/pens/qu2cuPen.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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