commit
f87a897c7f
@ -4,7 +4,7 @@ from .cu2qu import *
|
||||
import random
|
||||
import timeit
|
||||
|
||||
MAX_ERR = 5
|
||||
MAX_ERR = 0.05
|
||||
|
||||
|
||||
def generate_curve():
|
||||
@ -23,9 +23,7 @@ def setup_curves_to_quadratic():
|
||||
return ([generate_curve() for curve in range(num_curves)], [MAX_ERR] * num_curves)
|
||||
|
||||
|
||||
def run_benchmark(
|
||||
benchmark_module, module, function, setup_suffix="", repeat=5, number=1000
|
||||
):
|
||||
def run_benchmark(module, function, setup_suffix="", repeat=5, number=1000):
|
||||
setup_func = "setup_" + function
|
||||
if setup_suffix:
|
||||
print("%s with %s:" % (function, setup_suffix), end="")
|
||||
@ -48,8 +46,8 @@ def run_benchmark(
|
||||
|
||||
def main():
|
||||
"""Benchmark the cu2qu algorithm performance."""
|
||||
run_benchmark("cu2qu.benchmark", "cu2qu", "curve_to_quadratic")
|
||||
run_benchmark("cu2qu.benchmark", "cu2qu", "curves_to_quadratic")
|
||||
run_benchmark("cu2qu", "curve_to_quadratic")
|
||||
run_benchmark("cu2qu", "curves_to_quadratic")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -7,6 +7,13 @@ from fontTools.misc.transform import Identity
|
||||
import math
|
||||
from collections import namedtuple
|
||||
|
||||
try:
|
||||
import cython
|
||||
except ImportError:
|
||||
# if cython not installed, use mock module with no-op decorators and types
|
||||
from fontTools.misc import cython
|
||||
|
||||
|
||||
Intersection = namedtuple("Intersection", ["pt", "t1", "t2"])
|
||||
|
||||
|
||||
@ -26,10 +33,13 @@ __all__ = [
|
||||
"splitCubic",
|
||||
"splitQuadraticAtT",
|
||||
"splitCubicAtT",
|
||||
"splitCubicAtTC",
|
||||
"splitCubicIntoTwoAtTC",
|
||||
"solveQuadratic",
|
||||
"solveCubic",
|
||||
"quadraticPointAtT",
|
||||
"cubicPointAtT",
|
||||
"cubicPointAtTC",
|
||||
"linePointAtT",
|
||||
"segmentPointAtT",
|
||||
"lineLineIntersections",
|
||||
@ -38,6 +48,13 @@ __all__ = [
|
||||
"segmentSegmentIntersections",
|
||||
]
|
||||
|
||||
if cython.compiled:
|
||||
# Yep, I'm compiled.
|
||||
COMPILED = True
|
||||
else:
|
||||
# Just a lowly interpreted script.
|
||||
COMPILED = False
|
||||
|
||||
|
||||
def calcCubicArcLength(pt1, pt2, pt3, pt4, tolerance=0.005):
|
||||
"""Calculates the arc length for a cubic Bezier segment.
|
||||
@ -67,6 +84,14 @@ def _split_cubic_into_two(p0, p1, p2, p3):
|
||||
)
|
||||
|
||||
|
||||
@cython.returns(cython.double)
|
||||
@cython.locals(
|
||||
p0=cython.complex,
|
||||
p1=cython.complex,
|
||||
p2=cython.complex,
|
||||
p3=cython.complex,
|
||||
)
|
||||
@cython.locals(mult=cython.double, arch=cython.double, box=cython.double)
|
||||
def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3):
|
||||
arch = abs(p0 - p3)
|
||||
box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3)
|
||||
@ -79,6 +104,17 @@ def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3):
|
||||
)
|
||||
|
||||
|
||||
@cython.returns(cython.double)
|
||||
@cython.locals(
|
||||
pt1=cython.complex,
|
||||
pt2=cython.complex,
|
||||
pt3=cython.complex,
|
||||
pt4=cython.complex,
|
||||
)
|
||||
@cython.locals(
|
||||
tolerance=cython.double,
|
||||
mult=cython.double,
|
||||
)
|
||||
def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005):
|
||||
"""Calculates the arc length for a cubic Bezier segment.
|
||||
|
||||
@ -97,10 +133,18 @@ epsilonDigits = 6
|
||||
epsilon = 1e-10
|
||||
|
||||
|
||||
@cython.cfunc
|
||||
@cython.inline
|
||||
@cython.returns(cython.double)
|
||||
@cython.locals(v1=cython.complex, v2=cython.complex)
|
||||
def _dot(v1, v2):
|
||||
return (v1 * v2.conjugate()).real
|
||||
|
||||
|
||||
@cython.cfunc
|
||||
@cython.inline
|
||||
@cython.returns(cython.double)
|
||||
@cython.locals(x=cython.complex)
|
||||
def _intSecAtan(x):
|
||||
# In : sympy.integrate(sp.sec(sp.atan(x)))
|
||||
# Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2
|
||||
@ -142,6 +186,25 @@ def calcQuadraticArcLength(pt1, pt2, pt3):
|
||||
return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3))
|
||||
|
||||
|
||||
@cython.returns(cython.double)
|
||||
@cython.locals(
|
||||
pt1=cython.complex,
|
||||
pt2=cython.complex,
|
||||
pt3=cython.complex,
|
||||
d0=cython.complex,
|
||||
d1=cython.complex,
|
||||
d=cython.complex,
|
||||
n=cython.complex,
|
||||
)
|
||||
@cython.locals(
|
||||
scale=cython.double,
|
||||
origDist=cython.double,
|
||||
a=cython.double,
|
||||
b=cython.double,
|
||||
x0=cython.double,
|
||||
x1=cython.double,
|
||||
Len=cython.double,
|
||||
)
|
||||
def calcQuadraticArcLengthC(pt1, pt2, pt3):
|
||||
"""Calculates the arc length for a quadratic Bezier segment.
|
||||
|
||||
@ -191,6 +254,17 @@ def approximateQuadraticArcLength(pt1, pt2, pt3):
|
||||
return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3))
|
||||
|
||||
|
||||
@cython.returns(cython.double)
|
||||
@cython.locals(
|
||||
pt1=cython.complex,
|
||||
pt2=cython.complex,
|
||||
pt3=cython.complex,
|
||||
)
|
||||
@cython.locals(
|
||||
v0=cython.double,
|
||||
v1=cython.double,
|
||||
v2=cython.double,
|
||||
)
|
||||
def approximateQuadraticArcLengthC(pt1, pt2, pt3):
|
||||
"""Calculates the arc length for a quadratic Bezier segment.
|
||||
|
||||
@ -288,6 +362,20 @@ def approximateCubicArcLength(pt1, pt2, pt3, pt4):
|
||||
)
|
||||
|
||||
|
||||
@cython.returns(cython.double)
|
||||
@cython.locals(
|
||||
pt1=cython.complex,
|
||||
pt2=cython.complex,
|
||||
pt3=cython.complex,
|
||||
pt4=cython.complex,
|
||||
)
|
||||
@cython.locals(
|
||||
v0=cython.double,
|
||||
v1=cython.double,
|
||||
v2=cython.double,
|
||||
v3=cython.double,
|
||||
v4=cython.double,
|
||||
)
|
||||
def approximateCubicArcLengthC(pt1, pt2, pt3, pt4):
|
||||
"""Approximates the arc length for a cubic Bezier segment.
|
||||
|
||||
@ -549,6 +637,70 @@ def splitCubicAtT(pt1, pt2, pt3, pt4, *ts):
|
||||
return _splitCubicAtT(a, b, c, d, *ts)
|
||||
|
||||
|
||||
@cython.locals(
|
||||
pt1=cython.complex,
|
||||
pt2=cython.complex,
|
||||
pt3=cython.complex,
|
||||
pt4=cython.complex,
|
||||
a=cython.complex,
|
||||
b=cython.complex,
|
||||
c=cython.complex,
|
||||
d=cython.complex,
|
||||
)
|
||||
def splitCubicAtTC(pt1, pt2, pt3, pt4, *ts):
|
||||
"""Split a cubic Bezier curve at one or more values of t.
|
||||
|
||||
Args:
|
||||
pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers..
|
||||
*ts: Positions at which to split the curve.
|
||||
|
||||
Yields:
|
||||
Curve segments (each curve segment being four complex numbers).
|
||||
"""
|
||||
a, b, c, d = calcCubicParametersC(pt1, pt2, pt3, pt4)
|
||||
yield from _splitCubicAtTC(a, b, c, d, *ts)
|
||||
|
||||
|
||||
@cython.returns(cython.complex)
|
||||
@cython.locals(
|
||||
t=cython.double,
|
||||
pt1=cython.complex,
|
||||
pt2=cython.complex,
|
||||
pt3=cython.complex,
|
||||
pt4=cython.complex,
|
||||
pointAtT=cython.complex,
|
||||
off1=cython.complex,
|
||||
off2=cython.complex,
|
||||
)
|
||||
@cython.locals(
|
||||
t2=cython.double, _1_t=cython.double, _1_t_2=cython.double, _2_t_1_t=cython.double
|
||||
)
|
||||
def splitCubicIntoTwoAtTC(pt1, pt2, pt3, pt4, t):
|
||||
"""Split a cubic Bezier curve at t.
|
||||
|
||||
Args:
|
||||
pt1,pt2,pt3,pt4: Control points of the Bezier as complex numbers.
|
||||
t: Position at which to split the curve.
|
||||
|
||||
Returns:
|
||||
A tuple of two curve segments (each curve segment being four complex numbers).
|
||||
"""
|
||||
t2 = t * t
|
||||
_1_t = 1 - t
|
||||
_1_t_2 = _1_t * _1_t
|
||||
_2_t_1_t = 2 * t * _1_t
|
||||
pointAtT = (
|
||||
_1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4
|
||||
)
|
||||
off1 = _1_t_2 * pt1 + _2_t_1_t * pt2 + t2 * pt3
|
||||
off2 = _1_t_2 * pt2 + _2_t_1_t * pt3 + t2 * pt4
|
||||
|
||||
pt2 = pt1 + (pt2 - pt1) * t
|
||||
pt3 = pt4 + (pt3 - pt4) * _1_t
|
||||
|
||||
return ((pt1, pt2, off1, pointAtT), (pointAtT, off2, pt3, pt4))
|
||||
|
||||
|
||||
def _splitQuadraticAtT(a, b, c, *ts):
|
||||
ts = list(ts)
|
||||
segments = []
|
||||
@ -611,6 +763,44 @@ def _splitCubicAtT(a, b, c, d, *ts):
|
||||
return segments
|
||||
|
||||
|
||||
@cython.locals(
|
||||
a=cython.complex,
|
||||
b=cython.complex,
|
||||
c=cython.complex,
|
||||
d=cython.complex,
|
||||
t1=cython.double,
|
||||
t2=cython.double,
|
||||
delta=cython.double,
|
||||
delta_2=cython.double,
|
||||
delta_3=cython.double,
|
||||
a1=cython.complex,
|
||||
b1=cython.complex,
|
||||
c1=cython.complex,
|
||||
d1=cython.complex,
|
||||
)
|
||||
def _splitCubicAtTC(a, b, c, d, *ts):
|
||||
ts = list(ts)
|
||||
ts.insert(0, 0.0)
|
||||
ts.append(1.0)
|
||||
for i in range(len(ts) - 1):
|
||||
t1 = ts[i]
|
||||
t2 = ts[i + 1]
|
||||
delta = t2 - t1
|
||||
|
||||
delta_2 = delta * delta
|
||||
delta_3 = delta * delta_2
|
||||
t1_2 = t1 * t1
|
||||
t1_3 = t1 * t1_2
|
||||
|
||||
# calc new a, b, c and d
|
||||
a1 = a * delta_3
|
||||
b1 = (3 * a * t1 + b) * delta_2
|
||||
c1 = (2 * b * t1 + c + 3 * a * t1_2) * delta
|
||||
d1 = a * t1_3 + b * t1_2 + c * t1 + d
|
||||
pt1, pt2, pt3, pt4 = calcCubicPointsC(a1, b1, c1, d1)
|
||||
yield (pt1, pt2, pt3, pt4)
|
||||
|
||||
|
||||
#
|
||||
# Equation solvers.
|
||||
#
|
||||
@ -773,6 +963,22 @@ def calcCubicParameters(pt1, pt2, pt3, pt4):
|
||||
return (ax, ay), (bx, by), (cx, cy), (dx, dy)
|
||||
|
||||
|
||||
@cython.locals(
|
||||
pt1=cython.complex,
|
||||
pt2=cython.complex,
|
||||
pt3=cython.complex,
|
||||
pt4=cython.complex,
|
||||
a=cython.complex,
|
||||
b=cython.complex,
|
||||
c=cython.complex,
|
||||
)
|
||||
def calcCubicParametersC(pt1, pt2, pt3, pt4):
|
||||
c = (pt2 - pt1) * 3.0
|
||||
b = (pt3 - pt2) * 3.0 - c
|
||||
a = pt4 - pt1 - c - b
|
||||
return (a, b, c, pt1)
|
||||
|
||||
|
||||
def calcQuadraticPoints(a, b, c):
|
||||
ax, ay = a
|
||||
bx, by = b
|
||||
@ -802,6 +1008,23 @@ def calcCubicPoints(a, b, c, d):
|
||||
return (x1, y1), (x2, y2), (x3, y3), (x4, y4)
|
||||
|
||||
|
||||
@cython.locals(
|
||||
a=cython.complex,
|
||||
b=cython.complex,
|
||||
c=cython.complex,
|
||||
d=cython.complex,
|
||||
p2=cython.complex,
|
||||
p3=cython.complex,
|
||||
p4=cython.complex,
|
||||
_1_3=cython.double,
|
||||
)
|
||||
def calcCubicPointsC(a, b, c, d, _1_3=1.0 / 3):
|
||||
p2 = (c * _1_3) + d
|
||||
p3 = (b + c) * _1_3 + p2
|
||||
p4 = a + b + c + d
|
||||
return (d, p2, p3, p4)
|
||||
|
||||
|
||||
#
|
||||
# Point at time
|
||||
#
|
||||
@ -845,21 +1068,47 @@ def cubicPointAtT(pt1, pt2, pt3, pt4, t):
|
||||
Returns:
|
||||
A 2D tuple with the coordinates of the point.
|
||||
"""
|
||||
t2 = t * t
|
||||
_1_t = 1 - t
|
||||
_1_t_2 = _1_t * _1_t
|
||||
x = (
|
||||
(1 - t) * (1 - t) * (1 - t) * pt1[0]
|
||||
+ 3 * (1 - t) * (1 - t) * t * pt2[0]
|
||||
+ 3 * (1 - t) * t * t * pt3[0]
|
||||
+ t * t * t * pt4[0]
|
||||
_1_t_2 * _1_t * pt1[0]
|
||||
+ 3 * (_1_t_2 * t * pt2[0] + _1_t * t2 * pt3[0])
|
||||
+ t2 * t * pt4[0]
|
||||
)
|
||||
y = (
|
||||
(1 - t) * (1 - t) * (1 - t) * pt1[1]
|
||||
+ 3 * (1 - t) * (1 - t) * t * pt2[1]
|
||||
+ 3 * (1 - t) * t * t * pt3[1]
|
||||
+ t * t * t * pt4[1]
|
||||
_1_t_2 * _1_t * pt1[1]
|
||||
+ 3 * (_1_t_2 * t * pt2[1] + _1_t * t2 * pt3[1])
|
||||
+ t2 * t * pt4[1]
|
||||
)
|
||||
return (x, y)
|
||||
|
||||
|
||||
@cython.returns(cython.complex)
|
||||
@cython.locals(
|
||||
t=cython.double,
|
||||
pt1=cython.complex,
|
||||
pt2=cython.complex,
|
||||
pt3=cython.complex,
|
||||
pt4=cython.complex,
|
||||
)
|
||||
@cython.locals(t2=cython.double, _1_t=cython.double, _1_t_2=cython.double)
|
||||
def cubicPointAtTC(pt1, pt2, pt3, pt4, t):
|
||||
"""Finds the point at time `t` on a cubic curve.
|
||||
|
||||
Args:
|
||||
pt1, pt2, pt3, pt4: Coordinates of the curve as complex numbers.
|
||||
t: The time along the curve.
|
||||
|
||||
Returns:
|
||||
A complex number with the coordinates of the point.
|
||||
"""
|
||||
t2 = t * t
|
||||
_1_t = 1 - t
|
||||
_1_t_2 = _1_t * _1_t
|
||||
return _1_t_2 * _1_t * pt1 + 3 * (_1_t_2 * t * pt2 + _1_t * t2 * pt3) + t2 * t * pt4
|
||||
|
||||
|
||||
def segmentPointAtT(seg, t):
|
||||
if len(seg) == 2:
|
||||
return linePointAtT(*seg, t)
|
||||
|
@ -13,13 +13,14 @@
|
||||
# limitations under the License.
|
||||
|
||||
from fontTools.cu2qu import curve_to_quadratic, curves_to_quadratic
|
||||
from fontTools.pens.basePen import AbstractPen, decomposeSuperBezierSegment
|
||||
from fontTools.pens.basePen import decomposeSuperBezierSegment
|
||||
from fontTools.pens.filterPen import FilterPen
|
||||
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||
from fontTools.pens.pointPen import BasePointToSegmentPen
|
||||
from fontTools.pens.pointPen import ReverseContourPointPen
|
||||
|
||||
|
||||
class Cu2QuPen(AbstractPen):
|
||||
class Cu2QuPen(FilterPen):
|
||||
"""A filter pen to convert cubic bezier curves to quadratic b-splines
|
||||
using the FontTools SegmentPen protocol.
|
||||
|
||||
@ -31,13 +32,6 @@ class Cu2QuPen(AbstractPen):
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: flip the contours' direction but keep starting point.
|
||||
stats: a dictionary counting the point numbers of quadratic segments.
|
||||
ignore_single_points: don't emit contours containing only a single point
|
||||
|
||||
NOTE: The "ignore_single_points" argument is deprecated since v1.3.0,
|
||||
which dropped Robofab support. It's no longer needed to special-case
|
||||
UFO2-style anchors (aka "named points") when using ufoLib >= 2.0,
|
||||
as these are no longer drawn onto pens as single-point contours,
|
||||
but are handled separately as anchors.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@ -46,63 +40,12 @@ class Cu2QuPen(AbstractPen):
|
||||
max_err,
|
||||
reverse_direction=False,
|
||||
stats=None,
|
||||
ignore_single_points=False,
|
||||
):
|
||||
if reverse_direction:
|
||||
self.pen = ReverseContourPen(other_pen)
|
||||
else:
|
||||
self.pen = other_pen
|
||||
other_pen = ReverseContourPen(other_pen)
|
||||
super().__init__(other_pen)
|
||||
self.max_err = max_err
|
||||
self.stats = stats
|
||||
if ignore_single_points:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"ignore_single_points is deprecated and "
|
||||
"will be removed in future versions",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.ignore_single_points = ignore_single_points
|
||||
self.start_pt = None
|
||||
self.current_pt = None
|
||||
|
||||
def _check_contour_is_open(self):
|
||||
if self.current_pt is None:
|
||||
raise AssertionError("moveTo is required")
|
||||
|
||||
def _check_contour_is_closed(self):
|
||||
if self.current_pt is not None:
|
||||
raise AssertionError("closePath or endPath is required")
|
||||
|
||||
def _add_moveTo(self):
|
||||
if self.start_pt is not None:
|
||||
self.pen.moveTo(self.start_pt)
|
||||
self.start_pt = None
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._check_contour_is_closed()
|
||||
self.start_pt = self.current_pt = pt
|
||||
if not self.ignore_single_points:
|
||||
self._add_moveTo()
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._check_contour_is_open()
|
||||
self._add_moveTo()
|
||||
self.pen.lineTo(pt)
|
||||
self.current_pt = pt
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
self._check_contour_is_open()
|
||||
n = len(points)
|
||||
if n == 1:
|
||||
self.lineTo(points[0])
|
||||
elif n > 1:
|
||||
self._add_moveTo()
|
||||
self.pen.qCurveTo(*points)
|
||||
self.current_pt = points[-1]
|
||||
else:
|
||||
raise AssertionError("illegal qcurve segment point count: %d" % n)
|
||||
|
||||
def _curve_to_quadratic(self, pt1, pt2, pt3):
|
||||
curve = (self.current_pt, pt1, pt2, pt3)
|
||||
@ -113,7 +56,6 @@ class Cu2QuPen(AbstractPen):
|
||||
self.qCurveTo(*quadratic[1:])
|
||||
|
||||
def curveTo(self, *points):
|
||||
self._check_contour_is_open()
|
||||
n = len(points)
|
||||
if n == 3:
|
||||
# this is the most common case, so we special-case it
|
||||
@ -121,29 +63,8 @@ class Cu2QuPen(AbstractPen):
|
||||
elif n > 3:
|
||||
for segment in decomposeSuperBezierSegment(points):
|
||||
self._curve_to_quadratic(*segment)
|
||||
elif n == 2:
|
||||
self.qCurveTo(*points)
|
||||
elif n == 1:
|
||||
self.lineTo(points[0])
|
||||
else:
|
||||
raise AssertionError("illegal curve segment point count: %d" % n)
|
||||
|
||||
def closePath(self):
|
||||
self._check_contour_is_open()
|
||||
if self.start_pt is None:
|
||||
# if 'start_pt' is _not_ None, we are ignoring single-point paths
|
||||
self.pen.closePath()
|
||||
self.current_pt = self.start_pt = None
|
||||
|
||||
def endPath(self):
|
||||
self._check_contour_is_open()
|
||||
if self.start_pt is None:
|
||||
self.pen.endPath()
|
||||
self.current_pt = self.start_pt = None
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
self._check_contour_is_closed()
|
||||
self.pen.addComponent(glyphName, transformation)
|
||||
self.qCurveTo(*points)
|
||||
|
||||
|
||||
class Cu2QuPointPen(BasePointToSegmentPen):
|
||||
@ -288,6 +209,9 @@ class Cu2QuMultiPen:
|
||||
each of the pens in other_pens.
|
||||
"""
|
||||
|
||||
# TODO Simplify like 3e8ebcdce592fe8a59ca4c3a294cc9724351e1ce
|
||||
# Remove start_pts and _add_moveTO
|
||||
|
||||
def __init__(self, other_pens, max_err, reverse_direction=False):
|
||||
if reverse_direction:
|
||||
other_pens = [
|
||||
|
@ -56,24 +56,31 @@ class FilterPen(_PassThruComponentsMixin, AbstractPen):
|
||||
|
||||
def __init__(self, outPen):
|
||||
self._outPen = outPen
|
||||
self.current_pt = None
|
||||
|
||||
def moveTo(self, pt):
|
||||
self._outPen.moveTo(pt)
|
||||
self.current_pt = pt
|
||||
|
||||
def lineTo(self, pt):
|
||||
self._outPen.lineTo(pt)
|
||||
self.current_pt = pt
|
||||
|
||||
def curveTo(self, *points):
|
||||
self._outPen.curveTo(*points)
|
||||
self.current_pt = points[-1]
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
self._outPen.qCurveTo(*points)
|
||||
self.current_pt = points[-1]
|
||||
|
||||
def closePath(self):
|
||||
self._outPen.closePath()
|
||||
self.current_pt = None
|
||||
|
||||
def endPath(self):
|
||||
self._outPen.endPath()
|
||||
self.current_pt = None
|
||||
|
||||
|
||||
class ContourFilterPen(_PassThruComponentsMixin, RecordingPen):
|
||||
|
101
Lib/fontTools/pens/qu2cuPen.py
Normal file
101
Lib/fontTools/pens/qu2cuPen.py
Normal file
@ -0,0 +1,101 @@
|
||||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
# Copyright 2023 Behdad Esfahbod. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from fontTools.qu2cu import quadratic_to_curves
|
||||
from fontTools.pens.filterPen import ContourFilterPen
|
||||
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||
|
||||
|
||||
class Qu2CuPen(ContourFilterPen):
|
||||
"""A filter pen to convert quadratic bezier splines to cubic curves
|
||||
using the FontTools SegmentPen protocol.
|
||||
|
||||
Args:
|
||||
|
||||
other_pen: another SegmentPen used to draw the transformed outline.
|
||||
max_err: maximum approximation error in font units. For optimal results,
|
||||
if you know the UPEM of the font, we recommend setting this to a
|
||||
value equal, or close to UPEM / 1000.
|
||||
reverse_direction: flip the contours' direction but keep starting point.
|
||||
stats: a dictionary counting the point numbers of cubic segments.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
other_pen,
|
||||
max_err,
|
||||
all_cubic=False,
|
||||
reverse_direction=False,
|
||||
stats=None,
|
||||
):
|
||||
if reverse_direction:
|
||||
other_pen = ReverseContourPen(other_pen)
|
||||
super().__init__(other_pen)
|
||||
self.all_cubic = all_cubic
|
||||
self.max_err = max_err
|
||||
self.stats = stats
|
||||
|
||||
def _quadratics_to_curve(self, q):
|
||||
curves = quadratic_to_curves(q, self.max_err, self.all_cubic)
|
||||
if self.stats is not None:
|
||||
n = str(len(curves))
|
||||
self.stats[n] = self.stats.get(n, 0) + 1
|
||||
for curve in curves:
|
||||
if len(curve) == 4:
|
||||
yield ("curveTo", curve[1:])
|
||||
else:
|
||||
yield ("qCurveTo", curve[1:])
|
||||
|
||||
def filterContour(self, contour):
|
||||
quadratics = []
|
||||
currentPt = None
|
||||
newContour = []
|
||||
for op, args in contour:
|
||||
if op == "qCurveTo" and (
|
||||
self.all_cubic or (len(args) > 2 and args[-1] is not None)
|
||||
):
|
||||
if args[-1] is None:
|
||||
raise NotImplementedError(
|
||||
"oncurve-less contours with all_cubic not implemented"
|
||||
)
|
||||
quadratics.append((currentPt,) + args)
|
||||
else:
|
||||
if quadratics:
|
||||
newContour.extend(self._quadratics_to_curve(quadratics))
|
||||
quadratics = []
|
||||
newContour.append((op, args))
|
||||
currentPt = args[-1] if args else None
|
||||
if quadratics:
|
||||
newContour.extend(self._quadratics_to_curve(quadratics))
|
||||
|
||||
# Add back implicit oncurve points
|
||||
contour = newContour
|
||||
newContour = []
|
||||
for op, args in contour:
|
||||
if op == "qCurveTo" and newContour and newContour[-1][0] == "qCurveTo":
|
||||
pt0 = newContour[-1][1][-2]
|
||||
pt1 = newContour[-1][1][-1]
|
||||
pt2 = args[0]
|
||||
if (
|
||||
pt2[0] - pt1[0] == pt1[0] - pt0[0]
|
||||
and pt2[1] - pt1[1] == pt1[1] - pt0[1]
|
||||
):
|
||||
newArgs = newContour[-1][1][:-1] + args
|
||||
newContour[-1] = (op, newArgs)
|
||||
continue
|
||||
|
||||
newContour.append((op, args))
|
||||
|
||||
return newContour
|
@ -7,6 +7,7 @@ from fontTools.misc.roundTools import otRound
|
||||
from fontTools.pens.basePen import LoggingPen, PenError
|
||||
from fontTools.pens.transformPen import TransformPen, TransformPointPen
|
||||
from fontTools.ttLib.tables import ttProgram
|
||||
from fontTools.ttLib.tables._g_l_y_f import flagOnCurve
|
||||
from fontTools.ttLib.tables._g_l_y_f import Glyph
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
||||
@ -124,7 +125,7 @@ class _TTGlyphBasePen:
|
||||
components.append(component)
|
||||
return components
|
||||
|
||||
def glyph(self, componentFlags: int = 0x4) -> Glyph:
|
||||
def glyph(self, componentFlags: int = 0x4, preserveTopology=True) -> Glyph:
|
||||
"""
|
||||
Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
|
||||
"""
|
||||
@ -149,6 +150,52 @@ class _TTGlyphBasePen:
|
||||
glyph.program = ttProgram.Program()
|
||||
glyph.program.fromBytecode(b"")
|
||||
|
||||
if not preserveTopology:
|
||||
|
||||
# Drop implied on-curve points
|
||||
|
||||
drop = set()
|
||||
start = 0
|
||||
flags = glyph.flags
|
||||
coords = glyph.coordinates
|
||||
for last in glyph.endPtsOfContours:
|
||||
for i in range(start, last + 1):
|
||||
if not (flags[i] & flagOnCurve):
|
||||
continue
|
||||
prv = i - 1 if i > start else last
|
||||
nxt = i + 1 if i < last else start
|
||||
if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]:
|
||||
continue
|
||||
p0 = coords[prv]
|
||||
p1 = coords[i]
|
||||
p2 = coords[nxt]
|
||||
if p1[0] - p0[0] != p2[0] - p1[0] or p1[1] - p0[1] != p2[1] - p1[1]:
|
||||
continue
|
||||
|
||||
drop.add(i)
|
||||
if drop:
|
||||
# Do the actual dropping
|
||||
glyph.coordinates = GlyphCoordinates(
|
||||
coords[i] for i in range(len(coords)) if i not in drop
|
||||
)
|
||||
glyph.flags = array(
|
||||
"B", (flags[i] for i in range(len(flags)) if i not in drop)
|
||||
)
|
||||
|
||||
endPts = glyph.endPtsOfContours
|
||||
newEndPts = []
|
||||
i = 0
|
||||
delta = 0
|
||||
for d in sorted(drop):
|
||||
while d > endPts[i]:
|
||||
newEndPts.append(endPts[i] - delta)
|
||||
i += 1
|
||||
delta += 1
|
||||
while i < len(endPts):
|
||||
newEndPts.append(endPts[i] - delta)
|
||||
i += 1
|
||||
glyph.endPtsOfContours = newEndPts
|
||||
|
||||
return glyph
|
||||
|
||||
|
||||
|
15
Lib/fontTools/qu2cu/__init__.py
Normal file
15
Lib/fontTools/qu2cu/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .qu2cu import *
|
7
Lib/fontTools/qu2cu/__main__.py
Normal file
7
Lib/fontTools/qu2cu/__main__.py
Normal file
@ -0,0 +1,7 @@
|
||||
import sys
|
||||
|
||||
from .cli import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
52
Lib/fontTools/qu2cu/benchmark.py
Normal file
52
Lib/fontTools/qu2cu/benchmark.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""Benchmark the qu2cu algorithm performance."""
|
||||
|
||||
from .qu2cu import *
|
||||
from fontTools.cu2qu import curve_to_quadratic
|
||||
import random
|
||||
import timeit
|
||||
|
||||
MAX_ERR = 0.05
|
||||
|
||||
|
||||
def generate_curve():
|
||||
return [
|
||||
tuple(float(random.randint(0, 2048)) for coord in range(2))
|
||||
for point in range(4)
|
||||
]
|
||||
|
||||
|
||||
def setup_quadratic_to_curves():
|
||||
curve = generate_curve()
|
||||
quadratics = curve_to_quadratic(curve, MAX_ERR)
|
||||
return [quadratics], MAX_ERR
|
||||
|
||||
|
||||
def run_benchmark(module, function, setup_suffix="", repeat=10, number=20):
|
||||
setup_func = "setup_" + function
|
||||
if setup_suffix:
|
||||
print("%s with %s:" % (function, setup_suffix), end="")
|
||||
setup_func += "_" + setup_suffix
|
||||
else:
|
||||
print("%s:" % function, end="")
|
||||
|
||||
def wrapper(function, setup_func):
|
||||
function = globals()[function]
|
||||
setup_func = globals()[setup_func]
|
||||
|
||||
def wrapped():
|
||||
return function(*setup_func())
|
||||
|
||||
return wrapped
|
||||
|
||||
results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number)
|
||||
print("\t%5.1fus" % (min(results) * 1000000.0 / number))
|
||||
|
||||
|
||||
def main():
|
||||
"""Benchmark the qu2cu algorithm performance."""
|
||||
run_benchmark("qu2cu", "quadratic_to_curves")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
random.seed(1)
|
||||
main()
|
109
Lib/fontTools/qu2cu/cli.py
Normal file
109
Lib/fontTools/qu2cu/cli.py
Normal file
@ -0,0 +1,109 @@
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
from fontTools.misc.cliTools import makeOutputFileName
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.qu2cuPen import Qu2CuPen
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
import fontTools
|
||||
|
||||
|
||||
logger = logging.getLogger("fontTools.qu2cu")
|
||||
|
||||
|
||||
def _font_to_cubic(input_path, output_path=None, **kwargs):
|
||||
font = TTFont(input_path)
|
||||
logger.info("Converting curves for %s", input_path)
|
||||
|
||||
qu2cu_kwargs = {
|
||||
"stats": {} if kwargs["dump_stats"] else None,
|
||||
"max_err": kwargs["max_err_em"] * font["head"].unitsPerEm,
|
||||
}
|
||||
|
||||
assert "gvar" not in font, "Cannot convert variable font"
|
||||
glyphSet = font.getGlyphSet()
|
||||
glyphOrder = font.getGlyphOrder()
|
||||
glyf = font["glyf"]
|
||||
for glyphName in glyphOrder:
|
||||
glyph = glyphSet[glyphName]
|
||||
ttpen = TTGlyphPen(glyphSet)
|
||||
pen = Qu2CuPen(ttpen, **qu2cu_kwargs)
|
||||
glyph.draw(pen)
|
||||
glyf[glyphName] = ttpen.glyph(preserveTopology=False)
|
||||
|
||||
logger.info("Saving %s", output_path)
|
||||
font.save(output_path)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
parser = argparse.ArgumentParser(prog="qu2cu")
|
||||
parser.add_argument("--version", action="version", version=fontTools.__version__)
|
||||
parser.add_argument(
|
||||
"infiles",
|
||||
nargs="+",
|
||||
metavar="INPUT",
|
||||
help="one or more input TTF source file(s).",
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", action="count", default=0)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--conversion-error",
|
||||
type=float,
|
||||
metavar="ERROR",
|
||||
default=0.001,
|
||||
help="maxiumum approximation error measured in EM (default: 0.001)",
|
||||
)
|
||||
|
||||
output_parser = parser.add_mutually_exclusive_group()
|
||||
output_parser.add_argument(
|
||||
"-o",
|
||||
"--output-file",
|
||||
default=None,
|
||||
metavar="OUTPUT",
|
||||
help=("output filename for the converted TTF."),
|
||||
)
|
||||
output_parser.add_argument(
|
||||
"-d",
|
||||
"--output-dir",
|
||||
default=None,
|
||||
metavar="DIRECTORY",
|
||||
help="output directory where to save converted TTFs",
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
if not options.verbose:
|
||||
level = "WARNING"
|
||||
elif options.verbose == 1:
|
||||
level = "INFO"
|
||||
else:
|
||||
level = "DEBUG"
|
||||
logging.basicConfig(level=level)
|
||||
|
||||
if len(options.infiles) > 1 and options.output_file:
|
||||
parser.error("-o/--output-file can't be used with multile inputs")
|
||||
|
||||
if options.output_dir:
|
||||
output_dir = options.output_dir
|
||||
if not os.path.exists(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
elif not os.path.isdir(output_dir):
|
||||
parser.error("'%s' is not a directory" % output_dir)
|
||||
output_paths = [
|
||||
os.path.join(output_dir, os.path.basename(p)) for p in options.infiles
|
||||
]
|
||||
elif options.output_file:
|
||||
output_paths = [options.output_file]
|
||||
else:
|
||||
output_paths = [
|
||||
makeOutputFileName(p, overWrite=True, suffix=".cubic")
|
||||
for p in options.infiles
|
||||
]
|
||||
|
||||
kwargs = dict(
|
||||
dump_stats=options.verbose > 0,
|
||||
max_err_em=options.conversion_error,
|
||||
)
|
||||
|
||||
for input_path, output_path in zip(options.infiles, output_paths):
|
||||
_font_to_cubic(input_path, output_path, **kwargs)
|
363
Lib/fontTools/qu2cu/qu2cu.py
Normal file
363
Lib/fontTools/qu2cu/qu2cu.py
Normal file
@ -0,0 +1,363 @@
|
||||
# cython: language_level=3
|
||||
# distutils: define_macros=CYTHON_TRACE_NOGIL=1
|
||||
|
||||
# Copyright 2023 Google Inc. All Rights Reserved.
|
||||
# Copyright 2023 Behdad Esfahbod. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
try:
|
||||
import cython
|
||||
except ImportError:
|
||||
# if cython not installed, use mock module with no-op decorators and types
|
||||
from fontTools.misc import cython
|
||||
|
||||
from fontTools.misc.bezierTools import splitCubicAtTC
|
||||
from collections import namedtuple
|
||||
from typing import (
|
||||
List,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["quadratic_to_curves"]
|
||||
|
||||
|
||||
if cython.compiled:
|
||||
# Yep, I'm compiled.
|
||||
COMPILED = True
|
||||
else:
|
||||
# Just a lowly interpreted script.
|
||||
COMPILED = False
|
||||
|
||||
|
||||
# Copied from cu2qu
|
||||
@cython.cfunc
|
||||
@cython.returns(cython.int)
|
||||
@cython.locals(
|
||||
tolerance=cython.double,
|
||||
p0=cython.complex,
|
||||
p1=cython.complex,
|
||||
p2=cython.complex,
|
||||
p3=cython.complex,
|
||||
)
|
||||
@cython.locals(mid=cython.complex, deriv3=cython.complex)
|
||||
def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
|
||||
"""Check if a cubic Bezier lies within a given distance of the origin.
|
||||
|
||||
"Origin" means *the* origin (0,0), not the start of the curve. Note that no
|
||||
checks are made on the start and end positions of the curve; this function
|
||||
only checks the inside of the curve.
|
||||
|
||||
Args:
|
||||
p0 (complex): Start point of curve.
|
||||
p1 (complex): First handle of curve.
|
||||
p2 (complex): Second handle of curve.
|
||||
p3 (complex): End point of curve.
|
||||
tolerance (double): Distance from origin.
|
||||
|
||||
Returns:
|
||||
bool: True if the cubic Bezier ``p`` entirely lies within a distance
|
||||
``tolerance`` of the origin, False otherwise.
|
||||
"""
|
||||
# First check p2 then p1, as p2 has higher error early on.
|
||||
if abs(p2) <= tolerance and abs(p1) <= tolerance:
|
||||
return True
|
||||
|
||||
# Split.
|
||||
mid = (p0 + 3 * (p1 + p2) + p3) * 0.125
|
||||
if abs(mid) > tolerance:
|
||||
return False
|
||||
deriv3 = (p3 + p2 - p1 - p0) * 0.125
|
||||
return cubic_farthest_fit_inside(
|
||||
p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance
|
||||
) and cubic_farthest_fit_inside(mid, mid + deriv3, (p2 + p3) * 0.5, p3, tolerance)
|
||||
|
||||
|
||||
@cython.locals(_1_3=cython.double, _2_3=cython.double)
|
||||
@cython.locals(
|
||||
p0=cython.complex,
|
||||
p1=cython.complex,
|
||||
p2=cython.complex,
|
||||
p1_2_3=cython.complex,
|
||||
)
|
||||
def elevate_quadratic(p0, p1, p2, _1_3=1 / 3, _2_3=2 / 3):
|
||||
"""Given a quadratic bezier curve, return its degree-elevated cubic."""
|
||||
|
||||
# https://pomax.github.io/bezierinfo/#reordering
|
||||
p1_2_3 = p1 * _2_3
|
||||
return (
|
||||
p0,
|
||||
(p0 * _1_3 + p1_2_3),
|
||||
(p2 * _1_3 + p1_2_3),
|
||||
p2,
|
||||
)
|
||||
|
||||
|
||||
@cython.locals(
|
||||
n=cython.int,
|
||||
k=cython.int,
|
||||
prod_ratio=cython.double,
|
||||
sum_ratio=cython.double,
|
||||
ratio=cython.double,
|
||||
p0=cython.complex,
|
||||
p1=cython.complex,
|
||||
p2=cython.complex,
|
||||
p3=cython.complex,
|
||||
)
|
||||
def merge_curves(curves):
|
||||
"""Give a cubic-Bezier spline, reconstruct one cubic-Bezier
|
||||
that has the same endpoints and tangents and approxmates
|
||||
the spline."""
|
||||
|
||||
# Reconstruct the t values of the cut segments
|
||||
n = len(curves)
|
||||
prod_ratio = 1.0
|
||||
sum_ratio = 1.0
|
||||
ts = [1]
|
||||
for k in range(1, n):
|
||||
ck = curves[k]
|
||||
c_before = curves[k - 1]
|
||||
|
||||
# |t_(k+1) - t_k| / |t_k - t_(k - 1)| = ratio
|
||||
assert ck[0] == c_before[3]
|
||||
ratio = abs(ck[1] - ck[0]) / abs(c_before[3] - c_before[2])
|
||||
|
||||
prod_ratio *= ratio
|
||||
sum_ratio += prod_ratio
|
||||
ts.append(sum_ratio)
|
||||
|
||||
# (t(n) - t(n - 1)) / (t_(1) - t(0)) = prod_ratio
|
||||
|
||||
ts = [t / sum_ratio for t in ts[:-1]]
|
||||
|
||||
p0 = curves[0][0]
|
||||
p1 = curves[0][1]
|
||||
p2 = curves[n - 1][2]
|
||||
p3 = curves[n - 1][3]
|
||||
|
||||
# Build the curve by scaling the control-points.
|
||||
p1 = p0 + (p1 - p0) / (ts[0] if ts else 1)
|
||||
p2 = p3 + (p2 - p3) / ((1 - ts[-1]) if ts else 1)
|
||||
|
||||
curve = (p0, p1, p2, p3)
|
||||
|
||||
return curve, ts
|
||||
|
||||
|
||||
def add_implicit_on_curves(p):
|
||||
q = list(p)
|
||||
count = 0
|
||||
num_offcurves = len(p) - 2
|
||||
for i in range(1, num_offcurves):
|
||||
off1 = p[i]
|
||||
off2 = p[i + 1]
|
||||
on = off1 + (off2 - off1) * 0.5
|
||||
q.insert(i + 1 + count, on)
|
||||
count += 1
|
||||
return q
|
||||
|
||||
|
||||
Point = Union[Tuple[float, float], complex]
|
||||
|
||||
|
||||
def quadratic_to_curves(
|
||||
quads: List[List[Point]],
|
||||
max_err: float = 0.5,
|
||||
all_cubic: bool = False,
|
||||
) -> List[Tuple[Point, ...]]:
|
||||
"""Converts a connecting list of quadratic splines to a list of quadratic
|
||||
and cubic curves.
|
||||
|
||||
A quadratic spline is specified as a list of points. Either each point is
|
||||
a 2-tuple of X,Y coordinates, or each point is a complex number with
|
||||
real/imaginary components representing X,Y coordinates.
|
||||
|
||||
The first and last points are on-curve points and the rest are off-curve
|
||||
points, with an implied on-curve point in the middle between every two
|
||||
consequtive off-curve points.
|
||||
|
||||
Returns:
|
||||
The output is a list of tuples of points. Points are represented
|
||||
in the same format as the input, either as 2-tuples or complex numbers.
|
||||
|
||||
Each tuple is either of length three, for a quadratic curve, or four,
|
||||
for a cubic curve. Each curve's last point is the same as the next
|
||||
curve's first point.
|
||||
|
||||
Args:
|
||||
quads: quadratic splines
|
||||
|
||||
max_err: absolute error tolerance; defaults to 0.5
|
||||
|
||||
all_cubic: if True, only cubic curves are generated; defaults to False
|
||||
"""
|
||||
is_complex = type(quads[0][0]) is complex
|
||||
if not is_complex:
|
||||
quads = [[complex(x, y) for (x, y) in p] for p in quads]
|
||||
|
||||
q = [quads[0][0]]
|
||||
cost = 0
|
||||
costs = [0]
|
||||
for p in quads:
|
||||
assert q[-1] == p[0]
|
||||
for i in range(len(p) - 2):
|
||||
cost += 1
|
||||
costs.append(cost)
|
||||
costs.append(cost + 1)
|
||||
qq = add_implicit_on_curves(p)[1:]
|
||||
q.extend(qq)
|
||||
cost += 1
|
||||
costs.append(cost)
|
||||
costs.append(cost + 1)
|
||||
|
||||
curves = spline_to_curves(q, costs, max_err, all_cubic)
|
||||
|
||||
if not is_complex:
|
||||
curves = [tuple((c.real, c.imag) for c in curve) for curve in curves]
|
||||
return curves
|
||||
|
||||
|
||||
Solution = namedtuple("Solution", ["num_points", "error", "start_index", "is_cubic"])
|
||||
|
||||
|
||||
def spline_to_curves(q, costs, tolerance=0.5, all_cubic=False):
|
||||
"""
|
||||
q: quadratic spline with alternating on-curve / off-curve points.
|
||||
|
||||
costs: cumulative list of encoding cost of q in terms of number of
|
||||
points that need to be encoded. Implied on-curve points do not
|
||||
contribute to the cost. If all points need to be encoded, then
|
||||
costs will be range(len(q)+1).
|
||||
"""
|
||||
|
||||
assert len(q) >= 3, "quadratic spline requires at least 3 points"
|
||||
|
||||
# Elevate quadratic segments to cubic
|
||||
elevated_quadratics = [
|
||||
elevate_quadratic(*q[i : i + 3]) for i in range(0, len(q) - 2, 2)
|
||||
]
|
||||
|
||||
# Dynamic-Programming to find the solution with fewest number of
|
||||
# cubic curves, and within those the one with smallest error.
|
||||
sols = [Solution(0, 0, 0, False)]
|
||||
for i in range(1, len(elevated_quadratics) + 1):
|
||||
best_sol = Solution(len(q) + 2, 0, 1, False)
|
||||
for j in range(0, i):
|
||||
|
||||
j_sol_count, j_sol_error = sols[j].num_points, sols[j].error
|
||||
|
||||
if not all_cubic:
|
||||
# Solution with quadratics between j:i
|
||||
this_count = costs[2 * i] - costs[2 * j]
|
||||
i_sol_count = j_sol_count + this_count
|
||||
i_sol_error = j_sol_error
|
||||
i_sol = Solution(i_sol_count, i_sol_error, i - j, False)
|
||||
if i_sol < best_sol:
|
||||
best_sol = i_sol
|
||||
|
||||
if this_count == 3:
|
||||
# Can't get any better than this
|
||||
break
|
||||
|
||||
# Fit elevated_quadratics[j:i] into one cubic
|
||||
try:
|
||||
curve, ts = merge_curves(elevated_quadratics[j:i])
|
||||
except ZeroDivisionError:
|
||||
continue
|
||||
|
||||
# Now reconstruct the segments from the fitted curve
|
||||
reconstructed_iter = splitCubicAtTC(*curve, *ts)
|
||||
reconstructed = []
|
||||
|
||||
# Knot errors
|
||||
error = 0
|
||||
for k, reconst in enumerate(reconstructed_iter):
|
||||
orig = elevated_quadratics[j + k]
|
||||
err = abs(reconst[3] - orig[3])
|
||||
error = max(error, err)
|
||||
if error > tolerance:
|
||||
break
|
||||
reconstructed.append(reconst)
|
||||
if error > tolerance:
|
||||
# Not feasible
|
||||
continue
|
||||
|
||||
# Interior errors
|
||||
for k, reconst in enumerate(reconstructed):
|
||||
orig = elevated_quadratics[j + k]
|
||||
p0, p1, p2, p3 = tuple(v - u for v, u in zip(reconst, orig))
|
||||
|
||||
if not cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
|
||||
error = tolerance + 1
|
||||
break
|
||||
if error > tolerance:
|
||||
# Not feasible
|
||||
continue
|
||||
|
||||
# Save best solution
|
||||
i_sol_count = j_sol_count + 3
|
||||
i_sol_error = max(j_sol_error, error)
|
||||
i_sol = Solution(i_sol_count, i_sol_error, i - j, True)
|
||||
if i_sol < best_sol:
|
||||
best_sol = i_sol
|
||||
|
||||
if i_sol_count == 3:
|
||||
# Can't get any better than this
|
||||
break
|
||||
|
||||
sols.append(best_sol)
|
||||
|
||||
# Reconstruct solution
|
||||
splits = []
|
||||
cubic = []
|
||||
i = len(sols) - 1
|
||||
while i:
|
||||
count, is_cubic = sols[i].start_index, sols[i].is_cubic
|
||||
splits.append(i)
|
||||
cubic.append(is_cubic)
|
||||
i -= count
|
||||
curves = []
|
||||
j = 0
|
||||
for i, is_cubic in reversed(list(zip(splits, cubic))):
|
||||
if is_cubic:
|
||||
curves.append(merge_curves(elevated_quadratics[j:i])[0])
|
||||
else:
|
||||
for k in range(j, i):
|
||||
curves.append(q[k * 2 : k * 2 + 3])
|
||||
j = i
|
||||
|
||||
return curves
|
||||
|
||||
|
||||
def main():
|
||||
from fontTools.cu2qu.benchmark import generate_curve
|
||||
from fontTools.cu2qu import curve_to_quadratic
|
||||
|
||||
tolerance = 0.05
|
||||
reconstruct_tolerance = tolerance * 1
|
||||
curve = generate_curve()
|
||||
quadratics = curve_to_quadratic(curve, tolerance)
|
||||
print(
|
||||
"cu2qu tolerance %g. qu2cu tolerance %g." % (tolerance, reconstruct_tolerance)
|
||||
)
|
||||
print("One random cubic turned into %d quadratics." % len(quadratics))
|
||||
curves = quadratic_to_curves([quadratics], reconstruct_tolerance)
|
||||
print("Those quadratics turned back into %d cubics. " % len(curves))
|
||||
print("Original curve:", curve)
|
||||
print("Reconstructed curve(s):", curves)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -117,52 +117,6 @@ class TestCu2QuPen(unittest.TestCase, _TestPenMixin):
|
||||
self.pen_getter_name = "getPen"
|
||||
self.draw_method_name = "draw"
|
||||
|
||||
def test__check_contour_is_open(self):
|
||||
msg = "moveTo is required"
|
||||
quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
|
||||
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.lineTo((0, 0))
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.qCurveTo((0, 0), (1, 1))
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.curveTo((0, 0), (1, 1), (2, 2))
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.closePath()
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.endPath()
|
||||
|
||||
quadpen.moveTo((0, 0)) # now it works
|
||||
quadpen.lineTo((1, 1))
|
||||
quadpen.qCurveTo((2, 2), (3, 3))
|
||||
quadpen.curveTo((4, 4), (5, 5), (6, 6))
|
||||
quadpen.closePath()
|
||||
|
||||
def test__check_contour_closed(self):
|
||||
msg = "closePath or endPath is required"
|
||||
quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.moveTo((1, 1))
|
||||
with self.assertRaisesRegex(AssertionError, msg):
|
||||
quadpen.addComponent("a", (1, 0, 0, 1, 0, 0))
|
||||
|
||||
# it works if contour is closed
|
||||
quadpen.closePath()
|
||||
quadpen.moveTo((1, 1))
|
||||
quadpen.endPath()
|
||||
quadpen.addComponent("a", (1, 0, 0, 1, 0, 0))
|
||||
|
||||
def test_qCurveTo_no_points(self):
|
||||
quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
AssertionError, "illegal qcurve segment point count: 0"
|
||||
):
|
||||
quadpen.qCurveTo()
|
||||
|
||||
def test_qCurveTo_1_point(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
@ -173,7 +127,7 @@ class TestCu2QuPen(unittest.TestCase, _TestPenMixin):
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.lineTo((1, 1))",
|
||||
"pen.qCurveTo((1, 1))",
|
||||
],
|
||||
)
|
||||
|
||||
@ -191,15 +145,6 @@ class TestCu2QuPen(unittest.TestCase, _TestPenMixin):
|
||||
],
|
||||
)
|
||||
|
||||
def test_curveTo_no_points(self):
|
||||
quadpen = Cu2QuPen(DummyPen(), MAX_ERR)
|
||||
quadpen.moveTo((0, 0))
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
AssertionError, "illegal curve segment point count: 0"
|
||||
):
|
||||
quadpen.curveTo()
|
||||
|
||||
def test_curveTo_1_point(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
@ -210,7 +155,7 @@ class TestCu2QuPen(unittest.TestCase, _TestPenMixin):
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.lineTo((1, 1))",
|
||||
"pen.qCurveTo((1, 1))",
|
||||
],
|
||||
)
|
||||
|
||||
@ -258,59 +203,6 @@ class TestCu2QuPen(unittest.TestCase, _TestPenMixin):
|
||||
],
|
||||
)
|
||||
|
||||
def test_addComponent(self):
|
||||
pen = DummyPen()
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR)
|
||||
quadpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0))
|
||||
|
||||
# components are passed through without changes
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))",
|
||||
],
|
||||
)
|
||||
|
||||
def test_ignore_single_points(self):
|
||||
pen = DummyPen()
|
||||
try:
|
||||
logging.captureWarnings(True)
|
||||
with CapturingLogHandler("py.warnings", level="WARNING") as log:
|
||||
quadpen = Cu2QuPen(pen, MAX_ERR, ignore_single_points=True)
|
||||
finally:
|
||||
logging.captureWarnings(False)
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.endPath()
|
||||
quadpen.moveTo((1, 1))
|
||||
quadpen.closePath()
|
||||
|
||||
self.assertGreaterEqual(len(log.records), 1)
|
||||
if sys.version_info < (3, 11):
|
||||
self.assertIn("ignore_single_points is deprecated", log.records[0].args[0])
|
||||
else:
|
||||
self.assertIn("ignore_single_points is deprecated", log.records[0].msg)
|
||||
|
||||
# single-point contours were ignored, so the pen commands are empty
|
||||
self.assertFalse(pen.commands)
|
||||
|
||||
# redraw without ignoring single points
|
||||
quadpen.ignore_single_points = False
|
||||
quadpen.moveTo((0, 0))
|
||||
quadpen.endPath()
|
||||
quadpen.moveTo((1, 1))
|
||||
quadpen.closePath()
|
||||
|
||||
self.assertTrue(pen.commands)
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.endPath()",
|
||||
"pen.moveTo((1, 1))",
|
||||
"pen.closePath()",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestCu2QuPointPen(unittest.TestCase, _TestPenMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
224
Tests/pens/qu2cuPen_test.py
Normal file
224
Tests/pens/qu2cuPen_test.py
Normal file
@ -0,0 +1,224 @@
|
||||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from fontTools.pens.qu2cuPen import Qu2CuPen
|
||||
from fontTools.pens.recordingPen import RecordingPen
|
||||
from . import CUBIC_GLYPHS, QUAD_GLYPHS
|
||||
from .utils import DummyGlyph
|
||||
from .utils import DummyPen
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
MAX_ERR = 1.0
|
||||
|
||||
|
||||
class _TestPenMixin(object):
|
||||
"""Collection of tests that are shared by both the SegmentPen and the
|
||||
PointPen test cases, plus some helper methods.
|
||||
Note: We currently don't have a PointPen.
|
||||
"""
|
||||
|
||||
maxDiff = None
|
||||
|
||||
def diff(self, expected, actual):
|
||||
import difflib
|
||||
|
||||
expected = str(self.Glyph(expected)).splitlines(True)
|
||||
actual = str(self.Glyph(actual)).splitlines(True)
|
||||
diff = difflib.unified_diff(
|
||||
expected, actual, fromfile="expected", tofile="actual"
|
||||
)
|
||||
return "".join(diff)
|
||||
|
||||
def convert_glyph(self, glyph, **kwargs):
|
||||
# draw source glyph onto a new glyph using a Cu2Qu pen and return it
|
||||
converted = self.Glyph()
|
||||
pen = getattr(converted, self.pen_getter_name)()
|
||||
cubicpen = self.Qu2CuPen(pen, MAX_ERR, all_cubic=True, **kwargs)
|
||||
getattr(glyph, self.draw_method_name)(cubicpen)
|
||||
return converted
|
||||
|
||||
def expect_glyph(self, source, expected):
|
||||
converted = self.convert_glyph(source)
|
||||
self.assertNotEqual(converted, source)
|
||||
if not converted.approx(expected):
|
||||
print(self.diff(expected, converted))
|
||||
self.fail("converted glyph is different from expected")
|
||||
|
||||
def test_convert_simple_glyph(self):
|
||||
self.expect_glyph(QUAD_GLYPHS["a"], CUBIC_GLYPHS["a"])
|
||||
self.expect_glyph(QUAD_GLYPHS["A"], CUBIC_GLYPHS["A"])
|
||||
|
||||
def test_convert_composite_glyph(self):
|
||||
source = CUBIC_GLYPHS["Aacute"]
|
||||
converted = self.convert_glyph(source)
|
||||
# components don't change after quadratic conversion
|
||||
self.assertEqual(converted, source)
|
||||
|
||||
def test_reverse_direction(self):
|
||||
for name in ("a", "A", "Eacute"):
|
||||
source = QUAD_GLYPHS[name]
|
||||
normal_glyph = self.convert_glyph(source)
|
||||
reversed_glyph = self.convert_glyph(source, reverse_direction=True)
|
||||
|
||||
# the number of commands is the same, just their order is iverted
|
||||
self.assertTrue(len(normal_glyph.outline), len(reversed_glyph.outline))
|
||||
self.assertNotEqual(normal_glyph, reversed_glyph)
|
||||
|
||||
def test_stats(self):
|
||||
stats = {}
|
||||
for name in QUAD_GLYPHS.keys():
|
||||
source = QUAD_GLYPHS[name]
|
||||
self.convert_glyph(source, stats=stats)
|
||||
|
||||
self.assertTrue(stats)
|
||||
self.assertTrue("1" in stats)
|
||||
self.assertEqual(type(stats["1"]), int)
|
||||
|
||||
def test_addComponent(self):
|
||||
pen = self.Pen()
|
||||
cubicpen = self.Qu2CuPen(pen, MAX_ERR)
|
||||
cubicpen.addComponent("a", (1, 2, 3, 4, 5.0, 6.0))
|
||||
|
||||
# components are passed through without changes
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.addComponent('a', (1, 2, 3, 4, 5.0, 6.0))",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestQu2CuPen(unittest.TestCase, _TestPenMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestQu2CuPen, self).__init__(*args, **kwargs)
|
||||
self.Glyph = DummyGlyph
|
||||
self.Pen = DummyPen
|
||||
self.Qu2CuPen = Qu2CuPen
|
||||
self.pen_getter_name = "getPen"
|
||||
self.draw_method_name = "draw"
|
||||
|
||||
def test_qCurveTo_1_point(self):
|
||||
pen = DummyPen()
|
||||
cubicpen = Qu2CuPen(pen, MAX_ERR)
|
||||
cubicpen.moveTo((0, 0))
|
||||
cubicpen.qCurveTo((1, 1))
|
||||
cubicpen.closePath()
|
||||
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.qCurveTo((1, 1))",
|
||||
"pen.closePath()",
|
||||
],
|
||||
)
|
||||
|
||||
def test_qCurveTo_2_points(self):
|
||||
pen = DummyPen()
|
||||
cubicpen = Qu2CuPen(pen, MAX_ERR)
|
||||
cubicpen.moveTo((0, 0))
|
||||
cubicpen.qCurveTo((1, 1), (2, 2))
|
||||
cubicpen.closePath()
|
||||
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.qCurveTo((1, 1), (2, 2))",
|
||||
"pen.closePath()",
|
||||
],
|
||||
)
|
||||
|
||||
def test_qCurveTo_3_points_no_conversion(self):
|
||||
pen = DummyPen()
|
||||
cubicpen = Qu2CuPen(pen, MAX_ERR)
|
||||
cubicpen.moveTo((0, 0))
|
||||
cubicpen.qCurveTo((0, 3), (1, 3), (1, 0))
|
||||
cubicpen.closePath()
|
||||
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.qCurveTo((0, 3), (1, 3), (1, 0))",
|
||||
"pen.closePath()",
|
||||
],
|
||||
)
|
||||
|
||||
def test_qCurveTo_no_oncurve_points(self):
|
||||
pen = DummyPen()
|
||||
cubicpen = Qu2CuPen(pen, MAX_ERR)
|
||||
cubicpen.qCurveTo((0, 0), (1, 0), (1, 1), (0, 1), None)
|
||||
cubicpen.closePath()
|
||||
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
["pen.qCurveTo((0, 0), (1, 0), (1, 1), (0, 1), None)", "pen.closePath()"],
|
||||
)
|
||||
|
||||
def test_curveTo_1_point(self):
|
||||
pen = DummyPen()
|
||||
cubicpen = Qu2CuPen(pen, MAX_ERR)
|
||||
cubicpen.moveTo((0, 0))
|
||||
cubicpen.curveTo((1, 1))
|
||||
cubicpen.closePath()
|
||||
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.curveTo((1, 1))",
|
||||
"pen.closePath()",
|
||||
],
|
||||
)
|
||||
|
||||
def test_curveTo_2_points(self):
|
||||
pen = DummyPen()
|
||||
cubicpen = Qu2CuPen(pen, MAX_ERR)
|
||||
cubicpen.moveTo((0, 0))
|
||||
cubicpen.curveTo((1, 1), (2, 2))
|
||||
cubicpen.closePath()
|
||||
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.curveTo((1, 1), (2, 2))",
|
||||
"pen.closePath()",
|
||||
],
|
||||
)
|
||||
|
||||
def test_curveTo_3_points(self):
|
||||
pen = DummyPen()
|
||||
cubicpen = Qu2CuPen(pen, MAX_ERR)
|
||||
cubicpen.moveTo((0, 0))
|
||||
cubicpen.curveTo((1, 1), (2, 2), (3, 3))
|
||||
cubicpen.closePath()
|
||||
|
||||
self.assertEqual(
|
||||
str(pen).splitlines(),
|
||||
[
|
||||
"pen.moveTo((0, 0))",
|
||||
"pen.curveTo((1, 1), (2, 2), (3, 3))",
|
||||
"pen.closePath()",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -226,7 +226,11 @@ def _repr_pen_commands(commands):
|
||||
# cast float to int if there're no digits after decimal point,
|
||||
# and round floats to 12 decimal digits (more than enough)
|
||||
args = [
|
||||
tuple((int(v) if int(v) == v else round(v, 12)) for v in pt)
|
||||
(
|
||||
tuple((int(v) if int(v) == v else round(v, 12)) for v in pt)
|
||||
if pt is not None
|
||||
else None
|
||||
)
|
||||
for pt in args
|
||||
]
|
||||
args = ", ".join(repr(a) for a in args)
|
||||
|
106
Tests/qu2cu/qu2cu_test.py
Normal file
106
Tests/qu2cu/qu2cu_test.py
Normal file
@ -0,0 +1,106 @@
|
||||
# Copyright 2023 Behdad Esfahbod. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
from fontTools.qu2cu import quadratic_to_curves
|
||||
from fontTools.qu2cu.qu2cu import main as qu2cu_main
|
||||
from fontTools.qu2cu.benchmark import main as benchmark_main
|
||||
|
||||
import os
|
||||
import json
|
||||
from fontTools.cu2qu import curve_to_quadratic
|
||||
|
||||
|
||||
class Qu2CuTest:
|
||||
@pytest.mark.parametrize(
|
||||
"quadratics, expected, tolerance, cubic_only",
|
||||
[
|
||||
(
|
||||
[
|
||||
[(0, 0), (0, 1), (2, 1), (2, 0)],
|
||||
],
|
||||
[
|
||||
((0, 0), (0, 4 / 3), (2, 4 / 3), (2, 0)),
|
||||
],
|
||||
0.1,
|
||||
True,
|
||||
),
|
||||
(
|
||||
[
|
||||
[(0, 0), (0, 1), (2, 1), (2, 2)],
|
||||
],
|
||||
[
|
||||
((0, 0), (0, 4 / 3), (2, 2 / 3), (2, 2)),
|
||||
],
|
||||
0.2,
|
||||
True,
|
||||
),
|
||||
(
|
||||
[
|
||||
[(0, 0), (0, 1), (1, 1)],
|
||||
[(1, 1), (3, 1), (3, 0)],
|
||||
],
|
||||
[
|
||||
((0, 0), (0, 1), (1, 1)),
|
||||
((1, 1), (3, 1), (3, 0)),
|
||||
],
|
||||
0.2,
|
||||
False,
|
||||
),
|
||||
(
|
||||
[
|
||||
[(0, 0), (0, 1), (1, 1)],
|
||||
[(1, 1), (3, 1), (3, 0)],
|
||||
],
|
||||
[
|
||||
((0, 0), (0, 2 / 3), (1 / 3, 1), (1, 1)),
|
||||
((1, 1), (7 / 3, 1), (3, 2 / 3), (3, 0)),
|
||||
],
|
||||
0.2,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_simple(self, quadratics, expected, tolerance, cubic_only):
|
||||
|
||||
expected = [
|
||||
tuple((pytest.approx(p[0]), pytest.approx(p[1])) for p in curve)
|
||||
for curve in expected
|
||||
]
|
||||
|
||||
c = quadratic_to_curves(quadratics, tolerance, cubic_only)
|
||||
assert c == expected
|
||||
|
||||
def test_roundtrip(self):
|
||||
|
||||
DATADIR = os.path.join(os.path.dirname(__file__), "..", "cu2qu", "data")
|
||||
with open(os.path.join(DATADIR, "curves.json"), "r") as fp:
|
||||
curves = json.load(fp)
|
||||
|
||||
tolerance = 1
|
||||
|
||||
splines = [curve_to_quadratic(c, tolerance) for c in curves]
|
||||
reconsts = [quadratic_to_curves([spline], tolerance) for spline in splines]
|
||||
|
||||
for curve, reconst in zip(curves, reconsts):
|
||||
assert len(reconst) == 1
|
||||
curve = tuple((pytest.approx(p[0]), pytest.approx(p[1])) for p in curve)
|
||||
assert curve == reconst[0]
|
||||
|
||||
def test_main(self):
|
||||
# Just for coverage
|
||||
qu2cu_main()
|
||||
benchmark_main()
|
6
setup.py
6
setup.py
@ -73,6 +73,12 @@ if with_cython is True or (with_cython is None and has_cython):
|
||||
ext_modules.append(
|
||||
Extension("fontTools.cu2qu.cu2qu", ["Lib/fontTools/cu2qu/cu2qu.py"]),
|
||||
)
|
||||
ext_modules.append(
|
||||
Extension("fontTools.qu2cu.qu2cu", ["Lib/fontTools/qu2cu/qu2cu.py"]),
|
||||
)
|
||||
ext_modules.append(
|
||||
Extension("fontTools.misc.bezierTools", ["Lib/fontTools/misc/bezierTools.py"]),
|
||||
)
|
||||
ext_modules.append(
|
||||
Extension("fontTools.pens.momentsPen", ["Lib/fontTools/pens/momentsPen.py"]),
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user