Spaces:
Running
Running
from fontTools.misc.arrayTools import pairwise | |
from fontTools.pens.filterPen import ContourFilterPen | |
__all__ = ["reversedContour", "ReverseContourPen"] | |
class ReverseContourPen(ContourFilterPen): | |
"""Filter pen that passes outline data to another pen, but reversing | |
the winding direction of all contours. Components are simply passed | |
through unchanged. | |
Closed contours are reversed in such a way that the first point remains | |
the first point. | |
""" | |
def __init__(self, outPen, outputImpliedClosingLine=False): | |
super().__init__(outPen) | |
self.outputImpliedClosingLine = outputImpliedClosingLine | |
def filterContour(self, contour): | |
return reversedContour(contour, self.outputImpliedClosingLine) | |
def reversedContour(contour, outputImpliedClosingLine=False): | |
"""Generator that takes a list of pen's (operator, operands) tuples, | |
and yields them with the winding direction reversed. | |
""" | |
if not contour: | |
return # nothing to do, stop iteration | |
# valid contours must have at least a starting and ending command, | |
# can't have one without the other | |
assert len(contour) > 1, "invalid contour" | |
# the type of the last command determines if the contour is closed | |
contourType = contour.pop()[0] | |
assert contourType in ("endPath", "closePath") | |
closed = contourType == "closePath" | |
firstType, firstPts = contour.pop(0) | |
assert firstType in ("moveTo", "qCurveTo"), ( | |
"invalid initial segment type: %r" % firstType | |
) | |
firstOnCurve = firstPts[-1] | |
if firstType == "qCurveTo": | |
# special case for TrueType paths contaning only off-curve points | |
assert firstOnCurve is None, "off-curve only paths must end with 'None'" | |
assert not contour, "only one qCurveTo allowed per off-curve path" | |
firstPts = (firstPts[0],) + tuple(reversed(firstPts[1:-1])) + (None,) | |
if not contour: | |
# contour contains only one segment, nothing to reverse | |
if firstType == "moveTo": | |
closed = False # single-point paths can't be closed | |
else: | |
closed = True # off-curve paths are closed by definition | |
yield firstType, firstPts | |
else: | |
lastType, lastPts = contour[-1] | |
lastOnCurve = lastPts[-1] | |
if closed: | |
# for closed paths, we keep the starting point | |
yield firstType, firstPts | |
if firstOnCurve != lastOnCurve: | |
# emit an implied line between the last and first points | |
yield "lineTo", (lastOnCurve,) | |
contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,)) | |
if len(contour) > 1: | |
secondType, secondPts = contour[0] | |
else: | |
# contour has only two points, the second and last are the same | |
secondType, secondPts = lastType, lastPts | |
if not outputImpliedClosingLine: | |
# if a lineTo follows the initial moveTo, after reversing it | |
# will be implied by the closePath, so we don't emit one; | |
# unless the lineTo and moveTo overlap, in which case we keep the | |
# duplicate points | |
if secondType == "lineTo" and firstPts != secondPts: | |
del contour[0] | |
if contour: | |
contour[-1] = (lastType, tuple(lastPts[:-1]) + secondPts) | |
else: | |
# for open paths, the last point will become the first | |
yield firstType, (lastOnCurve,) | |
contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,)) | |
# we iterate over all segment pairs in reverse order, and yield | |
# each one with the off-curve points reversed (if any), and | |
# with the on-curve point of the following segment | |
for (curType, curPts), (_, nextPts) in pairwise(contour, reverse=True): | |
yield curType, tuple(reversed(curPts[:-1])) + (nextPts[-1],) | |
yield "closePath" if closed else "endPath", () | |