reverseContourPen.py 3.9 KB

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