xref: /aosp_15_r20/external/fonttools/Lib/fontTools/pens/pointPen.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""
2*e1fe3e4aSElliott Hughes=========
3*e1fe3e4aSElliott HughesPointPens
4*e1fe3e4aSElliott Hughes=========
5*e1fe3e4aSElliott Hughes
6*e1fe3e4aSElliott HughesWhere **SegmentPens** have an intuitive approach to drawing
7*e1fe3e4aSElliott Hughes(if you're familiar with postscript anyway), the **PointPen**
8*e1fe3e4aSElliott Hughesis geared towards accessing all the data in the contours of
9*e1fe3e4aSElliott Hughesthe glyph. A PointPen has a very simple interface, it just
10*e1fe3e4aSElliott Hughessteps through all the points in a call from glyph.drawPoints().
11*e1fe3e4aSElliott HughesThis allows the caller to provide more data for each point.
12*e1fe3e4aSElliott HughesFor instance, whether or not a point is smooth, and its name.
13*e1fe3e4aSElliott Hughes"""
14*e1fe3e4aSElliott Hughes
15*e1fe3e4aSElliott Hughesimport math
16*e1fe3e4aSElliott Hughesfrom typing import Any, Optional, Tuple, Dict
17*e1fe3e4aSElliott Hughes
18*e1fe3e4aSElliott Hughesfrom fontTools.pens.basePen import AbstractPen, PenError
19*e1fe3e4aSElliott Hughesfrom fontTools.misc.transform import DecomposedTransform
20*e1fe3e4aSElliott Hughes
21*e1fe3e4aSElliott Hughes__all__ = [
22*e1fe3e4aSElliott Hughes    "AbstractPointPen",
23*e1fe3e4aSElliott Hughes    "BasePointToSegmentPen",
24*e1fe3e4aSElliott Hughes    "PointToSegmentPen",
25*e1fe3e4aSElliott Hughes    "SegmentToPointPen",
26*e1fe3e4aSElliott Hughes    "GuessSmoothPointPen",
27*e1fe3e4aSElliott Hughes    "ReverseContourPointPen",
28*e1fe3e4aSElliott Hughes]
29*e1fe3e4aSElliott Hughes
30*e1fe3e4aSElliott Hughes
31*e1fe3e4aSElliott Hughesclass AbstractPointPen:
32*e1fe3e4aSElliott Hughes    """Baseclass for all PointPens."""
33*e1fe3e4aSElliott Hughes
34*e1fe3e4aSElliott Hughes    def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
35*e1fe3e4aSElliott Hughes        """Start a new sub path."""
36*e1fe3e4aSElliott Hughes        raise NotImplementedError
37*e1fe3e4aSElliott Hughes
38*e1fe3e4aSElliott Hughes    def endPath(self) -> None:
39*e1fe3e4aSElliott Hughes        """End the current sub path."""
40*e1fe3e4aSElliott Hughes        raise NotImplementedError
41*e1fe3e4aSElliott Hughes
42*e1fe3e4aSElliott Hughes    def addPoint(
43*e1fe3e4aSElliott Hughes        self,
44*e1fe3e4aSElliott Hughes        pt: Tuple[float, float],
45*e1fe3e4aSElliott Hughes        segmentType: Optional[str] = None,
46*e1fe3e4aSElliott Hughes        smooth: bool = False,
47*e1fe3e4aSElliott Hughes        name: Optional[str] = None,
48*e1fe3e4aSElliott Hughes        identifier: Optional[str] = None,
49*e1fe3e4aSElliott Hughes        **kwargs: Any,
50*e1fe3e4aSElliott Hughes    ) -> None:
51*e1fe3e4aSElliott Hughes        """Add a point to the current sub path."""
52*e1fe3e4aSElliott Hughes        raise NotImplementedError
53*e1fe3e4aSElliott Hughes
54*e1fe3e4aSElliott Hughes    def addComponent(
55*e1fe3e4aSElliott Hughes        self,
56*e1fe3e4aSElliott Hughes        baseGlyphName: str,
57*e1fe3e4aSElliott Hughes        transformation: Tuple[float, float, float, float, float, float],
58*e1fe3e4aSElliott Hughes        identifier: Optional[str] = None,
59*e1fe3e4aSElliott Hughes        **kwargs: Any,
60*e1fe3e4aSElliott Hughes    ) -> None:
61*e1fe3e4aSElliott Hughes        """Add a sub glyph."""
62*e1fe3e4aSElliott Hughes        raise NotImplementedError
63*e1fe3e4aSElliott Hughes
64*e1fe3e4aSElliott Hughes    def addVarComponent(
65*e1fe3e4aSElliott Hughes        self,
66*e1fe3e4aSElliott Hughes        glyphName: str,
67*e1fe3e4aSElliott Hughes        transformation: DecomposedTransform,
68*e1fe3e4aSElliott Hughes        location: Dict[str, float],
69*e1fe3e4aSElliott Hughes        identifier: Optional[str] = None,
70*e1fe3e4aSElliott Hughes        **kwargs: Any,
71*e1fe3e4aSElliott Hughes    ) -> None:
72*e1fe3e4aSElliott Hughes        """Add a VarComponent sub glyph. The 'transformation' argument
73*e1fe3e4aSElliott Hughes        must be a DecomposedTransform from the fontTools.misc.transform module,
74*e1fe3e4aSElliott Hughes        and the 'location' argument must be a dictionary mapping axis tags
75*e1fe3e4aSElliott Hughes        to their locations.
76*e1fe3e4aSElliott Hughes        """
77*e1fe3e4aSElliott Hughes        # ttGlyphSet decomposes for us
78*e1fe3e4aSElliott Hughes        raise AttributeError
79*e1fe3e4aSElliott Hughes
80*e1fe3e4aSElliott Hughes
81*e1fe3e4aSElliott Hughesclass BasePointToSegmentPen(AbstractPointPen):
82*e1fe3e4aSElliott Hughes    """
83*e1fe3e4aSElliott Hughes    Base class for retrieving the outline in a segment-oriented
84*e1fe3e4aSElliott Hughes    way. The PointPen protocol is simple yet also a little tricky,
85*e1fe3e4aSElliott Hughes    so when you need an outline presented as segments but you have
86*e1fe3e4aSElliott Hughes    as points, do use this base implementation as it properly takes
87*e1fe3e4aSElliott Hughes    care of all the edge cases.
88*e1fe3e4aSElliott Hughes    """
89*e1fe3e4aSElliott Hughes
90*e1fe3e4aSElliott Hughes    def __init__(self):
91*e1fe3e4aSElliott Hughes        self.currentPath = None
92*e1fe3e4aSElliott Hughes
93*e1fe3e4aSElliott Hughes    def beginPath(self, identifier=None, **kwargs):
94*e1fe3e4aSElliott Hughes        if self.currentPath is not None:
95*e1fe3e4aSElliott Hughes            raise PenError("Path already begun.")
96*e1fe3e4aSElliott Hughes        self.currentPath = []
97*e1fe3e4aSElliott Hughes
98*e1fe3e4aSElliott Hughes    def _flushContour(self, segments):
99*e1fe3e4aSElliott Hughes        """Override this method.
100*e1fe3e4aSElliott Hughes
101*e1fe3e4aSElliott Hughes        It will be called for each non-empty sub path with a list
102*e1fe3e4aSElliott Hughes        of segments: the 'segments' argument.
103*e1fe3e4aSElliott Hughes
104*e1fe3e4aSElliott Hughes        The segments list contains tuples of length 2:
105*e1fe3e4aSElliott Hughes                (segmentType, points)
106*e1fe3e4aSElliott Hughes
107*e1fe3e4aSElliott Hughes        segmentType is one of "move", "line", "curve" or "qcurve".
108*e1fe3e4aSElliott Hughes        "move" may only occur as the first segment, and it signifies
109*e1fe3e4aSElliott Hughes        an OPEN path. A CLOSED path does NOT start with a "move", in
110*e1fe3e4aSElliott Hughes        fact it will not contain a "move" at ALL.
111*e1fe3e4aSElliott Hughes
112*e1fe3e4aSElliott Hughes        The 'points' field in the 2-tuple is a list of point info
113*e1fe3e4aSElliott Hughes        tuples. The list has 1 or more items, a point tuple has
114*e1fe3e4aSElliott Hughes        four items:
115*e1fe3e4aSElliott Hughes                (point, smooth, name, kwargs)
116*e1fe3e4aSElliott Hughes        'point' is an (x, y) coordinate pair.
117*e1fe3e4aSElliott Hughes
118*e1fe3e4aSElliott Hughes        For a closed path, the initial moveTo point is defined as
119*e1fe3e4aSElliott Hughes        the last point of the last segment.
120*e1fe3e4aSElliott Hughes
121*e1fe3e4aSElliott Hughes        The 'points' list of "move" and "line" segments always contains
122*e1fe3e4aSElliott Hughes        exactly one point tuple.
123*e1fe3e4aSElliott Hughes        """
124*e1fe3e4aSElliott Hughes        raise NotImplementedError
125*e1fe3e4aSElliott Hughes
126*e1fe3e4aSElliott Hughes    def endPath(self):
127*e1fe3e4aSElliott Hughes        if self.currentPath is None:
128*e1fe3e4aSElliott Hughes            raise PenError("Path not begun.")
129*e1fe3e4aSElliott Hughes        points = self.currentPath
130*e1fe3e4aSElliott Hughes        self.currentPath = None
131*e1fe3e4aSElliott Hughes        if not points:
132*e1fe3e4aSElliott Hughes            return
133*e1fe3e4aSElliott Hughes        if len(points) == 1:
134*e1fe3e4aSElliott Hughes            # Not much more we can do than output a single move segment.
135*e1fe3e4aSElliott Hughes            pt, segmentType, smooth, name, kwargs = points[0]
136*e1fe3e4aSElliott Hughes            segments = [("move", [(pt, smooth, name, kwargs)])]
137*e1fe3e4aSElliott Hughes            self._flushContour(segments)
138*e1fe3e4aSElliott Hughes            return
139*e1fe3e4aSElliott Hughes        segments = []
140*e1fe3e4aSElliott Hughes        if points[0][1] == "move":
141*e1fe3e4aSElliott Hughes            # It's an open contour, insert a "move" segment for the first
142*e1fe3e4aSElliott Hughes            # point and remove that first point from the point list.
143*e1fe3e4aSElliott Hughes            pt, segmentType, smooth, name, kwargs = points[0]
144*e1fe3e4aSElliott Hughes            segments.append(("move", [(pt, smooth, name, kwargs)]))
145*e1fe3e4aSElliott Hughes            points.pop(0)
146*e1fe3e4aSElliott Hughes        else:
147*e1fe3e4aSElliott Hughes            # It's a closed contour. Locate the first on-curve point, and
148*e1fe3e4aSElliott Hughes            # rotate the point list so that it _ends_ with an on-curve
149*e1fe3e4aSElliott Hughes            # point.
150*e1fe3e4aSElliott Hughes            firstOnCurve = None
151*e1fe3e4aSElliott Hughes            for i in range(len(points)):
152*e1fe3e4aSElliott Hughes                segmentType = points[i][1]
153*e1fe3e4aSElliott Hughes                if segmentType is not None:
154*e1fe3e4aSElliott Hughes                    firstOnCurve = i
155*e1fe3e4aSElliott Hughes                    break
156*e1fe3e4aSElliott Hughes            if firstOnCurve is None:
157*e1fe3e4aSElliott Hughes                # Special case for quadratics: a contour with no on-curve
158*e1fe3e4aSElliott Hughes                # points. Add a "None" point. (See also the Pen protocol's
159*e1fe3e4aSElliott Hughes                # qCurveTo() method and fontTools.pens.basePen.py.)
160*e1fe3e4aSElliott Hughes                points.append((None, "qcurve", None, None, None))
161*e1fe3e4aSElliott Hughes            else:
162*e1fe3e4aSElliott Hughes                points = points[firstOnCurve + 1 :] + points[: firstOnCurve + 1]
163*e1fe3e4aSElliott Hughes
164*e1fe3e4aSElliott Hughes        currentSegment = []
165*e1fe3e4aSElliott Hughes        for pt, segmentType, smooth, name, kwargs in points:
166*e1fe3e4aSElliott Hughes            currentSegment.append((pt, smooth, name, kwargs))
167*e1fe3e4aSElliott Hughes            if segmentType is None:
168*e1fe3e4aSElliott Hughes                continue
169*e1fe3e4aSElliott Hughes            segments.append((segmentType, currentSegment))
170*e1fe3e4aSElliott Hughes            currentSegment = []
171*e1fe3e4aSElliott Hughes
172*e1fe3e4aSElliott Hughes        self._flushContour(segments)
173*e1fe3e4aSElliott Hughes
174*e1fe3e4aSElliott Hughes    def addPoint(
175*e1fe3e4aSElliott Hughes        self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
176*e1fe3e4aSElliott Hughes    ):
177*e1fe3e4aSElliott Hughes        if self.currentPath is None:
178*e1fe3e4aSElliott Hughes            raise PenError("Path not begun")
179*e1fe3e4aSElliott Hughes        self.currentPath.append((pt, segmentType, smooth, name, kwargs))
180*e1fe3e4aSElliott Hughes
181*e1fe3e4aSElliott Hughes
182*e1fe3e4aSElliott Hughesclass PointToSegmentPen(BasePointToSegmentPen):
183*e1fe3e4aSElliott Hughes    """
184*e1fe3e4aSElliott Hughes    Adapter class that converts the PointPen protocol to the
185*e1fe3e4aSElliott Hughes    (Segment)Pen protocol.
186*e1fe3e4aSElliott Hughes
187*e1fe3e4aSElliott Hughes    NOTE: The segment pen does not support and will drop point names, identifiers
188*e1fe3e4aSElliott Hughes    and kwargs.
189*e1fe3e4aSElliott Hughes    """
190*e1fe3e4aSElliott Hughes
191*e1fe3e4aSElliott Hughes    def __init__(self, segmentPen, outputImpliedClosingLine=False):
192*e1fe3e4aSElliott Hughes        BasePointToSegmentPen.__init__(self)
193*e1fe3e4aSElliott Hughes        self.pen = segmentPen
194*e1fe3e4aSElliott Hughes        self.outputImpliedClosingLine = outputImpliedClosingLine
195*e1fe3e4aSElliott Hughes
196*e1fe3e4aSElliott Hughes    def _flushContour(self, segments):
197*e1fe3e4aSElliott Hughes        if not segments:
198*e1fe3e4aSElliott Hughes            raise PenError("Must have at least one segment.")
199*e1fe3e4aSElliott Hughes        pen = self.pen
200*e1fe3e4aSElliott Hughes        if segments[0][0] == "move":
201*e1fe3e4aSElliott Hughes            # It's an open path.
202*e1fe3e4aSElliott Hughes            closed = False
203*e1fe3e4aSElliott Hughes            points = segments[0][1]
204*e1fe3e4aSElliott Hughes            if len(points) != 1:
205*e1fe3e4aSElliott Hughes                raise PenError(f"Illegal move segment point count: {len(points)}")
206*e1fe3e4aSElliott Hughes            movePt, _, _, _ = points[0]
207*e1fe3e4aSElliott Hughes            del segments[0]
208*e1fe3e4aSElliott Hughes        else:
209*e1fe3e4aSElliott Hughes            # It's a closed path, do a moveTo to the last
210*e1fe3e4aSElliott Hughes            # point of the last segment.
211*e1fe3e4aSElliott Hughes            closed = True
212*e1fe3e4aSElliott Hughes            segmentType, points = segments[-1]
213*e1fe3e4aSElliott Hughes            movePt, _, _, _ = points[-1]
214*e1fe3e4aSElliott Hughes        if movePt is None:
215*e1fe3e4aSElliott Hughes            # quad special case: a contour with no on-curve points contains
216*e1fe3e4aSElliott Hughes            # one "qcurve" segment that ends with a point that's None. We
217*e1fe3e4aSElliott Hughes            # must not output a moveTo() in that case.
218*e1fe3e4aSElliott Hughes            pass
219*e1fe3e4aSElliott Hughes        else:
220*e1fe3e4aSElliott Hughes            pen.moveTo(movePt)
221*e1fe3e4aSElliott Hughes        outputImpliedClosingLine = self.outputImpliedClosingLine
222*e1fe3e4aSElliott Hughes        nSegments = len(segments)
223*e1fe3e4aSElliott Hughes        lastPt = movePt
224*e1fe3e4aSElliott Hughes        for i in range(nSegments):
225*e1fe3e4aSElliott Hughes            segmentType, points = segments[i]
226*e1fe3e4aSElliott Hughes            points = [pt for pt, _, _, _ in points]
227*e1fe3e4aSElliott Hughes            if segmentType == "line":
228*e1fe3e4aSElliott Hughes                if len(points) != 1:
229*e1fe3e4aSElliott Hughes                    raise PenError(f"Illegal line segment point count: {len(points)}")
230*e1fe3e4aSElliott Hughes                pt = points[0]
231*e1fe3e4aSElliott Hughes                # For closed contours, a 'lineTo' is always implied from the last oncurve
232*e1fe3e4aSElliott Hughes                # point to the starting point, thus we can omit it when the last and
233*e1fe3e4aSElliott Hughes                # starting point don't overlap.
234*e1fe3e4aSElliott Hughes                # However, when the last oncurve point is a "line" segment and has same
235*e1fe3e4aSElliott Hughes                # coordinates as the starting point of a closed contour, we need to output
236*e1fe3e4aSElliott Hughes                # the closing 'lineTo' explicitly (regardless of the value of the
237*e1fe3e4aSElliott Hughes                # 'outputImpliedClosingLine' option) in order to disambiguate this case from
238*e1fe3e4aSElliott Hughes                # the implied closing 'lineTo', otherwise the duplicate point would be lost.
239*e1fe3e4aSElliott Hughes                # See https://github.com/googlefonts/fontmake/issues/572.
240*e1fe3e4aSElliott Hughes                if (
241*e1fe3e4aSElliott Hughes                    i + 1 != nSegments
242*e1fe3e4aSElliott Hughes                    or outputImpliedClosingLine
243*e1fe3e4aSElliott Hughes                    or not closed
244*e1fe3e4aSElliott Hughes                    or pt == lastPt
245*e1fe3e4aSElliott Hughes                ):
246*e1fe3e4aSElliott Hughes                    pen.lineTo(pt)
247*e1fe3e4aSElliott Hughes                    lastPt = pt
248*e1fe3e4aSElliott Hughes            elif segmentType == "curve":
249*e1fe3e4aSElliott Hughes                pen.curveTo(*points)
250*e1fe3e4aSElliott Hughes                lastPt = points[-1]
251*e1fe3e4aSElliott Hughes            elif segmentType == "qcurve":
252*e1fe3e4aSElliott Hughes                pen.qCurveTo(*points)
253*e1fe3e4aSElliott Hughes                lastPt = points[-1]
254*e1fe3e4aSElliott Hughes            else:
255*e1fe3e4aSElliott Hughes                raise PenError(f"Illegal segmentType: {segmentType}")
256*e1fe3e4aSElliott Hughes        if closed:
257*e1fe3e4aSElliott Hughes            pen.closePath()
258*e1fe3e4aSElliott Hughes        else:
259*e1fe3e4aSElliott Hughes            pen.endPath()
260*e1fe3e4aSElliott Hughes
261*e1fe3e4aSElliott Hughes    def addComponent(self, glyphName, transform, identifier=None, **kwargs):
262*e1fe3e4aSElliott Hughes        del identifier  # unused
263*e1fe3e4aSElliott Hughes        del kwargs  # unused
264*e1fe3e4aSElliott Hughes        self.pen.addComponent(glyphName, transform)
265*e1fe3e4aSElliott Hughes
266*e1fe3e4aSElliott Hughes
267*e1fe3e4aSElliott Hughesclass SegmentToPointPen(AbstractPen):
268*e1fe3e4aSElliott Hughes    """
269*e1fe3e4aSElliott Hughes    Adapter class that converts the (Segment)Pen protocol to the
270*e1fe3e4aSElliott Hughes    PointPen protocol.
271*e1fe3e4aSElliott Hughes    """
272*e1fe3e4aSElliott Hughes
273*e1fe3e4aSElliott Hughes    def __init__(self, pointPen, guessSmooth=True):
274*e1fe3e4aSElliott Hughes        if guessSmooth:
275*e1fe3e4aSElliott Hughes            self.pen = GuessSmoothPointPen(pointPen)
276*e1fe3e4aSElliott Hughes        else:
277*e1fe3e4aSElliott Hughes            self.pen = pointPen
278*e1fe3e4aSElliott Hughes        self.contour = None
279*e1fe3e4aSElliott Hughes
280*e1fe3e4aSElliott Hughes    def _flushContour(self):
281*e1fe3e4aSElliott Hughes        pen = self.pen
282*e1fe3e4aSElliott Hughes        pen.beginPath()
283*e1fe3e4aSElliott Hughes        for pt, segmentType in self.contour:
284*e1fe3e4aSElliott Hughes            pen.addPoint(pt, segmentType=segmentType)
285*e1fe3e4aSElliott Hughes        pen.endPath()
286*e1fe3e4aSElliott Hughes
287*e1fe3e4aSElliott Hughes    def moveTo(self, pt):
288*e1fe3e4aSElliott Hughes        self.contour = []
289*e1fe3e4aSElliott Hughes        self.contour.append((pt, "move"))
290*e1fe3e4aSElliott Hughes
291*e1fe3e4aSElliott Hughes    def lineTo(self, pt):
292*e1fe3e4aSElliott Hughes        if self.contour is None:
293*e1fe3e4aSElliott Hughes            raise PenError("Contour missing required initial moveTo")
294*e1fe3e4aSElliott Hughes        self.contour.append((pt, "line"))
295*e1fe3e4aSElliott Hughes
296*e1fe3e4aSElliott Hughes    def curveTo(self, *pts):
297*e1fe3e4aSElliott Hughes        if not pts:
298*e1fe3e4aSElliott Hughes            raise TypeError("Must pass in at least one point")
299*e1fe3e4aSElliott Hughes        if self.contour is None:
300*e1fe3e4aSElliott Hughes            raise PenError("Contour missing required initial moveTo")
301*e1fe3e4aSElliott Hughes        for pt in pts[:-1]:
302*e1fe3e4aSElliott Hughes            self.contour.append((pt, None))
303*e1fe3e4aSElliott Hughes        self.contour.append((pts[-1], "curve"))
304*e1fe3e4aSElliott Hughes
305*e1fe3e4aSElliott Hughes    def qCurveTo(self, *pts):
306*e1fe3e4aSElliott Hughes        if not pts:
307*e1fe3e4aSElliott Hughes            raise TypeError("Must pass in at least one point")
308*e1fe3e4aSElliott Hughes        if pts[-1] is None:
309*e1fe3e4aSElliott Hughes            self.contour = []
310*e1fe3e4aSElliott Hughes        else:
311*e1fe3e4aSElliott Hughes            if self.contour is None:
312*e1fe3e4aSElliott Hughes                raise PenError("Contour missing required initial moveTo")
313*e1fe3e4aSElliott Hughes        for pt in pts[:-1]:
314*e1fe3e4aSElliott Hughes            self.contour.append((pt, None))
315*e1fe3e4aSElliott Hughes        if pts[-1] is not None:
316*e1fe3e4aSElliott Hughes            self.contour.append((pts[-1], "qcurve"))
317*e1fe3e4aSElliott Hughes
318*e1fe3e4aSElliott Hughes    def closePath(self):
319*e1fe3e4aSElliott Hughes        if self.contour is None:
320*e1fe3e4aSElliott Hughes            raise PenError("Contour missing required initial moveTo")
321*e1fe3e4aSElliott Hughes        if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]:
322*e1fe3e4aSElliott Hughes            self.contour[0] = self.contour[-1]
323*e1fe3e4aSElliott Hughes            del self.contour[-1]
324*e1fe3e4aSElliott Hughes        else:
325*e1fe3e4aSElliott Hughes            # There's an implied line at the end, replace "move" with "line"
326*e1fe3e4aSElliott Hughes            # for the first point
327*e1fe3e4aSElliott Hughes            pt, tp = self.contour[0]
328*e1fe3e4aSElliott Hughes            if tp == "move":
329*e1fe3e4aSElliott Hughes                self.contour[0] = pt, "line"
330*e1fe3e4aSElliott Hughes        self._flushContour()
331*e1fe3e4aSElliott Hughes        self.contour = None
332*e1fe3e4aSElliott Hughes
333*e1fe3e4aSElliott Hughes    def endPath(self):
334*e1fe3e4aSElliott Hughes        if self.contour is None:
335*e1fe3e4aSElliott Hughes            raise PenError("Contour missing required initial moveTo")
336*e1fe3e4aSElliott Hughes        self._flushContour()
337*e1fe3e4aSElliott Hughes        self.contour = None
338*e1fe3e4aSElliott Hughes
339*e1fe3e4aSElliott Hughes    def addComponent(self, glyphName, transform):
340*e1fe3e4aSElliott Hughes        if self.contour is not None:
341*e1fe3e4aSElliott Hughes            raise PenError("Components must be added before or after contours")
342*e1fe3e4aSElliott Hughes        self.pen.addComponent(glyphName, transform)
343*e1fe3e4aSElliott Hughes
344*e1fe3e4aSElliott Hughes
345*e1fe3e4aSElliott Hughesclass GuessSmoothPointPen(AbstractPointPen):
346*e1fe3e4aSElliott Hughes    """
347*e1fe3e4aSElliott Hughes    Filtering PointPen that tries to determine whether an on-curve point
348*e1fe3e4aSElliott Hughes    should be "smooth", ie. that it's a "tangent" point or a "curve" point.
349*e1fe3e4aSElliott Hughes    """
350*e1fe3e4aSElliott Hughes
351*e1fe3e4aSElliott Hughes    def __init__(self, outPen, error=0.05):
352*e1fe3e4aSElliott Hughes        self._outPen = outPen
353*e1fe3e4aSElliott Hughes        self._error = error
354*e1fe3e4aSElliott Hughes        self._points = None
355*e1fe3e4aSElliott Hughes
356*e1fe3e4aSElliott Hughes    def _flushContour(self):
357*e1fe3e4aSElliott Hughes        if self._points is None:
358*e1fe3e4aSElliott Hughes            raise PenError("Path not begun")
359*e1fe3e4aSElliott Hughes        points = self._points
360*e1fe3e4aSElliott Hughes        nPoints = len(points)
361*e1fe3e4aSElliott Hughes        if not nPoints:
362*e1fe3e4aSElliott Hughes            return
363*e1fe3e4aSElliott Hughes        if points[0][1] == "move":
364*e1fe3e4aSElliott Hughes            # Open path.
365*e1fe3e4aSElliott Hughes            indices = range(1, nPoints - 1)
366*e1fe3e4aSElliott Hughes        elif nPoints > 1:
367*e1fe3e4aSElliott Hughes            # Closed path. To avoid having to mod the contour index, we
368*e1fe3e4aSElliott Hughes            # simply abuse Python's negative index feature, and start at -1
369*e1fe3e4aSElliott Hughes            indices = range(-1, nPoints - 1)
370*e1fe3e4aSElliott Hughes        else:
371*e1fe3e4aSElliott Hughes            # closed path containing 1 point (!), ignore.
372*e1fe3e4aSElliott Hughes            indices = []
373*e1fe3e4aSElliott Hughes        for i in indices:
374*e1fe3e4aSElliott Hughes            pt, segmentType, _, name, kwargs = points[i]
375*e1fe3e4aSElliott Hughes            if segmentType is None:
376*e1fe3e4aSElliott Hughes                continue
377*e1fe3e4aSElliott Hughes            prev = i - 1
378*e1fe3e4aSElliott Hughes            next = i + 1
379*e1fe3e4aSElliott Hughes            if points[prev][1] is not None and points[next][1] is not None:
380*e1fe3e4aSElliott Hughes                continue
381*e1fe3e4aSElliott Hughes            # At least one of our neighbors is an off-curve point
382*e1fe3e4aSElliott Hughes            pt = points[i][0]
383*e1fe3e4aSElliott Hughes            prevPt = points[prev][0]
384*e1fe3e4aSElliott Hughes            nextPt = points[next][0]
385*e1fe3e4aSElliott Hughes            if pt != prevPt and pt != nextPt:
386*e1fe3e4aSElliott Hughes                dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1]
387*e1fe3e4aSElliott Hughes                dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1]
388*e1fe3e4aSElliott Hughes                a1 = math.atan2(dy1, dx1)
389*e1fe3e4aSElliott Hughes                a2 = math.atan2(dy2, dx2)
390*e1fe3e4aSElliott Hughes                if abs(a1 - a2) < self._error:
391*e1fe3e4aSElliott Hughes                    points[i] = pt, segmentType, True, name, kwargs
392*e1fe3e4aSElliott Hughes
393*e1fe3e4aSElliott Hughes        for pt, segmentType, smooth, name, kwargs in points:
394*e1fe3e4aSElliott Hughes            self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
395*e1fe3e4aSElliott Hughes
396*e1fe3e4aSElliott Hughes    def beginPath(self, identifier=None, **kwargs):
397*e1fe3e4aSElliott Hughes        if self._points is not None:
398*e1fe3e4aSElliott Hughes            raise PenError("Path already begun")
399*e1fe3e4aSElliott Hughes        self._points = []
400*e1fe3e4aSElliott Hughes        if identifier is not None:
401*e1fe3e4aSElliott Hughes            kwargs["identifier"] = identifier
402*e1fe3e4aSElliott Hughes        self._outPen.beginPath(**kwargs)
403*e1fe3e4aSElliott Hughes
404*e1fe3e4aSElliott Hughes    def endPath(self):
405*e1fe3e4aSElliott Hughes        self._flushContour()
406*e1fe3e4aSElliott Hughes        self._outPen.endPath()
407*e1fe3e4aSElliott Hughes        self._points = None
408*e1fe3e4aSElliott Hughes
409*e1fe3e4aSElliott Hughes    def addPoint(
410*e1fe3e4aSElliott Hughes        self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
411*e1fe3e4aSElliott Hughes    ):
412*e1fe3e4aSElliott Hughes        if self._points is None:
413*e1fe3e4aSElliott Hughes            raise PenError("Path not begun")
414*e1fe3e4aSElliott Hughes        if identifier is not None:
415*e1fe3e4aSElliott Hughes            kwargs["identifier"] = identifier
416*e1fe3e4aSElliott Hughes        self._points.append((pt, segmentType, False, name, kwargs))
417*e1fe3e4aSElliott Hughes
418*e1fe3e4aSElliott Hughes    def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
419*e1fe3e4aSElliott Hughes        if self._points is not None:
420*e1fe3e4aSElliott Hughes            raise PenError("Components must be added before or after contours")
421*e1fe3e4aSElliott Hughes        if identifier is not None:
422*e1fe3e4aSElliott Hughes            kwargs["identifier"] = identifier
423*e1fe3e4aSElliott Hughes        self._outPen.addComponent(glyphName, transformation, **kwargs)
424*e1fe3e4aSElliott Hughes
425*e1fe3e4aSElliott Hughes    def addVarComponent(
426*e1fe3e4aSElliott Hughes        self, glyphName, transformation, location, identifier=None, **kwargs
427*e1fe3e4aSElliott Hughes    ):
428*e1fe3e4aSElliott Hughes        if self._points is not None:
429*e1fe3e4aSElliott Hughes            raise PenError("VarComponents must be added before or after contours")
430*e1fe3e4aSElliott Hughes        if identifier is not None:
431*e1fe3e4aSElliott Hughes            kwargs["identifier"] = identifier
432*e1fe3e4aSElliott Hughes        self._outPen.addVarComponent(glyphName, transformation, location, **kwargs)
433*e1fe3e4aSElliott Hughes
434*e1fe3e4aSElliott Hughes
435*e1fe3e4aSElliott Hughesclass ReverseContourPointPen(AbstractPointPen):
436*e1fe3e4aSElliott Hughes    """
437*e1fe3e4aSElliott Hughes    This is a PointPen that passes outline data to another PointPen, but
438*e1fe3e4aSElliott Hughes    reversing the winding direction of all contours. Components are simply
439*e1fe3e4aSElliott Hughes    passed through unchanged.
440*e1fe3e4aSElliott Hughes
441*e1fe3e4aSElliott Hughes    Closed contours are reversed in such a way that the first point remains
442*e1fe3e4aSElliott Hughes    the first point.
443*e1fe3e4aSElliott Hughes    """
444*e1fe3e4aSElliott Hughes
445*e1fe3e4aSElliott Hughes    def __init__(self, outputPointPen):
446*e1fe3e4aSElliott Hughes        self.pen = outputPointPen
447*e1fe3e4aSElliott Hughes        # a place to store the points for the current sub path
448*e1fe3e4aSElliott Hughes        self.currentContour = None
449*e1fe3e4aSElliott Hughes
450*e1fe3e4aSElliott Hughes    def _flushContour(self):
451*e1fe3e4aSElliott Hughes        pen = self.pen
452*e1fe3e4aSElliott Hughes        contour = self.currentContour
453*e1fe3e4aSElliott Hughes        if not contour:
454*e1fe3e4aSElliott Hughes            pen.beginPath(identifier=self.currentContourIdentifier)
455*e1fe3e4aSElliott Hughes            pen.endPath()
456*e1fe3e4aSElliott Hughes            return
457*e1fe3e4aSElliott Hughes
458*e1fe3e4aSElliott Hughes        closed = contour[0][1] != "move"
459*e1fe3e4aSElliott Hughes        if not closed:
460*e1fe3e4aSElliott Hughes            lastSegmentType = "move"
461*e1fe3e4aSElliott Hughes        else:
462*e1fe3e4aSElliott Hughes            # Remove the first point and insert it at the end. When
463*e1fe3e4aSElliott Hughes            # the list of points gets reversed, this point will then
464*e1fe3e4aSElliott Hughes            # again be at the start. In other words, the following
465*e1fe3e4aSElliott Hughes            # will hold:
466*e1fe3e4aSElliott Hughes            #   for N in range(len(originalContour)):
467*e1fe3e4aSElliott Hughes            #       originalContour[N] == reversedContour[-N]
468*e1fe3e4aSElliott Hughes            contour.append(contour.pop(0))
469*e1fe3e4aSElliott Hughes            # Find the first on-curve point.
470*e1fe3e4aSElliott Hughes            firstOnCurve = None
471*e1fe3e4aSElliott Hughes            for i in range(len(contour)):
472*e1fe3e4aSElliott Hughes                if contour[i][1] is not None:
473*e1fe3e4aSElliott Hughes                    firstOnCurve = i
474*e1fe3e4aSElliott Hughes                    break
475*e1fe3e4aSElliott Hughes            if firstOnCurve is None:
476*e1fe3e4aSElliott Hughes                # There are no on-curve points, be basically have to
477*e1fe3e4aSElliott Hughes                # do nothing but contour.reverse().
478*e1fe3e4aSElliott Hughes                lastSegmentType = None
479*e1fe3e4aSElliott Hughes            else:
480*e1fe3e4aSElliott Hughes                lastSegmentType = contour[firstOnCurve][1]
481*e1fe3e4aSElliott Hughes
482*e1fe3e4aSElliott Hughes        contour.reverse()
483*e1fe3e4aSElliott Hughes        if not closed:
484*e1fe3e4aSElliott Hughes            # Open paths must start with a move, so we simply dump
485*e1fe3e4aSElliott Hughes            # all off-curve points leading up to the first on-curve.
486*e1fe3e4aSElliott Hughes            while contour[0][1] is None:
487*e1fe3e4aSElliott Hughes                contour.pop(0)
488*e1fe3e4aSElliott Hughes        pen.beginPath(identifier=self.currentContourIdentifier)
489*e1fe3e4aSElliott Hughes        for pt, nextSegmentType, smooth, name, kwargs in contour:
490*e1fe3e4aSElliott Hughes            if nextSegmentType is not None:
491*e1fe3e4aSElliott Hughes                segmentType = lastSegmentType
492*e1fe3e4aSElliott Hughes                lastSegmentType = nextSegmentType
493*e1fe3e4aSElliott Hughes            else:
494*e1fe3e4aSElliott Hughes                segmentType = None
495*e1fe3e4aSElliott Hughes            pen.addPoint(
496*e1fe3e4aSElliott Hughes                pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs
497*e1fe3e4aSElliott Hughes            )
498*e1fe3e4aSElliott Hughes        pen.endPath()
499*e1fe3e4aSElliott Hughes
500*e1fe3e4aSElliott Hughes    def beginPath(self, identifier=None, **kwargs):
501*e1fe3e4aSElliott Hughes        if self.currentContour is not None:
502*e1fe3e4aSElliott Hughes            raise PenError("Path already begun")
503*e1fe3e4aSElliott Hughes        self.currentContour = []
504*e1fe3e4aSElliott Hughes        self.currentContourIdentifier = identifier
505*e1fe3e4aSElliott Hughes        self.onCurve = []
506*e1fe3e4aSElliott Hughes
507*e1fe3e4aSElliott Hughes    def endPath(self):
508*e1fe3e4aSElliott Hughes        if self.currentContour is None:
509*e1fe3e4aSElliott Hughes            raise PenError("Path not begun")
510*e1fe3e4aSElliott Hughes        self._flushContour()
511*e1fe3e4aSElliott Hughes        self.currentContour = None
512*e1fe3e4aSElliott Hughes
513*e1fe3e4aSElliott Hughes    def addPoint(
514*e1fe3e4aSElliott Hughes        self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs
515*e1fe3e4aSElliott Hughes    ):
516*e1fe3e4aSElliott Hughes        if self.currentContour is None:
517*e1fe3e4aSElliott Hughes            raise PenError("Path not begun")
518*e1fe3e4aSElliott Hughes        if identifier is not None:
519*e1fe3e4aSElliott Hughes            kwargs["identifier"] = identifier
520*e1fe3e4aSElliott Hughes        self.currentContour.append((pt, segmentType, smooth, name, kwargs))
521*e1fe3e4aSElliott Hughes
522*e1fe3e4aSElliott Hughes    def addComponent(self, glyphName, transform, identifier=None, **kwargs):
523*e1fe3e4aSElliott Hughes        if self.currentContour is not None:
524*e1fe3e4aSElliott Hughes            raise PenError("Components must be added before or after contours")
525*e1fe3e4aSElliott Hughes        self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs)
526