Spaces:
Sleeping
Sleeping
| 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", () | |