From 67414f21ff9b350f894a80fad85dbb6a20d78313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20T=C3=A9tar?= Date: Fri, 13 Nov 2015 10:02:35 +0100 Subject: [PATCH] pointPen: add adapter pens and annihilate remaining robofab dependency --- Lib/ufoLib/glifLib.py | 3 +- Lib/ufoLib/pointPen.py | 222 +++++++++++++++++++++++++++++++++- Lib/ufoLib/test/test_GLIF1.py | 2 +- Lib/ufoLib/test/test_GLIF2.py | 2 +- Lib/ufoLib/test/test_UFO1.py | 2 +- Lib/ufoLib/test/test_UFO2.py | 2 +- Lib/ufoLib/test/test_UFO3.py | 2 +- 7 files changed, 226 insertions(+), 9 deletions(-) diff --git a/Lib/ufoLib/glifLib.py b/Lib/ufoLib/glifLib.py index ad6293e37..7efea20dc 100755 --- a/Lib/ufoLib/glifLib.py +++ b/Lib/ufoLib/glifLib.py @@ -17,7 +17,7 @@ from io import BytesIO, StringIO, open from warnings import warn from fontTools.misc.py23 import tobytes from ufoLib.xmlTreeBuilder import buildTree, stripCharacterData -from ufoLib.pointPen import AbstractPointPen +from ufoLib.pointPen import AbstractPointPen, PointToSegmentPen from ufoLib.filenames import userNameToFileName from ufoLib.validators import isDictEnough, genericTypeValidator, colorValidator,\ guidelinesValidator, anchorsValidator, identifierValidator, imageValidator, glyphLibValidator @@ -75,7 +75,6 @@ class Glyph(object): """ Draw this glyph onto a *FontTools* Pen. """ - from robofab.pens.adapterPens import PointToSegmentPen pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) diff --git a/Lib/ufoLib/pointPen.py b/Lib/ufoLib/pointPen.py index 07da3ff8f..2497c016a 100644 --- a/Lib/ufoLib/pointPen.py +++ b/Lib/ufoLib/pointPen.py @@ -11,11 +11,13 @@ 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 fontTools.pens.basePen import AbstractPen + +__all__ = ["AbstractPointPen", "BasePointToSegmentPen", "PointToSegmentPen", + "SegmentToPointPen"] -__all__ = ["AbstractPointPen"] class AbstractPointPen(object): - """ Baseclass for all PointPens. """ @@ -35,3 +37,219 @@ class AbstractPointPen(object): def addComponent(self, baseGlyphName, transformation): """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 + 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 + 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): + 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) diff --git a/Lib/ufoLib/test/test_GLIF1.py b/Lib/ufoLib/test/test_GLIF1.py index 2642fb39e..1ec9181a1 100644 --- a/Lib/ufoLib/test/test_GLIF1.py +++ b/Lib/ufoLib/test/test_GLIF1.py @@ -1253,5 +1253,5 @@ class TestGLIF1(unittest.TestCase): if __name__ == "__main__": - from robofab.test.testSupport import runTests + from ufoLib.test.testSupport import runTests runTests() diff --git a/Lib/ufoLib/test/test_GLIF2.py b/Lib/ufoLib/test/test_GLIF2.py index 08f74dd67..3f8a8b1ec 100644 --- a/Lib/ufoLib/test/test_GLIF2.py +++ b/Lib/ufoLib/test/test_GLIF2.py @@ -2275,5 +2275,5 @@ class TestGLIF2(unittest.TestCase): if __name__ == "__main__": - from robofab.test.testSupport import runTests + from ufoLib.test.testSupport import runTests runTests() diff --git a/Lib/ufoLib/test/test_UFO1.py b/Lib/ufoLib/test/test_UFO1.py index 55df98096..0be2859c5 100644 --- a/Lib/ufoLib/test/test_UFO1.py +++ b/Lib/ufoLib/test/test_UFO1.py @@ -157,5 +157,5 @@ class WriteFontInfoVersion1TestCase(unittest.TestCase): if __name__ == "__main__": - from robofab.test.testSupport import runTests + from ufoLib.test.testSupport import runTests runTests() diff --git a/Lib/ufoLib/test/test_UFO2.py b/Lib/ufoLib/test/test_UFO2.py index 4fd6d483e..d7d92f08b 100644 --- a/Lib/ufoLib/test/test_UFO2.py +++ b/Lib/ufoLib/test/test_UFO2.py @@ -1419,5 +1419,5 @@ class WriteFontInfoVersion2TestCase(unittest.TestCase): if __name__ == "__main__": - from robofab.test.testSupport import runTests + from ufoLib.test.testSupport import runTests runTests() diff --git a/Lib/ufoLib/test/test_UFO3.py b/Lib/ufoLib/test/test_UFO3.py index 56b5bac32..0ecf90185 100644 --- a/Lib/ufoLib/test/test_UFO3.py +++ b/Lib/ufoLib/test/test_UFO3.py @@ -4671,5 +4671,5 @@ class UFO3WriteLayerInfoTestCase(unittest.TestCase): self.assertRaises(GlifLibError, glyphSet.writeLayerInfo, info) if __name__ == "__main__": - from robofab.test.testSupport import runTests + from ufoLib.test.testSupport import runTests runTests()