xref: /aosp_15_r20/external/fonttools/Lib/fontTools/pens/basePen.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""fontTools.pens.basePen.py -- Tools and base classes to build pen objects.
2*e1fe3e4aSElliott Hughes
3*e1fe3e4aSElliott HughesThe Pen Protocol
4*e1fe3e4aSElliott Hughes
5*e1fe3e4aSElliott HughesA Pen is a kind of object that standardizes the way how to "draw" outlines:
6*e1fe3e4aSElliott Hughesit is a middle man between an outline and a drawing. In other words:
7*e1fe3e4aSElliott Hughesit is an abstraction for drawing outlines, making sure that outline objects
8*e1fe3e4aSElliott Hughesdon't need to know the details about how and where they're being drawn, and
9*e1fe3e4aSElliott Hughesthat drawings don't need to know the details of how outlines are stored.
10*e1fe3e4aSElliott Hughes
11*e1fe3e4aSElliott HughesThe most basic pattern is this::
12*e1fe3e4aSElliott Hughes
13*e1fe3e4aSElliott Hughes	outline.draw(pen)  # 'outline' draws itself onto 'pen'
14*e1fe3e4aSElliott Hughes
15*e1fe3e4aSElliott HughesPens can be used to render outlines to the screen, but also to construct
16*e1fe3e4aSElliott Hughesnew outlines. Eg. an outline object can be both a drawable object (it has a
17*e1fe3e4aSElliott Hughesdraw() method) as well as a pen itself: you *build* an outline using pen
18*e1fe3e4aSElliott Hughesmethods.
19*e1fe3e4aSElliott Hughes
20*e1fe3e4aSElliott HughesThe AbstractPen class defines the Pen protocol. It implements almost
21*e1fe3e4aSElliott Hughesnothing (only no-op closePath() and endPath() methods), but is useful
22*e1fe3e4aSElliott Hughesfor documentation purposes. Subclassing it basically tells the reader:
23*e1fe3e4aSElliott Hughes"this class implements the Pen protocol.". An examples of an AbstractPen
24*e1fe3e4aSElliott Hughessubclass is :py:class:`fontTools.pens.transformPen.TransformPen`.
25*e1fe3e4aSElliott Hughes
26*e1fe3e4aSElliott HughesThe BasePen class is a base implementation useful for pens that actually
27*e1fe3e4aSElliott Hughesdraw (for example a pen renders outlines using a native graphics engine).
28*e1fe3e4aSElliott HughesBasePen contains a lot of base functionality, making it very easy to build
29*e1fe3e4aSElliott Hughesa pen that fully conforms to the pen protocol. Note that if you subclass
30*e1fe3e4aSElliott HughesBasePen, you *don't* override moveTo(), lineTo(), etc., but _moveTo(),
31*e1fe3e4aSElliott Hughes_lineTo(), etc. See the BasePen doc string for details. Examples of
32*e1fe3e4aSElliott HughesBasePen subclasses are fontTools.pens.boundsPen.BoundsPen and
33*e1fe3e4aSElliott HughesfontTools.pens.cocoaPen.CocoaPen.
34*e1fe3e4aSElliott Hughes
35*e1fe3e4aSElliott HughesCoordinates are usually expressed as (x, y) tuples, but generally any
36*e1fe3e4aSElliott Hughessequence of length 2 will do.
37*e1fe3e4aSElliott Hughes"""
38*e1fe3e4aSElliott Hughes
39*e1fe3e4aSElliott Hughesfrom typing import Tuple, Dict
40*e1fe3e4aSElliott Hughes
41*e1fe3e4aSElliott Hughesfrom fontTools.misc.loggingTools import LogMixin
42*e1fe3e4aSElliott Hughesfrom fontTools.misc.transform import DecomposedTransform
43*e1fe3e4aSElliott Hughes
44*e1fe3e4aSElliott Hughes__all__ = [
45*e1fe3e4aSElliott Hughes    "AbstractPen",
46*e1fe3e4aSElliott Hughes    "NullPen",
47*e1fe3e4aSElliott Hughes    "BasePen",
48*e1fe3e4aSElliott Hughes    "PenError",
49*e1fe3e4aSElliott Hughes    "decomposeSuperBezierSegment",
50*e1fe3e4aSElliott Hughes    "decomposeQuadraticSegment",
51*e1fe3e4aSElliott Hughes]
52*e1fe3e4aSElliott Hughes
53*e1fe3e4aSElliott Hughes
54*e1fe3e4aSElliott Hughesclass PenError(Exception):
55*e1fe3e4aSElliott Hughes    """Represents an error during penning."""
56*e1fe3e4aSElliott Hughes
57*e1fe3e4aSElliott Hughes
58*e1fe3e4aSElliott Hughesclass OpenContourError(PenError):
59*e1fe3e4aSElliott Hughes    pass
60*e1fe3e4aSElliott Hughes
61*e1fe3e4aSElliott Hughes
62*e1fe3e4aSElliott Hughesclass AbstractPen:
63*e1fe3e4aSElliott Hughes    def moveTo(self, pt: Tuple[float, float]) -> None:
64*e1fe3e4aSElliott Hughes        """Begin a new sub path, set the current point to 'pt'. You must
65*e1fe3e4aSElliott Hughes        end each sub path with a call to pen.closePath() or pen.endPath().
66*e1fe3e4aSElliott Hughes        """
67*e1fe3e4aSElliott Hughes        raise NotImplementedError
68*e1fe3e4aSElliott Hughes
69*e1fe3e4aSElliott Hughes    def lineTo(self, pt: Tuple[float, float]) -> None:
70*e1fe3e4aSElliott Hughes        """Draw a straight line from the current point to 'pt'."""
71*e1fe3e4aSElliott Hughes        raise NotImplementedError
72*e1fe3e4aSElliott Hughes
73*e1fe3e4aSElliott Hughes    def curveTo(self, *points: Tuple[float, float]) -> None:
74*e1fe3e4aSElliott Hughes        """Draw a cubic bezier with an arbitrary number of control points.
75*e1fe3e4aSElliott Hughes
76*e1fe3e4aSElliott Hughes        The last point specified is on-curve, all others are off-curve
77*e1fe3e4aSElliott Hughes        (control) points. If the number of control points is > 2, the
78*e1fe3e4aSElliott Hughes        segment is split into multiple bezier segments. This works
79*e1fe3e4aSElliott Hughes        like this:
80*e1fe3e4aSElliott Hughes
81*e1fe3e4aSElliott Hughes        Let n be the number of control points (which is the number of
82*e1fe3e4aSElliott Hughes        arguments to this call minus 1). If n==2, a plain vanilla cubic
83*e1fe3e4aSElliott Hughes        bezier is drawn. If n==1, we fall back to a quadratic segment and
84*e1fe3e4aSElliott Hughes        if n==0 we draw a straight line. It gets interesting when n>2:
85*e1fe3e4aSElliott Hughes        n-1 PostScript-style cubic segments will be drawn as if it were
86*e1fe3e4aSElliott Hughes        one curve. See decomposeSuperBezierSegment().
87*e1fe3e4aSElliott Hughes
88*e1fe3e4aSElliott Hughes        The conversion algorithm used for n>2 is inspired by NURB
89*e1fe3e4aSElliott Hughes        splines, and is conceptually equivalent to the TrueType "implied
90*e1fe3e4aSElliott Hughes        points" principle. See also decomposeQuadraticSegment().
91*e1fe3e4aSElliott Hughes        """
92*e1fe3e4aSElliott Hughes        raise NotImplementedError
93*e1fe3e4aSElliott Hughes
94*e1fe3e4aSElliott Hughes    def qCurveTo(self, *points: Tuple[float, float]) -> None:
95*e1fe3e4aSElliott Hughes        """Draw a whole string of quadratic curve segments.
96*e1fe3e4aSElliott Hughes
97*e1fe3e4aSElliott Hughes        The last point specified is on-curve, all others are off-curve
98*e1fe3e4aSElliott Hughes        points.
99*e1fe3e4aSElliott Hughes
100*e1fe3e4aSElliott Hughes        This method implements TrueType-style curves, breaking up curves
101*e1fe3e4aSElliott Hughes        using 'implied points': between each two consequtive off-curve points,
102*e1fe3e4aSElliott Hughes        there is one implied point exactly in the middle between them. See
103*e1fe3e4aSElliott Hughes        also decomposeQuadraticSegment().
104*e1fe3e4aSElliott Hughes
105*e1fe3e4aSElliott Hughes        The last argument (normally the on-curve point) may be None.
106*e1fe3e4aSElliott Hughes        This is to support contours that have NO on-curve points (a rarely
107*e1fe3e4aSElliott Hughes        seen feature of TrueType outlines).
108*e1fe3e4aSElliott Hughes        """
109*e1fe3e4aSElliott Hughes        raise NotImplementedError
110*e1fe3e4aSElliott Hughes
111*e1fe3e4aSElliott Hughes    def closePath(self) -> None:
112*e1fe3e4aSElliott Hughes        """Close the current sub path. You must call either pen.closePath()
113*e1fe3e4aSElliott Hughes        or pen.endPath() after each sub path.
114*e1fe3e4aSElliott Hughes        """
115*e1fe3e4aSElliott Hughes        pass
116*e1fe3e4aSElliott Hughes
117*e1fe3e4aSElliott Hughes    def endPath(self) -> None:
118*e1fe3e4aSElliott Hughes        """End the current sub path, but don't close it. You must call
119*e1fe3e4aSElliott Hughes        either pen.closePath() or pen.endPath() after each sub path.
120*e1fe3e4aSElliott Hughes        """
121*e1fe3e4aSElliott Hughes        pass
122*e1fe3e4aSElliott Hughes
123*e1fe3e4aSElliott Hughes    def addComponent(
124*e1fe3e4aSElliott Hughes        self,
125*e1fe3e4aSElliott Hughes        glyphName: str,
126*e1fe3e4aSElliott Hughes        transformation: Tuple[float, float, float, float, float, float],
127*e1fe3e4aSElliott Hughes    ) -> None:
128*e1fe3e4aSElliott Hughes        """Add a sub glyph. The 'transformation' argument must be a 6-tuple
129*e1fe3e4aSElliott Hughes        containing an affine transformation, or a Transform object from the
130*e1fe3e4aSElliott Hughes        fontTools.misc.transform module. More precisely: it should be a
131*e1fe3e4aSElliott Hughes        sequence containing 6 numbers.
132*e1fe3e4aSElliott Hughes        """
133*e1fe3e4aSElliott Hughes        raise NotImplementedError
134*e1fe3e4aSElliott Hughes
135*e1fe3e4aSElliott Hughes    def addVarComponent(
136*e1fe3e4aSElliott Hughes        self,
137*e1fe3e4aSElliott Hughes        glyphName: str,
138*e1fe3e4aSElliott Hughes        transformation: DecomposedTransform,
139*e1fe3e4aSElliott Hughes        location: Dict[str, float],
140*e1fe3e4aSElliott Hughes    ) -> None:
141*e1fe3e4aSElliott Hughes        """Add a VarComponent sub glyph. The 'transformation' argument
142*e1fe3e4aSElliott Hughes        must be a DecomposedTransform from the fontTools.misc.transform module,
143*e1fe3e4aSElliott Hughes        and the 'location' argument must be a dictionary mapping axis tags
144*e1fe3e4aSElliott Hughes        to their locations.
145*e1fe3e4aSElliott Hughes        """
146*e1fe3e4aSElliott Hughes        # GlyphSet decomposes for us
147*e1fe3e4aSElliott Hughes        raise AttributeError
148*e1fe3e4aSElliott Hughes
149*e1fe3e4aSElliott Hughes
150*e1fe3e4aSElliott Hughesclass NullPen(AbstractPen):
151*e1fe3e4aSElliott Hughes    """A pen that does nothing."""
152*e1fe3e4aSElliott Hughes
153*e1fe3e4aSElliott Hughes    def moveTo(self, pt):
154*e1fe3e4aSElliott Hughes        pass
155*e1fe3e4aSElliott Hughes
156*e1fe3e4aSElliott Hughes    def lineTo(self, pt):
157*e1fe3e4aSElliott Hughes        pass
158*e1fe3e4aSElliott Hughes
159*e1fe3e4aSElliott Hughes    def curveTo(self, *points):
160*e1fe3e4aSElliott Hughes        pass
161*e1fe3e4aSElliott Hughes
162*e1fe3e4aSElliott Hughes    def qCurveTo(self, *points):
163*e1fe3e4aSElliott Hughes        pass
164*e1fe3e4aSElliott Hughes
165*e1fe3e4aSElliott Hughes    def closePath(self):
166*e1fe3e4aSElliott Hughes        pass
167*e1fe3e4aSElliott Hughes
168*e1fe3e4aSElliott Hughes    def endPath(self):
169*e1fe3e4aSElliott Hughes        pass
170*e1fe3e4aSElliott Hughes
171*e1fe3e4aSElliott Hughes    def addComponent(self, glyphName, transformation):
172*e1fe3e4aSElliott Hughes        pass
173*e1fe3e4aSElliott Hughes
174*e1fe3e4aSElliott Hughes    def addVarComponent(self, glyphName, transformation, location):
175*e1fe3e4aSElliott Hughes        pass
176*e1fe3e4aSElliott Hughes
177*e1fe3e4aSElliott Hughes
178*e1fe3e4aSElliott Hughesclass LoggingPen(LogMixin, AbstractPen):
179*e1fe3e4aSElliott Hughes    """A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)"""
180*e1fe3e4aSElliott Hughes
181*e1fe3e4aSElliott Hughes    pass
182*e1fe3e4aSElliott Hughes
183*e1fe3e4aSElliott Hughes
184*e1fe3e4aSElliott Hughesclass MissingComponentError(KeyError):
185*e1fe3e4aSElliott Hughes    """Indicates a component pointing to a non-existent glyph in the glyphset."""
186*e1fe3e4aSElliott Hughes
187*e1fe3e4aSElliott Hughes
188*e1fe3e4aSElliott Hughesclass DecomposingPen(LoggingPen):
189*e1fe3e4aSElliott Hughes    """Implements a 'addComponent' method that decomposes components
190*e1fe3e4aSElliott Hughes    (i.e. draws them onto self as simple contours).
191*e1fe3e4aSElliott Hughes    It can also be used as a mixin class (e.g. see ContourRecordingPen).
192*e1fe3e4aSElliott Hughes
193*e1fe3e4aSElliott Hughes    You must override moveTo, lineTo, curveTo and qCurveTo. You may
194*e1fe3e4aSElliott Hughes    additionally override closePath, endPath and addComponent.
195*e1fe3e4aSElliott Hughes
196*e1fe3e4aSElliott Hughes    By default a warning message is logged when a base glyph is missing;
197*e1fe3e4aSElliott Hughes    set the class variable ``skipMissingComponents`` to False if you want
198*e1fe3e4aSElliott Hughes    to raise a :class:`MissingComponentError` exception.
199*e1fe3e4aSElliott Hughes    """
200*e1fe3e4aSElliott Hughes
201*e1fe3e4aSElliott Hughes    skipMissingComponents = True
202*e1fe3e4aSElliott Hughes
203*e1fe3e4aSElliott Hughes    def __init__(self, glyphSet):
204*e1fe3e4aSElliott Hughes        """Takes a single 'glyphSet' argument (dict), in which the glyphs
205*e1fe3e4aSElliott Hughes        that are referenced as components are looked up by their name.
206*e1fe3e4aSElliott Hughes        """
207*e1fe3e4aSElliott Hughes        super(DecomposingPen, self).__init__()
208*e1fe3e4aSElliott Hughes        self.glyphSet = glyphSet
209*e1fe3e4aSElliott Hughes
210*e1fe3e4aSElliott Hughes    def addComponent(self, glyphName, transformation):
211*e1fe3e4aSElliott Hughes        """Transform the points of the base glyph and draw it onto self."""
212*e1fe3e4aSElliott Hughes        from fontTools.pens.transformPen import TransformPen
213*e1fe3e4aSElliott Hughes
214*e1fe3e4aSElliott Hughes        try:
215*e1fe3e4aSElliott Hughes            glyph = self.glyphSet[glyphName]
216*e1fe3e4aSElliott Hughes        except KeyError:
217*e1fe3e4aSElliott Hughes            if not self.skipMissingComponents:
218*e1fe3e4aSElliott Hughes                raise MissingComponentError(glyphName)
219*e1fe3e4aSElliott Hughes            self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName)
220*e1fe3e4aSElliott Hughes        else:
221*e1fe3e4aSElliott Hughes            tPen = TransformPen(self, transformation)
222*e1fe3e4aSElliott Hughes            glyph.draw(tPen)
223*e1fe3e4aSElliott Hughes
224*e1fe3e4aSElliott Hughes    def addVarComponent(self, glyphName, transformation, location):
225*e1fe3e4aSElliott Hughes        # GlyphSet decomposes for us
226*e1fe3e4aSElliott Hughes        raise AttributeError
227*e1fe3e4aSElliott Hughes
228*e1fe3e4aSElliott Hughes
229*e1fe3e4aSElliott Hughesclass BasePen(DecomposingPen):
230*e1fe3e4aSElliott Hughes    """Base class for drawing pens. You must override _moveTo, _lineTo and
231*e1fe3e4aSElliott Hughes    _curveToOne. You may additionally override _closePath, _endPath,
232*e1fe3e4aSElliott Hughes    addComponent, addVarComponent, and/or _qCurveToOne. You should not
233*e1fe3e4aSElliott Hughes    override any other methods.
234*e1fe3e4aSElliott Hughes    """
235*e1fe3e4aSElliott Hughes
236*e1fe3e4aSElliott Hughes    def __init__(self, glyphSet=None):
237*e1fe3e4aSElliott Hughes        super(BasePen, self).__init__(glyphSet)
238*e1fe3e4aSElliott Hughes        self.__currentPoint = None
239*e1fe3e4aSElliott Hughes
240*e1fe3e4aSElliott Hughes    # must override
241*e1fe3e4aSElliott Hughes
242*e1fe3e4aSElliott Hughes    def _moveTo(self, pt):
243*e1fe3e4aSElliott Hughes        raise NotImplementedError
244*e1fe3e4aSElliott Hughes
245*e1fe3e4aSElliott Hughes    def _lineTo(self, pt):
246*e1fe3e4aSElliott Hughes        raise NotImplementedError
247*e1fe3e4aSElliott Hughes
248*e1fe3e4aSElliott Hughes    def _curveToOne(self, pt1, pt2, pt3):
249*e1fe3e4aSElliott Hughes        raise NotImplementedError
250*e1fe3e4aSElliott Hughes
251*e1fe3e4aSElliott Hughes    # may override
252*e1fe3e4aSElliott Hughes
253*e1fe3e4aSElliott Hughes    def _closePath(self):
254*e1fe3e4aSElliott Hughes        pass
255*e1fe3e4aSElliott Hughes
256*e1fe3e4aSElliott Hughes    def _endPath(self):
257*e1fe3e4aSElliott Hughes        pass
258*e1fe3e4aSElliott Hughes
259*e1fe3e4aSElliott Hughes    def _qCurveToOne(self, pt1, pt2):
260*e1fe3e4aSElliott Hughes        """This method implements the basic quadratic curve type. The
261*e1fe3e4aSElliott Hughes        default implementation delegates the work to the cubic curve
262*e1fe3e4aSElliott Hughes        function. Optionally override with a native implementation.
263*e1fe3e4aSElliott Hughes        """
264*e1fe3e4aSElliott Hughes        pt0x, pt0y = self.__currentPoint
265*e1fe3e4aSElliott Hughes        pt1x, pt1y = pt1
266*e1fe3e4aSElliott Hughes        pt2x, pt2y = pt2
267*e1fe3e4aSElliott Hughes        mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x)
268*e1fe3e4aSElliott Hughes        mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y)
269*e1fe3e4aSElliott Hughes        mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x)
270*e1fe3e4aSElliott Hughes        mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y)
271*e1fe3e4aSElliott Hughes        self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2)
272*e1fe3e4aSElliott Hughes
273*e1fe3e4aSElliott Hughes    # don't override
274*e1fe3e4aSElliott Hughes
275*e1fe3e4aSElliott Hughes    def _getCurrentPoint(self):
276*e1fe3e4aSElliott Hughes        """Return the current point. This is not part of the public
277*e1fe3e4aSElliott Hughes        interface, yet is useful for subclasses.
278*e1fe3e4aSElliott Hughes        """
279*e1fe3e4aSElliott Hughes        return self.__currentPoint
280*e1fe3e4aSElliott Hughes
281*e1fe3e4aSElliott Hughes    def closePath(self):
282*e1fe3e4aSElliott Hughes        self._closePath()
283*e1fe3e4aSElliott Hughes        self.__currentPoint = None
284*e1fe3e4aSElliott Hughes
285*e1fe3e4aSElliott Hughes    def endPath(self):
286*e1fe3e4aSElliott Hughes        self._endPath()
287*e1fe3e4aSElliott Hughes        self.__currentPoint = None
288*e1fe3e4aSElliott Hughes
289*e1fe3e4aSElliott Hughes    def moveTo(self, pt):
290*e1fe3e4aSElliott Hughes        self._moveTo(pt)
291*e1fe3e4aSElliott Hughes        self.__currentPoint = pt
292*e1fe3e4aSElliott Hughes
293*e1fe3e4aSElliott Hughes    def lineTo(self, pt):
294*e1fe3e4aSElliott Hughes        self._lineTo(pt)
295*e1fe3e4aSElliott Hughes        self.__currentPoint = pt
296*e1fe3e4aSElliott Hughes
297*e1fe3e4aSElliott Hughes    def curveTo(self, *points):
298*e1fe3e4aSElliott Hughes        n = len(points) - 1  # 'n' is the number of control points
299*e1fe3e4aSElliott Hughes        assert n >= 0
300*e1fe3e4aSElliott Hughes        if n == 2:
301*e1fe3e4aSElliott Hughes            # The common case, we have exactly two BCP's, so this is a standard
302*e1fe3e4aSElliott Hughes            # cubic bezier. Even though decomposeSuperBezierSegment() handles
303*e1fe3e4aSElliott Hughes            # this case just fine, we special-case it anyway since it's so
304*e1fe3e4aSElliott Hughes            # common.
305*e1fe3e4aSElliott Hughes            self._curveToOne(*points)
306*e1fe3e4aSElliott Hughes            self.__currentPoint = points[-1]
307*e1fe3e4aSElliott Hughes        elif n > 2:
308*e1fe3e4aSElliott Hughes            # n is the number of control points; split curve into n-1 cubic
309*e1fe3e4aSElliott Hughes            # bezier segments. The algorithm used here is inspired by NURB
310*e1fe3e4aSElliott Hughes            # splines and the TrueType "implied point" principle, and ensures
311*e1fe3e4aSElliott Hughes            # the smoothest possible connection between two curve segments,
312*e1fe3e4aSElliott Hughes            # with no disruption in the curvature. It is practical since it
313*e1fe3e4aSElliott Hughes            # allows one to construct multiple bezier segments with a much
314*e1fe3e4aSElliott Hughes            # smaller amount of points.
315*e1fe3e4aSElliott Hughes            _curveToOne = self._curveToOne
316*e1fe3e4aSElliott Hughes            for pt1, pt2, pt3 in decomposeSuperBezierSegment(points):
317*e1fe3e4aSElliott Hughes                _curveToOne(pt1, pt2, pt3)
318*e1fe3e4aSElliott Hughes                self.__currentPoint = pt3
319*e1fe3e4aSElliott Hughes        elif n == 1:
320*e1fe3e4aSElliott Hughes            self.qCurveTo(*points)
321*e1fe3e4aSElliott Hughes        elif n == 0:
322*e1fe3e4aSElliott Hughes            self.lineTo(points[0])
323*e1fe3e4aSElliott Hughes        else:
324*e1fe3e4aSElliott Hughes            raise AssertionError("can't get there from here")
325*e1fe3e4aSElliott Hughes
326*e1fe3e4aSElliott Hughes    def qCurveTo(self, *points):
327*e1fe3e4aSElliott Hughes        n = len(points) - 1  # 'n' is the number of control points
328*e1fe3e4aSElliott Hughes        assert n >= 0
329*e1fe3e4aSElliott Hughes        if points[-1] is None:
330*e1fe3e4aSElliott Hughes            # Special case for TrueType quadratics: it is possible to
331*e1fe3e4aSElliott Hughes            # define a contour with NO on-curve points. BasePen supports
332*e1fe3e4aSElliott Hughes            # this by allowing the final argument (the expected on-curve
333*e1fe3e4aSElliott Hughes            # point) to be None. We simulate the feature by making the implied
334*e1fe3e4aSElliott Hughes            # on-curve point between the last and the first off-curve points
335*e1fe3e4aSElliott Hughes            # explicit.
336*e1fe3e4aSElliott Hughes            x, y = points[-2]  # last off-curve point
337*e1fe3e4aSElliott Hughes            nx, ny = points[0]  # first off-curve point
338*e1fe3e4aSElliott Hughes            impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny))
339*e1fe3e4aSElliott Hughes            self.__currentPoint = impliedStartPoint
340*e1fe3e4aSElliott Hughes            self._moveTo(impliedStartPoint)
341*e1fe3e4aSElliott Hughes            points = points[:-1] + (impliedStartPoint,)
342*e1fe3e4aSElliott Hughes        if n > 0:
343*e1fe3e4aSElliott Hughes            # Split the string of points into discrete quadratic curve
344*e1fe3e4aSElliott Hughes            # segments. Between any two consecutive off-curve points
345*e1fe3e4aSElliott Hughes            # there's an implied on-curve point exactly in the middle.
346*e1fe3e4aSElliott Hughes            # This is where the segment splits.
347*e1fe3e4aSElliott Hughes            _qCurveToOne = self._qCurveToOne
348*e1fe3e4aSElliott Hughes            for pt1, pt2 in decomposeQuadraticSegment(points):
349*e1fe3e4aSElliott Hughes                _qCurveToOne(pt1, pt2)
350*e1fe3e4aSElliott Hughes                self.__currentPoint = pt2
351*e1fe3e4aSElliott Hughes        else:
352*e1fe3e4aSElliott Hughes            self.lineTo(points[0])
353*e1fe3e4aSElliott Hughes
354*e1fe3e4aSElliott Hughes
355*e1fe3e4aSElliott Hughesdef decomposeSuperBezierSegment(points):
356*e1fe3e4aSElliott Hughes    """Split the SuperBezier described by 'points' into a list of regular
357*e1fe3e4aSElliott Hughes    bezier segments. The 'points' argument must be a sequence with length
358*e1fe3e4aSElliott Hughes    3 or greater, containing (x, y) coordinates. The last point is the
359*e1fe3e4aSElliott Hughes    destination on-curve point, the rest of the points are off-curve points.
360*e1fe3e4aSElliott Hughes    The start point should not be supplied.
361*e1fe3e4aSElliott Hughes
362*e1fe3e4aSElliott Hughes    This function returns a list of (pt1, pt2, pt3) tuples, which each
363*e1fe3e4aSElliott Hughes    specify a regular curveto-style bezier segment.
364*e1fe3e4aSElliott Hughes    """
365*e1fe3e4aSElliott Hughes    n = len(points) - 1
366*e1fe3e4aSElliott Hughes    assert n > 1
367*e1fe3e4aSElliott Hughes    bezierSegments = []
368*e1fe3e4aSElliott Hughes    pt1, pt2, pt3 = points[0], None, None
369*e1fe3e4aSElliott Hughes    for i in range(2, n + 1):
370*e1fe3e4aSElliott Hughes        # calculate points in between control points.
371*e1fe3e4aSElliott Hughes        nDivisions = min(i, 3, n - i + 2)
372*e1fe3e4aSElliott Hughes        for j in range(1, nDivisions):
373*e1fe3e4aSElliott Hughes            factor = j / nDivisions
374*e1fe3e4aSElliott Hughes            temp1 = points[i - 1]
375*e1fe3e4aSElliott Hughes            temp2 = points[i - 2]
376*e1fe3e4aSElliott Hughes            temp = (
377*e1fe3e4aSElliott Hughes                temp2[0] + factor * (temp1[0] - temp2[0]),
378*e1fe3e4aSElliott Hughes                temp2[1] + factor * (temp1[1] - temp2[1]),
379*e1fe3e4aSElliott Hughes            )
380*e1fe3e4aSElliott Hughes            if pt2 is None:
381*e1fe3e4aSElliott Hughes                pt2 = temp
382*e1fe3e4aSElliott Hughes            else:
383*e1fe3e4aSElliott Hughes                pt3 = (0.5 * (pt2[0] + temp[0]), 0.5 * (pt2[1] + temp[1]))
384*e1fe3e4aSElliott Hughes                bezierSegments.append((pt1, pt2, pt3))
385*e1fe3e4aSElliott Hughes                pt1, pt2, pt3 = temp, None, None
386*e1fe3e4aSElliott Hughes    bezierSegments.append((pt1, points[-2], points[-1]))
387*e1fe3e4aSElliott Hughes    return bezierSegments
388*e1fe3e4aSElliott Hughes
389*e1fe3e4aSElliott Hughes
390*e1fe3e4aSElliott Hughesdef decomposeQuadraticSegment(points):
391*e1fe3e4aSElliott Hughes    """Split the quadratic curve segment described by 'points' into a list
392*e1fe3e4aSElliott Hughes    of "atomic" quadratic segments. The 'points' argument must be a sequence
393*e1fe3e4aSElliott Hughes    with length 2 or greater, containing (x, y) coordinates. The last point
394*e1fe3e4aSElliott Hughes    is the destination on-curve point, the rest of the points are off-curve
395*e1fe3e4aSElliott Hughes    points. The start point should not be supplied.
396*e1fe3e4aSElliott Hughes
397*e1fe3e4aSElliott Hughes    This function returns a list of (pt1, pt2) tuples, which each specify a
398*e1fe3e4aSElliott Hughes    plain quadratic bezier segment.
399*e1fe3e4aSElliott Hughes    """
400*e1fe3e4aSElliott Hughes    n = len(points) - 1
401*e1fe3e4aSElliott Hughes    assert n > 0
402*e1fe3e4aSElliott Hughes    quadSegments = []
403*e1fe3e4aSElliott Hughes    for i in range(n - 1):
404*e1fe3e4aSElliott Hughes        x, y = points[i]
405*e1fe3e4aSElliott Hughes        nx, ny = points[i + 1]
406*e1fe3e4aSElliott Hughes        impliedPt = (0.5 * (x + nx), 0.5 * (y + ny))
407*e1fe3e4aSElliott Hughes        quadSegments.append((points[i], impliedPt))
408*e1fe3e4aSElliott Hughes    quadSegments.append((points[-2], points[-1]))
409*e1fe3e4aSElliott Hughes    return quadSegments
410*e1fe3e4aSElliott Hughes
411*e1fe3e4aSElliott Hughes
412*e1fe3e4aSElliott Hughesclass _TestPen(BasePen):
413*e1fe3e4aSElliott Hughes    """Test class that prints PostScript to stdout."""
414*e1fe3e4aSElliott Hughes
415*e1fe3e4aSElliott Hughes    def _moveTo(self, pt):
416*e1fe3e4aSElliott Hughes        print("%s %s moveto" % (pt[0], pt[1]))
417*e1fe3e4aSElliott Hughes
418*e1fe3e4aSElliott Hughes    def _lineTo(self, pt):
419*e1fe3e4aSElliott Hughes        print("%s %s lineto" % (pt[0], pt[1]))
420*e1fe3e4aSElliott Hughes
421*e1fe3e4aSElliott Hughes    def _curveToOne(self, bcp1, bcp2, pt):
422*e1fe3e4aSElliott Hughes        print(
423*e1fe3e4aSElliott Hughes            "%s %s %s %s %s %s curveto"
424*e1fe3e4aSElliott Hughes            % (bcp1[0], bcp1[1], bcp2[0], bcp2[1], pt[0], pt[1])
425*e1fe3e4aSElliott Hughes        )
426*e1fe3e4aSElliott Hughes
427*e1fe3e4aSElliott Hughes    def _closePath(self):
428*e1fe3e4aSElliott Hughes        print("closepath")
429*e1fe3e4aSElliott Hughes
430*e1fe3e4aSElliott Hughes
431*e1fe3e4aSElliott Hughesif __name__ == "__main__":
432*e1fe3e4aSElliott Hughes    pen = _TestPen(None)
433*e1fe3e4aSElliott Hughes    pen.moveTo((0, 0))
434*e1fe3e4aSElliott Hughes    pen.lineTo((0, 100))
435*e1fe3e4aSElliott Hughes    pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0))
436*e1fe3e4aSElliott Hughes    pen.closePath()
437*e1fe3e4aSElliott Hughes
438*e1fe3e4aSElliott Hughes    pen = _TestPen(None)
439*e1fe3e4aSElliott Hughes    # testing the "no on-curve point" scenario
440*e1fe3e4aSElliott Hughes    pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None)
441*e1fe3e4aSElliott Hughes    pen.closePath()
442