diff --git a/Lib/fontTools/qu2cu/qu2cu.py b/Lib/fontTools/qu2cu/qu2cu.py index 486072f7d..065f29d9f 100644 --- a/Lib/fontTools/qu2cu/qu2cu.py +++ b/Lib/fontTools/qu2cu/qu2cu.py @@ -169,11 +169,21 @@ def quadratics_to_curves(pp, tolerance=0.5, all_cubic=False): pp = [[complex(x, y) for (x, y) in p] for p in pp] q = [pp[0][0]] + cost = 0 + costs = [0] for p in pp: assert q[-1] == p[0] - q.extend(add_implicit_on_curves(p)[1:]) + for i in range(len(p) - 2): + cost += 1 + costs.append(cost) + costs.append(cost + 1) + qq = add_implicit_on_curves(p)[1:] + q.extend(qq) + cost += 1 + costs.append(cost) + costs.append(cost + 1) - curves = spline_to_curves(q, tolerance, all_cubic) + curves = spline_to_curves(q, costs, tolerance, all_cubic) if not is_complex: curves = [tuple((c.real, c.imag) for c in curve) for curve in curves] @@ -185,16 +195,22 @@ def quadratic_to_curves(q, tolerance=0.5, all_cubic=False): if not is_complex: q = [complex(x, y) for (x, y) in q] + costs = [0] + for i in range(len(q) - 2): + costs.append(i + 1) + costs.append(i + 2) + costs.append(len(q) - 1) + costs.append(len(q)) q = add_implicit_on_curves(q) - curves = spline_to_curves(q, tolerance, all_cubic) + curves = spline_to_curves(q, costs, tolerance, all_cubic) if not is_complex: curves = [tuple((c.real, c.imag) for c in curve) for curve in curves] return curves -def spline_to_curves(q, tolerance=0.5, all_cubic=False): +def spline_to_curves(q, costs, tolerance=0.5, all_cubic=False): assert len(q) >= 3, "quadratic spline requires at least 3 points" # Elevate quadratic segments to cubic @@ -204,11 +220,21 @@ def spline_to_curves(q, tolerance=0.5, all_cubic=False): # Dynamic-Programming to find the solution with fewest number of # cubic curves, and within those the one with smallest error. - sols = [(0, 0, 0)] # (best_num_segments, best_error, start_index) + sols = [(0, 0, 0, False)] # (best_num_points, best_error, start_index, cubic) for i in range(1, len(elevated_quadratics) + 1): - best_sol = (len(q) + 1, 0, 1) + best_sol = (len(q) + 2, 0, 1, False) for j in range(0, i): + j_sol_count, j_sol_error, _, _ = sols[j] + + if not all_cubic: + # Solution with quadratics between j:i + i_sol_count = j_sol_count + costs[2 * i] - costs[2 * j] + i_sol_error = j_sol_error + i_sol = (i_sol_count, i_sol_error, i - j, False) + if i_sol < best_sol: + best_sol = i_sol + # Fit elevated_quadratics[j:i] into one cubic try: curve, ts = merge_curves(elevated_quadratics[j:i]) @@ -245,14 +271,13 @@ def spline_to_curves(q, tolerance=0.5, all_cubic=False): continue # Save best solution - j_sol_count, j_sol_error, _ = sols[j] - i_sol_count = j_sol_count + 1 + i_sol_count = j_sol_count + 3 i_sol_error = max(j_sol_error, error) - i_sol = (i_sol_count, i_sol_error, i - j) + i_sol = (i_sol_count, i_sol_error, i - j, True) if i_sol < best_sol: best_sol = i_sol - if i_sol_count == 1: + if i_sol_count == 4: # Can't get any better than this break @@ -260,18 +285,21 @@ def spline_to_curves(q, tolerance=0.5, all_cubic=False): # Reconstruct solution splits = [] + cubic = [] i = len(sols) - 1 while i: + _, _, count, is_cubic = sols[i] splits.append(i) - _, _, count = sols[i] + cubic.append(is_cubic) i -= count curves = [] j = 0 - for i in reversed(splits): - if not all_cubic and j + 1 == i: - curves.append(q[j * 2 : j * 2 + 3]) - else: + for i, is_cubic in reversed(list(zip(splits, cubic))): + if is_cubic: curves.append(merge_curves(elevated_quadratics[j:i])[0]) + else: + for k in range(j, i): + curves.append(q[k * 2 : k * 2 + 3]) j = i return curves diff --git a/Tests/pens/qu2cuPen_test.py b/Tests/pens/qu2cuPen_test.py index 812d11fd2..8f05ca7b8 100644 --- a/Tests/pens/qu2cuPen_test.py +++ b/Tests/pens/qu2cuPen_test.py @@ -48,7 +48,7 @@ class _TestPenMixin(object): # draw source glyph onto a new glyph using a Cu2Qu pen and return it converted = self.Glyph() pen = getattr(converted, self.pen_getter_name)() - cubicpen = self.Qu2CuPen(pen, MAX_ERR, **kwargs) + cubicpen = self.Qu2CuPen(pen, MAX_ERR, all_cubic=True, **kwargs) getattr(glyph, self.draw_method_name)(cubicpen) return converted @@ -144,7 +144,7 @@ class TestQu2CuPen(unittest.TestCase, _TestPenMixin): ], ) - def test_qCurveTo_3_points(self): + def test_qCurveTo_3_points_no_conversion(self): pen = DummyPen() cubicpen = Qu2CuPen(pen, MAX_ERR) cubicpen.moveTo((0, 0)) @@ -155,7 +155,7 @@ class TestQu2CuPen(unittest.TestCase, _TestPenMixin): str(pen).splitlines(), [ "pen.moveTo((0, 0))", - "pen.curveTo((0, 4.0), (1, 4.0), (1, 0))", + "pen.qCurveTo((0, 3), (1, 3), (1, 0))", "pen.closePath()", ], ) diff --git a/Tests/qu2cu/qu2cu_test.py b/Tests/qu2cu/qu2cu_test.py index c59f1f189..bb45435f1 100644 --- a/Tests/qu2cu/qu2cu_test.py +++ b/Tests/qu2cu/qu2cu_test.py @@ -36,7 +36,7 @@ class Qu2CuTest: ((0, 0), (0, 4 / 3), (2, 4 / 3), (2, 0)), ], 0.1, - False, + True, ), ( [ @@ -46,7 +46,7 @@ class Qu2CuTest: ((0, 0), (0, 4 / 3), (2, 2 / 3), (2, 2)), ], 0.2, - False, + True, ), ( [