xref: /aosp_15_r20/external/fonttools/Lib/fontTools/pens/reverseContourPen.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.misc.arrayTools import pairwise
2from fontTools.pens.filterPen import ContourFilterPen
3
4
5__all__ = ["reversedContour", "ReverseContourPen"]
6
7
8class ReverseContourPen(ContourFilterPen):
9    """Filter pen that passes outline data to another pen, but reversing
10    the winding direction of all contours. Components are simply passed
11    through unchanged.
12
13    Closed contours are reversed in such a way that the first point remains
14    the first point.
15    """
16
17    def __init__(self, outPen, outputImpliedClosingLine=False):
18        super().__init__(outPen)
19        self.outputImpliedClosingLine = outputImpliedClosingLine
20
21    def filterContour(self, contour):
22        return reversedContour(contour, self.outputImpliedClosingLine)
23
24
25def reversedContour(contour, outputImpliedClosingLine=False):
26    """Generator that takes a list of pen's (operator, operands) tuples,
27    and yields them with the winding direction reversed.
28    """
29    if not contour:
30        return  # nothing to do, stop iteration
31
32    # valid contours must have at least a starting and ending command,
33    # can't have one without the other
34    assert len(contour) > 1, "invalid contour"
35
36    # the type of the last command determines if the contour is closed
37    contourType = contour.pop()[0]
38    assert contourType in ("endPath", "closePath")
39    closed = contourType == "closePath"
40
41    firstType, firstPts = contour.pop(0)
42    assert firstType in ("moveTo", "qCurveTo"), (
43        "invalid initial segment type: %r" % firstType
44    )
45    firstOnCurve = firstPts[-1]
46    if firstType == "qCurveTo":
47        # special case for TrueType paths contaning only off-curve points
48        assert firstOnCurve is None, "off-curve only paths must end with 'None'"
49        assert not contour, "only one qCurveTo allowed per off-curve path"
50        firstPts = (firstPts[0],) + tuple(reversed(firstPts[1:-1])) + (None,)
51
52    if not contour:
53        # contour contains only one segment, nothing to reverse
54        if firstType == "moveTo":
55            closed = False  # single-point paths can't be closed
56        else:
57            closed = True  # off-curve paths are closed by definition
58        yield firstType, firstPts
59    else:
60        lastType, lastPts = contour[-1]
61        lastOnCurve = lastPts[-1]
62        if closed:
63            # for closed paths, we keep the starting point
64            yield firstType, firstPts
65            if firstOnCurve != lastOnCurve:
66                # emit an implied line between the last and first points
67                yield "lineTo", (lastOnCurve,)
68                contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,))
69
70            if len(contour) > 1:
71                secondType, secondPts = contour[0]
72            else:
73                # contour has only two points, the second and last are the same
74                secondType, secondPts = lastType, lastPts
75
76            if not outputImpliedClosingLine:
77                # if a lineTo follows the initial moveTo, after reversing it
78                # will be implied by the closePath, so we don't emit one;
79                # unless the lineTo and moveTo overlap, in which case we keep the
80                # duplicate points
81                if secondType == "lineTo" and firstPts != secondPts:
82                    del contour[0]
83                    if contour:
84                        contour[-1] = (lastType, tuple(lastPts[:-1]) + secondPts)
85        else:
86            # for open paths, the last point will become the first
87            yield firstType, (lastOnCurve,)
88            contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,))
89
90        # we iterate over all segment pairs in reverse order, and yield
91        # each one with the off-curve points reversed (if any), and
92        # with the on-curve point of the following segment
93        for (curType, curPts), (_, nextPts) in pairwise(contour, reverse=True):
94            yield curType, tuple(reversed(curPts[:-1])) + (nextPts[-1],)
95
96    yield "closePath" if closed else "endPath", ()
97