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