diff -Nru cu2qu-1.5.0/.coveragerc cu2qu-1.6.5/.coveragerc --- cu2qu-1.5.0/.coveragerc 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/.coveragerc 2018-11-01 16:41:08.000000000 +0000 @@ -6,6 +6,10 @@ # list of directories or packages to measure source = cu2qu +# this is simply vendored, no need to include in coverage report +omit = + */cu2qu/cython.py + # these are treated as equivalent when combining data [paths] source = diff -Nru cu2qu-1.5.0/debian/changelog cu2qu-1.6.5/debian/changelog --- cu2qu-1.5.0/debian/changelog 2018-09-05 03:53:56.000000000 +0000 +++ cu2qu-1.6.5/debian/changelog 2018-12-13 14:12:51.000000000 +0000 @@ -1,3 +1,21 @@ +cu2qu (1.6.5-1) unstable; urgency=medium + + [ Yao Wei (魏銘廷) ] + * New upstream release 1.6.5 + * debian/control: + - Update maintainer address + - Bump Standards-Version to 4.2.1 + - Bump debhelper version to 11 + - Update dependencies, remove ufolib dependency + * debian/rules: + - Add workaround for setuptools + * add dep for setuptools-scm + + [ Jeremy Bicha ] + * Add Testsuite: autopkgtest-pkg-python + + -- Yao Wei (魏銘廷) Thu, 13 Dec 2018 09:12:51 -0500 + cu2qu (1.5.0-1) unstable; urgency=medium * New upstream release 1.5.0 diff -Nru cu2qu-1.5.0/debian/compat cu2qu-1.6.5/debian/compat --- cu2qu-1.5.0/debian/compat 2018-09-05 03:53:56.000000000 +0000 +++ cu2qu-1.6.5/debian/compat 2018-12-13 14:12:51.000000000 +0000 @@ -1 +1 @@ -10 +11 diff -Nru cu2qu-1.5.0/debian/control cu2qu-1.6.5/debian/control --- cu2qu-1.5.0/debian/control 2018-09-05 03:53:56.000000000 +0000 +++ cu2qu-1.6.5/debian/control 2018-12-13 14:12:51.000000000 +0000 @@ -4,16 +4,17 @@ Maintainer: Debian Fonts Task Force Uploaders: Jeremy Bicha , - Yao Wei (魏銘廷) -Build-Depends: debhelper (>= 10), + Yao Wei (魏銘廷) +Build-Depends: debhelper (>= 11), dh-python, python3-all, - python3-defcon (>= 0.3.5) , - python3-fonttools (>= 3.22.0), + python3-defcon (>= 0.6.0), + python3-fonttools (>= 3.32.0), python3-pytest , python3-setuptools, - python3-ufolib (>= 2.1.1) -Standards-Version: 4.1.4 + python3-setuptools-scm +Standards-Version: 4.2.1 +Testsuite: autopkgtest-pkg-python Homepage: https://github.com/googlei18n/cu2qu Vcs-Git: https://salsa.debian.org/fonts-team/cu2qu.git Vcs-Browser: https://salsa.debian.org/fonts-team/cu2qu @@ -23,7 +24,9 @@ Multi-Arch: foreign Section: python Depends: ${misc:Depends}, - ${python3:Depends} + ${python3:Depends}, + python3-fonttools (>= 3.32.0), + python3-defcon (>= 0.6.0) Description: Python library for cubic-to-quadratic bezier curve conversion cu2qu is a library that approximates cubic bezier curves with quadratic splines. This has general utility, but is especially useful for generating diff -Nru cu2qu-1.5.0/debian/copyright cu2qu-1.6.5/debian/copyright --- cu2qu-1.5.0/debian/copyright 2018-09-05 03:53:56.000000000 +0000 +++ cu2qu-1.6.5/debian/copyright 2018-12-13 14:12:51.000000000 +0000 @@ -1,4 +1,4 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: cu2qu Source: https://github.com/googlei18n/cu2qu diff -Nru cu2qu-1.5.0/debian/rules cu2qu-1.6.5/debian/rules --- cu2qu-1.5.0/debian/rules 2018-09-05 03:53:56.000000000 +0000 +++ cu2qu-1.6.5/debian/rules 2018-12-13 14:12:51.000000000 +0000 @@ -4,3 +4,7 @@ %: dh $@ --with python3 --buildsystem=pybuild + +override_dh_auto_clean: + dh_auto_clean + rm Lib/cu2qu/_version.py diff -Nru cu2qu-1.5.0/.gitignore cu2qu-1.6.5/.gitignore --- cu2qu-1.5.0/.gitignore 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/.gitignore 2018-11-01 16:41:08.000000000 +0000 @@ -1,7 +1,12 @@ # Byte-compiled and optimized files __pycache__/ -*.py[co] +*.py[cod] *$py.class +*.so + +# cython generated C/HTML files +Lib/cu2qu/*.c +Lib/cu2qu/*.html # Packaging *.egg-info @@ -15,6 +20,10 @@ .coverage.* .tox htmlcov +.pytest_cache/ # OS X Finder .DS_Store + +# auto-generated version file +Lib/cu2qu/_version.py diff -Nru cu2qu-1.5.0/Lib/cu2qu/cli.py cu2qu-1.6.5/Lib/cu2qu/cli.py --- cu2qu-1.5.0/Lib/cu2qu/cli.py 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/Lib/cu2qu/cli.py 2018-11-01 16:41:08.000000000 +0000 @@ -1,3 +1,4 @@ +from __future__ import print_function, division, absolute_import import os import argparse import logging @@ -117,8 +118,13 @@ 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(options.output_dir, os.path.basename(p)) + os.path.join(output_dir, os.path.basename(p)) for p in options.infiles ] elif options.output_file: diff -Nru cu2qu-1.5.0/Lib/cu2qu/cu2qu.py cu2qu-1.6.5/Lib/cu2qu/cu2qu.py --- cu2qu-1.5.0/Lib/cu2qu/cu2qu.py 1970-01-01 00:00:00.000000000 +0000 +++ cu2qu-1.6.5/Lib/cu2qu/cu2qu.py 2018-11-01 16:41:08.000000000 +0000 @@ -0,0 +1,373 @@ +#cython: language_level=3 +#distutils: define_macros=CYTHON_TRACE_NOGIL=1 + +# Copyright 2015 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 __future__ import print_function, division, absolute_import + +try: + import cython +except ImportError: + # if not installed, use the embedded (no-op) copy of Cython.Shadow + from . import cython + +import math + + +__all__ = ['curve_to_quadratic', 'curves_to_quadratic'] + +MAX_N = 100 + +NAN = float("NaN") + + +if cython.compiled: + # Yep, I'm compiled. + COMPILED = True +else: + # Just a lowly interpreted script. + COMPILED = False + + +class Cu2QuError(Exception): + pass + + +class ApproxNotFoundError(Cu2QuError): + def __init__(self, curve): + message = "no approximation found: %s" % curve + super(Cu2QuError, self).__init__(message) + self.curve = curve + +@cython.cfunc +@cython.inline +@cython.returns(cython.double) +@cython.locals(v1=cython.complex, v2=cython.complex) +def dot(v1, v2): + """Return the dot product of two vectors.""" + return (v1 * v2.conjugate()).real + + +@cython.cfunc +@cython.inline +@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) +@cython.locals(_1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex) +def calc_cubic_points(a, b, c, d): + _1 = d + _2 = (c / 3.0) + d + _3 = (b + c) / 3.0 + _2 + _4 = a + d + c + b + return _1, _2, _3, _4 + + +@cython.cfunc +@cython.inline +@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) +@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) +def calc_cubic_parameters(p0, p1, p2, p3): + c = (p1 - p0) * 3.0 + b = (p2 - p1) * 3.0 - c + d = p0 + a = p3 - d - c - b + return a, b, c, d + + +@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): + # Hand-coded special-cases + if n == 2: + return iter(split_cubic_into_two(p0, p1, p2, p3)) + if n == 3: + return iter(split_cubic_into_three(p0, p1, p2, p3)) + if n == 4: + a, b = split_cubic_into_two(p0, p1, p2, p3) + return iter(split_cubic_into_two(*a) + split_cubic_into_two(*b)) + if n == 6: + a, b = split_cubic_into_two(p0, p1, p2, p3) + return iter(split_cubic_into_three(*a) + split_cubic_into_three(*b)) + + return _split_cubic_into_n_gen(p0,p1,p2,p3,n) + + +@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, n=cython.int) +@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) +@cython.locals(dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int) +@cython.locals(a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex) +def _split_cubic_into_n_gen(p0, p1, p2, p3, n): + a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3) + dt = 1 / n + delta_2 = dt * dt + delta_3 = dt * delta_2 + for i in range(n): + t1 = i * dt + t1_2 = t1 * t1 + # 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) * dt + d1 = a*t1*t1_2 + b*t1_2 + c*t1 + d + yield calc_cubic_points(a1, b1, c1, d1) + + +@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): + mid = (p0 + 3 * (p1 + p2) + p3) * .125 + deriv3 = (p3 + p2 - p1 - p0) * .125 + return ((p0, (p0 + p1) * .5, mid - deriv3, mid), + (mid, mid + deriv3, (p2 + p3) * .5, 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): + # 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 + deriv1 = (p3 + 3*p2 - 4*p0) * _27 + mid2 = (p0 + 6*p1 + 12*p2 + 8*p3) * _27 + deriv2 = (4*p3 - 3*p1 - p0) * _27 + return ((p0, (2*p0 + p1) / 3.0, mid1 - deriv1, mid1), + (mid1, mid1 + deriv1, mid2 - deriv2, mid2), + (mid2, mid2 + deriv2, (p2 + 2*p3) / 3.0, p3)) + + +@cython.returns(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) +def cubic_approx_control(t, p0, p1, p2, p3): + """Approximate a cubic bezier curve with a quadratic one. + Returns the candidate control point.""" + _p1 = p0 + (p1 - p0) * 1.5 + _p2 = p3 + (p2 - p3) * 1.5 + return _p1 + (_p2 - _p1) * t + + +@cython.returns(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) +def calc_intersect(a, b, c, d): + """Calculate the intersection of ab and cd, given a, b, c, d.""" + + ab = b - a + cd = d - c + p = ab * 1j + try: + h = dot(p, a - c) / dot(p, cd) + except ZeroDivisionError: + return complex(NAN, NAN) + return c + cd * h + + +@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): + """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.""" + + # 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) * .125 + if abs(mid) > tolerance: + return False + deriv3 = (p3 + p2 - p1 - p0) * .125 + return (cubic_farthest_fit_inside(p0, (p0+p1)*.5, mid-deriv3, mid, tolerance) and + cubic_farthest_fit_inside(mid, mid+deriv3, (p2+p3)*.5, p3, tolerance)) + + +@cython.cfunc +@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.""" + # we define 2/3 as a keyword argument so that it will be evaluated only + # once but still in the scope of this function + + q1 = calc_intersect(*cubic) + if math.isnan(q1.imag): + return None + c0 = cubic[0] + c3 = cubic[3] + c1 = c0 + (q1 - c0) * _2_3 + c2 = c3 + (q1 - c3) * _2_3 + if not cubic_farthest_fit_inside(0, + c1 - cubic[1], + c2 - cubic[2], + 0, tolerance): + return None + return c0, q1, c3 + + +@cython.cfunc +@cython.locals(n=cython.int, tolerance=cython.double, _2_3=cython.double) +@cython.locals(i=cython.int) +@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. + + Returns None if no quadratic approximation is found which lies entirely + within a distance `tolerance` from the original curve. + """ + # we define 2/3 as a keyword argument so that it will be evaluated only + # once but still in the scope of this function + + if n == 1: + return cubic_approx_quadratic(cubic, tolerance) + + cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n) + + # calculate the spline of quadratics and check errors at the same time. + next_cubic = next(cubics) + next_q1 = cubic_approx_control(0, *next_cubic) + q2 = cubic[0] + d1 = 0j + spline = [cubic[0], next_q1] + for i in range(1, n+1): + + # Current cubic to convert + c0, c1, c2, c3 = next_cubic + + # Current quadratic approximation of current cubic + q0 = q2 + q1 = next_q1 + if i < n: + next_cubic = next(cubics) + next_q1 = cubic_approx_control(i / (n-1), *next_cubic) + spline.append(next_q1) + q2 = (q1 + next_q1) * .5 + else: + q2 = c3 + + # End-point deltas + d0 = d1 + d1 = q2 - c3 + + if (abs(d1) > tolerance or + not cubic_farthest_fit_inside(d0, + q0 + (q1 - q0) * _2_3 - c1, + q2 + (q1 - q2) * _2_3 - c2, + d1, + tolerance)): + return None + spline.append(cubic[3]) + + return spline + + +@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. + """ + + curve = [complex(*p) for p in curve] + + for n in range(1, MAX_N + 1): + spline = cubic_approx_spline(curve, n, max_err) + if spline is not None: + # done. go home + return [(s.real, s.imag) for s in spline] + + raise ApproxNotFoundError(curve) + + + +@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. + """ + + curves = [[complex(*p) for p in curve] for curve in curves] + assert len(max_errors) == len(curves) + + l = len(curves) + splines = [None] * l + last_i = i = 0 + n = 1 + while True: + spline = cubic_approx_spline(curves[i], n, max_errors[i]) + if spline is None: + if n == MAX_N: + break + n += 1 + last_i = i + continue + splines[i] = spline + i = (i + 1) % l + if i == last_i: + # done. go home + return [[(s.real, s.imag) for s in spline] for spline in splines] + + raise ApproxNotFoundError(curves) + + +if __name__ == '__main__': + import random + import timeit + + MAX_ERR = 5 + + def generate_curve(): + return [ + tuple(float(random.randint(0, 2048)) for coord in range(2)) + for point in range(4)] + + def setup_curve_to_quadratic(): + return generate_curve(), MAX_ERR + + def setup_curves_to_quadratic(): + num_curves = 3 + 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): + 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. / number)) + + def main(): + run_benchmark('cu2qu.benchmark', 'cu2qu', 'curve_to_quadratic') + run_benchmark('cu2qu.benchmark', 'cu2qu', 'curves_to_quadratic') + + random.seed(1) + main() diff -Nru cu2qu-1.5.0/Lib/cu2qu/cython.py cu2qu-1.6.5/Lib/cu2qu/cython.py --- cu2qu-1.5.0/Lib/cu2qu/cython.py 1970-01-01 00:00:00.000000000 +0000 +++ cu2qu-1.6.5/Lib/cu2qu/cython.py 2018-11-01 16:41:08.000000000 +0000 @@ -0,0 +1,468 @@ +""" This module is copied verbatim from the "Cython.Shadow" module: +https://github.com/cython/cython/blob/master/Cython/Shadow.py + +Cython is licensed under the Apache 2.0 Software License. +""" +# cython.* namespace for pure mode. +from __future__ import absolute_import + +__version__ = "0.28.5" + +try: + from __builtin__ import basestring +except ImportError: + basestring = str + + +# BEGIN shameless copy from Cython/minivect/minitypes.py + +class _ArrayType(object): + + is_array = True + subtypes = ['dtype'] + + def __init__(self, dtype, ndim, is_c_contig=False, is_f_contig=False, + inner_contig=False, broadcasting=None): + self.dtype = dtype + self.ndim = ndim + self.is_c_contig = is_c_contig + self.is_f_contig = is_f_contig + self.inner_contig = inner_contig or is_c_contig or is_f_contig + self.broadcasting = broadcasting + + def __repr__(self): + axes = [":"] * self.ndim + if self.is_c_contig: + axes[-1] = "::1" + elif self.is_f_contig: + axes[0] = "::1" + + return "%s[%s]" % (self.dtype, ", ".join(axes)) + + +def index_type(base_type, item): + """ + Support array type creation by slicing, e.g. double[:, :] specifies + a 2D strided array of doubles. The syntax is the same as for + Cython memoryviews. + """ + class InvalidTypeSpecification(Exception): + pass + + def verify_slice(s): + if s.start or s.stop or s.step not in (None, 1): + raise InvalidTypeSpecification( + "Only a step of 1 may be provided to indicate C or " + "Fortran contiguity") + + if isinstance(item, tuple): + step_idx = None + for idx, s in enumerate(item): + verify_slice(s) + if s.step and (step_idx or idx not in (0, len(item) - 1)): + raise InvalidTypeSpecification( + "Step may only be provided once, and only in the " + "first or last dimension.") + + if s.step == 1: + step_idx = idx + + return _ArrayType(base_type, len(item), + is_c_contig=step_idx == len(item) - 1, + is_f_contig=step_idx == 0) + elif isinstance(item, slice): + verify_slice(item) + return _ArrayType(base_type, 1, is_c_contig=bool(item.step)) + else: + # int[8] etc. + assert int(item) == item # array size must be a plain integer + array(base_type, item) + +# END shameless copy + + +compiled = False + +_Unspecified = object() + +# Function decorators + +def _empty_decorator(x): + return x + +def locals(**arg_types): + return _empty_decorator + +def test_assert_path_exists(*paths): + return _empty_decorator + +def test_fail_if_path_exists(*paths): + return _empty_decorator + +class _EmptyDecoratorAndManager(object): + def __call__(self, x): + return x + def __enter__(self): + pass + def __exit__(self, exc_type, exc_value, traceback): + pass + +class _Optimization(object): + pass + +cclass = ccall = cfunc = _EmptyDecoratorAndManager() + +returns = wraparound = boundscheck = initializedcheck = nonecheck = \ + overflowcheck = embedsignature = cdivision = cdivision_warnings = \ + always_allows_keywords = profile = linetrace = infer_types = \ + unraisable_tracebacks = freelist = \ + lambda _: _EmptyDecoratorAndManager() + +exceptval = lambda _=None, check=True: _EmptyDecoratorAndManager() + +optimization = _Optimization() + +overflowcheck.fold = optimization.use_switch = \ + optimization.unpack_method_calls = lambda arg: _EmptyDecoratorAndManager() + +final = internal = type_version_tag = no_gc_clear = no_gc = _empty_decorator + + +_cython_inline = None +def inline(f, *args, **kwds): + if isinstance(f, basestring): + global _cython_inline + if _cython_inline is None: + from Cython.Build.Inline import cython_inline as _cython_inline + return _cython_inline(f, *args, **kwds) + else: + assert len(args) == len(kwds) == 0 + return f + + +def compile(f): + from Cython.Build.Inline import RuntimeCompiledFunction + return RuntimeCompiledFunction(f) + + +# Special functions + +def cdiv(a, b): + q = a / b + if q < 0: + q += 1 + return q + +def cmod(a, b): + r = a % b + if (a*b) < 0: + r -= b + return r + + +# Emulated language constructs + +def cast(type, *args, **kwargs): + kwargs.pop('typecheck', None) + assert not kwargs + if hasattr(type, '__call__'): + return type(*args) + else: + return args[0] + +def sizeof(arg): + return 1 + +def typeof(arg): + return arg.__class__.__name__ + # return type(arg) + +def address(arg): + return pointer(type(arg))([arg]) + +def declare(type=None, value=_Unspecified, **kwds): + if type not in (None, object) and hasattr(type, '__call__'): + if value is not _Unspecified: + return type(value) + else: + return type() + else: + return value + +class _nogil(object): + """Support for 'with nogil' statement + """ + def __enter__(self): + pass + def __exit__(self, exc_class, exc, tb): + return exc_class is None + +nogil = _nogil() +gil = _nogil() +del _nogil + +# Emulated types + +class CythonMetaType(type): + + def __getitem__(type, ix): + return array(type, ix) + +CythonTypeObject = CythonMetaType('CythonTypeObject', (object,), {}) + +class CythonType(CythonTypeObject): + + def _pointer(self, n=1): + for i in range(n): + self = pointer(self) + return self + +class PointerType(CythonType): + + def __init__(self, value=None): + if isinstance(value, (ArrayType, PointerType)): + self._items = [cast(self._basetype, a) for a in value._items] + elif isinstance(value, list): + self._items = [cast(self._basetype, a) for a in value] + elif value is None or value == 0: + self._items = [] + else: + raise ValueError + + def __getitem__(self, ix): + if ix < 0: + raise IndexError("negative indexing not allowed in C") + return self._items[ix] + + def __setitem__(self, ix, value): + if ix < 0: + raise IndexError("negative indexing not allowed in C") + self._items[ix] = cast(self._basetype, value) + + def __eq__(self, value): + if value is None and not self._items: + return True + elif type(self) != type(value): + return False + else: + return not self._items and not value._items + + def __repr__(self): + return "%s *" % (self._basetype,) + +class ArrayType(PointerType): + + def __init__(self): + self._items = [None] * self._n + + +class StructType(CythonType): + + def __init__(self, cast_from=_Unspecified, **data): + if cast_from is not _Unspecified: + # do cast + if len(data) > 0: + raise ValueError('Cannot accept keyword arguments when casting.') + if type(cast_from) is not type(self): + raise ValueError('Cannot cast from %s'%cast_from) + for key, value in cast_from.__dict__.items(): + setattr(self, key, value) + else: + for key, value in data.items(): + setattr(self, key, value) + + def __setattr__(self, key, value): + if key in self._members: + self.__dict__[key] = cast(self._members[key], value) + else: + raise AttributeError("Struct has no member '%s'" % key) + + +class UnionType(CythonType): + + def __init__(self, cast_from=_Unspecified, **data): + if cast_from is not _Unspecified: + # do type cast + if len(data) > 0: + raise ValueError('Cannot accept keyword arguments when casting.') + if isinstance(cast_from, dict): + datadict = cast_from + elif type(cast_from) is type(self): + datadict = cast_from.__dict__ + else: + raise ValueError('Cannot cast from %s'%cast_from) + else: + datadict = data + if len(datadict) > 1: + raise AttributeError("Union can only store one field at a time.") + for key, value in datadict.items(): + setattr(self, key, value) + + def __setattr__(self, key, value): + if key in '__dict__': + CythonType.__setattr__(self, key, value) + elif key in self._members: + self.__dict__ = {key: cast(self._members[key], value)} + else: + raise AttributeError("Union has no member '%s'" % key) + +def pointer(basetype): + class PointerInstance(PointerType): + _basetype = basetype + return PointerInstance + +def array(basetype, n): + class ArrayInstance(ArrayType): + _basetype = basetype + _n = n + return ArrayInstance + +def struct(**members): + class StructInstance(StructType): + _members = members + for key in members: + setattr(StructInstance, key, None) + return StructInstance + +def union(**members): + class UnionInstance(UnionType): + _members = members + for key in members: + setattr(UnionInstance, key, None) + return UnionInstance + +class typedef(CythonType): + + def __init__(self, type, name=None): + self._basetype = type + self.name = name + + def __call__(self, *arg): + value = cast(self._basetype, *arg) + return value + + def __repr__(self): + return self.name or str(self._basetype) + + __getitem__ = index_type + +class _FusedType(CythonType): + pass + + +def fused_type(*args): + if not args: + raise TypeError("Expected at least one type as argument") + + # Find the numeric type with biggest rank if all types are numeric + rank = -1 + for type in args: + if type not in (py_int, py_long, py_float, py_complex): + break + + if type_ordering.index(type) > rank: + result_type = type + else: + return result_type + + # Not a simple numeric type, return a fused type instance. The result + # isn't really meant to be used, as we can't keep track of the context in + # pure-mode. Casting won't do anything in this case. + return _FusedType() + + +def _specialized_from_args(signatures, args, kwargs): + "Perhaps this should be implemented in a TreeFragment in Cython code" + raise Exception("yet to be implemented") + + +py_int = typedef(int, "int") +try: + py_long = typedef(long, "long") +except NameError: # Py3 + py_long = typedef(int, "long") +py_float = typedef(float, "float") +py_complex = typedef(complex, "double complex") + + +# Predefined types + +int_types = ['char', 'short', 'Py_UNICODE', 'int', 'Py_UCS4', 'long', 'longlong', 'Py_ssize_t', 'size_t'] +float_types = ['longdouble', 'double', 'float'] +complex_types = ['longdoublecomplex', 'doublecomplex', 'floatcomplex', 'complex'] +other_types = ['bint', 'void', 'Py_tss_t'] + +to_repr = { + 'longlong': 'long long', + 'longdouble': 'long double', + 'longdoublecomplex': 'long double complex', + 'doublecomplex': 'double complex', + 'floatcomplex': 'float complex', +}.get + +gs = globals() + +# note: cannot simply name the unicode type here as 2to3 gets in the way and replaces it by str +try: + import __builtin__ as builtins +except ImportError: # Py3 + import builtins + +gs['unicode'] = typedef(getattr(builtins, 'unicode', str), 'unicode') +del builtins + +for name in int_types: + reprname = to_repr(name, name) + gs[name] = typedef(py_int, reprname) + if name not in ('Py_UNICODE', 'Py_UCS4') and not name.endswith('size_t'): + gs['u'+name] = typedef(py_int, "unsigned " + reprname) + gs['s'+name] = typedef(py_int, "signed " + reprname) + +for name in float_types: + gs[name] = typedef(py_float, to_repr(name, name)) + +for name in complex_types: + gs[name] = typedef(py_complex, to_repr(name, name)) + +bint = typedef(bool, "bint") +void = typedef(None, "void") +Py_tss_t = typedef(None, "Py_tss_t") + +for t in int_types + float_types + complex_types + other_types: + for i in range(1, 4): + gs["%s_%s" % ('p'*i, t)] = gs[t]._pointer(i) + +NULL = gs['p_void'](0) + +# looks like 'gs' has some users out there by now... +#del gs + +integral = floating = numeric = _FusedType() + +type_ordering = [py_int, py_long, py_float, py_complex] + +class CythonDotParallel(object): + """ + The cython.parallel module. + """ + + __all__ = ['parallel', 'prange', 'threadid'] + + def parallel(self, num_threads=None): + return nogil + + def prange(self, start=0, stop=None, step=1, schedule=None, nogil=False): + if stop is None: + stop = start + start = 0 + return range(start, stop, step) + + def threadid(self): + return 0 + + # def threadsavailable(self): + # return 1 + +import sys +sys.modules['cython.parallel'] = CythonDotParallel() +del sys diff -Nru cu2qu-1.5.0/Lib/cu2qu/__init__.py cu2qu-1.6.5/Lib/cu2qu/__init__.py --- cu2qu-1.5.0/Lib/cu2qu/__init__.py 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/Lib/cu2qu/__init__.py 2018-11-01 16:41:08.000000000 +0000 @@ -15,253 +15,9 @@ from __future__ import print_function, division, absolute_import -__version__ = "1.5.0" - -__all__ = ['curve_to_quadratic', 'curves_to_quadratic'] - -MAX_N = 100 - - -class Cu2QuError(Exception): - pass - - -class ApproxNotFoundError(Cu2QuError): - def __init__(self, curve): - message = "no approximation found: %s" % curve - super(Cu2QuError, self).__init__(message) - self.curve = curve - - -def dot(v1, v2): - """Return the dot product of two vectors.""" - return (v1 * v2.conjugate()).real - - -def calc_cubic_points(a, b, c, d): - _1 = d - _2 = (c / 3.0) + d - _3 = (b + c) / 3.0 + _2 - _4 = a + d + c + b - return _1, _2, _3, _4 - - -def calc_cubic_parameters(p0, p1, p2, p3): - c = (p1 - p0) * 3.0 - b = (p2 - p1) * 3.0 - c - d = p0 - a = p3 - d - c - b - return a, b, c, d - - -def split_cubic_into_n_iter(p0, p1, p2, p3, n): - # Hand-coded special-cases - if n == 2: - return iter(split_cubic_into_two(p0, p1, p2, p3)) - if n == 3: - return iter(split_cubic_into_three(p0, p1, p2, p3)) - if n == 4: - a, b = split_cubic_into_two(p0, p1, p2, p3) - return iter(split_cubic_into_two(*a) + split_cubic_into_two(*b)) - if n == 6: - a, b = split_cubic_into_two(p0, p1, p2, p3) - return iter(split_cubic_into_three(*a) + split_cubic_into_three(*b)) - - return _split_cubic_into_n_gen(p0,p1,p2,p3,n) - - -def _split_cubic_into_n_gen(p0, p1, p2, p3, n): - a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3) - dt = 1 / n - delta_2 = dt * dt - delta_3 = dt * delta_2 - for i in range(n): - t1 = i * dt - t1_2 = t1 * t1 - # 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) * dt - d1 = a*t1*t1_2 + b*t1_2 + c*t1 + d - yield calc_cubic_points(a1, b1, c1, d1) - - -def split_cubic_into_two(p0, p1, p2, p3): - mid = (p0 + 3 * (p1 + p2) + p3) * .125 - deriv3 = (p3 + p2 - p1 - p0) * .125 - return ((p0, (p0 + p1) * .5, mid - deriv3, mid), - (mid, mid + deriv3, (p2 + p3) * .5, p3)) - - -def split_cubic_into_three(p0, p1, p2, p3, _27=1/27): - # 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 - deriv1 = (p3 + 3*p2 - 4*p0) * _27 - mid2 = (p0 + 6*p1 + 12*p2 + 8*p3) * _27 - deriv2 = (4*p3 - 3*p1 - p0) * _27 - return ((p0, (2*p0 + p1) / 3, mid1 - deriv1, mid1), - (mid1, mid1 + deriv1, mid2 - deriv2, mid2), - (mid2, mid2 + deriv2, (p2 + 2*p3) / 3, p3)) - - -def cubic_approx_control(p, t): - """Approximate a cubic bezier curve with a quadratic one. - Returns the candidate control point.""" - - p1 = p[0] + (p[1] - p[0]) * 1.5 - p2 = p[3] + (p[2] - p[3]) * 1.5 - return p1 + (p2 - p1) * t - - -def calc_intersect(a, b, c, d): - """Calculate the intersection of ab and cd, given a, b, c, d.""" - - ab = b - a - cd = d - c - p = ab * 1j - try: - h = dot(p, a - c) / dot(p, cd) - except ZeroDivisionError: - return None - return c + cd * h - - -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.""" - - # 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) * .125 - if abs(mid) > tolerance: - return False - deriv3 = (p3 + p2 - p1 - p0) * .125 - return (cubic_farthest_fit_inside(p0, (p0+p1)*.5, mid-deriv3, mid, tolerance) and - cubic_farthest_fit_inside(mid, mid+deriv3, (p2+p3)*.5, p3, tolerance)) - - -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.""" - # we define 2/3 as a keyword argument so that it will be evaluated only - # once but still in the scope of this function - - q1 = calc_intersect(*cubic) - if q1 is None: - return None - c0 = cubic[0] - c3 = cubic[3] - c1 = c0 + (q1 - c0) * _2_3 - c2 = c3 + (q1 - c3) * _2_3 - if not cubic_farthest_fit_inside(0, - c1 - cubic[1], - c2 - cubic[2], - 0, tolerance): - return None - return c0, q1, c3 - - -def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3): - """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. - """ - # we define 2/3 as a keyword argument so that it will be evaluated only - # once but still in the scope of this function - - if n == 1: - return cubic_approx_quadratic(cubic, tolerance) - - cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n) - - # calculate the spline of quadratics and check errors at the same time. - next_cubic = next(cubics) - next_q1 = cubic_approx_control(next_cubic, 0) - q2 = cubic[0] - d1 = 0j - spline = [cubic[0], next_q1] - for i in range(1, n+1): - - # Current cubic to convert - c0, c1, c2, c3 = next_cubic - - # Current quadratic approximation of current cubic - q0 = q2 - q1 = next_q1 - if i < n: - next_cubic = next(cubics) - next_q1 = cubic_approx_control(next_cubic, i / (n-1)) - spline.append(next_q1) - q2 = (q1 + next_q1) * .5 - else: - q2 = c3 - - # End-point deltas - d0 = d1 - d1 = q2 - c3 - - if (abs(d1) > tolerance or - not cubic_farthest_fit_inside(d0, - q0 + (q1 - q0) * _2_3 - c1, - q2 + (q1 - q2) * _2_3 - c2, - d1, - tolerance)): - return None - spline.append(cubic[3]) - - return spline - - -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. - """ - - curve = [complex(*p) for p in curve] - - for n in range(1, MAX_N + 1): - spline = cubic_approx_spline(curve, n, max_err) - if spline is not None: - # done. go home - return [(s.real, s.imag) for s in spline] - - raise ApproxNotFoundError(curve) - - - -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. - """ - - curves = [[complex(*p) for p in curve] for curve in curves] - assert len(max_errors) == len(curves) - - l = len(curves) - splines = [None] * l - last_i = i = 0 - n = 1 - while True: - spline = cubic_approx_spline(curves[i], n, max_errors[i]) - if spline is None: - if n == MAX_N: - break - n += 1 - last_i = i - continue - splines[i] = spline - i = (i + 1) % l - if i == last_i: - # done. go home - return [[(s.real, s.imag) for s in spline] for spline in splines] - - raise ApproxNotFoundError(curves) +try: + from ._version import version as __version__ +except ImportError: + __version__ = "0.0.0+unknown" +from .cu2qu import * diff -Nru cu2qu-1.5.0/Lib/cu2qu/pens.py cu2qu-1.6.5/Lib/cu2qu/pens.py --- cu2qu-1.5.0/Lib/cu2qu/pens.py 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/Lib/cu2qu/pens.py 2018-11-01 16:41:08.000000000 +0000 @@ -2,8 +2,8 @@ from cu2qu import curve_to_quadratic from fontTools.pens.basePen import AbstractPen, decomposeSuperBezierSegment from fontTools.pens.reverseContourPen import ReverseContourPen -from ufoLib.pointPen import BasePointToSegmentPen -from ufoLib.pointPen import ReverseContourPointPen +from fontTools.pens.pointPen import BasePointToSegmentPen +from fontTools.pens.pointPen import ReverseContourPointPen class Cu2QuPen(AbstractPen): @@ -220,7 +220,13 @@ for (pt, smooth, name, kwargs) in offcurves: pen.addPoint(pt, None, smooth, name, **kwargs) pt, smooth, name, kwargs = points[-1] - pen.addPoint(pt, segment_type, smooth, name, **kwargs) + if pt is None: + # special quadratic contour with no on-curve points: + # we need to skip the "None" point. See also the Pen + # protocol's qCurveTo() method and fontTools.pens.basePen + pass + else: + pen.addPoint(pt, segment_type, smooth, name, **kwargs) else: # 'curve' segments must have been converted to 'qcurve' by now raise AssertionError( diff -Nru cu2qu-1.5.0/pyproject.toml cu2qu-1.6.5/pyproject.toml --- cu2qu-1.5.0/pyproject.toml 1970-01-01 00:00:00.000000000 +0000 +++ cu2qu-1.6.5/pyproject.toml 2018-11-01 16:41:08.000000000 +0000 @@ -0,0 +1,10 @@ +[build-system] +requires = [ + "setuptools", + "wheel", + "setuptools_scm", + # Cython is an optional build requirement, as the sdist includes + # pre-cythonized *.c sources + # "cython", +] +build-backend = "setuptools.build_meta" diff -Nru cu2qu-1.5.0/README.rst cu2qu-1.6.5/README.rst --- cu2qu-1.5.0/README.rst 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/README.rst 2018-11-01 16:41:08.000000000 +0000 @@ -58,6 +58,59 @@ fonts_to_quadratic([font], stats=stats) # "stats" will report combined statistics for both fonts +Installation +------------ + +You can install/upgrade cu2qu using pip, like any other Python package. + +.. code:: sh + + $ pip install --upgrade cu2qu + +This will download the latest stable version available from the Python +Package Index (PyPI). + +If you wish to modify the sources in-place, you can clone the git repository +from Github and install in ``--editable`` (or ``-e``) mode: + +.. code:: sh + + $ git clone https://github.com/googlei18n/cu2qu + $ cd cu2qu + $ pip install --editable . + +Optionally, you can build an optimized version of cu2qu which uses Cython_ +to compile Python to C. The extension module thus created is *more than +twice as fast* than its pure-Python equivalent. + +When installing cu2qu from PyPI using pip, as long as you have a C compiler +available, the cu2qu setup script will automatically attempt to build a +C/Python extension module. If the compilation fails for any reasons, an error +is printed and cu2qu will be installed as pure-Python, without the optimized +extension. + +If you have cloned the git repository, the C source files are not present and +need to be regenerated. To do that, you need to install the latest Cython +(as usual, ``pip install -U cython``), and then use the global option +``--with-cython`` when invoking the ``setup.py`` script. You can also export +a ``CU2QU_WITH_CYTHON=1`` environment variable if you prefer. + +For example, to build the cu2qu extension module in-place (i.e. in the same +source directory): + +.. code:: sh + + $ python setup.py --with-cython build_ext --inplace + +You can also pass ``--global-option`` when installing with pip from a local +source checkout, like so: + +.. code:: sh + + $ pip install --global-option="--with-cython" -e . + + +.. _Cython: https://github.com/cython/cython .. |Build Status| image:: https://travis-ci.org/googlei18n/cu2qu.svg :target: https://travis-ci.org/googlei18n/cu2qu .. |PyPI Version| image:: https://img.shields.io/pypi/v/cu2qu.svg diff -Nru cu2qu-1.5.0/requirements.txt cu2qu-1.6.5/requirements.txt --- cu2qu-1.5.0/requirements.txt 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/requirements.txt 2018-11-01 16:41:08.000000000 +0000 @@ -1,2 +1,2 @@ -fonttools==3.22.0 -ufoLib==2.1.1 +fonttools[ufo]==3.32.0 +defcon==0.6.0 diff -Nru cu2qu-1.5.0/setup.cfg cu2qu-1.6.5/setup.cfg --- cu2qu-1.5.0/setup.cfg 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/setup.cfg 2018-11-01 16:41:08.000000000 +0000 @@ -1,30 +1,4 @@ -[bumpversion] -current_version = 1.5.0 -commit = True -tag = False -tag_name = v{new_version} -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? -serialize = - {major}.{minor}.{patch}.{release}{dev} - {major}.{minor}.{patch} - -[bumpversion:part:release] -optional_value = final -values = - dev - final - -[bumpversion:part:dev] - -[bumpversion:file:Lib/cu2qu/__init__.py] -search = __version__ = "{current_version}" -replace = __version__ = "{new_version}" - -[bumpversion:file:setup.py] -search = version="{current_version}" -replace = version="{new_version}" - -[wheel] +[bdist_wheel] universal = 1 [sdist] @@ -38,16 +12,15 @@ [tool:pytest] minversion = 3.0 -testpaths = +testpaths = tests -python_files = +python_files = *_test.py -python_classes = +python_classes = *Test -addopts = +addopts = -s -v -r a --doctest-modules --doctest-ignore-import-errors - diff -Nru cu2qu-1.5.0/setup.py cu2qu-1.6.5/setup.py --- cu2qu-1.5.0/setup.py 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/setup.py 2018-11-01 16:41:08.000000000 +0000 @@ -13,155 +13,198 @@ # limitations under the License. -from setuptools import setup, find_packages, Command -import sys +from setuptools import setup, find_packages, Extension +from setuptools.command.build_ext import build_ext as _build_ext +from setuptools.command.sdist import sdist as _sdist +import pkg_resources from distutils import log +import sys +import os +import re +from io import open -class bump_version(Command): - - description = "increment the package version and commit the changes" - - user_options = [ - ("major", None, "bump the first digit, for incompatible API changes"), - ("minor", None, "bump the second digit, for new backward-compatible features"), - ("patch", None, "bump the third digit, for bug fixes (default)"), - ] - - def initialize_options(self): - self.minor = False - self.major = False - self.patch = False - - def finalize_options(self): - part = None - for attr in ("major", "minor", "patch"): - if getattr(self, attr, False): - if part is None: - part = attr - else: - from distutils.errors import DistutilsOptionError - raise DistutilsOptionError( - "version part options are mutually exclusive") - self.part = part or "patch" - - def bumpversion(self, part, **kwargs): - """ Run bumpversion.main() with the specified arguments. - """ - import bumpversion - - args = ['--verbose'] if self.verbose > 1 else [] - for k, v in kwargs.items(): - k = "--{}".format(k.replace("_", "-")) - is_bool = isinstance(v, bool) and v is True - args.extend([k] if is_bool else [k, str(v)]) - args.append(part) - - log.debug( - "$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args)) - - bumpversion.main(args) - - def run(self): - log.info("bumping '%s' version" % self.part) - self.bumpversion(self.part) - +needs_pytest = {'pytest', 'test'}.intersection(sys.argv) +pytest_runner = ['pytest_runner'] if needs_pytest else [] +needs_wheel = {'bdist_wheel'}.intersection(sys.argv) +wheel = ['wheel'] if needs_wheel else [] -class release(bump_version): - """Drop the developmental release '.devN' suffix from the package version, - open the default text $EDITOR to write release notes, commit the changes - and generate a git tag. +# Check if minimum required Cython is available. +# For consistency, we require the same as our vendored Cython.Shadow module +cymod = "Lib/cu2qu/cython.py" +cython_version_re = re.compile('__version__ = ["\']([0-9][0-9\w\.]+)["\']') +with open(cymod, "r", encoding="utf-8") as fp: + for line in fp: + m = cython_version_re.match(line) + if m: + cython_min_version = m.group(1) + break + else: + sys.exit("error: failed to parse cython version in '%s'" % cymod) + +required_cython = "cython >= %s" % cython_min_version +try: + pkg_resources.require(required_cython) +except pkg_resources.ResolutionError: + has_cython = False +else: + has_cython = True + +# First, check if the CU2QU_WITH_CYTHON environment variable is set. +# Values "1", "true" or "yes" mean that Cython is required and will be used +# to regenerate the *.c sources from which the native extension is built; +# "0", "false" or "no" mean that Cython is not required and no extension +# module will be compiled (i.e. the wheel is pure-python and universal). +# If the variable is not set, then the pre-generated *.c sources that +# are included in the sdist package will be used to try build the extension. +# However, if any error occurs during compilation (e.g. the host +# machine doesn't have the required compiler toolchain installed), the +# installation proceeds without the compiled extensions, but will only have +# the pure-python module. +env_with_cython = os.environ.get("CU2QU_WITH_CYTHON") +with_cython = ( + True if env_with_cython in {"1", "true", "yes"} + else False if env_with_cython in {"0", "false", "no"} + else None +) - Release notes can also be set with the -m/--message option, or by reading - from standard input. +# command line options --with-cython and --without-cython are also supported. +# They override the environment variable +opt_with_cython = {'--with-cython'}.intersection(sys.argv) +opt_without_cython = {'--without-cython'}.intersection(sys.argv) +if opt_with_cython and opt_without_cython: + sys.exit( + "error: the options '--with-cython' and '--without-cython' are " + "mutually exclusive" + ) +elif opt_with_cython: + sys.argv.remove("--with-cython") + with_cython = True +elif opt_without_cython: + sys.argv.remove("--without-cython") + with_cython = False + + +class cython_build_ext(_build_ext): + """Compile *.pyx source files to *.c using cythonize if Cython is + installed, else use the pre-generated *.c sources. """ - description = "tag a new release" - - user_options = [ - ("message=", 'm', "message containing the release notes"), - ("sign", "s", "make a GPG-signed tag, using the default key"), - ] - - def initialize_options(self): - self.message = None - self.sign = False - def finalize_options(self): - import re + if with_cython: + if not has_cython: + from distutils.errors import DistutilsSetupError + + raise DistutilsSetupError( + "%s is required when using --with-cython" % required_cython + ) + + from Cython.Build import cythonize + + # optionally enable line tracing for test coverage support + linetrace = os.environ.get("CYTHON_TRACE") == "1" + + self.distribution.ext_modules[:] = cythonize( + self.distribution.ext_modules, + force=linetrace or self.force, + annotate=os.environ.get("CYTHON_ANNOTATE") == "1", + quiet=not self.verbose, + compiler_directives={ + "linetrace": linetrace, + "language_level": 3, + "embedsignature": True, + }, + ) + else: + # replace *.py/.pyx sources with their pre-generated *.c versions + for ext in self.distribution.ext_modules: + ext.sources = [re.sub("\.pyx?$", ".c", n) for n in ext.sources] + + _build_ext.finalize_options(self) + + def build_extensions(self): + if not has_cython: + log.info( + "%s is not installed. Pre-generated *.c sources will be " + "will be used to build the extensions." % required_cython + ) - current_version = self.distribution.metadata.get_version() - if not re.search(r"\.dev[0-9]+", current_version): - from distutils.errors import DistutilsSetupError - raise DistutilsSetupError( - "current version (%s) has no '.devN' suffix.\n " - "Run 'setup.py bump_version' with any of " - "--major, --minor, --patch options" % current_version) - - message = self.message - if message is None: - if sys.stdin.isatty(): - # stdin is interactive, use editor to write release notes - message = self.edit_release_notes() + try: + _build_ext.build_extensions(self) + except Exception as e: + if with_cython: + raise + from distutils.errors import DistutilsModuleError + + # optional compilation failed: we delete 'ext_modules' and make sure + # the generated wheel is 'pure' + del self.distribution.ext_modules[:] + try: + bdist_wheel = self.get_finalized_command("bdist_wheel") + except DistutilsModuleError: + # 'bdist_wheel' command not available as wheel is not installed + pass else: - # read release notes from stdin pipe - message = sys.stdin.read() + bdist_wheel.root_is_pure = True + log.error('error: building extensions failed: %s' % e) - if not message.strip(): - from distutils.errors import DistutilsSetupError - raise DistutilsSetupError("release notes message is empty") - - self.message = "v{new_version}\n\n%s" % message - self.sign = bool(self.sign) - - @staticmethod - def edit_release_notes(): - """Use the default text $EDITOR to write release notes. - If $EDITOR is not set, use 'nano'.""" - from tempfile import mkstemp - import os - import shlex - import subprocess + def get_source_files(self): + filenames = _build_ext.get_source_files(self) - text_editor = shlex.split(os.environ.get('EDITOR', 'nano')) - - fd, tmp = mkstemp(prefix='bumpversion-') - try: - os.close(fd) - with open(tmp, 'w') as f: - f.write("\n\n# Write release notes.\n" - "# Lines starting with '#' will be ignored.") - subprocess.check_call(text_editor + [tmp]) - with open(tmp, 'r') as f: - changes = "".join( - l for l in f.readlines() if not l.startswith('#')) - finally: - os.remove(tmp) - return changes + # include pre-generated *.c sources inside sdist, but only if cython is + # installed (and hence they will be updated upon making the sdist) + if has_cython: + for ext in self.extensions: + filenames.extend( + [re.sub("\.pyx?$", ".c", n) for n in ext.sources] + ) + return filenames + + +class cython_sdist(_sdist): + """ Run 'cythonize' on *.pyx sources to ensure the *.c files included + in the source distribution are up-to-date. + """ def run(self): - log.info("stripping developmental release suffix") - # drop '.dev0' suffix, commit with given message and create git tag - self.bumpversion("release", - tag=True, - message="Release {new_version}", - tag_message=self.message, - sign_tags=self.sign) - + if with_cython and not has_cython: + from distutils.errors import DistutilsSetupError -needs_pytest = {'pytest', 'test'}.intersection(sys.argv) -pytest_runner = ['pytest_runner'] if needs_pytest else [] -needs_wheel = {'bdist_wheel'}.intersection(sys.argv) -wheel = ['wheel'] if needs_wheel else [] -needs_bump2version = {'release', 'bump_version'}.intersection(sys.argv) -bump2version = ['bump2version >= 0.5.7'] if needs_bump2version else [] + raise DistutilsSetupError( + "%s is required when creating sdist --with-cython" + % required_cython + ) + + if has_cython: + from Cython.Build import cythonize + + cythonize( + self.distribution.ext_modules, + force=True, # always regenerate *.c sources + quiet=not self.verbose, + compiler_directives={ + "language_level": 3, + "embedsignature": True + }, + ) + + _sdist.run(self) + + +# don't build extensions if user explicitly requested --without-cython +if with_cython is False: + extensions = [] +else: + extensions = [ + Extension("cu2qu.cu2qu", ["Lib/cu2qu/cu2qu.py"]), + ] with open('README.rst', 'r') as f: long_description = f.read() setup( name='cu2qu', - version="1.5.0", + use_scm_version={"write_to": "Lib/cu2qu/_version.py"}, description='Cubic-to-quadratic bezier curve conversion', author="James Godfrey-Kittle, Behdad Esfahbod", author_email="jamesgk@google.com", @@ -170,21 +213,17 @@ long_description=long_description, packages=find_packages('Lib'), package_dir={'': 'Lib'}, + ext_modules=extensions, include_package_data=True, - setup_requires=pytest_runner + wheel + bump2version, + setup_requires=pytest_runner + wheel + ["setuptools_scm"], tests_require=[ 'pytest>=2.8', ], install_requires=[ - "fonttools>=3.18.0", - "ufoLib>=2.1.1", + "fonttools[ufo] >= 3.32.0", ], - extras_require={"cli": ["defcon>=0.4.0"]}, + extras_require={"cli": ["defcon >= 0.6.0"]}, entry_points={"console_scripts": ["cu2qu = cu2qu.cli:main [cli]"]}, - cmdclass={ - "release": release, - "bump_version": bump_version, - }, classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', @@ -198,4 +237,5 @@ 'Topic :: Multimedia :: Graphics :: Editors :: Vector-Based', 'Topic :: Software Development :: Libraries :: Python Modules', ], + cmdclass={"build_ext": cython_build_ext, "sdist": cython_sdist}, ) diff -Nru cu2qu-1.5.0/test-requirements.txt cu2qu-1.6.5/test-requirements.txt --- cu2qu-1.5.0/test-requirements.txt 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/test-requirements.txt 2018-11-01 16:41:08.000000000 +0000 @@ -1,3 +1,2 @@ coverage pytest -defcon==0.3.5 diff -Nru cu2qu-1.5.0/tests/__init__.py cu2qu-1.6.5/tests/__init__.py --- cu2qu-1.5.0/tests/__init__.py 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/tests/__init__.py 2018-11-01 16:41:08.000000000 +0000 @@ -1,5 +1,5 @@ import os -from ufoLib.glifLib import GlyphSet +from fontTools.ufoLib.glifLib import GlyphSet import pkg_resources DATADIR = os.path.join(os.path.dirname(__file__), 'data') diff -Nru cu2qu-1.5.0/tests/pens_test.py cu2qu-1.6.5/tests/pens_test.py --- cu2qu-1.5.0/tests/pens_test.py 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/tests/pens_test.py 2018-11-01 16:41:08.000000000 +0000 @@ -6,6 +6,7 @@ from .utils import DummyGlyph, DummyPointGlyph from .utils import DummyPen, DummyPointPen from fontTools.misc.loggingTools import CapturingLogHandler +from textwrap import dedent import logging @@ -17,6 +18,8 @@ PointPen test cases, plus some helper methods. """ + maxDiff = None + def diff(self, expected, actual): import difflib expected = str(self.Glyph(expected)).splitlines(True) @@ -343,6 +346,32 @@ self.assertEqual(new_segments[0][1][-1][0], (0, 0)) self.assertEqual(new_segments[-1][1][-1][0], (3, 3)) + def test_quad_no_oncurve(self): + """When passed a contour which has no on-curve points, the + Cu2QuPointPen will treat it as a special quadratic contour whose + first point has 'None' coordinates. + """ + self.maxDiff = None + pen = DummyPointPen() + quadpen = Cu2QuPointPen(pen, MAX_ERR) + quadpen.beginPath() + quadpen.addPoint((1, 1)) + quadpen.addPoint((2, 2)) + quadpen.addPoint((3, 3)) + quadpen.endPath() + + self.assertEqual( + str(pen), + dedent( + """\ + pen.beginPath() + pen.addPoint((1, 1), name=None, segmentType=None, smooth=False) + pen.addPoint((2, 2), name=None, segmentType=None, smooth=False) + pen.addPoint((3, 3), name=None, segmentType=None, smooth=False) + pen.endPath()""" + ) + ) + if __name__ == "__main__": unittest.main() diff -Nru cu2qu-1.5.0/tests/utils.py cu2qu-1.6.5/tests/utils.py --- cu2qu-1.5.0/tests/utils.py 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/tests/utils.py 2018-11-01 16:41:08.000000000 +0000 @@ -1,6 +1,7 @@ from __future__ import print_function, division, absolute_import from . import CUBIC_GLYPHS -from ufoLib.pointPen import PointToSegmentPen, SegmentToPointPen +from fontTools.pens.pointPen import PointToSegmentPen, SegmentToPointPen +from fontTools.misc.py23 import isclose import unittest @@ -50,7 +51,7 @@ self.commands.append(('endPath', tuple(), {})) def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): - kwargs['segmentType'] = segmentType + kwargs['segmentType'] = str(segmentType) if segmentType else None kwargs['smooth'] = smooth kwargs['name'] = name self.commands.append(('addPoint', (pt,), kwargs)) @@ -111,6 +112,34 @@ """Return True if 'other' glyph's outline is different from self.""" return not (self == other) + def approx(self, other): + if hasattr(other, 'outline'): + outline2 == other.outline + elif hasattr(other, 'draw'): + outline2 = self.__class__(other).outline + else: + raise TypeError(type(other).__name__) + outline1 = self.outline + if len(outline1) != len(outline2): + return False + for (cmd1, arg1, kwd1), (cmd2, arg2, kwd2) in zip(outline1, outline2): + if cmd1 != cmd2: + return False + if kwd1 != kwd2: + return False + if arg1: + if isinstance(arg1[0], tuple): + if not arg2 or not isinstance(arg2[0], tuple): + return False + for (x1, y1), (x2, y2) in zip(arg1, arg2): + if not isclose(x1, x2) or not isclose(y1, y2): + return False + elif arg1 != arg2: + return False + elif arg2: + return False + return True + def __str__(self): """Return commands making up the glyph's outline as a string.""" return str(self._pen) diff -Nru cu2qu-1.5.0/tools/update_cython_shadow.py cu2qu-1.6.5/tools/update_cython_shadow.py --- cu2qu-1.5.0/tools/update_cython_shadow.py 1970-01-01 00:00:00.000000000 +0000 +++ cu2qu-1.6.5/tools/update_cython_shadow.py 2018-11-01 16:41:08.000000000 +0000 @@ -0,0 +1,32 @@ +""" Update the embedded Lib/cu2qu/cython.py module with the contents of +the latest cython repository. + +Usage: + $ python tools/update_cython_shadow.py 0.28.5 +""" + +import requests +import sys + + +header = b'''\ +""" This module is copied verbatim from the "Cython.Shadow" module: +https://github.com/cython/cython/blob/master/Cython/Shadow.py + +Cython is licensed under the Apache 2.0 Software License. +""" +''' + +try: + version = sys.argv[1] +except IndexError: + version = "master" + +CYTHON_SHADOW_URL = ( + "https://raw.githubusercontent.com/cython/cython/%s/Cython/Shadow.py" +) % version + +r = requests.get(CYTHON_SHADOW_URL, allow_redirects=True) +with open("Lib/cu2qu/cython.py", "wb") as f: + f.write(header) + f.write(r.content) diff -Nru cu2qu-1.5.0/tox.ini cu2qu-1.6.5/tox.ini --- cu2qu-1.5.0/tox.ini 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/tox.ini 2018-11-01 16:41:08.000000000 +0000 @@ -1,18 +1,39 @@ [tox] -envlist = py27, py36, htmlcov +envlist = py{27,37}-{cy,nocy}, htmlcov +package_name = cu2qu +; we skip tox's own sdist generation as we need to pass different environment +; variables for testing buiding with and without cython +skipsdist = true [testenv] +setenv = + nocy: CU2QU_WITH_CYTHON=0 + cy: CU2QU_WITH_CYTHON=1 + cy: CYTHON_TRACE=1 + cy: CYTHON_ANNOTATE=1 deps = -rtest-requirements.txt -rrequirements.txt + cy: cython +changedir = {toxinidir} commands = + # create source distribution in a temp dir + python setup.py --quiet sdist --dist-dir {envtmpdir} + + # install from sdist + python -m pip install --ignore-installed --pre --no-deps --no-cache-dir --find-links {envtmpdir} {[tox]package_name} + + # ensure we are running the requested cu2qu version (compiled vs interpreted) + nocy: python -c "import sys, cu2qu.cu2qu; cu2qu.cu2qu.COMPILED and sys.exit(1)" + cy: python -c "import sys, cu2qu.cu2qu; cu2qu.cu2qu.COMPILED or sys.exit(1)" + + # run tests with code coverage enabled coverage run --parallel-mode -m pytest {posargs} [testenv:htmlcov] -basepython = python3.6 deps = coverage -skip_install = true +changedir = {toxinidir} commands = coverage combine coverage report @@ -23,8 +44,52 @@ deps = coverage codecov -skip_install = true ignore_outcome = true +changedir = {toxinidir} commands = coverage combine codecov --env TRAVIS_PYTHON_VERSION + +[testenv:update-cython] +deps = requests +changedir = {toxinidir} +commands = + python tools/update_cython_shadow.py {posargs} + +[testenv:sdist] +deps = + setuptools + cython +changedir = {toxinidir} +commands = + python -c 'import shutil; shutil.rmtree("dist", ignore_errors=True)' + python setup.py --with-cython sdist --dist-dir dist + +[testenv:pure-wheel] +deps = + {[testenv:sdist]deps} + pip + wheel +setenv = CU2QU_WITH_CYTHON=0 +changedir = {toxinidir} +commands = + {[testenv:sdist]commands} + pip wheel --pre --no-deps --no-cache-dir --wheel-dir dist --find-links dist \ + --no-binary {[tox]package_name} {[tox]package_name} + +[testenv:native-wheel] +deps = {[testenv:pure-wheel]deps} +setenv = CU2QU_WITH_CYTHON=1 +changedir = {toxinidir} +commands = {[testenv:pure-wheel]commands} + +; we only upload the source distribution to PyPI (for now) +[testenv:pypi] +deps = + {[testenv:sdist]deps} + twine +passenv = TWINE_USERNAME TWINE_PASSWORD +changedir = {toxinidir} +commands = + {[testenv:sdist]commands} + twine upload dist/*.zip diff -Nru cu2qu-1.5.0/.travis.yml cu2qu-1.6.5/.travis.yml --- cu2qu-1.5.0/.travis.yml 2018-04-10 20:48:46.000000000 +0000 +++ cu2qu-1.6.5/.travis.yml 2018-11-01 16:41:08.000000000 +0000 @@ -1,29 +1,37 @@ sudo: false language: python -python: - - "2.7" - - "3.6" + +env: + global: + - TWINE_USERNAME="anthrotype" + - secure: uIlWYz63F0Y/pDZawW2mS5DolWghdIodM8VtJtGbyIYB3fL3/edwTG5z+FWKaNeRtNfTAGDMs0y2dF45IJ7ZqTzw4yotgHz0uzLqjGjQ1/MOu99tppoXA6wQocZvmrdZpUcRbvBzJQzpAUcjldPLAP200mV9cG8+LfODn8Di2eJ329Ts3aA140pjUF8791jRLHBhUTpxK5RDPn1Q7OlugjryS7yNVIfT1/DaNDXAu4OZ8oNkygioRcyZ9QiFLjv5yBZ7uHB4UXWxw89RYPyz4NfHwyzDR38X/A6vfP19w2V0kecAK5BVBUE+WI7d26XjQzxDuH6Z0phB3x6MFuCXrX/pdrvNr7hs5kTAdQ7R1YA6MH4lPa+7oXha1/j353StzDMUKByXGVHyLAv7Ct2RSXOHUM6hAB4T+JbyJp/YkPWh968GKpl1lwvziKTi7K1qpngbXCdYIYKJ78IbmDcxzmQ+3j+fsXt9+gArZW7ICLgWrs+Lr1FiJsBOKLmqigOSmqrjHa+ef+wjieFSgzCVIfr9zvibzCEtYeqkuJuDQcSBIS0JG/heOfBGQ6FIxSXzwvICyWpldmfP67nBjVOVzPcyEAT8w+45LOM0HPCm6+Xjn7mKstc7x9TD7dVjWeyKJzZuKSmuAFA4UtGGKDQ93YQlAY2SCW4irYsusj80LHU= + +matrix: + include: + - env: TOXENV=py27-nocy + python: 2.7 + - env: TOXENV=py36-nocy + python: 3.6 + - env: TOXENV=py27-cy + python: 2.7 + - env: + - TOXENV=py36-cy + - BUILD_DIST=true + python: 3.6 + branches: only: - master - /^v\d+\.\d+.*$/ -install: pip install tox-travis +install: pip install tox script: tox -after_success: tox -e codecov - -deploy: +after_success: + - if [ -z "$TRAVIS_TAG" ]; then tox -e codecov; fi # deploy to PyPI on tags - provider: pypi - server: https://upload.pypi.org/legacy/ - on: - repo: googlei18n/cu2qu - tags: true - all_branches: true - python: 3.6 - user: anthrotype - password: - secure: LdEsho2OyiHKOsqjdwa1s6LQNuzbiHJGpk7L+Qn6XV8bmzznI15Z5yaC4kO8vE0ZfE0dwTcdA4BrjUBxFZnmWZtGp/la9pcwUF5fX8LnKwRsw8oPHJZNvvi9IEgnIg68VUJ4X787+hJKilQKhyE+J3UKDrJ0on6UJjpTciO8Tsins80EMD5wB000VriCXiZ3wvaCm/yaXDeGKkb8Us3NWT8pshgZ2SpoQyIJ1pik7p4UtjcZM2tPKbCPkim17UCOYQ/0II4KmoT19JGceC0xWbD0cssZDM2rd0vIQ+OMQTD7fkoTE2pY9L3dLDyHamJCECq/ZX0rNgFUBylEJq1+gin+8g81vXsewzBZA5Zc4/D+ER0INdLF8LbLAZOqu40eMa2X6bu3w++Vo8dK0wZibVlrA3EnBKcvTePTFuXnlAPuE252lsq+zn1nO8SO6xfzvB4JF3iB7GO7dajnR+8C+m9ctO/Lx043+FoxH607N1E/WOsFvOCPXQhpeNJZHyPtA4no++O0fp3KcKkvvJrt5nVxs55AT+p5uAXvfOATwwjaZYivayeZj2ICeFhh3A2LciMOvaqqXAJ64zFmjnmgVYyJPc7Xh30cnsvdKOR5QR35gzAZfEAyBeYIckIyIwpF9N+ogtGMQvHSprkbf1Bm6Hpmhq/XTkNzW+SM8+3/cfI= - distributions: sdist bdist_wheel + - | + if [ -n "$TRAVIS_TAG" ] && [ "$TRAVIS_REPO_SLUG" == "googlei18n/cu2qu" ] && [ "$BUILD_DIST" == true ]; then + tox -e pypi + fi