diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..2d4cce0ba --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +sudo: false +language: python +python: + - "3.4" +branches: + only: + - master + +install: + - python setup.py install + - pip install git+https://github.com/behdad/fonttools + - pip install git+https://github.com/unified-font-object/ufoLib + +script: + - python setup.py test diff --git a/Lib/cu2qu/__init__.py b/Lib/cu2qu/__init__.py index 77d92dd8d..9c4de981e 100644 --- a/Lib/cu2qu/__init__.py +++ b/Lib/cu2qu/__init__.py @@ -234,4 +234,4 @@ def curves_to_quadratic(curves, max_errors): for c, s, max_err in zip(curves, splines, max_errors): if s is None: raise ApproxNotFoundError(c) - return [[(s.real,s.imag) for s in spline] for spline in splines], None + return [[(s.real,s.imag) for s in spline] for spline in splines] diff --git a/Lib/cu2qu/pens.py b/Lib/cu2qu/pens.py index 63a49c7de..625630378 100644 --- a/Lib/cu2qu/pens.py +++ b/Lib/cu2qu/pens.py @@ -72,7 +72,7 @@ class Cu2QuPen(AbstractPen): def _curve_to_quadratic(self, pt1, pt2, pt3): curve = (self.current_pt, pt1, pt2, pt3) - quadratic, _ = curve_to_quadratic(curve, self.max_err) + quadratic = curve_to_quadratic(curve, self.max_err) if self.stats is not None: n = str(len(quadratic)) self.stats[n] = self.stats.get(n, 0) + 1 @@ -144,7 +144,7 @@ class Cu2QuPointPen(BasePointToSegmentPen): on_curve, smooth, name, kwargs = sub_points[-1] bcp1, bcp2 = sub_points[0][0], sub_points[1][0] cubic = [prev_on_curve, bcp1, bcp2, on_curve] - quad, _ = curve_to_quadratic(cubic, self.max_err) + quad = curve_to_quadratic(cubic, self.max_err) if self.stats is not None: n = str(len(quad)) self.stats[n] = self.stats.get(n, 0) + 1 diff --git a/Lib/cu2qu/test/cu2qu_test.py b/Lib/cu2qu/test/cu2qu_test.py index 3ef96c308..a9a51465c 100644 --- a/Lib/cu2qu/test/cu2qu_test.py +++ b/Lib/cu2qu/test/cu2qu_test.py @@ -16,6 +16,7 @@ from __future__ import print_function, division, absolute_import import collections +import math import unittest import random @@ -34,12 +35,18 @@ class CurveToQuadraticTest(unittest.TestCase): random.seed(1) curves = [generate_curve() for i in range(1000)] - cls.single_splines, cls.single_errors = zip(*[ - curve_to_quadratic(c, MAX_ERR) for c in curves]) + cls.single_splines = [ + curve_to_quadratic(c, MAX_ERR) for c in curves] + cls.single_errors = [ + cls.curve_spline_dist(c, s) + for c, s in zip(curves, cls.single_splines)] - cls.compat_splines, cls.compat_errors = zip(*[ - curves_to_quadratic(curves[i:i + 3], [MAX_ERR] * 3) - for i in range(0, 300, 3)]) + curve_groups = [curves[i:i + 3] for i in range(0, 300, 3)] + cls.compat_splines = [ + curves_to_quadratic(c, [MAX_ERR] * 3) for c in curve_groups] + cls.compat_errors = [ + [cls.curve_spline_dist(c, s) for c, s in zip(curve_group, splines)] + for curve_group, splines in zip(curve_groups, cls.compat_splines)] cls.results = [] @@ -60,13 +67,13 @@ class CurveToQuadraticTest(unittest.TestCase): """ expected = { - 3: 5, - 4: 31, - 5: 74, - 6: 228, - 7: 416, - 8: 242, - 9: 4} + 3: 6, + 4: 26, + 5: 82, + 6: 232, + 7: 360, + 8: 266, + 9: 28} results = collections.defaultdict(int) for spline in self.single_splines: @@ -79,10 +86,10 @@ class CurveToQuadraticTest(unittest.TestCase): """Test that conversion results are unchanged for multiple curves.""" expected = { - 6: 3, - 7: 34, - 8: 62, - 9: 1} + 6: 11, + 7: 35, + 8: 49, + 9: 5} results = collections.defaultdict(int) for splines in self.compat_splines: @@ -113,6 +120,60 @@ class CurveToQuadraticTest(unittest.TestCase): self.assertLessEqual(error, MAX_ERR) self.results.append(('compatible errors', results)) + @classmethod + def curve_spline_dist(cls, bezier, spline, total_steps=20): + """Max distance between a bezier and quadratic spline at sampled points.""" + + error = 0 + n = len(spline) - 2 + steps = total_steps // n + for i in range(0, n - 1): + p1 = spline[0] if i == 0 else p3 + p2 = spline[i + 1] + if i < n - 1: + p3 = cls.lerp(spline[i + 1], spline[i + 2], 0.5) + else: + p3 = spline[n + 2] + segment = p1, p2, p3 + for j in range(steps): + error = max(error, cls.dist( + cls.cubic_bezier_at(bezier, (j / steps + i) / n), + cls.quadratic_bezier_at(segment, j / steps))) + return error + + @classmethod + def lerp(cls, p1, p2, t): + (x1, y1), (x2, y2) = p1, p2 + return x1 + (x2 - x1) * t, y1 + (y2 - y1) * t + + @classmethod + def dist(cls, p1, p2): + (x1, y1), (x2, y2) = p1, p2 + return math.hypot(x1 - x2, y1 - y2) + + @classmethod + def quadratic_bezier_at(cls, b, t): + (x1, y1), (x2, y2), (x3, y3) = b + _t = 1 - t + t2 = t * t + _t2 = _t * _t + _2_t_t = 2 * t * _t + return (_t2 * x1 + _2_t_t * x2 + t2 * x3, + _t2 * y1 + _2_t_t * y2 + t2 * y3) + + @classmethod + def cubic_bezier_at(cls, b, t): + (x1, y1), (x2, y2), (x3, y3), (x4, y4) = b + _t = 1 - t + t2 = t * t + _t2 = _t * _t + t3 = t * t2 + _t3 = _t * _t2 + _3_t2_t = 3 * t2 * _t + _3_t_t2 = 3 * t * _t2 + return (_t3 * x1 + _3_t_t2 * x2 + _3_t2_t * x3 + t3 * x4, + _t3 * y1 + _3_t_t2 * y2 + _3_t2_t * y3 + t3 * y4) + if __name__ == '__main__': unittest.main() diff --git a/Lib/cu2qu/test/data/quadratic/A_.glif b/Lib/cu2qu/test/data/quadratic/A_.glif index f70bfa6c3..673eb514e 100644 --- a/Lib/cu2qu/test/data/quadratic/A_.glif +++ b/Lib/cu2qu/test/data/quadratic/A_.glif @@ -6,8 +6,8 @@ - - + + @@ -22,12 +22,12 @@ - + - - + + @@ -37,27 +37,27 @@ - - - - - - + + + + + + - + - + - - + + @@ -71,22 +71,4 @@ - - - org.robofab.fontlab.guides - - hguides - - - angle - 0 - position - -180 - - - - org.robofab.fontlab.mark - 80 - - - \ No newline at end of file + diff --git a/Lib/cu2qu/test/data/quadratic/E_acute.glif b/Lib/cu2qu/test/data/quadratic/E_acute.glif index b0a5ec486..e5bb4fd5b 100644 --- a/Lib/cu2qu/test/data/quadratic/E_acute.glif +++ b/Lib/cu2qu/test/data/quadratic/E_acute.glif @@ -6,15 +6,15 @@ - - + + - - + + @@ -30,7 +30,8 @@ - + + @@ -38,26 +39,27 @@ - - - + + + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/Lib/cu2qu/test/data/quadratic/a.glif b/Lib/cu2qu/test/data/quadratic/a.glif index 141bc75fb..dc776e117 100644 --- a/Lib/cu2qu/test/data/quadratic/a.glif +++ b/Lib/cu2qu/test/data/quadratic/a.glif @@ -6,8 +6,8 @@ - - + + @@ -17,17 +17,17 @@ - + - - + + - - + + @@ -37,17 +37,17 @@ - - + + - - + + - - + + @@ -57,8 +57,8 @@ - - + + @@ -90,4 +90,4 @@ - \ No newline at end of file + diff --git a/Lib/cu2qu/ufo.py b/Lib/cu2qu/ufo.py index a671db826..2d3543ecd 100644 --- a/Lib/cu2qu/ufo.py +++ b/Lib/cu2qu/ufo.py @@ -126,7 +126,7 @@ def _segments_to_quadratic(segments, max_err, stats): assert all(s[0] == 'curve' for s in segments), 'Non-cubic given to convert' - new_points, _ = curves_to_quadratic([s[1] for s in segments], max_err) + new_points = curves_to_quadratic([s[1] for s in segments], max_err) n = len(new_points[0]) assert all(len(s) == n for s in new_points[1:]), 'Converted incompatibly'