aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/pens/qu2cuPen.py
blob: 7e400f98c45cb7fdbbba00df009b7819adffec4c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# Copyright 2016 Google Inc. All Rights Reserved.
# Copyright 2023 Behdad Esfahbod. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from fontTools.qu2cu import quadratic_to_curves
from fontTools.pens.filterPen import ContourFilterPen
from fontTools.pens.reverseContourPen import ReverseContourPen
import math


class Qu2CuPen(ContourFilterPen):
    """A filter pen to convert quadratic bezier splines to cubic curves
    using the FontTools SegmentPen protocol.

    Args:

        other_pen: another SegmentPen used to draw the transformed outline.
        max_err: maximum approximation error in font units. For optimal results,
            if you know the UPEM of the font, we recommend setting this to a
            value equal, or close to UPEM / 1000.
        reverse_direction: flip the contours' direction but keep starting point.
        stats: a dictionary counting the point numbers of cubic segments.
    """

    def __init__(
        self,
        other_pen,
        max_err,
        all_cubic=False,
        reverse_direction=False,
        stats=None,
    ):
        if reverse_direction:
            other_pen = ReverseContourPen(other_pen)
        super().__init__(other_pen)
        self.all_cubic = all_cubic
        self.max_err = max_err
        self.stats = stats

    def _quadratics_to_curve(self, q):
        curves = quadratic_to_curves(q, self.max_err, all_cubic=self.all_cubic)
        if self.stats is not None:
            for curve in curves:
                n = str(len(curve) - 2)
                self.stats[n] = self.stats.get(n, 0) + 1
        for curve in curves:
            if len(curve) == 4:
                yield ("curveTo", curve[1:])
            else:
                yield ("qCurveTo", curve[1:])

    def filterContour(self, contour):
        quadratics = []
        currentPt = None
        newContour = []
        for op, args in contour:
            if op == "qCurveTo" and (
                self.all_cubic or (len(args) > 2 and args[-1] is not None)
            ):
                if args[-1] is None:
                    raise NotImplementedError(
                        "oncurve-less contours with all_cubic not implemented"
                    )
                quadratics.append((currentPt,) + args)
            else:
                if quadratics:
                    newContour.extend(self._quadratics_to_curve(quadratics))
                    quadratics = []
                newContour.append((op, args))
            currentPt = args[-1] if args else None
        if quadratics:
            newContour.extend(self._quadratics_to_curve(quadratics))

        if not self.all_cubic:
            # Add back implicit oncurve points
            contour = newContour
            newContour = []
            for op, args in contour:
                if op == "qCurveTo" and newContour and newContour[-1][0] == "qCurveTo":
                    pt0 = newContour[-1][1][-2]
                    pt1 = newContour[-1][1][-1]
                    pt2 = args[0]
                    if (
                        pt1 is not None
                        and math.isclose(pt2[0] - pt1[0], pt1[0] - pt0[0])
                        and math.isclose(pt2[1] - pt1[1], pt1[1] - pt0[1])
                    ):
                        newArgs = newContour[-1][1][:-1] + args
                        newContour[-1] = (op, newArgs)
                        continue

                newContour.append((op, args))

        return newContour