summaryrefslogtreecommitdiff
path: root/misc/pylib/robofab/pens
diff options
context:
space:
mode:
authorRasmus Andersson <rasmus@notion.se>2017-09-04 06:03:17 +0300
committerRasmus Andersson <rasmus@notion.se>2017-09-04 18:12:34 +0300
commit8234b62ab762637ef24c3398b4204a8ce8db31a7 (patch)
tree1c8df547021cdb58951630a015e4101ede46dbf1 /misc/pylib/robofab/pens
parent31ae014e0c827dd76696fdab7e4ca3fed9f6402b (diff)
downloadinter-8234b62ab762637ef24c3398b4204a8ce8db31a7.tar.xz
Speeds up font compilation by around 200%
Cython is used to compile some hot paths into native Python extensions. These hot paths were identified through running ufocompile with the hotshot profiler and then converting file by file to Cython, starting with the "hottest" paths and continuing until returns were deminishing. This means that only a few Python files were converted to Cython. Closes #23 Closes #20 (really this time)
Diffstat (limited to 'misc/pylib/robofab/pens')
-rwxr-xr-xmisc/pylib/robofab/pens/__init__.py11
-rw-r--r--misc/pylib/robofab/pens/adapterPens.py293
-rw-r--r--misc/pylib/robofab/pens/angledMarginPen.py132
-rw-r--r--misc/pylib/robofab/pens/boundsPen.pyx95
-rwxr-xr-xmisc/pylib/robofab/pens/digestPen.py106
-rwxr-xr-xmisc/pylib/robofab/pens/filterPen.py407
-rwxr-xr-xmisc/pylib/robofab/pens/flPen.py274
-rw-r--r--misc/pylib/robofab/pens/marginPen.py155
-rwxr-xr-xmisc/pylib/robofab/pens/mathPens.py185
-rw-r--r--misc/pylib/robofab/pens/pointPen.py173
-rw-r--r--misc/pylib/robofab/pens/quartzPen.py21
-rwxr-xr-xmisc/pylib/robofab/pens/reverseContourPointPen.py125
-rwxr-xr-xmisc/pylib/robofab/pens/rfUFOPen.pyx103
13 files changed, 2080 insertions, 0 deletions
diff --git a/misc/pylib/robofab/pens/__init__.py b/misc/pylib/robofab/pens/__init__.py
new file mode 100755
index 000000000..a5afc0dc3
--- /dev/null
+++ b/misc/pylib/robofab/pens/__init__.py
@@ -0,0 +1,11 @@
+"""
+
+Directory for all pen modules.
+If you make a pen, put it here so that we can keep track of it.
+
+
+"""
+
+
+
+
diff --git a/misc/pylib/robofab/pens/adapterPens.py b/misc/pylib/robofab/pens/adapterPens.py
new file mode 100644
index 000000000..0cd9ae3f7
--- /dev/null
+++ b/misc/pylib/robofab/pens/adapterPens.py
@@ -0,0 +1,293 @@
+import math
+from fontTools.pens.basePen import AbstractPen
+from robofab.pens.pointPen import AbstractPointPen, BasePointToSegmentPen
+
+
+class FabToFontToolsPenAdapter:
+
+ """Class that covers up the subtle differences between RoboFab
+ Pens and FontTools Pens. 'Fab should eventually move to FontTools
+ Pens, this class may help to make the transition smoother.
+ """
+
+ # XXX The change to FontTools pens has almost been completed. Any
+ # usage of this class should be highly suspect.
+
+ def __init__(self, fontToolsPen):
+ self.fontToolsPen = fontToolsPen
+
+ def moveTo(self, pt, **kargs):
+ self.fontToolsPen.moveTo(pt)
+
+ def lineTo(self, pt, **kargs):
+ self.fontToolsPen.lineTo(pt)
+
+ def curveTo(self, *pts, **kargs):
+ self.fontToolsPen.curveTo(*pts)
+
+ def qCurveTo(self, *pts, **kargs):
+ self.fontToolsPen.qCurveTo(*pts)
+
+ def closePath(self):
+ self.fontToolsPen.closePath()
+
+ def endPath(self):
+ self.fontToolsPen.endPath()
+
+ def addComponent(self, glyphName, offset=(0, 0), scale=(1, 1)):
+ self.fontToolsPen.addComponent(glyphName,
+ (scale[0], 0, 0, scale[1], offset[0], offset[1]))
+
+ def setWidth(self, width):
+ self.width = width
+
+ def setNote(self, note):
+ pass
+
+ def addAnchor(self, name, pt):
+ self.fontToolsPen.moveTo(pt)
+ self.fontToolsPen.endPath()
+
+ def doneDrawing(self):
+ pass
+
+
+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)
+
+
+class TransformPointPen(AbstractPointPen):
+
+ """PointPen that transforms all coordinates, and passes them to another
+ PointPen. It also transforms the transformation given to addComponent().
+ """
+
+ def __init__(self, outPen, transformation):
+ if not hasattr(transformation, "transformPoint"):
+ from fontTools.misc.transform import Transform
+ transformation = Transform(*transformation)
+ self._transformation = transformation
+ self._transformPoint = transformation.transformPoint
+ self._outPen = outPen
+ self._stack = []
+
+ def beginPath(self):
+ self._outPen.beginPath()
+
+ def endPath(self):
+ self._outPen.endPath()
+
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
+ pt = self._transformPoint(pt)
+ self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
+
+ def addComponent(self, glyphName, transformation):
+ transformation = self._transformation.transform(transformation)
+ self._outPen.addComponent(glyphName, transformation)
+
+
+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)
+
+
+if __name__ == "__main__":
+ from fontTools.pens.basePen import _TestPen as PSPen
+ from robofab.pens.pointPen import PrintingPointPen
+ segmentPen = PSPen(None)
+# pen = PointToSegmentPen(SegmentToPointPen(PointToSegmentPen(PSPen(None))))
+ pen = PointToSegmentPen(SegmentToPointPen(PrintingPointPen()))
+# pen = PrintingPointPen()
+ pen = PointToSegmentPen(PSPen(None), outputImpliedClosingLine=False)
+# pen.beginPath()
+# pen.addPoint((50, 50), name="an anchor")
+# pen.endPath()
+ pen.beginPath()
+ pen.addPoint((-100, 0), segmentType="line")
+ pen.addPoint((0, 0), segmentType="line")
+ pen.addPoint((0, 100), segmentType="line")
+ pen.addPoint((30, 200))
+ pen.addPoint((50, 100), name="superbezcontrolpoint!")
+ pen.addPoint((70, 200))
+ pen.addPoint((100, 100), segmentType="curve")
+ pen.addPoint((100, 0), segmentType="line")
+ pen.endPath()
+# pen.addComponent("a", (1, 0, 0, 1, 100, 200))
diff --git a/misc/pylib/robofab/pens/angledMarginPen.py b/misc/pylib/robofab/pens/angledMarginPen.py
new file mode 100644
index 000000000..49ff8eed3
--- /dev/null
+++ b/misc/pylib/robofab/pens/angledMarginPen.py
@@ -0,0 +1,132 @@
+from robofab.world import RFont
+from fontTools.pens.basePen import BasePen
+from robofab.misc.arrayTools import updateBounds, pointInRect, unionRect
+from robofab.misc.bezierTools import calcCubicBounds, calcQuadraticBounds
+from robofab.pens.filterPen import _estimateCubicCurveLength, _getCubicPoint
+import math
+
+
+
+__all__ = ["AngledMarginPen", "getAngledMargins",
+ "setAngledLeftMargin", "setAngledRightMargin",
+ "centerAngledMargins"]
+
+
+
+class AngledMarginPen(BasePen):
+ """
+ Angled Margin Pen
+
+ Pen to calculate the margins as if the margin lines were slanted
+ according to the font.info.italicAngle.
+
+ Notes:
+ - this pen works on the on-curve points, and approximates the distance to curves.
+ - results will be float.
+ - when used in FontLab, the resulting margins may be slightly
+ different from the values originally set, due to rounding errors.
+ - similar to what RoboFog used to do.
+ - RoboFog had a special attribute for "italicoffset", horizontal
+ shift of all glyphs. This is missing in Robofab.
+ """
+ def __init__(self, glyphSet, width, italicAngle):
+ BasePen.__init__(self, glyphSet)
+ self.width = width
+ self._angle = math.radians(90+italicAngle)
+ self.maxSteps = 100
+ self.margin = None
+ self._left = None
+ self._right = None
+ self._start = None
+ self.currentPt = None
+
+ def _getAngled(self, pt):
+ r = (g.width + (pt[1] / math.tan(self._angle)))-pt[0]
+ l = pt[0]-((pt[1] / math.tan(self._angle)))
+ if self._right is None:
+ self._right = r
+ else:
+ self._right = min(self._right, r)
+ if self._left is None:
+ self._left = l
+ else:
+ self._left = min(self._left, l)
+ #print pt, l, r
+ self.margin = self._left, self._right
+
+ def _moveTo(self, pt):
+ self._start = self.currentPt = pt
+
+ def _addMoveTo(self):
+ if self._start is None:
+ return
+ self._start = self.currentPt = None
+
+ def _lineTo(self, pt):
+ self._addMoveTo()
+ self._getAngled(pt)
+
+ def _curveToOne(self, pt1, pt2, pt3):
+ step = 1.0/self.maxSteps
+ factors = range(0, self.maxSteps+1)
+ for i in factors:
+ pt = _getCubicPoint(i*step, self.currentPt, pt1, pt2, pt3)
+ self._getAngled(pt)
+ self.currentPt = pt3
+
+ def _qCurveToOne(self, bcp, pt):
+ self._addMoveTo()
+ # add curve tracing magic here.
+ self._getAngled(pt)
+ self.currentPt = pt3
+
+def getAngledMargins(glyph, font):
+ """Get the angled margins for this glyph."""
+ pen = AngledMarginPen(font, glyph.width, font.info.italicAngle)
+ glyph.draw(pen)
+ return pen.margin
+
+def setAngledLeftMargin(glyph, font, value):
+ """Set the left angled margin to value, adjusted for font.info.italicAngle."""
+ pen = AngledMarginPen(font, glyph.width, font.info.italicAngle)
+ g.draw(pen)
+ isLeft, isRight = pen.margin
+ glyph.leftMargin += value-isLeft
+
+def setAngledRightMargin(glyph, font, value):
+ """Set the right angled margin to value, adjusted for font.info.italicAngle."""
+ pen = AngledMarginPen(font, glyph.width, font.info.italicAngle)
+ g.draw(pen)
+ isLeft, isRight = pen.margin
+ glyph.rightMargin += value-isRight
+
+def centerAngledMargins(glyph, font):
+ """Center the glyph on angled margins."""
+ pen = AngledMarginPen(font, glyph.width, font.info.italicAngle)
+ g.draw(pen)
+ isLeft, isRight = pen.margin
+ setAngledLeftMargin(glyph, font, (isLeft+isRight)*.5)
+ setAngledRightMargin(glyph, font, (isLeft+isRight)*.5)
+
+def guessItalicOffset(glyph, font):
+ """Guess the italic offset based on the margins of a symetric glyph.
+ For instance H or I.
+ """
+ l, r = getAngledMargins(glyph, font)
+ return l - (l+r)*.5
+
+
+if __name__ == "__main__":
+
+ # example for FontLab, with a glyph open.
+ from robofab.world import CurrentFont, CurrentGlyph
+ g = CurrentGlyph()
+ f = CurrentFont()
+
+ print "margins!", getAngledMargins(g, f)
+ # set the angled margin to a value
+ m = 50
+ setAngledLeftMargin(g, f, m)
+ setAngledRightMargin(g, f, m)
+ g.update()
+
diff --git a/misc/pylib/robofab/pens/boundsPen.pyx b/misc/pylib/robofab/pens/boundsPen.pyx
new file mode 100644
index 000000000..a1a72bd12
--- /dev/null
+++ b/misc/pylib/robofab/pens/boundsPen.pyx
@@ -0,0 +1,95 @@
+from fontTools.pens.basePen import BasePen
+from robofab.misc.arrayTools import updateBounds, pointInRect, unionRect
+from robofab.misc.bezierTools import calcCubicBounds, calcQuadraticBounds
+
+
+__all__ = ["BoundsPen", "ControlBoundsPen"]
+
+
+class ControlBoundsPen(BasePen):
+
+ """Pen to calculate the "control bounds" of a shape. This is the
+ bounding box of all control points __on closed paths__, so may be larger than the
+ actual bounding box if there are curves that don't have points
+ on their extremes.
+
+ Single points, or anchors, are ignored.
+
+ When the shape has been drawn, the bounds are available as the
+ 'bounds' attribute of the pen object. It's a 4-tuple:
+ (xMin, yMin, xMax, yMax)
+
+ This replaces fontTools/pens/boundsPen (temporarily?)
+ The fontTools bounds pen takes lose anchor points into account,
+ this one doesn't.
+ """
+
+ def __init__(self, glyphSet):
+ BasePen.__init__(self, glyphSet)
+ self.bounds = None
+ self._start = None
+
+ def _moveTo(self, pt):
+ self._start = pt
+
+ def _addMoveTo(self):
+ if self._start is None:
+ return
+ bounds = self.bounds
+ if bounds:
+ self.bounds = updateBounds(bounds, self._start)
+ else:
+ x, y = self._start
+ self.bounds = (x, y, x, y)
+ self._start = None
+
+ def _lineTo(self, pt):
+ self._addMoveTo()
+ self.bounds = updateBounds(self.bounds, pt)
+
+ def _curveToOne(self, bcp1, bcp2, pt):
+ self._addMoveTo()
+ bounds = self.bounds
+ bounds = updateBounds(bounds, bcp1)
+ bounds = updateBounds(bounds, bcp2)
+ bounds = updateBounds(bounds, pt)
+ self.bounds = bounds
+
+ def _qCurveToOne(self, bcp, pt):
+ self._addMoveTo()
+ bounds = self.bounds
+ bounds = updateBounds(bounds, bcp)
+ bounds = updateBounds(bounds, pt)
+ self.bounds = bounds
+
+
+class BoundsPen(ControlBoundsPen):
+
+ """Pen to calculate the bounds of a shape. It calculates the
+ correct bounds even when the shape contains curves that don't
+ have points on their extremes. This is somewhat slower to compute
+ than the "control bounds".
+
+ When the shape has been drawn, the bounds are available as the
+ 'bounds' attribute of the pen object. It's a 4-tuple:
+ (xMin, yMin, xMax, yMax)
+ """
+
+ def _curveToOne(self, bcp1, bcp2, pt):
+ self._addMoveTo()
+ bounds = self.bounds
+ bounds = updateBounds(bounds, pt)
+ if not pointInRect(bcp1, bounds) or not pointInRect(bcp2, bounds):
+ bounds = unionRect(bounds, calcCubicBounds(
+ self._getCurrentPoint(), bcp1, bcp2, pt))
+ self.bounds = bounds
+
+ def _qCurveToOne(self, bcp, pt):
+ self._addMoveTo()
+ bounds = self.bounds
+ bounds = updateBounds(bounds, pt)
+ if not pointInRect(bcp, bounds):
+ bounds = unionRect(bounds, calcQuadraticBounds(
+ self._getCurrentPoint(), bcp, pt))
+ self.bounds = bounds
+
diff --git a/misc/pylib/robofab/pens/digestPen.py b/misc/pylib/robofab/pens/digestPen.py
new file mode 100755
index 000000000..930daf468
--- /dev/null
+++ b/misc/pylib/robofab/pens/digestPen.py
@@ -0,0 +1,106 @@
+"""A couple of point pens which return the glyph as a list of basic values."""
+
+
+from robofab.pens.pointPen import AbstractPointPen
+
+
+class DigestPointPen(AbstractPointPen):
+
+ """Calculate a digest of all points
+ AND coordinates
+ AND components
+ in a glyph.
+ """
+
+ def __init__(self, ignoreSmoothAndName=False):
+ self._data = []
+ self.ignoreSmoothAndName = ignoreSmoothAndName
+
+ def beginPath(self):
+ self._data.append('beginPath')
+
+ def endPath(self):
+ self._data.append('endPath')
+
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
+ if self.ignoreSmoothAndName:
+ self._data.append((pt, segmentType))
+ else:
+ self._data.append((pt, segmentType, smooth, name))
+
+ def addComponent(self, baseGlyphName, transformation):
+ t = []
+ for v in transformation:
+ if int(v) == v:
+ t.append(int(v))
+ else:
+ t.append(v)
+ self._data.append((baseGlyphName, tuple(t)))
+
+ def getDigest(self):
+ return tuple(self._data)
+
+ def getDigestPointsOnly(self, needSort=True):
+ """ Return a tuple with all coordinates of all points,
+ but without smooth info or drawing instructions.
+ For instance if you want to compare 2 glyphs in shape,
+ but not interpolatability.
+ """
+ points = []
+ from types import TupleType
+ for item in self._data:
+ if type(item) == TupleType:
+ points.append(item[0])
+ if needSort:
+ points.sort()
+ return tuple(points)
+
+
+class DigestPointStructurePen(DigestPointPen):
+
+ """Calculate a digest of the structure of the glyph
+ NOT coordinates
+ NOT values.
+ """
+
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
+ self._data.append(segmentType)
+
+ def addComponent(self, baseGlyphName, transformation):
+ self._data.append(baseGlyphName)
+
+if __name__ == "__main__":
+ """
+
+ beginPath
+ ((112, 651), 'line', False, None)
+ ((112, 55), 'line', False, None)
+ ((218, 55), 'line', False, None)
+ ((218, 651), 'line', False, None)
+ endPath
+
+ """
+ # a test
+
+ from robofab.objects.objectsRF import RGlyph
+
+ g = RGlyph()
+ p = g.getPen()
+ p.moveTo((112, 651))
+ p.lineTo((112, 55))
+ p.lineTo((218, 55))
+ p.lineTo((218, 651))
+ p.closePath()
+
+ print g, len(g)
+
+ digestPen = DigestPointPen()
+ g.drawPoints(digestPen)
+
+ print
+ print "getDigest", digestPen.getDigest()
+
+ print
+ print "getDigestPointsOnly", digestPen.getDigestPointsOnly()
+
+ \ No newline at end of file
diff --git a/misc/pylib/robofab/pens/filterPen.py b/misc/pylib/robofab/pens/filterPen.py
new file mode 100755
index 000000000..f7c84c082
--- /dev/null
+++ b/misc/pylib/robofab/pens/filterPen.py
@@ -0,0 +1,407 @@
+"""A couple of point pens to filter contours in various ways."""
+
+from fontTools.pens.basePen import AbstractPen, BasePen
+
+from robofab.pens.pointPen import AbstractPointPen
+from robofab.objects.objectsRF import RGlyph as _RGlyph
+from robofab.objects.objectsBase import _interpolatePt
+
+import math
+
+#
+# threshold filtering
+#
+
+def distance(pt1, pt2):
+ return math.hypot(pt1[0]-pt2[0], pt1[1]-pt2[1])
+
+class ThresholdPointPen(AbstractPointPen):
+
+ """
+ Rewrite of the ThresholdPen as a PointPen
+ so that we can preserve named points and other arguments.
+ This pen will add components from the original glyph, but
+ but it won't filter those components.
+
+ "move", "line", "curve" or "qcurve"
+
+ """
+ def __init__(self, otherPointPen, threshold=10):
+ self.threshold = threshold
+ self._lastPt = None
+ self._offCurveBuffer = []
+ self.otherPointPen = otherPointPen
+
+ def beginPath(self):
+ """Start a new sub path."""
+ self.otherPointPen.beginPath()
+ self._lastPt = None
+
+ def endPath(self):
+ """End the current sub path."""
+ self.otherPointPen.endPath()
+
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
+ """Add a point to the current sub path."""
+ if segmentType in ['curve', 'qcurve']:
+ # it's an offcurve, let's buffer them until we get another oncurve
+ # and we know what to do with them
+ self._offCurveBuffer.append((pt, segmentType, smooth, name, kwargs))
+ return
+
+ elif segmentType == "move":
+ # start of an open contour
+ self.otherPointPen.addPoint(pt, segmentType, smooth, name) # how to add kwargs?
+ self._lastPt = pt
+ self._offCurveBuffer = []
+
+ elif segmentType == "line":
+ if self._lastPt is None:
+ self.otherPointPen.addPoint(pt, segmentType, smooth, name) # how to add kwargs?
+ self._lastPt = pt
+ elif distance(pt, self._lastPt) >= self.threshold:
+ # we're oncurve and far enough from the last oncurve
+ if self._offCurveBuffer:
+ # empty any buffered offcurves
+ for buf_pt, buf_segmentType, buf_smooth, buf_name, buf_kwargs in self._offCurveBuffer:
+ self.otherPointPen.addPoint(buf_pt, buf_segmentType, buf_smooth, buf_name) # how to add kwargs?
+ self._offCurveBuffer = []
+ # finally add the oncurve.
+ self.otherPointPen.addPoint(pt, segmentType, smooth, name) # how to add kwargs?
+ self._lastPt = pt
+ else:
+ # we're too short, so we're not going to make it.
+ # we need to clear out the offcurve buffer.
+ self._offCurveBuffer = []
+
+ def addComponent(self, baseGlyphName, transformation):
+ """Add a sub glyph. Note: this way components are not filtered."""
+ self.otherPointPen.addComponent(baseGlyphName, transformation)
+
+
+class ThresholdPen(AbstractPen):
+
+ """Removes segments shorter in length than the threshold value."""
+
+ def __init__(self, otherPen, threshold=10):
+ self.threshold = threshold
+ self._lastPt = None
+ self.otherPen = otherPen
+
+ def moveTo(self, pt):
+ self._lastPt = pt
+ self.otherPen.moveTo(pt)
+
+ def lineTo(self, pt, smooth=False):
+ if self.threshold <= distance(pt, self._lastPt):
+ self.otherPen.lineTo(pt)
+ self._lastPt = pt
+
+ def curveTo(self, pt1, pt2, pt3):
+ if self.threshold <= distance(pt3, self._lastPt):
+ self.otherPen.curveTo(pt1, pt2, pt3)
+ self._lastPt = pt3
+
+ def qCurveTo(self, *points):
+ if self.threshold <= distance(points[-1], self._lastPt):
+ self.otherPen.qCurveTo(*points)
+ self._lastPt = points[-1]
+
+ def closePath(self):
+ self.otherPen.closePath()
+
+ def endPath(self):
+ self.otherPen.endPath()
+
+ def addComponent(self, glyphName, transformation):
+ self.otherPen.addComponent(glyphName, transformation)
+
+
+def thresholdGlyph(aGlyph, threshold=10):
+ """ Convenience function that handles the filtering. """
+ from robofab.pens.adapterPens import PointToSegmentPen
+ new = _RGlyph()
+ filterpen = ThresholdPen(new.getPen(), threshold)
+ wrappedPen = PointToSegmentPen(filterpen)
+ aGlyph.drawPoints(wrappedPen)
+ aGlyph.clear()
+ aGlyph.appendGlyph(new)
+ aGlyph.update()
+ return aGlyph
+
+def thresholdGlyphPointPen(aGlyph, threshold=10):
+ """ Same a thresholdGlyph, but using the ThresholdPointPen, which should respect anchors."""
+ from robofab.pens.adapterPens import PointToSegmentPen
+ new = _RGlyph()
+ wrappedPen = new.getPointPen()
+ filterpen = ThresholdPointPen(wrappedPen, threshold)
+ aGlyph.drawPoints(filterpen)
+ aGlyph.clear()
+ new.drawPoints(aGlyph.getPointPen())
+ aGlyph.update()
+ return aGlyph
+
+
+#
+# Curve flattening
+#
+
+def _estimateCubicCurveLength(pt0, pt1, pt2, pt3, precision=10):
+ """Estimate the length of this curve by iterating
+ through it and averaging the length of the flat bits.
+ """
+ points = []
+ length = 0
+ step = 1.0/precision
+ factors = range(0, precision+1)
+ for i in factors:
+ points.append(_getCubicPoint(i*step, pt0, pt1, pt2, pt3))
+ for i in range(len(points)-1):
+ pta = points[i]
+ ptb = points[i+1]
+ length += distance(pta, ptb)
+ return length
+
+def _mid((x0, y0), (x1, y1)):
+ """(Point, Point) -> Point\nReturn the point that lies in between the two input points."""
+ return 0.5 * (x0 + x1), 0.5 * (y0 + y1)
+
+def _getCubicPoint(t, pt0, pt1, pt2, pt3):
+ if t == 0:
+ return pt0
+ if t == 1:
+ return pt3
+ if t == 0.5:
+ a = _mid(pt0, pt1)
+ b = _mid(pt1, pt2)
+ c = _mid(pt2, pt3)
+ d = _mid(a, b)
+ e = _mid(b, c)
+ return _mid(d, e)
+ else:
+ cx = (pt1[0] - pt0[0]) * 3
+ cy = (pt1[1] - pt0[1]) * 3
+ bx = (pt2[0] - pt1[0]) * 3 - cx
+ by = (pt2[1] - pt1[1]) * 3 - cy
+ ax = pt3[0] - pt0[0] - cx - bx
+ ay = pt3[1] - pt0[1] - cy - by
+ t3 = t ** 3
+ t2 = t * t
+ x = ax * t3 + bx * t2 + cx * t + pt0[0]
+ y = ay * t3 + by * t2 + cy * t + pt0[1]
+ return x, y
+
+
+class FlattenPen(BasePen):
+
+ """Process the contours into a series of straight lines by flattening the curves.
+ """
+
+ def __init__(self, otherPen, approximateSegmentLength=5, segmentLines=False, filterDoubles=True):
+ self.approximateSegmentLength = approximateSegmentLength
+ BasePen.__init__(self, {})
+ self.otherPen = otherPen
+ self.currentPt = None
+ self.firstPt = None
+ self.segmentLines = segmentLines
+ self.filterDoubles = filterDoubles
+
+ def _moveTo(self, pt):
+ self.otherPen.moveTo(pt)
+ self.currentPt = pt
+ self.firstPt = pt
+
+ def _lineTo(self, pt):
+ if self.filterDoubles:
+ if pt == self.currentPt:
+ return
+ if not self.segmentLines:
+ self.otherPen.lineTo(pt)
+ self.currentPt = pt
+ return
+ d = distance(self.currentPt, pt)
+ maxSteps = int(round(d / self.approximateSegmentLength))
+ if maxSteps < 1:
+ self.otherPen.lineTo(pt)
+ self.currentPt = pt
+ return
+ step = 1.0/maxSteps
+ factors = range(0, maxSteps+1)
+ for i in factors[1:]:
+ self.otherPen.lineTo(_interpolatePt(self.currentPt, pt, i*step))
+ self.currentPt = pt
+
+ def _curveToOne(self, pt1, pt2, pt3):
+ est = _estimateCubicCurveLength(self.currentPt, pt1, pt2, pt3)/self.approximateSegmentLength
+ maxSteps = int(round(est))
+ falseCurve = (pt1==self.currentPt) and (pt2==pt3)
+ if maxSteps < 1 or falseCurve:
+ self.otherPen.lineTo(pt3)
+ self.currentPt = pt3
+ return
+ step = 1.0/maxSteps
+ factors = range(0, maxSteps+1)
+ for i in factors[1:]:
+ pt = _getCubicPoint(i*step, self.currentPt, pt1, pt2, pt3)
+ self.otherPen.lineTo(pt)
+ self.currentPt = pt3
+
+ def _closePath(self):
+ self.lineTo(self.firstPt)
+ self.otherPen.closePath()
+ self.currentPt = None
+
+ def _endPath(self):
+ self.otherPen.endPath()
+ self.currentPt = None
+
+ def addComponent(self, glyphName, transformation):
+ self.otherPen.addComponent(glyphName, transformation)
+
+
+def flattenGlyph(aGlyph, threshold=10, segmentLines=True):
+
+ """Replace curves with series of straight l ines."""
+
+ from robofab.pens.adapterPens import PointToSegmentPen
+ if len(aGlyph.contours) == 0:
+ return
+ new = _RGlyph()
+ writerPen = new.getPen()
+ filterpen = FlattenPen(writerPen, threshold, segmentLines)
+ wrappedPen = PointToSegmentPen(filterpen)
+ aGlyph.drawPoints(wrappedPen)
+ aGlyph.clear()
+ aGlyph.appendGlyph(new)
+ aGlyph.update()
+ return aGlyph
+
+
+def spikeGlyph(aGlyph, segmentLength=20, spikeLength=40, patternFunc=None):
+
+ """Add narly spikes or dents to the glyph.
+ patternFunc is an optional function which recalculates the offset."""
+
+ from math import atan2, sin, cos, pi
+
+ new = _RGlyph()
+ new.appendGlyph(aGlyph)
+ new.width = aGlyph.width
+
+ if len(new.contours) == 0:
+ return
+ flattenGlyph(new, segmentLength, segmentLines=True)
+ for contour in new:
+ l = len(contour.points)
+ lastAngle = None
+ for i in range(0, len(contour.points), 2):
+ prev = contour.points[i-1]
+ cur = contour.points[i]
+ next = contour.points[(i+1)%l]
+ angle = atan2(prev.x - next.x, prev.y - next.y)
+ lastAngle = angle
+ if patternFunc is not None:
+ thisSpikeLength = patternFunc(spikeLength)
+ else:
+ thisSpikeLength = spikeLength
+ cur.x -= sin(angle+.5*pi)*thisSpikeLength
+ cur.y -= cos(angle+.5*pi)*thisSpikeLength
+ new.update()
+ aGlyph.clear()
+ aGlyph.appendGlyph(new)
+ aGlyph.update()
+ return aGlyph
+
+
+def halftoneGlyph(aGlyph, invert=False):
+
+ """Convert the glyph into some sort of halftoning pattern.
+ Measure a bunch of inside/outside points to simulate grayscale levels.
+ Slow.
+ """
+ print 'halftoneGlyph is running...'
+ grid = {}
+ drawing = {}
+ dataDistance = 10
+ scan = 2
+ preload = 0
+ cellDistance = dataDistance * 5
+ overshoot = dataDistance * 2
+ (xMin, yMin, xMax, yMax) = aGlyph.box
+ for x in range(xMin-overshoot, xMax+overshoot, dataDistance):
+ print 'scanning..', x
+ for y in range(yMin-overshoot, yMax+overshoot, dataDistance):
+ if aGlyph.pointInside((x, y)):
+ grid[(x, y)] = True
+ else:
+ grid[(x, y)] = False
+ #print 'gathering data', x, y, grid[(x, y)]
+ print 'analyzing..'
+ for x in range(xMin-overshoot, xMax+overshoot, cellDistance):
+ for y in range(yMin-overshoot, yMax+overshoot, cellDistance):
+ total = preload
+ for scanx in range(-scan, scan):
+ for scany in range(-scan, scan):
+ if grid.get((x+scanx*dataDistance, y+scany*dataDistance)):
+ total += 1
+ if invert:
+ drawing[(x, y)] = 2*scan**2 - float(total)
+ else:
+ drawing[(x, y)] = float(total)
+ aGlyph.clear()
+ print drawing
+ for (x,y) in drawing.keys():
+ size = drawing[(x,y)] / float(2*scan**2) * 5
+ pen = aGlyph.getPen()
+ pen.moveTo((x-size, y-size))
+ pen.lineTo((x+size, y-size))
+ pen.lineTo((x+size, y+size))
+ pen.lineTo((x-size, y+size))
+ pen.lineTo((x-size, y-size))
+ pen.closePath()
+ aGlyph.update()
+
+
+if __name__ == "__main__":
+ from robofab.pens.pointPen import PrintingPointPen
+ pp = PrintingPointPen()
+ #pp.beginPath()
+ #pp.addPoint((100, 100))
+ #pp.endPath()
+
+ tpp = ThresholdPointPen(pp, threshold=20)
+ tpp.beginPath()
+ #segmentType=None, smooth=False, name=None
+ tpp.addPoint((100, 100), segmentType="line", smooth=True)
+ # section that should be too small
+ tpp.addPoint((100, 102), segmentType="line", smooth=True)
+ tpp.addPoint((200, 200), segmentType="line", smooth=True)
+ # curve section with final point that's far enough, but with offcurves that are under the threshold
+ tpp.addPoint((200, 205), segmentType="curve", smooth=True)
+ tpp.addPoint((300, 295), segmentType="curve", smooth=True)
+ tpp.addPoint((300, 300), segmentType="line", smooth=True)
+ # curve section with final point that is not far enough
+ tpp.addPoint((550, 350), segmentType="curve", smooth=True)
+ tpp.addPoint((360, 760), segmentType="curve", smooth=True)
+ tpp.addPoint((310, 310), segmentType="line", smooth=True)
+
+ tpp.addPoint((400, 400), segmentType="line", smooth=True)
+ tpp.addPoint((100, 100), segmentType="line", smooth=True)
+ tpp.endPath()
+
+ # couple of single points with names
+ tpp.beginPath()
+ tpp.addPoint((500, 500), segmentType="move", smooth=True, name="named point")
+ tpp.addPoint((600, 500), segmentType="move", smooth=True, name="named point")
+ tpp.addPoint((601, 501), segmentType="move", smooth=True, name="named point")
+ tpp.endPath()
+
+ # open path
+ tpp.beginPath()
+ tpp.addPoint((500, 500), segmentType="move", smooth=True)
+ tpp.addPoint((501, 500), segmentType="line", smooth=True)
+ tpp.addPoint((101, 500), segmentType="line", smooth=True)
+ tpp.addPoint((101, 100), segmentType="line", smooth=True)
+ tpp.addPoint((498, 498), segmentType="line", smooth=True)
+ tpp.endPath()
+ \ No newline at end of file
diff --git a/misc/pylib/robofab/pens/flPen.py b/misc/pylib/robofab/pens/flPen.py
new file mode 100755
index 000000000..d5a867cd9
--- /dev/null
+++ b/misc/pylib/robofab/pens/flPen.py
@@ -0,0 +1,274 @@
+"""Pens for creating glyphs in FontLab."""
+
+
+__all__ = ["FLPen", "FLPointPen", "drawFLGlyphOntoPointPen"]
+
+
+from FL import *
+
+try:
+ from fl_cmd import *
+except ImportError:
+ print "The fl_cmd module is not available here. flPen.py"
+
+from robofab.tools.toolsFL import NewGlyph
+from robofab.pens.pointPen import AbstractPointPen
+from robofab.pens.adapterPens import SegmentToPointPen
+
+
+def roundInt(x):
+ return int(round(x))
+
+
+class FLPen(SegmentToPointPen):
+
+ def __init__(self, glyph):
+ SegmentToPointPen.__init__(self, FLPointPen(glyph))
+
+
+class FLPointPen(AbstractPointPen):
+
+ def __init__(self, glyph):
+ if hasattr(glyph, "isRobofab"):
+ self.glyph = glyph.naked()
+ else:
+ self.glyph = glyph
+ self.currentPath = None
+
+ def beginPath(self):
+ self.currentPath = []
+
+ def endPath(self):
+ # Love is... abstracting away FL's madness.
+ path = self.currentPath
+ self.currentPath = None
+ glyph = self.glyph
+ if len(path) == 1 and path[0][3] is not None:
+ # Single point on the contour, it has a name. Make it an anchor.
+ x, y = path[0][0]
+ name = path[0][3]
+ anchor = Anchor(name, roundInt(x), roundInt(y))
+ glyph.anchors.append(anchor)
+ return
+ firstOnCurveIndex = None
+ for i in range(len(path)):
+ if path[i][1] is not None:
+ firstOnCurveIndex = i
+ break
+ if firstOnCurveIndex is None:
+ # TT special case: on-curve-less contour. FL doesn't support that,
+ # so we insert an implied point at the end.
+ x1, y1 = path[0][0]
+ x2, y2 = path[-1][0]
+ impliedPoint = 0.5 * (x1 + x2), 0.5 * (y1 + y2)
+ path.append((impliedPoint, "qcurve", True, None))
+ firstOnCurveIndex = 0
+ path = path[firstOnCurveIndex + 1:] + path[:firstOnCurveIndex + 1]
+ firstPoint, segmentType, smooth, name = path[-1]
+ closed = True
+ if segmentType == "move":
+ path = path[:-1]
+ closed = False
+ # XXX The contour is not closed, but I can't figure out how to
+ # create an open contour in FL. Creating one by hand shows type"0x8011"
+ # for a move node in an open contour, but I'm not able to access
+ # that flag.
+ elif segmentType == "line":
+ # The contour is closed and ends in a lineto, which is redundant
+ # as it's implied by closepath.
+ path = path[:-1]
+ x, y = firstPoint
+ node = Node(nMOVE, Point(roundInt(x), roundInt(y)))
+ if smooth and closed:
+ if segmentType == "line" or path[0][1] == "line":
+ node.alignment = nFIXED
+ else:
+ node.alignment = nSMOOTH
+ glyph.Insert(node, len(glyph))
+ segment = []
+ nPoints = len(path)
+ for i in range(nPoints):
+ pt, segmentType, smooth, name = path[i]
+ segment.append(pt)
+ if segmentType is None:
+ continue
+ if segmentType == "curve":
+ if len(segment) < 2:
+ segmentType = "line"
+ elif len(segment) == 2:
+ segmentType = "qcurve"
+ if segmentType == "qcurve":
+ for x, y in segment[:-1]:
+ glyph.Insert(Node(nOFF, Point(roundInt(x), roundInt(y))), len(glyph))
+ x, y = segment[-1]
+ node = Node(nLINE, Point(roundInt(x), roundInt(y)))
+ glyph.Insert(node, len(glyph))
+ elif segmentType == "curve":
+ if len(segment) == 3:
+ cubicSegments = [segment]
+ else:
+ from fontTools.pens.basePen import decomposeSuperBezierSegment
+ cubicSegments = decomposeSuperBezierSegment(segment)
+ nSegments = len(cubicSegments)
+ for i in range(nSegments):
+ pt1, pt2, pt3 = cubicSegments[i]
+ x, y = pt3
+ node = Node(nCURVE, Point(roundInt(x), roundInt(y)))
+ node.points[1].x, node.points[1].y = roundInt(pt1[0]), roundInt(pt1[1])
+ node.points[2].x, node.points[2].y = roundInt(pt2[0]), roundInt(pt2[1])
+ if i != nSegments - 1:
+ node.alignment = nSMOOTH
+ glyph.Insert(node, len(self.glyph))
+ elif segmentType == "line":
+ assert len(segment) == 1, segment
+ x, y = segment[0]
+ node = Node(nLINE, Point(roundInt(x), roundInt(y)))
+ glyph.Insert(node, len(glyph))
+ else:
+ assert 0, "unsupported curve type (%s)" % segmentType
+ if smooth:
+ if i + 1 < nPoints or closed:
+ # Can't use existing node, as you can't change node attributes
+ # AFTER it's been appended to the glyph.
+ node = glyph[-1]
+ if segmentType == "line" or path[(i+1) % nPoints][1] == "line":
+ # tangent
+ node.alignment = nFIXED
+ else:
+ # curve
+ node.alignment = nSMOOTH
+ segment = []
+ if closed:
+ # we may have output a node too much
+ node = glyph[-1]
+ if node.type == nLINE and (node.x, node.y) == (roundInt(firstPoint[0]), roundInt(firstPoint[1])):
+ glyph.DeleteNode(len(glyph) - 1)
+
+ def addPoint(self, pt, segmentType=None, smooth=None, name=None, **kwargs):
+ self.currentPath.append((pt, segmentType, smooth, name))
+
+ def addComponent(self, baseName, transformation):
+ assert self.currentPath is None
+ # make base glyph if needed, Component() needs the index
+ NewGlyph(self.glyph.parent, baseName, updateFont=False)
+ baseIndex = self.glyph.parent.FindGlyph(baseName)
+ if baseIndex == -1:
+ raise KeyError, "couldn't find or make base glyph"
+ xx, xy, yx, yy, dx, dy = transformation
+ # XXX warn when xy or yx != 0
+ new = Component(baseIndex, Point(dx, dy), Point(xx, yy))
+ self.glyph.components.append(new)
+
+
+def drawFLGlyphOntoPointPen(flGlyph, pen):
+ """Draw a FontLab glyph onto a PointPen."""
+ for anchor in flGlyph.anchors:
+ pen.beginPath()
+ pen.addPoint((anchor.x, anchor.y), name=anchor.name)
+ pen.endPath()
+ for contour in _getContours(flGlyph):
+ pen.beginPath()
+ for pt, segmentType, smooth in contour:
+ pen.addPoint(pt, segmentType=segmentType, smooth=smooth)
+ pen.endPath()
+ for baseGlyph, tranform in _getComponents(flGlyph):
+ pen.addComponent(baseGlyph, tranform)
+
+
+
+class FLPointContourPen(FLPointPen):
+ """Same as FLPointPen, except that it ignores components."""
+ def addComponent(self, baseName, transformation):
+ pass
+
+
+NODE_TYPES = {nMOVE: "move", nLINE: "line", nCURVE: "curve",
+ nOFF: None}
+
+def _getContours(glyph):
+ contours = []
+ for i in range(len(glyph)):
+ node = glyph[i]
+ segmentType = NODE_TYPES[node.type]
+ if segmentType == "move":
+ contours.append([])
+ for pt in node.points[1:]:
+ contours[-1].append(((pt.x, pt.y), None, False))
+ smooth = node.alignment != nSHARP
+ contours[-1].append(((node.x, node.y), segmentType, smooth))
+
+ for contour in contours:
+ # filter out or change the move
+ movePt, segmentType, smooth = contour[0]
+ assert segmentType == "move"
+ lastSegmentType = contour[-1][1]
+ if movePt == contour[-1][0] and lastSegmentType == "curve":
+ contour[0] = contour[-1]
+ contour.pop()
+ elif lastSegmentType is None:
+ contour[0] = movePt, "qcurve", smooth
+ else:
+ assert lastSegmentType in ("line", "curve")
+ contour[0] = movePt, "line", smooth
+
+ # change "line" to "qcurve" if appropriate
+ previousSegmentType = "ArbitraryValueOtherThanNone"
+ for i in range(len(contour)):
+ pt, segmentType, smooth = contour[i]
+ if segmentType == "line" and previousSegmentType is None:
+ contour[i] = pt, "qcurve", smooth
+ previousSegmentType = segmentType
+
+ return contours
+
+
+def _simplifyValues(*values):
+ """Given a set of numbers, convert items to ints if they are
+ integer float values, eg. 0.0, 1.0."""
+ newValues = []
+ for v in values:
+ i = int(v)
+ if v == i:
+ v = i
+ newValues.append(v)
+ return newValues
+
+
+def _getComponents(glyph):
+ components = []
+ for comp in glyph.components:
+ baseName = glyph.parent[comp.index].name
+ dx, dy = comp.delta.x, comp.delta.y
+ sx, sy = comp.scale.x, comp.scale.y
+ dx, dy, sx, sy = _simplifyValues(dx, dy, sx, sy)
+ components.append((baseName, (sx, 0, 0, sy, dx, dy)))
+ return components
+
+
+def test():
+ g = fl.glyph
+ g.Clear()
+
+ p = PLPen(g)
+ p.moveTo((50, 50))
+ p.lineTo((150,50))
+ p.lineTo((170, 200), smooth=2)
+ p.curveTo((173, 225), (150, 250), (120, 250), smooth=1)
+ p.curveTo((85, 250), (50, 200), (50, 200))
+ p.closePath()
+
+ p.moveTo((300, 300))
+ p.lineTo((400, 300))
+ p.curveTo((450, 325), (450, 375), (400, 400))
+ p.qCurveTo((400, 500), (350, 550), (300, 500), (300, 400))
+ p.closePath()
+ p.setWidth(600)
+ p.setNote("Hello, this is a note")
+ p.addAnchor("top", (250, 600))
+
+ fl.UpdateGlyph(-1)
+ fl.UpdateFont(-1)
+
+
+if __name__ == "__main__":
+ test()
diff --git a/misc/pylib/robofab/pens/marginPen.py b/misc/pylib/robofab/pens/marginPen.py
new file mode 100644
index 000000000..03f13f917
--- /dev/null
+++ b/misc/pylib/robofab/pens/marginPen.py
@@ -0,0 +1,155 @@
+from fontTools.pens.basePen import AbstractPen, BasePen
+from robofab.misc.bezierTools import splitLine, splitCubic
+
+
+from sets import Set
+
+class MarginPen(BasePen):
+
+ """
+ Pen to calculate the margins at a given value.
+ When isHorizontal is True, the margins at <value> are horizontal.
+ When isHorizontal is False, the margins at <value> are vertical.
+
+ When a glyphset or font is given, MarginPen will also calculate for glyphs with components.
+
+ pen.getMargins() returns the minimum and maximum intersections of the glyph.
+ pen.getContourMargins() returns the minimum and maximum intersections for each contour.
+
+
+ Possible optimisation:
+ Initialise the pen object with a list of points we want to measure,
+ then draw the glyph once, but do the splitLine() math for all measure points.
+
+ """
+
+ def __init__(self, glyphSet, value, isHorizontal=True):
+ BasePen.__init__(self, glyphSet)
+ self.value = value
+ self.hits = {}
+ self.filterDoubles = True
+ self.contourIndex = None
+ self.startPt = None
+ self.isHorizontal = isHorizontal
+
+ def _moveTo(self, pt):
+ self.currentPt = pt
+ self.startPt = pt
+ if self.contourIndex is None:
+ self.contourIndex = 0
+ else:
+ self.contourIndex += 1
+
+ def _lineTo(self, pt):
+ if self.filterDoubles:
+ if pt == self.currentPt:
+ return
+ hits = splitLine(self.currentPt, pt, self.value, self.isHorizontal)
+ if len(hits)>1:
+ # result will be 2 tuples of 2 coordinates
+ # first two points: start to intersect
+ # second two points: intersect to end
+ # so, second point in first tuple is the intersect
+ # then, the first coordinate of that point is the x.
+ if not self.contourIndex in self.hits:
+ self.hits[self.contourIndex] = []
+ if self.isHorizontal:
+ self.hits[self.contourIndex].append(round(hits[0][-1][0], 4))
+ else:
+ self.hits[self.contourIndex].append(round(hits[0][-1][1], 4))
+ if self.isHorizontal and pt[1] == self.value:
+ # it could happen
+ if not self.contourIndex in self.hits:
+ self.hits[self.contourIndex] = []
+ self.hits[self.contourIndex].append(pt[0])
+ elif (not self.isHorizontal) and (pt[0] == self.value):
+ # it could happen
+ if not self.contourIndex in self.hits:
+ self.hits[self.contourIndex] = []
+ self.hits[self.contourIndex].append(pt[1])
+ self.currentPt = pt
+
+ def _curveToOne(self, pt1, pt2, pt3):
+ hits = splitCubic(self.currentPt, pt1, pt2, pt3, self.value, self.isHorizontal)
+ for i in range(len(hits)-1):
+ # a number of intersections is possible. Just take the
+ # last point of each segment.
+ if not self.contourIndex in self.hits:
+ self.hits[self.contourIndex] = []
+ if self.isHorizontal:
+ self.hits[self.contourIndex].append(round(hits[i][-1][0], 4))
+ else:
+ self.hits[self.contourIndex].append(round(hits[i][-1][1], 4))
+ if self.isHorizontal and pt3[1] == self.value:
+ # it could happen
+ if not self.contourIndex in self.hits:
+ self.hits[self.contourIndex] = []
+ self.hits[self.contourIndex].append(pt3[0])
+ if (not self.isHorizontal) and (pt3[0] == self.value):
+ # it could happen
+ if not self.contourIndex in self.hits:
+ self.hits[self.contourIndex] = []
+ self.hits[self.contourIndex].append(pt3[1])
+ self.currentPt = pt3
+
+ def _closePath(self):
+ if self.currentPt != self.startPt:
+ self._lineTo(self.startPt)
+ self.currentPt = self.startPt = None
+
+ def _endPath(self):
+ self.currentPt = None
+
+ def addComponent(self, baseGlyph, transformation):
+ from fontTools.pens.transformPen import TransformPen
+ if self.glyphSet is None:
+ return
+ if baseGlyph in self.glyphSet:
+ glyph = self.glyphSet[baseGlyph]
+ if glyph is None:
+ return
+ tPen = TransformPen(self, transformation)
+ glyph.draw(tPen)
+
+ def getMargins(self):
+ """Get the horizontal margins for all contours combined, i.e. the whole glyph."""
+ allHits = []
+ for index, pts in self.hits.items():
+ allHits.extend(pts)
+ if allHits:
+ return min(allHits), max(allHits)
+ return None
+
+ def getContourMargins(self):
+ """Get the horizontal margins for each contour."""
+ allHits = {}
+ for index, pts in self.hits.items():
+ unique = list(Set(pts))
+ unique.sort()
+ allHits[index] = unique
+ return allHits
+
+ def getAll(self):
+ """Get all the slices."""
+ allHits = []
+ for index, pts in self.hits.items():
+ allHits.extend(pts)
+ unique = list(Set(allHits))
+ unique = list(unique)
+ unique.sort()
+ return unique
+
+
+if __name__ == "__main__":
+
+ from robofab.world import CurrentGlyph, CurrentFont
+ f = CurrentFont()
+ g = CurrentGlyph()
+
+ pt = (74, 216)
+
+ pen = MarginPen(f, pt[1], isHorizontal=False)
+ g.draw(pen)
+ print 'glyph Y margins', pen.getMargins()
+ print pen.getContourMargins()
+
diff --git a/misc/pylib/robofab/pens/mathPens.py b/misc/pylib/robofab/pens/mathPens.py
new file mode 100755
index 000000000..1fe1026e1
--- /dev/null
+++ b/misc/pylib/robofab/pens/mathPens.py
@@ -0,0 +1,185 @@
+"""Some pens that are needed during glyph math"""
+
+
+from robofab.pens.pointPen import BasePointToSegmentPen, AbstractPointPen
+
+
+class GetMathDataPointPen(AbstractPointPen):
+
+ """
+ Point pen that converts all "line" segments into
+ curve segments containing two off curve points.
+ """
+
+ def __init__(self):
+ self.contours = []
+ self.components = []
+ self.anchors = []
+ self._points = []
+
+ def _flushContour(self):
+ points = self._points
+ if len(points) == 1:
+ segmentType, pt, smooth, name = points[0]
+ self.anchors.append((pt, name))
+ else:
+ self.contours.append([])
+ prevOnCurve = None
+ offCurves = []
+ # deal with the first point
+ segmentType, pt, smooth, name = points[0]
+ # if it is an offcurve, add it to the offcurve list
+ if segmentType is None:
+ offCurves.append((segmentType, pt, smooth, name))
+ # if it is a line, change the type to curve and add it to the contour
+ # create offcurves corresponding with the last oncurve and
+ # this point and add them to the points list
+ elif segmentType == "line":
+ prevOnCurve = pt
+ self.contours[-1].append(("curve", pt, False, name))
+ lastPoint = points[-1][1]
+ points.append((None, lastPoint, False, None))
+ points.append((None, pt, False, None))
+ # a move, curve or qcurve. simple append the data.
+ else:
+ self.contours[-1].append((segmentType, pt, smooth, name))
+ prevOnCurve = pt
+ # now go through the rest of the points
+ for segmentType, pt, smooth, name in points[1:]:
+ # store the off curves
+ if segmentType is None:
+ offCurves.append((segmentType, pt, smooth, name))
+ continue
+ # make off curve corresponding the the previous
+ # on curve an dthis point
+ if segmentType == "line":
+ segmentType = "curve"
+ offCurves.append((None, prevOnCurve, False, None))
+ offCurves.append((None, pt, False, None))
+ # add the offcurves to the contour
+ for offCurve in offCurves:
+ self.contours[-1].append(offCurve)
+ # add the oncurve to the contour
+ self.contours[-1].append((segmentType, pt, smooth, name))
+ # reset the stored data
+ prevOnCurve = pt
+ offCurves = []
+ # catch offcurves that belong to the first
+ if len(offCurves) != 0:
+ self.contours[-1].extend(offCurves)
+
+ def beginPath(self):
+ self._points = []
+
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
+ self._points.append((segmentType, pt, smooth, name))
+
+ def endPath(self):
+ self._flushContour()
+
+ def addComponent(self, baseGlyphName, transformation):
+ self.components.append((baseGlyphName, transformation))
+
+ def getData(self):
+ return {
+ 'contours':list(self.contours),
+ 'components':list(self.components),
+ 'anchors':list(self.anchors)
+ }
+
+
+class CurveSegmentFilterPointPen(AbstractPointPen):
+
+ """
+ Point pen that turns curve segments that contain
+ unnecessary anchor points into line segments.
+ """
+ # XXX it would be great if this also checked to see if the
+ # off curves are on the segment and therefre unneeded
+
+ def __init__(self, anotherPointPen):
+ self._pen = anotherPointPen
+ self._points = []
+
+ def _flushContour(self):
+ points = self._points
+ # an anchor
+ if len(points) == 1:
+ pt, segmentType, smooth, name = points[0]
+ self._pen.addPoint(pt, segmentType, smooth, name)
+ else:
+ prevOnCurve = None
+ offCurves = []
+
+ pointsToDraw = []
+
+ # deal with the first point
+ pt, segmentType, smooth, name = points[0]
+ # if it is an offcurve, add it to the offcurve list
+ if segmentType is None:
+ offCurves.append((pt, segmentType, smooth, name))
+ else:
+ # potential redundancy
+ if segmentType == "curve":
+ # gather preceding off curves
+ testOffCurves = []
+ lastPoint = None
+ for i in xrange(len(points)):
+ i = -i - 1
+ testPoint = points[i]
+ testSegmentType = testPoint[1]
+ if testSegmentType is not None:
+ lastPoint = testPoint[0]
+ break
+ testOffCurves.append(testPoint[0])
+ # if two offcurves exist we can test for redundancy
+ if len(testOffCurves) == 2:
+ if testOffCurves[1] == lastPoint and testOffCurves[0] == pt:
+ segmentType = "line"
+ # remove the last two points
+ points = points[:-2]
+ # add the point to the contour
+ pointsToDraw.append((pt, segmentType, smooth, name))
+ prevOnCurve = pt
+ for pt, segmentType, smooth, name in points[1:]:
+ # store offcurves
+ if segmentType is None:
+ offCurves.append((pt, segmentType, smooth, name))
+ continue
+ # curves are a potential redundancy
+ elif segmentType == "curve":
+ if len(offCurves) == 2:
+ # test for redundancy
+ if offCurves[0][0] == prevOnCurve and offCurves[1][0] == pt:
+ offCurves = []
+ segmentType = "line"
+ # add all offcurves
+ for offCurve in offCurves:
+ pointsToDraw.append(offCurve)
+ # add the on curve
+ pointsToDraw.append((pt, segmentType, smooth, name))
+ # reset the stored data
+ prevOnCurve = pt
+ offCurves = []
+ # catch any remaining offcurves
+ if len(offCurves) != 0:
+ for offCurve in offCurves:
+ pointsToDraw.append(offCurve)
+ # draw to the pen
+ for pt, segmentType, smooth, name in pointsToDraw:
+ self._pen.addPoint(pt, segmentType, smooth, name)
+
+ def beginPath(self):
+ self._points = []
+ self._pen.beginPath()
+
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
+ self._points.append((pt, segmentType, smooth, name))
+
+ def endPath(self):
+ self._flushContour()
+ self._pen.endPath()
+
+ def addComponent(self, baseGlyphName, transformation):
+ self._pen.addComponent(baseGlyphName, transformation)
+
diff --git a/misc/pylib/robofab/pens/pointPen.py b/misc/pylib/robofab/pens/pointPen.py
new file mode 100644
index 000000000..dc81df38e
--- /dev/null
+++ b/misc/pylib/robofab/pens/pointPen.py
@@ -0,0 +1,173 @@
+__all__ = ["AbstractPointPen", "BasePointToSegmentPen", "PrintingPointPen",
+ "PrintingSegmentPen", "SegmentPrintingPointPen"]
+
+
+class AbstractPointPen:
+
+ def beginPath(self):
+ """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, **kwargs):
+ """Add a point to the current sub path."""
+ raise NotImplementedError
+
+ 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):
+ 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 PrintingPointPen(AbstractPointPen):
+ def __init__(self):
+ self.havePath = False
+ def beginPath(self):
+ self.havePath = True
+ print "pen.beginPath()"
+ def endPath(self):
+ self.havePath = False
+ print "pen.endPath()"
+ def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
+ assert self.havePath
+ args = ["(%s, %s)" % (pt[0], pt[1])]
+ if segmentType is not None:
+ args.append("segmentType=%r" % segmentType)
+ if smooth:
+ args.append("smooth=True")
+ if name is not None:
+ args.append("name=%r" % name)
+ if kwargs:
+ args.append("**%s" % kwargs)
+ print "pen.addPoint(%s)" % ", ".join(args)
+ def addComponent(self, baseGlyphName, transformation):
+ assert not self.havePath
+ print "pen.addComponent(%r, %s)" % (baseGlyphName, tuple(transformation))
+
+
+from fontTools.pens.basePen import AbstractPen
+
+class PrintingSegmentPen(AbstractPen):
+ def moveTo(self, pt):
+ print "pen.moveTo(%s)" % (pt,)
+ def lineTo(self, pt):
+ print "pen.lineTo(%s)" % (pt,)
+ def curveTo(self, *pts):
+ print "pen.curveTo%s" % (pts,)
+ def qCurveTo(self, *pts):
+ print "pen.qCurveTo%s" % (pts,)
+ def closePath(self):
+ print "pen.closePath()"
+ def endPath(self):
+ print "pen.endPath()"
+ def addComponent(self, baseGlyphName, transformation):
+ print "pen.addComponent(%r, %s)" % (baseGlyphName, tuple(transformation))
+
+
+class SegmentPrintingPointPen(BasePointToSegmentPen):
+ def _flushContour(self, segments):
+ from pprint import pprint
+ pprint(segments)
+
+
+if __name__ == "__main__":
+ p = SegmentPrintingPointPen()
+ from robofab.test.test_pens import TestShapes
+ TestShapes.onCurveLessQuadShape(p)
diff --git a/misc/pylib/robofab/pens/quartzPen.py b/misc/pylib/robofab/pens/quartzPen.py
new file mode 100644
index 000000000..dd1947ccf
--- /dev/null
+++ b/misc/pylib/robofab/pens/quartzPen.py
@@ -0,0 +1,21 @@
+from fontTools.pens.basePen import BasePen
+
+class QuartzPen(BasePen):
+
+ """Pen to draw onto a Quartz drawing context (Carbon.CG)."""
+
+ def __init__(self, glyphSet, quartzContext):
+ BasePen.__init__(self, glyphSet)
+ self._context = quartzContext
+
+ def _moveTo(self, (x, y)):
+ self._context.CGContextMoveToPoint(x, y)
+
+ def _lineTo(self, (x, y)):
+ self._context.CGContextAddLineToPoint(x, y)
+
+ def _curveToOne(self, (x1, y1), (x2, y2), (x3, y3)):
+ self._context.CGContextAddCurveToPoint(x1, y1, x2, y2, x3, y3)
+
+ def _closePath(self):
+ self._context.closePath()
diff --git a/misc/pylib/robofab/pens/reverseContourPointPen.py b/misc/pylib/robofab/pens/reverseContourPointPen.py
new file mode 100755
index 000000000..8ce001b4d
--- /dev/null
+++ b/misc/pylib/robofab/pens/reverseContourPointPen.py
@@ -0,0 +1,125 @@
+"""PointPen for reversing the winding direction of contours."""
+
+
+__all__ = ["ReverseContourPointPen"]
+
+
+from robofab.pens.pointPen import AbstractPointPen
+
+
+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
+ self.currentContour = None # a place to store the points for the current sub path
+
+ def _flushContour(self):
+ pen = self.pen
+ contour = self.currentContour
+ if not contour:
+ pen.beginPath()
+ 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()
+ for pt, nextSegmentType, smooth, name in contour:
+ if nextSegmentType is not None:
+ segmentType = lastSegmentType
+ lastSegmentType = nextSegmentType
+ else:
+ segmentType = None
+ pen.addPoint(pt, segmentType=segmentType, smooth=smooth, name=name)
+ pen.endPath()
+
+ def beginPath(self):
+ assert self.currentContour is None
+ self.currentContour = []
+ 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))
+
+ def addComponent(self, glyphName, transform):
+ assert self.currentContour is None
+ self.pen.addComponent(glyphName, transform)
+
+
+if __name__ == "__main__":
+ from robofab.pens.pointPen import PrintingPointPen
+ pP = PrintingPointPen()
+ rP = ReverseContourPointPen(pP)
+
+ rP.beginPath()
+ rP.addPoint((339, -8), "curve")
+ rP.addPoint((502, -8))
+ rP.addPoint((635, 65))
+ rP.addPoint((635, 305), "curve")
+ rP.addPoint((635, 545))
+ rP.addPoint((504, 623))
+ rP.addPoint((340, 623), "curve")
+ rP.addPoint((177, 623))
+ rP.addPoint((43, 545))
+ rP.addPoint((43, 305), "curve")
+ rP.addPoint((43, 65))
+ rP.addPoint((176, -8))
+ rP.endPath()
+
+ rP.beginPath()
+ rP.addPoint((100, 100), "move", smooth=False, name='a')
+ rP.addPoint((150, 150))
+ rP.addPoint((200, 200))
+ rP.addPoint((250, 250), "curve", smooth=True, name='b')
+ rP.addPoint((300, 300), "line", smooth=False, name='c')
+ rP.addPoint((350, 350))
+ rP.addPoint((400, 400))
+ rP.addPoint((450, 450))
+ rP.addPoint((500, 500), "curve", smooth=True, name='d')
+ rP.addPoint((550, 550))
+ rP.addPoint((600, 600))
+ rP.addPoint((650, 650))
+ rP.addPoint((700, 700))
+ rP.addPoint((750, 750), "qcurve", smooth=False, name='e')
+ rP.endPath()
diff --git a/misc/pylib/robofab/pens/rfUFOPen.pyx b/misc/pylib/robofab/pens/rfUFOPen.pyx
new file mode 100755
index 000000000..265d7aea0
--- /dev/null
+++ b/misc/pylib/robofab/pens/rfUFOPen.pyx
@@ -0,0 +1,103 @@
+"""Pens for creating UFO glyphs."""
+
+from robofab.objects.objectsBase import MOVE, LINE, CORNER, CURVE, QCURVE, OFFCURVE
+from robofab.objects.objectsRF import RContour, RSegment, RPoint
+from robofab.pens.pointPen import BasePointToSegmentPen
+from robofab.pens.adapterPens import SegmentToPointPen
+
+
+class RFUFOPen(SegmentToPointPen):
+
+ def __init__(self, glyph):
+ SegmentToPointPen.__init__(self, RFUFOPointPen(glyph))
+
+
+class RFUFOPointPen(BasePointToSegmentPen):
+
+ """Point pen for building objectsRF glyphs"""
+
+ def __init__(self, glyph):
+ BasePointToSegmentPen.__init__(self)
+ self.glyph = glyph
+
+ def _flushContour(self, segments):
+ #
+ # adapted from robofab.pens.adapterPens.PointToSegmentPen
+ #
+ assert len(segments) >= 1
+ # if we only have one point and it has a name, we must have an anchor
+ first = segments[0]
+ segmentType, points = first
+ pt, smooth, name, kwargs = points[0]
+ if len(segments) == 1 and name != None:
+ self.glyph.appendAnchor(name, pt)
+ return
+ # we must have a contour
+ contour = RContour()
+ contour.setParent(self.glyph)
+ 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. only if it isn't a qcurve
+ closed = True
+ segmentType, points = segments[-1]
+ movePt, smooth, name, kwargs = points[-1]
+ ## THIS IS STILL UNDECIDED!!!
+ # since objectsRF currently follows the FL model of not
+ # allowing open contours, remove the last segment
+ # since it is being replaced by a move
+ if segmentType == 'line':
+ del segments[-1]
+ # construct a move segment and apply it to the contour if we aren't dealing with a qcurve
+ segment = RSegment()
+ segment.setParent(contour)
+ segment.smooth = smooth
+ rPoint = RPoint(x=movePt[0], y=movePt[1], pointType=MOVE, name=name)
+ rPoint.setParent(segment)
+ segment.points = [rPoint]
+ contour.segments.append(segment)
+ # do the rest of the segments
+ for segmentType, points in segments:
+ points = [(pt, name) for pt, smooth, name, kwargs in points]
+ if segmentType == "line":
+ assert len(points) == 1
+ sType = LINE
+ elif segmentType == "curve":
+ sType = CURVE
+ elif segmentType == "qcurve":
+ sType = QCURVE
+ else:
+ assert 0, "illegal segmentType: %s" % segmentType
+ segment = RSegment()
+ segment.setParent(contour)
+ segment.smooth = smooth
+ rPoints = []
+ # handle the offCurves
+ for point in points[:-1]:
+ point, name = point
+ rPoint = RPoint(x=point[0], y=point[1], pointType=OFFCURVE, name=name)
+ rPoint.setParent(segment)
+ rPoints.append(rPoint)
+ # now the onCurve
+ point, name = points[-1]
+ rPoint = RPoint(x=point[0], y=point[1], pointType=sType, name=name)
+ rPoint.setParent(segment)
+ rPoints.append(rPoint)
+ # apply them to the segment
+ segment.points = rPoints
+ contour.segments.append(segment)
+ if contour.segments[-1].type == "curve":
+ contour.segments[-1].points[-1].name = None
+ self.glyph.contours.append(contour)
+
+ def addComponent(self, glyphName, transform):
+ xx, xy, yx, yy, dx, dy = transform
+ self.glyph.appendComponent(baseGlyph=glyphName, offset=(dx, dy), scale=(xx, yy))
+
+