Spaces:
Running
Running
"""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 :py:class:`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 typing import Tuple, Dict | |
from fontTools.misc.loggingTools import LogMixin | |
from fontTools.misc.transform import DecomposedTransform, Identity | |
__all__ = [ | |
"AbstractPen", | |
"NullPen", | |
"BasePen", | |
"PenError", | |
"decomposeSuperBezierSegment", | |
"decomposeQuadraticSegment", | |
] | |
class PenError(Exception): | |
"""Represents an error during penning.""" | |
class OpenContourError(PenError): | |
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 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.""" | |
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 | |
def addVarComponent(self, glyphName, transformation, location): | |
pass | |
class LoggingPen(LogMixin, AbstractPen): | |
"""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.""" | |
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 | |
all instances of a sub-class to raise a :class:`MissingComponentError` | |
exception by default. | |
""" | |
skipMissingComponents = True | |
# alias error for convenience | |
MissingComponentError = MissingComponentError | |
def __init__( | |
self, | |
glyphSet, | |
*args, | |
skipMissingComponents=None, | |
reverseFlipped=False, | |
**kwargs, | |
): | |
"""Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced | |
as components are looked up by their name. | |
If the optional 'reverseFlipped' argument is True, components whose transformation | |
matrix has a negative determinant will be decomposed with a reversed path direction | |
to compensate for the flip. | |
The optional 'skipMissingComponents' argument can be set to True/False to | |
override the homonymous class attribute for a given pen instance. | |
""" | |
super(DecomposingPen, self).__init__(*args, **kwargs) | |
self.glyphSet = glyphSet | |
self.skipMissingComponents = ( | |
self.__class__.skipMissingComponents | |
if skipMissingComponents is None | |
else skipMissingComponents | |
) | |
self.reverseFlipped = reverseFlipped | |
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: | |
pen = self | |
if transformation != Identity: | |
pen = TransformPen(pen, transformation) | |
if self.reverseFlipped: | |
# if the transformation has a negative determinant, it will | |
# reverse the contour direction of the component | |
a, b, c, d = transformation[:4] | |
det = a * d - b * c | |
if det < 0: | |
from fontTools.pens.reverseContourPen import ReverseContourPen | |
pen = ReverseContourPen(pen) | |
glyph.draw(pen) | |
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, 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 | |
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() | |