diff options
Diffstat (limited to 'Lib/fontTools/pens/basePen.py')
-rw-r--r-- | Lib/fontTools/pens/basePen.py | 683 |
1 files changed, 358 insertions, 325 deletions
diff --git a/Lib/fontTools/pens/basePen.py b/Lib/fontTools/pens/basePen.py index f981f806..ac8abd40 100644 --- a/Lib/fontTools/pens/basePen.py +++ b/Lib/fontTools/pens/basePen.py @@ -36,376 +36,409 @@ Coordinates are usually expressed as (x, y) tuples, but generally any sequence of length 2 will do. """ -from typing import Tuple +from typing import Tuple, Dict from fontTools.misc.loggingTools import LogMixin +from fontTools.misc.transform import DecomposedTransform -__all__ = ["AbstractPen", "NullPen", "BasePen", "PenError", - "decomposeSuperBezierSegment", "decomposeQuadraticSegment"] +__all__ = [ + "AbstractPen", + "NullPen", + "BasePen", + "PenError", + "decomposeSuperBezierSegment", + "decomposeQuadraticSegment", +] class PenError(Exception): - """Represents an error during penning.""" + """Represents an error during penning.""" + class OpenContourError(PenError): - pass + pass class AbstractPen: - - def moveTo(self, pt: Tuple[float, float]) -> None: - """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: Tuple[float, float]) -> None: - """Draw a straight line from the current point to 'pt'.""" - raise NotImplementedError - - def curveTo(self, *points: Tuple[float, float]) -> None: - """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: Tuple[float, float]) -> None: - """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) -> None: - """Close the current sub path. You must call either pen.closePath() - or pen.endPath() after each sub path. - """ - pass - - def endPath(self) -> None: - """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: str, - transformation: Tuple[float, float, float, float, float, float] - ) -> None: - """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 + def moveTo(self, pt: Tuple[float, float]) -> None: + """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: Tuple[float, float]) -> None: + """Draw a straight line from the current point to 'pt'.""" + raise NotImplementedError + + def curveTo(self, *points: Tuple[float, float]) -> None: + """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: Tuple[float, float]) -> None: + """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) -> None: + """Close the current sub path. You must call either pen.closePath() + or pen.endPath() after each sub path. + """ + pass + + def endPath(self) -> None: + """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: str, + transformation: Tuple[float, float, float, float, float, float], + ) -> None: + """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 + + def addVarComponent( + self, + glyphName: str, + transformation: DecomposedTransform, + location: Dict[str, float], + ) -> None: + """Add a VarComponent sub glyph. The 'transformation' argument + must be a DecomposedTransform from the fontTools.misc.transform module, + and the 'location' argument must be a dictionary mapping axis tags + to their locations. + """ + # GlyphSet decomposes for us + raise AttributeError class NullPen(AbstractPen): - """A pen that does nothing. - """ + """A pen that does nothing.""" - def moveTo(self, pt): - pass + def moveTo(self, pt): + pass - def lineTo(self, pt): - pass + def lineTo(self, pt): + pass - def curveTo(self, *points): - pass + def curveTo(self, *points): + pass - def qCurveTo(self, *points): - pass + def qCurveTo(self, *points): + pass - def closePath(self): - pass + def closePath(self): + pass - def endPath(self): - pass + def endPath(self): + pass - def addComponent(self, glyphName, transformation): - pass + def addComponent(self, glyphName, transformation): + pass + + def addVarComponent(self, glyphName, transformation, location): + pass class LoggingPen(LogMixin, AbstractPen): - """A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin) - """ - pass + """A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)""" + + pass class MissingComponentError(KeyError): - """Indicates a component pointing to a non-existent glyph in the glyphset.""" + """Indicates a component pointing to a non-existent glyph in the glyphset.""" class DecomposingPen(LoggingPen): - """ Implements a 'addComponent' method that decomposes components - (i.e. draws them onto self as simple contours). - It can also be used as a mixin class (e.g. see ContourRecordingPen). - - You must override moveTo, lineTo, curveTo and qCurveTo. You may - additionally override closePath, endPath and addComponent. - - By default a warning message is logged when a base glyph is missing; - set the class variable ``skipMissingComponents`` to False if you want - to raise a :class:`MissingComponentError` exception. - """ - - skipMissingComponents = True - - def __init__(self, glyphSet): - """ Takes a single 'glyphSet' argument (dict), in which the glyphs - that are referenced as components are looked up by their name. - """ - super(DecomposingPen, self).__init__() - self.glyphSet = glyphSet - - def addComponent(self, glyphName, transformation): - """ Transform the points of the base glyph and draw it onto self. - """ - from fontTools.pens.transformPen import TransformPen - try: - glyph = self.glyphSet[glyphName] - except KeyError: - if not self.skipMissingComponents: - raise MissingComponentError(glyphName) - self.log.warning( - "glyph '%s' is missing from glyphSet; skipped" % glyphName) - else: - tPen = TransformPen(self, transformation) - glyph.draw(tPen) + """Implements a 'addComponent' method that decomposes components + (i.e. draws them onto self as simple contours). + It can also be used as a mixin class (e.g. see ContourRecordingPen). + + You must override moveTo, lineTo, curveTo and qCurveTo. You may + additionally override closePath, endPath and addComponent. + + By default a warning message is logged when a base glyph is missing; + set the class variable ``skipMissingComponents`` to False if you want + to raise a :class:`MissingComponentError` exception. + """ + + skipMissingComponents = True + + def __init__(self, glyphSet): + """Takes a single 'glyphSet' argument (dict), in which the glyphs + that are referenced as components are looked up by their name. + """ + super(DecomposingPen, self).__init__() + self.glyphSet = glyphSet + + def addComponent(self, glyphName, transformation): + """Transform the points of the base glyph and draw it onto self.""" + from fontTools.pens.transformPen import TransformPen + + try: + glyph = self.glyphSet[glyphName] + except KeyError: + if not self.skipMissingComponents: + raise MissingComponentError(glyphName) + self.log.warning("glyph '%s' is missing from glyphSet; skipped" % glyphName) + else: + tPen = TransformPen(self, transformation) + glyph.draw(tPen) + + def addVarComponent(self, glyphName, transformation, location): + # GlyphSet decomposes for us + raise AttributeError class BasePen(DecomposingPen): - """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=None): - super(BasePen, self).__init__(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) - - # 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]) + """Base class for drawing pens. You must override _moveTo, _lineTo and + _curveToOne. You may additionally override _closePath, _endPath, + addComponent, addVarComponent, and/or _qCurveToOne. You should not + override any other methods. + """ + + def __init__(self, glyphSet=None): + super(BasePen, self).__init__(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) + + # 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 + """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 + """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") + """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() + 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() |