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