Previously, for closed paths, we were always dropping a lineTo segment that followed moveTo, because after reversing the contour this lineTo would become the last segment, and in the Pen protocol a closePath always implies a line to the fist point. This is OK when the move point and the following lineTo oncurve point (which becomes last after reversal) don't overlap. However, if they do, we ended up dropping the duplicate point. This cu2qu issue exemplify the problem (cu2qu actually uses the ReverseContourPointPen wrapped by ufoLib's converter pens, but fontTools' ReverseContourPen does exactly the same): https://github.com/googlei18n/cu2qu/issues/51 With this patch, the ReverseContourPen now emits the last lineTo when it is the same as moveTo.
98 lines
3.9 KiB
Python
98 lines
3.9 KiB
Python
from __future__ import print_function, division, absolute_import
|
|
from fontTools.misc.py23 import *
|
|
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", ()
|