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