| 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 filterContour(self, contour): |
| return reversedContour(contour) |
| |
| |
| def reversedContour(contour): |
| """ 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 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", () |