diff --git a/Lib/fontTools/pens/pointPen.py b/Lib/fontTools/pens/pointPen.py new file mode 100644 index 000000000..0d06554cc --- /dev/null +++ b/Lib/fontTools/pens/pointPen.py @@ -0,0 +1,407 @@ +""" +========= +PointPens +========= + +Where **SegmentPens** have an intuitive approach to drawing +(if you're familiar with postscript anyway), the **PointPen** +is geared towards accessing all the data in the contours of +the glyph. A PointPen has a very simple interface, it just +steps through all the points in a call from glyph.drawPoints(). +This allows the caller to provide more data for each point. +For instance, whether or not a point is smooth, and its name. +""" +from __future__ import absolute_import, unicode_literals +from fontTools.pens.basePen import AbstractPen +import math + +__all__ = ["AbstractPointPen", "BasePointToSegmentPen", "PointToSegmentPen", + "SegmentToPointPen"] + + +class AbstractPointPen(object): + """ + Baseclass for all PointPens. + """ + + def beginPath(self, identifier=None, **kwargs): + """Start a new sub path.""" + raise NotImplementedError + + def endPath(self): + """End the current sub path.""" + raise NotImplementedError + + def addPoint(self, pt, segmentType=None, smooth=False, name=None, + identifier=None, **kwargs): + """Add a point to the current sub path.""" + raise NotImplementedError + + def addComponent(self, baseGlyphName, transformation, identifier=None, + **kwargs): + """Add a sub glyph.""" + raise NotImplementedError + + +class BasePointToSegmentPen(AbstractPointPen): + """ + Base class for retrieving the outline in a segment-oriented + way. The PointPen protocol is simple yet also a little tricky, + so when you need an outline presented as segments but you have + as points, do use this base implementation as it properly takes + care of all the edge cases. + """ + + def __init__(self): + self.currentPath = None + + def beginPath(self, **kwargs): + assert self.currentPath is None + self.currentPath = [] + + def _flushContour(self, segments): + """Override this method. + + It will be called for each non-empty sub path with a list + of segments: the 'segments' argument. + + The segments list contains tuples of length 2: + (segmentType, points) + + segmentType is one of "move", "line", "curve" or "qcurve". + "move" may only occur as the first segment, and it signifies + an OPEN path. A CLOSED path does NOT start with a "move", in + fact it will not contain a "move" at ALL. + + The 'points' field in the 2-tuple is a list of point info + tuples. The list has 1 or more items, a point tuple has + four items: + (point, smooth, name, kwargs) + 'point' is an (x, y) coordinate pair. + + For a closed path, the initial moveTo point is defined as + the last point of the last segment. + + The 'points' list of "move" and "line" segments always contains + exactly one point tuple. + """ + raise NotImplementedError + + def endPath(self): + assert self.currentPath is not None + points = self.currentPath + self.currentPath = None + if not points: + return + if len(points) == 1: + # Not much more we can do than output a single move segment. + pt, segmentType, smooth, name, kwargs = points[0] + segments = [("move", [(pt, smooth, name, kwargs)])] + self._flushContour(segments) + return + segments = [] + if points[0][1] == "move": + # It's an open contour, insert a "move" segment for the first + # point and remove that first point from the point list. + pt, segmentType, smooth, name, kwargs = points[0] + segments.append(("move", [(pt, smooth, name, kwargs)])) + points.pop(0) + else: + # It's a closed contour. Locate the first on-curve point, and + # rotate the point list so that it _ends_ with an on-curve + # point. + firstOnCurve = None + for i in range(len(points)): + segmentType = points[i][1] + if segmentType is not None: + firstOnCurve = i + break + if firstOnCurve is None: + # Special case for quadratics: a contour with no on-curve + # points. Add a "None" point. (See also the Pen protocol's + # qCurveTo() method and fontTools.pens.basePen.py.) + points.append((None, "qcurve", None, None, None)) + else: + points = points[firstOnCurve+1:] + points[:firstOnCurve+1] + + currentSegment = [] + for pt, segmentType, smooth, name, kwargs in points: + currentSegment.append((pt, smooth, name, kwargs)) + if segmentType is None: + continue + segments.append((segmentType, currentSegment)) + currentSegment = [] + + self._flushContour(segments) + + def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + self.currentPath.append((pt, segmentType, smooth, name, kwargs)) + + +class PointToSegmentPen(BasePointToSegmentPen): + """ + Adapter class that converts the PointPen protocol to the + (Segment)Pen protocol. + """ + + def __init__(self, segmentPen, outputImpliedClosingLine=False): + BasePointToSegmentPen.__init__(self) + self.pen = segmentPen + self.outputImpliedClosingLine = outputImpliedClosingLine + + def _flushContour(self, segments): + assert len(segments) >= 1 + pen = self.pen + if segments[0][0] == "move": + # It's an open path. + closed = False + points = segments[0][1] + assert len(points) == 1, "illegal move segment point count: %d" % len(points) + movePt, smooth, name, kwargs = points[0] + del segments[0] + else: + # It's a closed path, do a moveTo to the last + # point of the last segment. + closed = True + segmentType, points = segments[-1] + movePt, smooth, name, kwargs = points[-1] + if movePt is None: + # quad special case: a contour with no on-curve points contains + # one "qcurve" segment that ends with a point that's None. We + # must not output a moveTo() in that case. + pass + else: + pen.moveTo(movePt) + outputImpliedClosingLine = self.outputImpliedClosingLine + nSegments = len(segments) + for i in range(nSegments): + segmentType, points = segments[i] + points = [pt for pt, smooth, name, kwargs in points] + if segmentType == "line": + assert len(points) == 1, "illegal line segment point count: %d" % len(points) + pt = points[0] + if i + 1 != nSegments or outputImpliedClosingLine or not closed: + pen.lineTo(pt) + elif segmentType == "curve": + pen.curveTo(*points) + elif segmentType == "qcurve": + pen.qCurveTo(*points) + else: + assert 0, "illegal segmentType: %s" % segmentType + if closed: + pen.closePath() + else: + pen.endPath() + + def addComponent(self, glyphName, transform, **kwargs): + self.pen.addComponent(glyphName, transform) + + +class SegmentToPointPen(AbstractPen): + """ + Adapter class that converts the (Segment)Pen protocol to the + PointPen protocol. + """ + + def __init__(self, pointPen, guessSmooth=True): + if guessSmooth: + self.pen = GuessSmoothPointPen(pointPen) + else: + self.pen = pointPen + self.contour = None + + def _flushContour(self): + pen = self.pen + pen.beginPath() + for pt, segmentType in self.contour: + pen.addPoint(pt, segmentType=segmentType) + pen.endPath() + + def moveTo(self, pt): + self.contour = [] + self.contour.append((pt, "move")) + + def lineTo(self, pt): + self.contour.append((pt, "line")) + + def curveTo(self, *pts): + for pt in pts[:-1]: + self.contour.append((pt, None)) + self.contour.append((pts[-1], "curve")) + + def qCurveTo(self, *pts): + if pts[-1] is None: + self.contour = [] + for pt in pts[:-1]: + self.contour.append((pt, None)) + if pts[-1] is not None: + self.contour.append((pts[-1], "qcurve")) + + def closePath(self): + if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]: + self.contour[0] = self.contour[-1] + del self.contour[-1] + else: + # There's an implied line at the end, replace "move" with "line" + # for the first point + pt, tp = self.contour[0] + if tp == "move": + self.contour[0] = pt, "line" + self._flushContour() + self.contour = None + + def endPath(self): + self._flushContour() + self.contour = None + + def addComponent(self, glyphName, transform): + assert self.contour is None + self.pen.addComponent(glyphName, transform) + + +class GuessSmoothPointPen(AbstractPointPen): + """ + Filtering PointPen that tries to determine whether an on-curve point + should be "smooth", ie. that it's a "tangent" point or a "curve" point. + """ + + def __init__(self, outPen): + self._outPen = outPen + self._points = None + + def _flushContour(self): + points = self._points + nPoints = len(points) + if not nPoints: + return + if points[0][1] == "move": + # Open path. + indices = range(1, nPoints - 1) + elif nPoints > 1: + # Closed path. To avoid having to mod the contour index, we + # simply abuse Python's negative index feature, and start at -1 + indices = range(-1, nPoints - 1) + else: + # closed path containing 1 point (!), ignore. + indices = [] + for i in indices: + pt, segmentType, dummy, name, kwargs = points[i] + if segmentType is None: + continue + prev = i - 1 + next = i + 1 + if points[prev][1] is not None and points[next][1] is not None: + continue + # At least one of our neighbors is an off-curve point + pt = points[i][0] + prevPt = points[prev][0] + nextPt = points[next][0] + if pt != prevPt and pt != nextPt: + dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1] + dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1] + a1 = math.atan2(dx1, dy1) + a2 = math.atan2(dx2, dy2) + if abs(a1 - a2) < 0.05: + points[i] = pt, segmentType, True, name, kwargs + + for pt, segmentType, smooth, name, kwargs in points: + self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) + + def beginPath(self): + assert self._points is None + self._points = [] + self._outPen.beginPath() + + def endPath(self): + self._flushContour() + self._outPen.endPath() + self._points = None + + def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + self._points.append((pt, segmentType, False, name, kwargs)) + + def addComponent(self, glyphName, transformation): + assert self._points is None + self._outPen.addComponent(glyphName, transformation) + + +class ReverseContourPointPen(AbstractPointPen): + """ + This is a PointPen that passes outline data to another PointPen, 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, outputPointPen): + self.pen = outputPointPen + # a place to store the points for the current sub path + self.currentContour = None + + def _flushContour(self): + pen = self.pen + contour = self.currentContour + if not contour: + pen.beginPath(identifier=self.currentContourIdentifier) + pen.endPath() + return + + closed = contour[0][1] != "move" + if not closed: + lastSegmentType = "move" + else: + # Remove the first point and insert it at the end. When + # the list of points gets reversed, this point will then + # again be at the start. In other words, the following + # will hold: + # for N in range(len(originalContour)): + # originalContour[N] == reversedContour[-N] + contour.append(contour.pop(0)) + # Find the first on-curve point. + firstOnCurve = None + for i in range(len(contour)): + if contour[i][1] is not None: + firstOnCurve = i + break + if firstOnCurve is None: + # There are no on-curve points, be basically have to + # do nothing but contour.reverse(). + lastSegmentType = None + else: + lastSegmentType = contour[firstOnCurve][1] + + contour.reverse() + if not closed: + # Open paths must start with a move, so we simply dump + # all off-curve points leading up to the first on-curve. + while contour[0][1] is None: + contour.pop(0) + pen.beginPath(identifier=self.currentContourIdentifier) + for pt, nextSegmentType, smooth, name, kwargs in contour: + if nextSegmentType is not None: + segmentType = lastSegmentType + lastSegmentType = nextSegmentType + else: + segmentType = None + pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs) + pen.endPath() + + def beginPath(self, identifier=None, **kwargs): + assert self.currentContour is None + self.currentContour = [] + self.currentContourIdentifier = identifier + self.onCurve = [] + + def endPath(self): + assert self.currentContour is not None + self._flushContour() + self.currentContour = None + + def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): + self.currentContour.append((pt, segmentType, smooth, name, kwargs)) + + def addComponent(self, glyphName, transform, identifier=None, **kwargs): + assert self.currentContour is None + self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs) diff --git a/Lib/fontTools/ufoLib/pointPen.py b/Lib/fontTools/ufoLib/pointPen.py index 0d06554cc..3433fdbc9 100644 --- a/Lib/fontTools/ufoLib/pointPen.py +++ b/Lib/fontTools/ufoLib/pointPen.py @@ -1,407 +1,5 @@ +"""DEPRECATED - This module is kept here only as a backward compatibility shim +for the old ufoLib.pointPen module, which was moved to fontTools.pens.pointPen. +Please use the latter instead. """ -========= -PointPens -========= - -Where **SegmentPens** have an intuitive approach to drawing -(if you're familiar with postscript anyway), the **PointPen** -is geared towards accessing all the data in the contours of -the glyph. A PointPen has a very simple interface, it just -steps through all the points in a call from glyph.drawPoints(). -This allows the caller to provide more data for each point. -For instance, whether or not a point is smooth, and its name. -""" -from __future__ import absolute_import, unicode_literals -from fontTools.pens.basePen import AbstractPen -import math - -__all__ = ["AbstractPointPen", "BasePointToSegmentPen", "PointToSegmentPen", - "SegmentToPointPen"] - - -class AbstractPointPen(object): - """ - Baseclass for all PointPens. - """ - - def beginPath(self, identifier=None, **kwargs): - """Start a new sub path.""" - raise NotImplementedError - - def endPath(self): - """End the current sub path.""" - raise NotImplementedError - - def addPoint(self, pt, segmentType=None, smooth=False, name=None, - identifier=None, **kwargs): - """Add a point to the current sub path.""" - raise NotImplementedError - - def addComponent(self, baseGlyphName, transformation, identifier=None, - **kwargs): - """Add a sub glyph.""" - raise NotImplementedError - - -class BasePointToSegmentPen(AbstractPointPen): - """ - Base class for retrieving the outline in a segment-oriented - way. The PointPen protocol is simple yet also a little tricky, - so when you need an outline presented as segments but you have - as points, do use this base implementation as it properly takes - care of all the edge cases. - """ - - def __init__(self): - self.currentPath = None - - def beginPath(self, **kwargs): - assert self.currentPath is None - self.currentPath = [] - - def _flushContour(self, segments): - """Override this method. - - It will be called for each non-empty sub path with a list - of segments: the 'segments' argument. - - The segments list contains tuples of length 2: - (segmentType, points) - - segmentType is one of "move", "line", "curve" or "qcurve". - "move" may only occur as the first segment, and it signifies - an OPEN path. A CLOSED path does NOT start with a "move", in - fact it will not contain a "move" at ALL. - - The 'points' field in the 2-tuple is a list of point info - tuples. The list has 1 or more items, a point tuple has - four items: - (point, smooth, name, kwargs) - 'point' is an (x, y) coordinate pair. - - For a closed path, the initial moveTo point is defined as - the last point of the last segment. - - The 'points' list of "move" and "line" segments always contains - exactly one point tuple. - """ - raise NotImplementedError - - def endPath(self): - assert self.currentPath is not None - points = self.currentPath - self.currentPath = None - if not points: - return - if len(points) == 1: - # Not much more we can do than output a single move segment. - pt, segmentType, smooth, name, kwargs = points[0] - segments = [("move", [(pt, smooth, name, kwargs)])] - self._flushContour(segments) - return - segments = [] - if points[0][1] == "move": - # It's an open contour, insert a "move" segment for the first - # point and remove that first point from the point list. - pt, segmentType, smooth, name, kwargs = points[0] - segments.append(("move", [(pt, smooth, name, kwargs)])) - points.pop(0) - else: - # It's a closed contour. Locate the first on-curve point, and - # rotate the point list so that it _ends_ with an on-curve - # point. - firstOnCurve = None - for i in range(len(points)): - segmentType = points[i][1] - if segmentType is not None: - firstOnCurve = i - break - if firstOnCurve is None: - # Special case for quadratics: a contour with no on-curve - # points. Add a "None" point. (See also the Pen protocol's - # qCurveTo() method and fontTools.pens.basePen.py.) - points.append((None, "qcurve", None, None, None)) - else: - points = points[firstOnCurve+1:] + points[:firstOnCurve+1] - - currentSegment = [] - for pt, segmentType, smooth, name, kwargs in points: - currentSegment.append((pt, smooth, name, kwargs)) - if segmentType is None: - continue - segments.append((segmentType, currentSegment)) - currentSegment = [] - - self._flushContour(segments) - - def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): - self.currentPath.append((pt, segmentType, smooth, name, kwargs)) - - -class PointToSegmentPen(BasePointToSegmentPen): - """ - Adapter class that converts the PointPen protocol to the - (Segment)Pen protocol. - """ - - def __init__(self, segmentPen, outputImpliedClosingLine=False): - BasePointToSegmentPen.__init__(self) - self.pen = segmentPen - self.outputImpliedClosingLine = outputImpliedClosingLine - - def _flushContour(self, segments): - assert len(segments) >= 1 - pen = self.pen - if segments[0][0] == "move": - # It's an open path. - closed = False - points = segments[0][1] - assert len(points) == 1, "illegal move segment point count: %d" % len(points) - movePt, smooth, name, kwargs = points[0] - del segments[0] - else: - # It's a closed path, do a moveTo to the last - # point of the last segment. - closed = True - segmentType, points = segments[-1] - movePt, smooth, name, kwargs = points[-1] - if movePt is None: - # quad special case: a contour with no on-curve points contains - # one "qcurve" segment that ends with a point that's None. We - # must not output a moveTo() in that case. - pass - else: - pen.moveTo(movePt) - outputImpliedClosingLine = self.outputImpliedClosingLine - nSegments = len(segments) - for i in range(nSegments): - segmentType, points = segments[i] - points = [pt for pt, smooth, name, kwargs in points] - if segmentType == "line": - assert len(points) == 1, "illegal line segment point count: %d" % len(points) - pt = points[0] - if i + 1 != nSegments or outputImpliedClosingLine or not closed: - pen.lineTo(pt) - elif segmentType == "curve": - pen.curveTo(*points) - elif segmentType == "qcurve": - pen.qCurveTo(*points) - else: - assert 0, "illegal segmentType: %s" % segmentType - if closed: - pen.closePath() - else: - pen.endPath() - - def addComponent(self, glyphName, transform, **kwargs): - self.pen.addComponent(glyphName, transform) - - -class SegmentToPointPen(AbstractPen): - """ - Adapter class that converts the (Segment)Pen protocol to the - PointPen protocol. - """ - - def __init__(self, pointPen, guessSmooth=True): - if guessSmooth: - self.pen = GuessSmoothPointPen(pointPen) - else: - self.pen = pointPen - self.contour = None - - def _flushContour(self): - pen = self.pen - pen.beginPath() - for pt, segmentType in self.contour: - pen.addPoint(pt, segmentType=segmentType) - pen.endPath() - - def moveTo(self, pt): - self.contour = [] - self.contour.append((pt, "move")) - - def lineTo(self, pt): - self.contour.append((pt, "line")) - - def curveTo(self, *pts): - for pt in pts[:-1]: - self.contour.append((pt, None)) - self.contour.append((pts[-1], "curve")) - - def qCurveTo(self, *pts): - if pts[-1] is None: - self.contour = [] - for pt in pts[:-1]: - self.contour.append((pt, None)) - if pts[-1] is not None: - self.contour.append((pts[-1], "qcurve")) - - def closePath(self): - if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]: - self.contour[0] = self.contour[-1] - del self.contour[-1] - else: - # There's an implied line at the end, replace "move" with "line" - # for the first point - pt, tp = self.contour[0] - if tp == "move": - self.contour[0] = pt, "line" - self._flushContour() - self.contour = None - - def endPath(self): - self._flushContour() - self.contour = None - - def addComponent(self, glyphName, transform): - assert self.contour is None - self.pen.addComponent(glyphName, transform) - - -class GuessSmoothPointPen(AbstractPointPen): - """ - Filtering PointPen that tries to determine whether an on-curve point - should be "smooth", ie. that it's a "tangent" point or a "curve" point. - """ - - def __init__(self, outPen): - self._outPen = outPen - self._points = None - - def _flushContour(self): - points = self._points - nPoints = len(points) - if not nPoints: - return - if points[0][1] == "move": - # Open path. - indices = range(1, nPoints - 1) - elif nPoints > 1: - # Closed path. To avoid having to mod the contour index, we - # simply abuse Python's negative index feature, and start at -1 - indices = range(-1, nPoints - 1) - else: - # closed path containing 1 point (!), ignore. - indices = [] - for i in indices: - pt, segmentType, dummy, name, kwargs = points[i] - if segmentType is None: - continue - prev = i - 1 - next = i + 1 - if points[prev][1] is not None and points[next][1] is not None: - continue - # At least one of our neighbors is an off-curve point - pt = points[i][0] - prevPt = points[prev][0] - nextPt = points[next][0] - if pt != prevPt and pt != nextPt: - dx1, dy1 = pt[0] - prevPt[0], pt[1] - prevPt[1] - dx2, dy2 = nextPt[0] - pt[0], nextPt[1] - pt[1] - a1 = math.atan2(dx1, dy1) - a2 = math.atan2(dx2, dy2) - if abs(a1 - a2) < 0.05: - points[i] = pt, segmentType, True, name, kwargs - - for pt, segmentType, smooth, name, kwargs in points: - self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs) - - def beginPath(self): - assert self._points is None - self._points = [] - self._outPen.beginPath() - - def endPath(self): - self._flushContour() - self._outPen.endPath() - self._points = None - - def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): - self._points.append((pt, segmentType, False, name, kwargs)) - - def addComponent(self, glyphName, transformation): - assert self._points is None - self._outPen.addComponent(glyphName, transformation) - - -class ReverseContourPointPen(AbstractPointPen): - """ - This is a PointPen that passes outline data to another PointPen, 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, outputPointPen): - self.pen = outputPointPen - # a place to store the points for the current sub path - self.currentContour = None - - def _flushContour(self): - pen = self.pen - contour = self.currentContour - if not contour: - pen.beginPath(identifier=self.currentContourIdentifier) - pen.endPath() - return - - closed = contour[0][1] != "move" - if not closed: - lastSegmentType = "move" - else: - # Remove the first point and insert it at the end. When - # the list of points gets reversed, this point will then - # again be at the start. In other words, the following - # will hold: - # for N in range(len(originalContour)): - # originalContour[N] == reversedContour[-N] - contour.append(contour.pop(0)) - # Find the first on-curve point. - firstOnCurve = None - for i in range(len(contour)): - if contour[i][1] is not None: - firstOnCurve = i - break - if firstOnCurve is None: - # There are no on-curve points, be basically have to - # do nothing but contour.reverse(). - lastSegmentType = None - else: - lastSegmentType = contour[firstOnCurve][1] - - contour.reverse() - if not closed: - # Open paths must start with a move, so we simply dump - # all off-curve points leading up to the first on-curve. - while contour[0][1] is None: - contour.pop(0) - pen.beginPath(identifier=self.currentContourIdentifier) - for pt, nextSegmentType, smooth, name, kwargs in contour: - if nextSegmentType is not None: - segmentType = lastSegmentType - lastSegmentType = nextSegmentType - else: - segmentType = None - pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name, **kwargs) - pen.endPath() - - def beginPath(self, identifier=None, **kwargs): - assert self.currentContour is None - self.currentContour = [] - self.currentContourIdentifier = identifier - self.onCurve = [] - - def endPath(self): - assert self.currentContour is not None - self._flushContour() - self.currentContour = None - - def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): - self.currentContour.append((pt, segmentType, smooth, name, kwargs)) - - def addComponent(self, glyphName, transform, identifier=None, **kwargs): - assert self.currentContour is None - self.pen.addComponent(glyphName, transform, identifier=identifier, **kwargs) +from fontTools.pens.pointPen import *