From ecc764ecc05772be03f208f7b9fb4a9e13d107ba Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 15 May 2020 10:53:41 +0100 Subject: [PATCH] [docs] Document cu2qu library (#1937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [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. --- Doc/source/cu2qu/cli.rst | 8 -- Doc/source/cu2qu/cu2qu.rst | 8 -- Doc/source/cu2qu/errors.rst | 8 -- Doc/source/cu2qu/index.rst | 44 ++++++--- Doc/source/cu2qu/ufo.rst | 8 -- Lib/fontTools/cu2qu/cu2qu.py | 170 +++++++++++++++++++++++++++++++---- 6 files changed, 185 insertions(+), 61 deletions(-) delete mode 100644 Doc/source/cu2qu/cli.rst delete mode 100644 Doc/source/cu2qu/cu2qu.rst delete mode 100644 Doc/source/cu2qu/errors.rst delete mode 100644 Doc/source/cu2qu/ufo.rst diff --git a/Doc/source/cu2qu/cli.rst b/Doc/source/cu2qu/cli.rst deleted file mode 100644 index 798b49f27..000000000 --- a/Doc/source/cu2qu/cli.rst +++ /dev/null @@ -1,8 +0,0 @@ -### -cli -### - -.. automodule:: fontTools.cu2qu.cli - :inherited-members: - :members: - :undoc-members: diff --git a/Doc/source/cu2qu/cu2qu.rst b/Doc/source/cu2qu/cu2qu.rst deleted file mode 100644 index 73f95c96f..000000000 --- a/Doc/source/cu2qu/cu2qu.rst +++ /dev/null @@ -1,8 +0,0 @@ -##### -cu2qu -##### - -.. automodule:: fontTools.cu2qu.cu2qu - :inherited-members: - :members: - :undoc-members: diff --git a/Doc/source/cu2qu/errors.rst b/Doc/source/cu2qu/errors.rst deleted file mode 100644 index 679c75145..000000000 --- a/Doc/source/cu2qu/errors.rst +++ /dev/null @@ -1,8 +0,0 @@ -###### -errors -###### - -.. automodule:: fontTools.cu2qu.errors - :inherited-members: - :members: - :undoc-members: diff --git a/Doc/source/cu2qu/index.rst b/Doc/source/cu2qu/index.rst index d8c00310e..41730e54b 100644 --- a/Doc/source/cu2qu/index.rst +++ b/Doc/source/cu2qu/index.rst @@ -1,16 +1,38 @@ -##### -cu2qu -##### +########################################## +cu2qu: Cubic to quadratic curve conversion +########################################## -.. toctree:: - :maxdepth: 1 +Routines for converting cubic curves to quadratic splines, suitable for use +in OpenType to TrueType outline conversion. - cli - cu2qu - errors - ufo +Conversion is carried out to a degree of tolerance provided by the user. While +it is relatively easy to find the best *single* quadratic curve to represent a +given cubic (see for example `this method from CAGD `_), +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: :members: - :undoc-members: \ No newline at end of file + :undoc-members: + +fontTools.cu2qu.ufo +------------------- + +.. automodule:: fontTools.cu2qu.ufo + :inherited-members: + :members: + :undoc-members: diff --git a/Doc/source/cu2qu/ufo.rst b/Doc/source/cu2qu/ufo.rst deleted file mode 100644 index ef66e74b5..000000000 --- a/Doc/source/cu2qu/ufo.rst +++ /dev/null @@ -1,8 +0,0 @@ -### -ufo -### - -.. automodule:: fontTools.cu2qu.ufo - :inherited-members: - :members: - :undoc-members: diff --git a/Lib/fontTools/cu2qu/cu2qu.py b/Lib/fontTools/cu2qu/cu2qu.py index 1fdd5e248..c9ce93ae3 100644 --- a/Lib/fontTools/cu2qu/cu2qu.py +++ b/Lib/fontTools/cu2qu/cu2qu.py @@ -46,7 +46,15 @@ else: @cython.returns(cython.double) @cython.locals(v1=cython.complex, v2=cython.complex) 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 @@ -77,6 +85,21 @@ def calc_cubic_parameters(p0, p1, p2, p3): @cython.cfunc @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): + """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 if n == 2: 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(mid=cython.complex, deriv3=cython.complex) 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 deriv3 = (p3 + p2 - p1 - p0) * .125 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(mid1=cython.complex, deriv1=cython.complex, mid2=cython.complex, deriv2=cython.complex) 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 # once but still in the scope of this function 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(_p1=cython.complex, _p2=cython.complex) def cubic_approx_control(t, p0, p1, p2, p3): - """Approximate a cubic bezier curve with a quadratic one. - Returns the candidate control point.""" + """Approximate a cubic Bezier using a quadratic one. + + 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 _p2 = p3 + (p2 - p3) * 1.5 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(ab=cython.complex, cd=cython.complex, p=cython.complex, h=cython.double) 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 cd = d - c 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(mid=cython.complex, deriv3=cython.complex) def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance): - """Returns True if the cubic Bezier p entirely lies within a distance - tolerance of origin, False otherwise. Assumes that p0 and p3 do fit - within tolerance of origin, and just checks the inside of the curve.""" + """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 @@ -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(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): - """Return the uniq quadratic approximating cubic that maintains - endpoint tangents if that is within tolerance, None otherwise.""" + """Approximate a cubic Bezier with a single quadratic within a given tolerance. + + 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 # 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(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): - """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 - within a distance `tolerance` from the original curve. + Args: + 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 # 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(n=cython.int) def curve_to_quadratic(curve, max_err): - """Return a quadratic spline approximating this cubic bezier. - Raise 'ApproxNotFoundError' if no suitable approximation can be found - with the given parameters. + """Approximate a cubic Bezier curve with a spline of n quadratics. + + 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] @@ -287,9 +397,33 @@ def curve_to_quadratic(curve, max_err): @cython.locals(l=cython.int, last_i=cython.int, i=cython.int) def curves_to_quadratic(curves, max_errors): - """Return quadratic splines approximating these cubic beziers. - Raise 'ApproxNotFoundError' if no suitable approximation can be found - for all curves with the given parameters. + """Return quadratic Bezier splines approximating the input cubic Beziers. + + 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]