[docs] Document cu2qu library (#1937)

[docs] Document cu2qu library

Reorganise the documentation so that everything is in one place and users are more clearly pointed to the modules which are likely to be useful for their purposes. (I still think it’s worth having at least a brief reference to ``cu2qu.cli`` in there, as a way of reminding users that there is a command-line implementation.) Docstrings are provided for non-API methods where I could understand them - trusting these will be useful for future maintainers.
This commit is contained in:
Simon Cozens 2020-05-15 10:53:41 +01:00 committed by GitHub
parent 3d705b29de
commit ecc764ecc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 185 additions and 61 deletions

View File

@ -1,8 +0,0 @@
###
cli
###
.. automodule:: fontTools.cu2qu.cli
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
#####
cu2qu
#####
.. automodule:: fontTools.cu2qu.cu2qu
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
######
errors
######
.. automodule:: fontTools.cu2qu.errors
:inherited-members:
:members:
:undoc-members:

View File

@ -1,16 +1,38 @@
##### ##########################################
cu2qu cu2qu: Cubic to quadratic curve conversion
##### ##########################################
.. toctree:: Routines for converting cubic curves to quadratic splines, suitable for use
:maxdepth: 1 in OpenType to TrueType outline conversion.
cli Conversion is carried out to a degree of tolerance provided by the user. While
cu2qu it is relatively easy to find the best *single* quadratic curve to represent a
errors given cubic (see for example `this method from CAGD <https://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/>`_),
ufo the best-fit method may not be sufficiently accurate for type design.
.. automodule:: fontTools.cu2qu Instead, this method chops the cubic curve into multiple segments before
converting each cubic segment to a quadratic, in order to ensure that the
resulting spline fits within the given tolerance.
The basic curve conversion routines are implemented in the
:mod:`fontTools.cu2qu.cu2qu` module; the :mod:`fontTools.cu2qu.ufo` module
applies these routines to all of the curves in a UFO file or files; while the
:mod:`fontTools.cu2qu.cli` module implements the ``fonttools cu2qu`` command
for converting a UFO format font with cubic curves into one with quadratic
curves.
fontTools.cu2qu.cu2qu
---------------------
.. automodule:: fontTools.cu2qu.cu2qu
:inherited-members: :inherited-members:
:members: :members:
:undoc-members: :undoc-members:
fontTools.cu2qu.ufo
-------------------
.. automodule:: fontTools.cu2qu.ufo
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
###
ufo
###
.. automodule:: fontTools.cu2qu.ufo
:inherited-members:
:members:
:undoc-members:

View File

@ -46,7 +46,15 @@ else:
@cython.returns(cython.double) @cython.returns(cython.double)
@cython.locals(v1=cython.complex, v2=cython.complex) @cython.locals(v1=cython.complex, v2=cython.complex)
def dot(v1, v2): def dot(v1, v2):
"""Return the dot product of two vectors.""" """Return the dot product of two vectors.
Args:
v1 (complex): First vector.
v2 (complex): Second vector.
Returns:
double: Dot product.
"""
return (v1 * v2.conjugate()).real return (v1 * v2.conjugate()).real
@ -77,6 +85,21 @@ def calc_cubic_parameters(p0, p1, p2, p3):
@cython.cfunc @cython.cfunc
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) @cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
def split_cubic_into_n_iter(p0, p1, p2, p3, n): def split_cubic_into_n_iter(p0, p1, p2, p3, n):
"""Split a cubic Bezier into n equal parts.
Splits the curve into `n` equal parts by curve time.
(t=0..1/n, t=1/n..2/n, ...)
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.
Returns:
An iterator yielding the control points (four complex values) of the
subcurves.
"""
# Hand-coded special-cases # Hand-coded special-cases
if n == 2: if n == 2:
return iter(split_cubic_into_two(p0, p1, p2, p3)) return iter(split_cubic_into_two(p0, p1, p2, p3))
@ -115,6 +138,20 @@ def _split_cubic_into_n_gen(p0, p1, p2, p3, n):
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) @cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
@cython.locals(mid=cython.complex, deriv3=cython.complex) @cython.locals(mid=cython.complex, deriv3=cython.complex)
def split_cubic_into_two(p0, p1, p2, p3): def split_cubic_into_two(p0, p1, p2, p3):
"""Split a cubic Bezier into two equal parts.
Splits the curve into two equal parts at t = 0.5
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.
Returns:
tuple: Two cubic Beziers (each expressed as a tuple of four complex
values).
"""
mid = (p0 + 3 * (p1 + p2) + p3) * .125 mid = (p0 + 3 * (p1 + p2) + p3) * .125
deriv3 = (p3 + p2 - p1 - p0) * .125 deriv3 = (p3 + p2 - p1 - p0) * .125
return ((p0, (p0 + p1) * .5, mid - deriv3, mid), return ((p0, (p0 + p1) * .5, mid - deriv3, mid),
@ -124,6 +161,20 @@ def split_cubic_into_two(p0, p1, p2, p3):
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, _27=cython.double) @cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, _27=cython.double)
@cython.locals(mid1=cython.complex, deriv1=cython.complex, mid2=cython.complex, deriv2=cython.complex) @cython.locals(mid1=cython.complex, deriv1=cython.complex, mid2=cython.complex, deriv2=cython.complex)
def split_cubic_into_three(p0, p1, p2, p3, _27=1/27): def split_cubic_into_three(p0, p1, p2, p3, _27=1/27):
"""Split a cubic Bezier into three equal parts.
Splits the curve into three equal parts at t = 1/3 and t = 2/3
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.
Returns:
tuple: Three cubic Beziers (each expressed as a tuple of four complex
values).
"""
# we define 1/27 as a keyword argument so that it will be evaluated only # we define 1/27 as a keyword argument so that it will be evaluated only
# once but still in the scope of this function # once but still in the scope of this function
mid1 = (8*p0 + 12*p1 + 6*p2 + p3) * _27 mid1 = (8*p0 + 12*p1 + 6*p2 + p3) * _27
@ -139,8 +190,18 @@ def split_cubic_into_three(p0, p1, p2, p3, _27=1/27):
@cython.locals(t=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) @cython.locals(t=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
@cython.locals(_p1=cython.complex, _p2=cython.complex) @cython.locals(_p1=cython.complex, _p2=cython.complex)
def cubic_approx_control(t, p0, p1, p2, p3): def cubic_approx_control(t, p0, p1, p2, p3):
"""Approximate a cubic bezier curve with a quadratic one. """Approximate a cubic Bezier using a quadratic one.
Returns the candidate control point."""
Args:
t (double): Position of control point.
p0 (complex): Start point of curve.
p1 (complex): First handle of curve.
p2 (complex): Second handle of curve.
p3 (complex): End point of curve.
Returns:
complex: Location of candidate control point on quadratic curve.
"""
_p1 = p0 + (p1 - p0) * 1.5 _p1 = p0 + (p1 - p0) * 1.5
_p2 = p3 + (p2 - p3) * 1.5 _p2 = p3 + (p2 - p3) * 1.5
return _p1 + (_p2 - _p1) * t return _p1 + (_p2 - _p1) * t
@ -150,8 +211,18 @@ def cubic_approx_control(t, p0, p1, p2, p3):
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
@cython.locals(ab=cython.complex, cd=cython.complex, p=cython.complex, h=cython.double) @cython.locals(ab=cython.complex, cd=cython.complex, p=cython.complex, h=cython.double)
def calc_intersect(a, b, c, d): def calc_intersect(a, b, c, d):
"""Calculate the intersection of ab and cd, given a, b, c, d.""" """Calculate the intersection of two lines.
Args:
a (complex): Start point of first line.
b (complex): End point of first line.
c (complex): Start point of second line.
d (complex): End point of second line.
Returns:
complex: Location of intersection if one present, ``complex(NaN,NaN)``
if no intersection was found.
"""
ab = b - a ab = b - a
cd = d - c cd = d - c
p = ab * 1j p = ab * 1j
@ -167,10 +238,23 @@ def calc_intersect(a, b, c, d):
@cython.locals(tolerance=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) @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) @cython.locals(mid=cython.complex, deriv3=cython.complex)
def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance): def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
"""Returns True if the cubic Bezier p entirely lies within a distance """Check if a cubic Bezier lies within a given distance of the origin.
tolerance of origin, False otherwise. Assumes that p0 and p3 do fit
within tolerance of origin, and just checks the inside of the curve."""
"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. # First check p2 then p1, as p2 has higher error early on.
if abs(p2) <= tolerance and abs(p1) <= tolerance: if abs(p2) <= tolerance and abs(p1) <= tolerance:
return True return True
@ -188,8 +272,18 @@ def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
@cython.locals(tolerance=cython.double, _2_3=cython.double) @cython.locals(tolerance=cython.double, _2_3=cython.double)
@cython.locals(q1=cython.complex, c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex) @cython.locals(q1=cython.complex, c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3): def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3):
"""Return the uniq quadratic approximating cubic that maintains """Approximate a cubic Bezier with a single quadratic within a given tolerance.
endpoint tangents if that is within tolerance, None otherwise."""
Args:
cubic (sequence): Four complex numbers representing control points of
the cubic Bezier curve.
tolerance (double): Permitted deviation from the original curve.
Returns:
Three complex numbers representing control points of the quadratic
curve if it fits within the given tolerance, or ``None`` if no suitable
curve could be calculated.
"""
# we define 2/3 as a keyword argument so that it will be evaluated only # we define 2/3 as a keyword argument so that it will be evaluated only
# once but still in the scope of this function # once but still in the scope of this function
@ -214,10 +308,18 @@ def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3):
@cython.locals(c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex) @cython.locals(c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
@cython.locals(q0=cython.complex, q1=cython.complex, next_q1=cython.complex, q2=cython.complex, d1=cython.complex) @cython.locals(q0=cython.complex, q1=cython.complex, next_q1=cython.complex, q2=cython.complex, d1=cython.complex)
def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3): def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3):
"""Approximate a cubic bezier curve with a spline of n quadratics. """Approximate a cubic Bezier curve with a spline of n quadratics.
Returns None if no quadratic approximation is found which lies entirely Args:
within a distance `tolerance` from the original curve. cubic (sequence): Four complex numbers representing control points of
the cubic Bezier curve.
n (int): Number of quadratic Bezier curves in the spline.
tolerance (double): Permitted deviation from the original curve.
Returns:
A list of ``n+2`` complex numbers, representing control points of the
quadratic spline if it fits within the given tolerance, or ``None`` if
no suitable spline could be calculated.
""" """
# we define 2/3 as a keyword argument so that it will be evaluated only # we define 2/3 as a keyword argument so that it will be evaluated only
# once but still in the scope of this function # once but still in the scope of this function
@ -268,9 +370,17 @@ def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3):
@cython.locals(max_err=cython.double) @cython.locals(max_err=cython.double)
@cython.locals(n=cython.int) @cython.locals(n=cython.int)
def curve_to_quadratic(curve, max_err): def curve_to_quadratic(curve, max_err):
"""Return a quadratic spline approximating this cubic bezier. """Approximate a cubic Bezier curve with a spline of n quadratics.
Raise 'ApproxNotFoundError' if no suitable approximation can be found
with the given parameters. Args:
cubic (sequence): Four 2D tuples representing control points of
the cubic Bezier curve.
max_err (double): Permitted deviation from the original curve.
Returns:
A list of 2D tuples, representing control points of the quadratic
spline if it fits within the given tolerance, or ``None`` if no
suitable spline could be calculated.
""" """
curve = [complex(*p) for p in curve] curve = [complex(*p) for p in curve]
@ -287,9 +397,33 @@ def curve_to_quadratic(curve, max_err):
@cython.locals(l=cython.int, last_i=cython.int, i=cython.int) @cython.locals(l=cython.int, last_i=cython.int, i=cython.int)
def curves_to_quadratic(curves, max_errors): def curves_to_quadratic(curves, max_errors):
"""Return quadratic splines approximating these cubic beziers. """Return quadratic Bezier splines approximating the input cubic Beziers.
Raise 'ApproxNotFoundError' if no suitable approximation can be found
for all curves with the given parameters. Args:
curves: A sequence of *n* curves, each curve being a sequence of four
2D tuples.
max_errors: A sequence of *n* floats representing the maximum permissible
deviation from each of the cubic Bezier curves.
Example::
>>> curves_to_quadratic( [
... [ (50,50), (100,100), (150,100), (200,50) ],
... [ (75,50), (120,100), (150,75), (200,60) ]
... ], [1,1] )
[[(50.0, 50.0), (75.0, 75.0), (125.0, 91.66666666666666), (175.0, 75.0), (200.0, 50.0)], [(75.0, 50.0), (97.5, 75.0), (135.41666666666666, 82.08333333333333), (175.0, 67.5), (200.0, 60.0)]]
The returned splines have "implied oncurve points" suitable for use in
TrueType ``glif`` outlines - i.e. in the first spline returned above,
the first quadratic segment runs from (50,50) to
( (75 + 125)/2 , (120 + 91.666..)/2 ) = (100, 83.333...).
Returns:
A list of splines, each spline being a list of 2D tuples.
Raises:
fontTools.cu2qu.Errors.ApproxNotFoundError: if no suitable approximation
can be found for all curves with the given parameters.
""" """
curves = [[complex(*p) for p in curve] for curve in curves] curves = [[complex(*p) for p in curve] for curve in curves]