| """fontTools.pens.basePen.py -- Tools and base classes to build pen objects. |
| |
| The Pen Protocol |
| |
| A Pen is a kind of object that standardizes the way how to "draw" outlines: |
| it is a middle man between an outline and a drawing. In other words: |
| it is an abstraction for drawing outlines, making sure that outline objects |
| don't need to know the details about how and where they're being drawn, and |
| that drawings don't need to know the details of how outlines are stored. |
| |
| The most basic pattern is this: |
| |
| outline.draw(pen) # 'outline' draws itself onto 'pen' |
| |
| Pens can be used to render outlines to the screen, but also to construct |
| new outlines. Eg. an outline object can be both a drawable object (it has a |
| draw() method) as well as a pen itself: you *build* an outline using pen |
| methods. |
| |
| The AbstractPen class defines the Pen protocol. It implements almost |
| nothing (only no-op closePath() and endPath() methods), but is useful |
| for documentation purposes. Subclassing it basically tells the reader: |
| "this class implements the Pen protocol.". An examples of an AbstractPen |
| subclass is fontTools.pens.transformPen.TransformPen. |
| |
| The BasePen class is a base implementation useful for pens that actually |
| draw (for example a pen renders outlines using a native graphics engine). |
| BasePen contains a lot of base functionality, making it very easy to build |
| a pen that fully conforms to the pen protocol. Note that if you subclass |
| BasePen, you _don't_ override moveTo(), lineTo(), etc., but _moveTo(), |
| _lineTo(), etc. See the BasePen doc string for details. Examples of |
| BasePen subclasses are fontTools.pens.boundsPen.BoundsPen and |
| fontTools.pens.cocoaPen.CocoaPen. |
| |
| Coordinates are usually expressed as (x, y) tuples, but generally any |
| sequence of length 2 will do. |
| """ |
| |
| from __future__ import print_function, division, absolute_import |
| from fontTools.misc.py23 import * |
| |
| __all__ = ["AbstractPen", "NullPen", "BasePen", |
| "decomposeSuperBezierSegment", "decomposeQuadraticSegment"] |
| |
| |
| class AbstractPen(object): |
| |
| def moveTo(self, pt): |
| """Begin a new sub path, set the current point to 'pt'. You must |
| end each sub path with a call to pen.closePath() or pen.endPath(). |
| """ |
| raise NotImplementedError |
| |
| def lineTo(self, pt): |
| """Draw a straight line from the current point to 'pt'.""" |
| raise NotImplementedError |
| |
| def curveTo(self, *points): |
| """Draw a cubic bezier with an arbitrary number of control points. |
| |
| The last point specified is on-curve, all others are off-curve |
| (control) points. If the number of control points is > 2, the |
| segment is split into multiple bezier segments. This works |
| like this: |
| |
| Let n be the number of control points (which is the number of |
| arguments to this call minus 1). If n==2, a plain vanilla cubic |
| bezier is drawn. If n==1, we fall back to a quadratic segment and |
| if n==0 we draw a straight line. It gets interesting when n>2: |
| n-1 PostScript-style cubic segments will be drawn as if it were |
| one curve. See decomposeSuperBezierSegment(). |
| |
| The conversion algorithm used for n>2 is inspired by NURB |
| splines, and is conceptually equivalent to the TrueType "implied |
| points" principle. See also decomposeQuadraticSegment(). |
| """ |
| raise NotImplementedError |
| |
| def qCurveTo(self, *points): |
| """Draw a whole string of quadratic curve segments. |
| |
| The last point specified is on-curve, all others are off-curve |
| points. |
| |
| This method implements TrueType-style curves, breaking up curves |
| using 'implied points': between each two consequtive off-curve points, |
| there is one implied point exactly in the middle between them. See |
| also decomposeQuadraticSegment(). |
| |
| The last argument (normally the on-curve point) may be None. |
| This is to support contours that have NO on-curve points (a rarely |
| seen feature of TrueType outlines). |
| """ |
| raise NotImplementedError |
| |
| def closePath(self): |
| """Close the current sub path. You must call either pen.closePath() |
| or pen.endPath() after each sub path. |
| """ |
| pass |
| |
| def endPath(self): |
| """End the current sub path, but don't close it. You must call |
| either pen.closePath() or pen.endPath() after each sub path. |
| """ |
| pass |
| |
| def addComponent(self, glyphName, transformation): |
| """Add a sub glyph. The 'transformation' argument must be a 6-tuple |
| containing an affine transformation, or a Transform object from the |
| fontTools.misc.transform module. More precisely: it should be a |
| sequence containing 6 numbers. |
| """ |
| raise NotImplementedError |
| |
| |
| class NullPen(object): |
| |
| """A pen that does nothing. |
| """ |
| |
| def moveTo(self, pt): |
| pass |
| |
| def lineTo(self, pt): |
| pass |
| |
| def curveTo(self, *points): |
| pass |
| |
| def qCurveTo(self, *points): |
| pass |
| |
| def closePath(self): |
| pass |
| |
| def endPath(self): |
| pass |
| |
| def addComponent(self, glyphName, transformation): |
| pass |
| |
| |
| class BasePen(AbstractPen): |
| |
| """Base class for drawing pens. You must override _moveTo, _lineTo and |
| _curveToOne. You may additionally override _closePath, _endPath, |
| addComponent and/or _qCurveToOne. You should not override any other |
| methods. |
| """ |
| |
| def __init__(self, glyphSet): |
| self.glyphSet = glyphSet |
| self.__currentPoint = None |
| |
| # must override |
| |
| def _moveTo(self, pt): |
| raise NotImplementedError |
| |
| def _lineTo(self, pt): |
| raise NotImplementedError |
| |
| def _curveToOne(self, pt1, pt2, pt3): |
| raise NotImplementedError |
| |
| # may override |
| |
| def _closePath(self): |
| pass |
| |
| def _endPath(self): |
| pass |
| |
| def _qCurveToOne(self, pt1, pt2): |
| """This method implements the basic quadratic curve type. The |
| default implementation delegates the work to the cubic curve |
| function. Optionally override with a native implementation. |
| """ |
| pt0x, pt0y = self.__currentPoint |
| pt1x, pt1y = pt1 |
| pt2x, pt2y = pt2 |
| mid1x = pt0x + 0.66666666666666667 * (pt1x - pt0x) |
| mid1y = pt0y + 0.66666666666666667 * (pt1y - pt0y) |
| mid2x = pt2x + 0.66666666666666667 * (pt1x - pt2x) |
| mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y) |
| self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2) |
| |
| def addComponent(self, glyphName, transformation): |
| """This default implementation simply transforms the points |
| of the base glyph and draws it onto self. |
| """ |
| from fontTools.pens.transformPen import TransformPen |
| try: |
| glyph = self.glyphSet[glyphName] |
| except KeyError: |
| pass |
| else: |
| tPen = TransformPen(self, transformation) |
| glyph.draw(tPen) |
| |
| # don't override |
| |
| def _getCurrentPoint(self): |
| """Return the current point. This is not part of the public |
| interface, yet is useful for subclasses. |
| """ |
| return self.__currentPoint |
| |
| def closePath(self): |
| self._closePath() |
| self.__currentPoint = None |
| |
| def endPath(self): |
| self._endPath() |
| self.__currentPoint = None |
| |
| def moveTo(self, pt): |
| self._moveTo(pt) |
| self.__currentPoint = pt |
| |
| def lineTo(self, pt): |
| self._lineTo(pt) |
| self.__currentPoint = pt |
| |
| def curveTo(self, *points): |
| n = len(points) - 1 # 'n' is the number of control points |
| assert n >= 0 |
| if n == 2: |
| # The common case, we have exactly two BCP's, so this is a standard |
| # cubic bezier. Even though decomposeSuperBezierSegment() handles |
| # this case just fine, we special-case it anyway since it's so |
| # common. |
| self._curveToOne(*points) |
| self.__currentPoint = points[-1] |
| elif n > 2: |
| # n is the number of control points; split curve into n-1 cubic |
| # bezier segments. The algorithm used here is inspired by NURB |
| # splines and the TrueType "implied point" principle, and ensures |
| # the smoothest possible connection between two curve segments, |
| # with no disruption in the curvature. It is practical since it |
| # allows one to construct multiple bezier segments with a much |
| # smaller amount of points. |
| _curveToOne = self._curveToOne |
| for pt1, pt2, pt3 in decomposeSuperBezierSegment(points): |
| _curveToOne(pt1, pt2, pt3) |
| self.__currentPoint = pt3 |
| elif n == 1: |
| self.qCurveTo(*points) |
| elif n == 0: |
| self.lineTo(points[0]) |
| else: |
| raise AssertionError("can't get there from here") |
| |
| def qCurveTo(self, *points): |
| n = len(points) - 1 # 'n' is the number of control points |
| assert n >= 0 |
| if points[-1] is None: |
| # Special case for TrueType quadratics: it is possible to |
| # define a contour with NO on-curve points. BasePen supports |
| # this by allowing the final argument (the expected on-curve |
| # point) to be None. We simulate the feature by making the implied |
| # on-curve point between the last and the first off-curve points |
| # explicit. |
| x, y = points[-2] # last off-curve point |
| nx, ny = points[0] # first off-curve point |
| impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny)) |
| self.__currentPoint = impliedStartPoint |
| self._moveTo(impliedStartPoint) |
| points = points[:-1] + (impliedStartPoint,) |
| if n > 0: |
| # Split the string of points into discrete quadratic curve |
| # segments. Between any two consecutive off-curve points |
| # there's an implied on-curve point exactly in the middle. |
| # This is where the segment splits. |
| _qCurveToOne = self._qCurveToOne |
| for pt1, pt2 in decomposeQuadraticSegment(points): |
| _qCurveToOne(pt1, pt2) |
| self.__currentPoint = pt2 |
| else: |
| self.lineTo(points[0]) |
| |
| |
| def decomposeSuperBezierSegment(points): |
| """Split the SuperBezier described by 'points' into a list of regular |
| bezier segments. The 'points' argument must be a sequence with length |
| 3 or greater, containing (x, y) coordinates. The last point is the |
| destination on-curve point, the rest of the points are off-curve points. |
| The start point should not be supplied. |
| |
| This function returns a list of (pt1, pt2, pt3) tuples, which each |
| specify a regular curveto-style bezier segment. |
| """ |
| n = len(points) - 1 |
| assert n > 1 |
| bezierSegments = [] |
| pt1, pt2, pt3 = points[0], None, None |
| for i in range(2, n+1): |
| # calculate points in between control points. |
| nDivisions = min(i, 3, n-i+2) |
| for j in range(1, nDivisions): |
| factor = j / nDivisions |
| temp1 = points[i-1] |
| temp2 = points[i-2] |
| temp = (temp2[0] + factor * (temp1[0] - temp2[0]), |
| temp2[1] + factor * (temp1[1] - temp2[1])) |
| if pt2 is None: |
| pt2 = temp |
| else: |
| pt3 = (0.5 * (pt2[0] + temp[0]), |
| 0.5 * (pt2[1] + temp[1])) |
| bezierSegments.append((pt1, pt2, pt3)) |
| pt1, pt2, pt3 = temp, None, None |
| bezierSegments.append((pt1, points[-2], points[-1])) |
| return bezierSegments |
| |
| |
| def decomposeQuadraticSegment(points): |
| """Split the quadratic curve segment described by 'points' into a list |
| of "atomic" quadratic segments. The 'points' argument must be a sequence |
| with length 2 or greater, containing (x, y) coordinates. The last point |
| is the destination on-curve point, the rest of the points are off-curve |
| points. The start point should not be supplied. |
| |
| This function returns a list of (pt1, pt2) tuples, which each specify a |
| plain quadratic bezier segment. |
| """ |
| n = len(points) - 1 |
| assert n > 0 |
| quadSegments = [] |
| for i in range(n - 1): |
| x, y = points[i] |
| nx, ny = points[i+1] |
| impliedPt = (0.5 * (x + nx), 0.5 * (y + ny)) |
| quadSegments.append((points[i], impliedPt)) |
| quadSegments.append((points[-2], points[-1])) |
| return quadSegments |
| |
| |
| class _TestPen(BasePen): |
| """Test class that prints PostScript to stdout.""" |
| def _moveTo(self, pt): |
| print("%s %s moveto" % (pt[0], pt[1])) |
| def _lineTo(self, pt): |
| print("%s %s lineto" % (pt[0], pt[1])) |
| def _curveToOne(self, bcp1, bcp2, pt): |
| print("%s %s %s %s %s %s curveto" % (bcp1[0], bcp1[1], |
| bcp2[0], bcp2[1], pt[0], pt[1])) |
| def _closePath(self): |
| print("closepath") |
| |
| |
| if __name__ == "__main__": |
| pen = _TestPen(None) |
| pen.moveTo((0, 0)) |
| pen.lineTo((0, 100)) |
| pen.curveTo((50, 75), (60, 50), (50, 25), (0, 0)) |
| pen.closePath() |
| |
| pen = _TestPen(None) |
| # testing the "no on-curve point" scenario |
| pen.qCurveTo((0, 0), (0, 100), (100, 100), (100, 0), None) |
| pen.closePath() |