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)
This commit is contained in:
Rasmus Andersson 2017-09-03 23:03:17 -04:00
parent 31ae014e0c
commit 8234b62ab7
108 changed files with 26933 additions and 110 deletions

View file

@ -0,0 +1,13 @@
"""
arrayTools and bezierTools, originally from fontTools and using Numpy,
now in a pure python implementation. This should ease the Numpy dependency
for normal UFO input/output and basic scripting tasks.
comparison test and speedtest provided.
"""

View file

@ -0,0 +1,160 @@
#
# Various array and rectangle tools, but mostly rectangles, hence the
# name of this module (not).
#
"""
Rewritten to elimate the numpy dependency
"""
import math
def calcBounds(array):
"""Return the bounding rectangle of a 2D points array as a tuple:
(xMin, yMin, xMax, yMax)
"""
if len(array) == 0:
return 0, 0, 0, 0
xs = [x for x, y in array]
ys = [y for x, y in array]
return min(xs), min(ys), max(xs), max(ys)
def updateBounds(bounds, pt, min=min, max=max):
"""Return the bounding recangle of rectangle bounds and point (x, y)."""
xMin, yMin, xMax, yMax = bounds
x, y = pt
return min(xMin, x), min(yMin, y), max(xMax, x), max(yMax, y)
def pointInRect(pt, rect):
"""Return True when point (x, y) is inside rect."""
xMin, yMin, xMax, yMax = rect
return (xMin <= pt[0] <= xMax) and (yMin <= pt[1] <= yMax)
def pointsInRect(array, rect):
"""Find out which points or array are inside rect.
Returns an array with a boolean for each point.
"""
if len(array) < 1:
return []
xMin, yMin, xMax, yMax = rect
return [(xMin <= x <= xMax) and (yMin <= y <= yMax) for x, y in array]
def vectorLength(vector):
"""Return the length of the given vector."""
x, y = vector
return math.sqrt(x**2 + y**2)
def asInt16(array):
"""Round and cast to 16 bit integer."""
return [int(math.floor(i+0.5)) for i in array]
def normRect(box):
"""Normalize the rectangle so that the following holds:
xMin <= xMax and yMin <= yMax
"""
return min(box[0], box[2]), min(box[1], box[3]), max(box[0], box[2]), max(box[1], box[3])
def scaleRect(box, x, y):
"""Scale the rectangle by x, y."""
return box[0] * x, box[1] * y, box[2] * x, box[3] * y
def offsetRect(box, dx, dy):
"""Offset the rectangle by dx, dy."""
return box[0]+dx, box[1]+dy, box[2]+dx, box[3]+dy
def insetRect(box, dx, dy):
"""Inset the rectangle by dx, dy on all sides."""
return box[0]+dx, box[1]+dy, box[2]-dx, box[3]-dy
def sectRect(box1, box2):
"""Return a boolean and a rectangle. If the input rectangles intersect, return
True and the intersecting rectangle. Return False and (0, 0, 0, 0) if the input
rectangles don't intersect.
"""
xMin, yMin, xMax, yMax = (max(box1[0], box2[0]), max(box1[1], box2[1]),
min(box1[2], box2[2]), min(box1[3], box2[3]))
if xMin >= xMax or yMin >= yMax:
return 0, (0, 0, 0, 0)
return 1, (xMin, yMin, xMax, yMax)
def unionRect(box1, box2):
"""Return the smallest rectangle in which both input rectangles are fully
enclosed. In other words, return the total bounding rectangle of both input
rectangles.
"""
return (max(box1[0], box2[0]), max(box1[1], box2[1]),
min(box1[2], box2[2]), min(box1[3], box2[3]))
def rectCenter(box):
"""Return the center of the rectangle as an (x, y) coordinate."""
return (box[0]+box[2])/2, (box[1]+box[3])/2
def intRect(box):
"""Return the rectangle, rounded off to integer values, but guaranteeing that
the resulting rectangle is NOT smaller than the original.
"""
xMin, yMin, xMax, yMax = box
xMin = int(math.floor(xMin))
yMin = int(math.floor(yMin))
xMax = int(math.ceil(xMax))
yMax = int(math.ceil(yMax))
return (xMin, yMin, xMax, yMax)
def _test():
"""
>>> import math
>>> calcBounds([(0, 40), (0, 100), (50, 50), (80, 10)])
(0, 10, 80, 100)
>>> updateBounds((0, 0, 0, 0), (100, 100))
(0, 0, 100, 100)
>>> pointInRect((50, 50), (0, 0, 100, 100))
True
>>> pointInRect((0, 0), (0, 0, 100, 100))
True
>>> pointInRect((100, 100), (0, 0, 100, 100))
True
>>> not pointInRect((101, 100), (0, 0, 100, 100))
True
>>> list(pointsInRect([(50, 50), (0, 0), (100, 100), (101, 100)], (0, 0, 100, 100)))
[True, True, True, False]
>>> vectorLength((3, 4))
5.0
>>> vectorLength((1, 1)) == math.sqrt(2)
True
>>> list(asInt16([0, 0.1, 0.5, 0.9]))
[0, 0, 1, 1]
>>> normRect((0, 10, 100, 200))
(0, 10, 100, 200)
>>> normRect((100, 200, 0, 10))
(0, 10, 100, 200)
>>> scaleRect((10, 20, 50, 150), 1.5, 2)
(15.0, 40, 75.0, 300)
>>> offsetRect((10, 20, 30, 40), 5, 6)
(15, 26, 35, 46)
>>> insetRect((10, 20, 50, 60), 5, 10)
(15, 30, 45, 50)
>>> insetRect((10, 20, 50, 60), -5, -10)
(5, 10, 55, 70)
>>> intersects, rect = sectRect((0, 10, 20, 30), (0, 40, 20, 50))
>>> not intersects
True
>>> intersects, rect = sectRect((0, 10, 20, 30), (5, 20, 35, 50))
>>> intersects
1
>>> rect
(5, 20, 20, 30)
>>> unionRect((0, 10, 20, 30), (0, 40, 20, 50))
(0, 10, 20, 50)
>>> rectCenter((0, 0, 100, 200))
(50, 100)
>>> rectCenter((0, 0, 100, 199.0))
(50, 99.5)
>>> intRect((0.9, 2.9, 3.1, 4.1))
(0, 2, 4, 5)
"""
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

@ -0,0 +1,416 @@
"""fontTools.misc.bezierTools.py -- tools for working with bezier path segments.
Rewritten to elimate the numpy dependency
"""
__all__ = [
"calcQuadraticBounds",
"calcCubicBounds",
"splitLine",
"splitQuadratic",
"splitCubic",
"splitQuadraticAtT",
"splitCubicAtT",
"solveQuadratic",
"solveCubic",
]
from robofab.misc.arrayTools import calcBounds
epsilon = 1e-12
def calcQuadraticBounds(pt1, pt2, pt3):
"""Return the bounding rectangle for a qudratic bezier segment.
pt1 and pt3 are the "anchor" points, pt2 is the "handle".
>>> calcQuadraticBounds((0, 0), (50, 100), (100, 0))
(0, 0, 100, 50.0)
>>> calcQuadraticBounds((0, 0), (100, 0), (100, 100))
(0.0, 0.0, 100, 100)
"""
(ax, ay), (bx, by), (cx, cy) = calcQuadraticParameters(pt1, pt2, pt3)
ax2 = ax*2.0
ay2 = ay*2.0
roots = []
if ax2 != 0:
roots.append(-bx/ax2)
if ay2 != 0:
roots.append(-by/ay2)
points = [(ax*t*t + bx*t + cx, ay*t*t + by*t + cy) for t in roots if 0 <= t < 1] + [pt1, pt3]
return calcBounds(points)
def calcCubicBounds(pt1, pt2, pt3, pt4):
"""Return the bounding rectangle for a cubic bezier segment.
pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles".
>>> calcCubicBounds((0, 0), (25, 100), (75, 100), (100, 0))
(0, 0, 100, 75.0)
>>> calcCubicBounds((0, 0), (50, 0), (100, 50), (100, 100))
(0.0, 0.0, 100, 100)
>>> calcCubicBounds((50, 0), (0, 100), (100, 100), (50, 0))
(35.566243270259356, 0, 64.43375672974068, 75.0)
"""
(ax, ay), (bx, by), (cx, cy), (dx, dy) = calcCubicParameters(pt1, pt2, pt3, pt4)
# calc first derivative
ax3 = ax * 3.0
ay3 = ay * 3.0
bx2 = bx * 2.0
by2 = by * 2.0
xRoots = [t for t in solveQuadratic(ax3, bx2, cx) if 0 <= t < 1]
yRoots = [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1]
roots = xRoots + yRoots
points = [(ax*t*t*t + bx*t*t + cx * t + dx, ay*t*t*t + by*t*t + cy * t + dy) for t in roots] + [pt1, pt4]
return calcBounds(points)
def splitLine(pt1, pt2, where, isHorizontal):
"""Split the line between pt1 and pt2 at position 'where', which
is an x coordinate if isHorizontal is False, a y coordinate if
isHorizontal is True. Return a list of two line segments if the
line was successfully split, or a list containing the original
line.
>>> printSegments(splitLine((0, 0), (100, 200), 50, False))
((0, 0), (50.0, 100.0))
((50.0, 100.0), (100, 200))
>>> printSegments(splitLine((0, 0), (100, 200), 50, True))
((0, 0), (25.0, 50.0))
((25.0, 50.0), (100, 200))
>>> printSegments(splitLine((0, 0), (100, 100), 50, True))
((0, 0), (50.0, 50.0))
((50.0, 50.0), (100, 100))
>>> printSegments(splitLine((0, 0), (100, 100), 100, True))
((0, 0), (100, 100))
>>> printSegments(splitLine((0, 0), (100, 100), 0, True))
((0, 0), (0.0, 0.0))
((0.0, 0.0), (100, 100))
>>> printSegments(splitLine((0, 0), (100, 100), 0, False))
((0, 0), (0.0, 0.0))
((0.0, 0.0), (100, 100))
"""
pt1x, pt1y = pt1
pt2x, pt2y = pt2
ax = (pt2x - pt1x)
ay = (pt2y - pt1y)
bx = pt1x
by = pt1y
ax1 = (ax, ay)[isHorizontal]
if ax1 == 0:
return [(pt1, pt2)]
t = float(where - (bx, by)[isHorizontal]) / ax1
if 0 <= t < 1:
midPt = ax * t + bx, ay * t + by
return [(pt1, midPt), (midPt, pt2)]
else:
return [(pt1, pt2)]
def splitQuadratic(pt1, pt2, pt3, where, isHorizontal):
"""Split the quadratic curve between pt1, pt2 and pt3 at position 'where',
which is an x coordinate if isHorizontal is False, a y coordinate if
isHorizontal is True. Return a list of curve segments.
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 150, False))
((0, 0), (50, 100), (100, 0))
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, False))
((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, False))
((0.0, 0.0), (12.5, 25.0), (25.0, 37.5))
((25.0, 37.5), (62.5, 75.0), (100.0, 0.0))
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, True))
((0.0, 0.0), (7.32233047034, 14.6446609407), (14.6446609407, 25.0))
((14.6446609407, 25.0), (50.0, 75.0), (85.3553390593, 25.0))
((85.3553390593, 25.0), (92.6776695297, 14.6446609407), (100.0, -7.1054273576e-15))
>>> # XXX I'm not at all sure if the following behavior is desirable:
>>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, True))
((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (50.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
"""
a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
solutions = solveQuadratic(a[isHorizontal], b[isHorizontal],
c[isHorizontal] - where)
solutions = [t for t in solutions if 0 <= t < 1]
solutions.sort()
if not solutions:
return [(pt1, pt2, pt3)]
return _splitQuadraticAtT(a, b, c, *solutions)
def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal):
"""Split the cubic curve between pt1, pt2, pt3 and pt4 at position 'where',
which is an x coordinate if isHorizontal is False, a y coordinate if
isHorizontal is True. Return a list of curve segments.
>>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 150, False))
((0, 0), (25, 100), (75, 100), (100, 0))
>>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 50, False))
((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0))
>>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 25, True))
((0.0, 0.0), (2.2937927384, 9.17517095361), (4.79804488188, 17.5085042869), (7.47413641001, 25.0))
((7.47413641001, 25.0), (31.2886200204, 91.6666666667), (68.7113799796, 91.6666666667), (92.52586359, 25.0))
((92.52586359, 25.0), (95.2019551181, 17.5085042869), (97.7062072616, 9.17517095361), (100.0, 1.7763568394e-15))
"""
a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
solutions = solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal],
d[isHorizontal] - where)
solutions = [t for t in solutions if 0 <= t < 1]
solutions.sort()
if not solutions:
return [(pt1, pt2, pt3, pt4)]
return _splitCubicAtT(a, b, c, d, *solutions)
def splitQuadraticAtT(pt1, pt2, pt3, *ts):
"""Split the quadratic curve between pt1, pt2 and pt3 at one or more
values of t. Return a list of curve segments.
>>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5))
((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
>>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5, 0.75))
((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
((50.0, 50.0), (62.5, 50.0), (75.0, 37.5))
((75.0, 37.5), (87.5, 25.0), (100.0, 0.0))
"""
a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
return _splitQuadraticAtT(a, b, c, *ts)
def splitCubicAtT(pt1, pt2, pt3, pt4, *ts):
"""Split the cubic curve between pt1, pt2, pt3 and pt4 at one or more
values of t. Return a list of curve segments.
>>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5))
((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0))
>>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5, 0.75))
((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
((50.0, 75.0), (59.375, 75.0), (68.75, 68.75), (77.34375, 56.25))
((77.34375, 56.25), (85.9375, 43.75), (93.75, 25.0), (100.0, 0.0))
"""
a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
return _splitCubicAtT(a, b, c, d, *ts)
def _splitQuadraticAtT(a, b, c, *ts):
ts = list(ts)
segments = []
ts.insert(0, 0.0)
ts.append(1.0)
ax, ay = a
bx, by = b
cx, cy = c
for i in range(len(ts) - 1):
t1 = ts[i]
t2 = ts[i+1]
delta = (t2 - t1)
# calc new a, b and c
a1x = ax * delta**2
a1y = ay * delta**2
b1x = (2*ax*t1 + bx) * delta
b1y = (2*ay*t1 + by) * delta
c1x = ax*t1**2 + bx*t1 + cx
c1y = ay*t1**2 + by*t1 + cy
pt1, pt2, pt3 = calcQuadraticPoints((a1x, a1y), (b1x, b1y), (c1x, c1y))
segments.append((pt1, pt2, pt3))
return segments
def _splitCubicAtT(a, b, c, d, *ts):
ts = list(ts)
ts.insert(0, 0.0)
ts.append(1.0)
segments = []
ax, ay = a
bx, by = b
cx, cy = c
dx, dy = d
for i in range(len(ts) - 1):
t1 = ts[i]
t2 = ts[i+1]
delta = (t2 - t1)
# calc new a, b, c and d
a1x = ax * delta**3
a1y = ay * delta**3
b1x = (3*ax*t1 + bx) * delta**2
b1y = (3*ay*t1 + by) * delta**2
c1x = (2*bx*t1 + cx + 3*ax*t1**2) * delta
c1y = (2*by*t1 + cy + 3*ay*t1**2) * delta
d1x = ax*t1**3 + bx*t1**2 + cx*t1 + dx
d1y = ay*t1**3 + by*t1**2 + cy*t1 + dy
pt1, pt2, pt3, pt4 = calcCubicPoints((a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y))
segments.append((pt1, pt2, pt3, pt4))
return segments
#
# Equation solvers.
#
from math import sqrt, acos, cos, pi
def solveQuadratic(a, b, c,
sqrt=sqrt):
"""Solve a quadratic equation where a, b and c are real.
a*x*x + b*x + c = 0
This function returns a list of roots. Note that the returned list
is neither guaranteed to be sorted nor to contain unique values!
"""
if abs(a) < epsilon:
if abs(b) < epsilon:
# We have a non-equation; therefore, we have no valid solution
roots = []
else:
# We have a linear equation with 1 root.
roots = [-c/b]
else:
# We have a true quadratic equation. Apply the quadratic formula to find two roots.
DD = b*b - 4.0*a*c
if DD >= 0.0:
rDD = sqrt(DD)
roots = [(-b+rDD)/2.0/a, (-b-rDD)/2.0/a]
else:
# complex roots, ignore
roots = []
return roots
def solveCubic(a, b, c, d,
abs=abs, pow=pow, sqrt=sqrt, cos=cos, acos=acos, pi=pi):
"""Solve a cubic equation where a, b, c and d are real.
a*x*x*x + b*x*x + c*x + d = 0
This function returns a list of roots. Note that the returned list
is neither guaranteed to be sorted nor to contain unique values!
"""
#
# adapted from:
# CUBIC.C - Solve a cubic polynomial
# public domain by Ross Cottrell
# found at: http://www.strangecreations.com/library/snippets/Cubic.C
#
if abs(a) < epsilon:
# don't just test for zero; for very small values of 'a' solveCubic()
# returns unreliable results, so we fall back to quad.
return solveQuadratic(b, c, d)
a = float(a)
a1 = b/a
a2 = c/a
a3 = d/a
Q = (a1*a1 - 3.0*a2)/9.0
R = (2.0*a1*a1*a1 - 9.0*a1*a2 + 27.0*a3)/54.0
R2_Q3 = R*R - Q*Q*Q
if R2_Q3 < 0:
theta = acos(R/sqrt(Q*Q*Q))
rQ2 = -2.0*sqrt(Q)
x0 = rQ2*cos(theta/3.0) - a1/3.0
x1 = rQ2*cos((theta+2.0*pi)/3.0) - a1/3.0
x2 = rQ2*cos((theta+4.0*pi)/3.0) - a1/3.0
return [x0, x1, x2]
else:
if Q == 0 and R == 0:
x = 0
else:
x = pow(sqrt(R2_Q3)+abs(R), 1/3.0)
x = x + Q/x
if R >= 0.0:
x = -x
x = x - a1/3.0
return [x]
#
# Conversion routines for points to parameters and vice versa
#
def calcQuadraticParameters(pt1, pt2, pt3):
x2, y2 = pt2
x3, y3 = pt3
cx, cy = pt1
bx = (x2 - cx) * 2.0
by = (y2 - cy) * 2.0
ax = x3 - cx - bx
ay = y3 - cy - by
return (ax, ay), (bx, by), (cx, cy)
def calcCubicParameters(pt1, pt2, pt3, pt4):
x2, y2 = pt2
x3, y3 = pt3
x4, y4 = pt4
dx, dy = pt1
cx = (x2 -dx) * 3.0
cy = (y2 -dy) * 3.0
bx = (x3 - x2) * 3.0 - cx
by = (y3 - y2) * 3.0 - cy
ax = x4 - dx - cx - bx
ay = y4 - dy - cy - by
return (ax, ay), (bx, by), (cx, cy), (dx, dy)
def calcQuadraticPoints(a, b, c):
ax, ay = a
bx, by = b
cx, cy = c
x1 = cx
y1 = cy
x2 = (bx * 0.5) + cx
y2 = (by * 0.5) + cy
x3 = ax + bx + cx
y3 = ay + by + cy
return (x1, y1), (x2, y2), (x3, y3)
def calcCubicPoints(a, b, c, d):
ax, ay = a
bx, by = b
cx, cy = c
dx, dy = d
x1 = dx
y1 = dy
x2 = (cx / 3.0) + dx
y2 = (cy / 3.0) + dy
x3 = (bx + cx) / 3.0 + x2
y3 = (by + cy) / 3.0 + y2
x4 = ax + dx + cx + bx
y4 = ay + dy + cy + by
return (x1, y1), (x2, y2), (x3, y3), (x4, y4)
def _segmentrepr(obj):
"""
>>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]])
'(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))'
"""
try:
it = iter(obj)
except TypeError:
return str(obj)
else:
return "(%s)" % ", ".join([_segmentrepr(x) for x in it])
def printSegments(segments):
"""Helper for the doctests, displaying each segment in a list of
segments on a single line as a tuple.
"""
for segment in segments:
print _segmentrepr(segment)
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

@ -0,0 +1,99 @@
"""
Speed comparison between the fontTools numpy based arrayTools and bezierTools,
and the pure python implementation in robofab.path.arrayTools and robofab.path.bezierTools
"""
import time
from fontTools.misc import arrayTools
from fontTools.misc import bezierTools
import numpy
import robofab.misc.arrayTools as noNumpyArrayTools
import robofab.misc.bezierTools as noNumpyBezierTools
################
pt1 = (100, 100)
pt2 = (200, 20)
pt3 = (30, 580)
pt4 = (153, 654)
rect = [20, 20, 100, 100]
## loops
c = 10000
print "(loop %s)"%c
print "with numpy:"
print "calcQuadraticParameters\t\t",
n = time.time()
for i in range(c):
bezierTools.calcQuadraticParameters(pt1, pt2, pt3)
print time.time() - n
print "calcBounds\t\t\t",
n = time.time()
for i in range(c):
arrayTools.calcBounds([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3])
print time.time() - n
print "pointsInRect\t\t\t",
n = time.time()
for i in range(c):
arrayTools.pointsInRect([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt4], rect)
print time.time() - n
print "calcQuadraticBounds\t\t",
n = time.time()
for i in range(c):
bezierTools.calcQuadraticBounds(pt1, pt2, pt3)
print time.time() - n
print "calcCubicBounds\t\t\t",
n = time.time()
for i in range(c):
bezierTools.calcCubicBounds(pt1, pt2, pt3, pt4)
print time.time() - n
print
##############
print "no-numpy"
print "calcQuadraticParameters\t\t",
n = time.time()
for i in range(c):
noNumpyBezierTools.calcQuadraticParameters(pt1, pt2, pt3)
print time.time() - n
print "calcBounds\t\t\t",
n = time.time()
for i in range(c):
noNumpyArrayTools.calcBounds([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3])
print time.time() - n
print "pointsInRect\t\t\t",
n = time.time()
for i in range(c):
noNumpyArrayTools.pointsInRect([pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt1, pt2, pt3, pt4], rect)
print time.time() - n
print "calcQuadraticBounds\t\t",
n = time.time()
for i in range(c):
noNumpyBezierTools.calcQuadraticBounds(pt1, pt2, pt3)
print time.time() - n
print "calcCubicBounds\t\t\t",
n = time.time()
for i in range(c):
noNumpyBezierTools.calcCubicBounds(pt1, pt2, pt3, pt4)
print time.time() - n

View file

@ -0,0 +1,119 @@
"""
doc test requires fontTools to compare and defon to make the test font.
"""
import random
from fontTools.pens.basePen import BasePen
from fontTools.misc import arrayTools
from fontTools.misc import bezierTools
import robofab.misc.arrayTools as noNumpyArrayTools
import robofab.misc.bezierTools as noNumpyBezierTools
def drawMoveTo(pen, maxBox):
pen.moveTo((maxBox*random.random(), maxBox*random.random()))
def drawLineTo(pen, maxBox):
pen.lineTo((maxBox*random.random(), maxBox*random.random()))
def drawCurveTo(pen, maxBox):
pen.curveTo((maxBox*random.random(), maxBox*random.random()),
(maxBox*random.random(), maxBox*random.random()),
(maxBox*random.random(), maxBox*random.random()))
def drawClosePath(pen):
pen.closePath()
def createRandomFont():
from defcon import Font
maxGlyphs = 1000
maxContours = 10
maxSegments = 10
maxBox = 700
drawCallbacks = [drawLineTo, drawCurveTo]
f = Font()
for i in range(maxGlyphs):
name = "%s" %i
f.newGlyph(name)
g = f[name]
g.width = maxBox
pen = g.getPen()
for c in range(maxContours):
drawMoveTo(pen, maxBox)
for s in range(maxSegments):
random.choice(drawCallbacks)(pen, maxBox)
drawClosePath(pen)
return f
class BoundsPen(BasePen):
def __init__(self, glyphSet, at, bt):
BasePen.__init__(self, glyphSet)
self.bounds = None
self._start = None
self._arrayTools = at
self._bezierTools = bt
def _moveTo(self, pt):
self._start = pt
def _addMoveTo(self):
if self._start is None:
return
bounds = self.bounds
if bounds:
self.bounds = self._arrayTools.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 = self._arrayTools.updateBounds(self.bounds, pt)
def _curveToOne(self, bcp1, bcp2, pt):
self._addMoveTo()
bounds = self.bounds
bounds = self._arrayTools.updateBounds(bounds, pt)
if not self._arrayTools.pointInRect(bcp1, bounds) or not self._arrayTools.pointInRect(bcp2, bounds):
bounds = self._arrayTools.unionRect(bounds, self._bezierTools.calcCubicBounds(
self._getCurrentPoint(), bcp1, bcp2, pt))
self.bounds = bounds
def _qCurveToOne(self, bcp, pt):
self._addMoveTo()
bounds = self.bounds
bounds = self._arrayTools.updateBounds(bounds, pt)
if not self._arrayTools.pointInRect(bcp, bounds):
bounds = self._arrayToolsunionRect(bounds, self._bezierTools.calcQuadraticBounds(
self._getCurrentPoint(), bcp, pt))
self.bounds = bounds
def _testFont(font):
succes = True
for glyph in font:
fontToolsBoundsPen = BoundsPen(font, arrayTools, bezierTools)
glyph.draw(fontToolsBoundsPen)
noNumpyBoundsPen = BoundsPen(font, noNumpyArrayTools, noNumpyBezierTools)
glyph.draw(noNumpyBoundsPen)
if fontToolsBoundsPen.bounds != noNumpyBoundsPen.bounds:
succes = False
return succes
def testCompareAgainstFontTools():
"""
>>> font = createRandomFont()
>>> _testFont(font)
True
"""
if __name__ == "__main__":
import doctest
doctest.testmod()