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:
parent
31ae014e0c
commit
8234b62ab7
108 changed files with 26933 additions and 110 deletions
13
misc/pylib/robofab/misc/__init__.py
Executable file
13
misc/pylib/robofab/misc/__init__.py
Executable 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.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
160
misc/pylib/robofab/misc/arrayTools.pyx
Normal file
160
misc/pylib/robofab/misc/arrayTools.pyx
Normal 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()
|
||||
416
misc/pylib/robofab/misc/bezierTools.py
Normal file
416
misc/pylib/robofab/misc/bezierTools.py
Normal 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()
|
||||
99
misc/pylib/robofab/misc/speedTestCase.py
Normal file
99
misc/pylib/robofab/misc/speedTestCase.py
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
119
misc/pylib/robofab/misc/test.py
Normal file
119
misc/pylib/robofab/misc/test.py
Normal 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()
|
||||
Reference in a new issue