You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
363 lines
12 KiB
363 lines
12 KiB
"""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()
|
|
|