| try: |
| import cython |
| |
| COMPILED = cython.compiled |
| except (AttributeError, ImportError): |
| # if cython not installed, use mock module with no-op decorators and types |
| from fontTools.misc import cython |
| |
| COMPILED = False |
| |
| from typing import ( |
| Sequence, |
| Tuple, |
| Union, |
| ) |
| from numbers import Integral, Real |
| |
| |
| _Point = Tuple[Real, Real] |
| _Delta = Tuple[Real, Real] |
| _PointSegment = Sequence[_Point] |
| _DeltaSegment = Sequence[_Delta] |
| _DeltaOrNone = Union[_Delta, None] |
| _DeltaOrNoneSegment = Sequence[_DeltaOrNone] |
| _Endpoints = Sequence[Integral] |
| |
| |
| MAX_LOOKBACK = 8 |
| |
| |
| @cython.cfunc |
| @cython.locals( |
| j=cython.int, |
| n=cython.int, |
| x1=cython.double, |
| x2=cython.double, |
| d1=cython.double, |
| d2=cython.double, |
| scale=cython.double, |
| x=cython.double, |
| d=cython.double, |
| ) |
| def iup_segment( |
| coords: _PointSegment, rc1: _Point, rd1: _Delta, rc2: _Point, rd2: _Delta |
| ): # -> _DeltaSegment: |
| """Given two reference coordinates `rc1` & `rc2` and their respective |
| delta vectors `rd1` & `rd2`, returns interpolated deltas for the set of |
| coordinates `coords`.""" |
| |
| # rc1 = reference coord 1 |
| # rd1 = reference delta 1 |
| out_arrays = [None, None] |
| for j in 0, 1: |
| out_arrays[j] = out = [] |
| x1, x2, d1, d2 = rc1[j], rc2[j], rd1[j], rd2[j] |
| |
| if x1 == x2: |
| n = len(coords) |
| if d1 == d2: |
| out.extend([d1] * n) |
| else: |
| out.extend([0] * n) |
| continue |
| |
| if x1 > x2: |
| x1, x2 = x2, x1 |
| d1, d2 = d2, d1 |
| |
| # x1 < x2 |
| scale = (d2 - d1) / (x2 - x1) |
| for pair in coords: |
| x = pair[j] |
| |
| if x <= x1: |
| d = d1 |
| elif x >= x2: |
| d = d2 |
| else: |
| # Interpolate |
| d = d1 + (x - x1) * scale |
| |
| out.append(d) |
| |
| return zip(*out_arrays) |
| |
| |
| def iup_contour(deltas: _DeltaOrNoneSegment, coords: _PointSegment) -> _DeltaSegment: |
| """For the contour given in `coords`, interpolate any missing |
| delta values in delta vector `deltas`. |
| |
| Returns fully filled-out delta vector.""" |
| |
| assert len(deltas) == len(coords) |
| if None not in deltas: |
| return deltas |
| |
| n = len(deltas) |
| # indices of points with explicit deltas |
| indices = [i for i, v in enumerate(deltas) if v is not None] |
| if not indices: |
| # All deltas are None. Return 0,0 for all. |
| return [(0, 0)] * n |
| |
| out = [] |
| it = iter(indices) |
| start = next(it) |
| if start != 0: |
| # Initial segment that wraps around |
| i1, i2, ri1, ri2 = 0, start, start, indices[-1] |
| out.extend( |
| iup_segment( |
| coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2] |
| ) |
| ) |
| out.append(deltas[start]) |
| for end in it: |
| if end - start > 1: |
| i1, i2, ri1, ri2 = start + 1, end, start, end |
| out.extend( |
| iup_segment( |
| coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2] |
| ) |
| ) |
| out.append(deltas[end]) |
| start = end |
| if start != n - 1: |
| # Final segment that wraps around |
| i1, i2, ri1, ri2 = start + 1, n, start, indices[0] |
| out.extend( |
| iup_segment( |
| coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2] |
| ) |
| ) |
| |
| assert len(deltas) == len(out), (len(deltas), len(out)) |
| return out |
| |
| |
| def iup_delta( |
| deltas: _DeltaOrNoneSegment, coords: _PointSegment, ends: _Endpoints |
| ) -> _DeltaSegment: |
| """For the outline given in `coords`, with contour endpoints given |
| in sorted increasing order in `ends`, interpolate any missing |
| delta values in delta vector `deltas`. |
| |
| Returns fully filled-out delta vector.""" |
| |
| assert sorted(ends) == ends and len(coords) == (ends[-1] + 1 if ends else 0) + 4 |
| n = len(coords) |
| ends = ends + [n - 4, n - 3, n - 2, n - 1] |
| out = [] |
| start = 0 |
| for end in ends: |
| end += 1 |
| contour = iup_contour(deltas[start:end], coords[start:end]) |
| out.extend(contour) |
| start = end |
| |
| return out |
| |
| |
| # Optimizer |
| |
| |
| @cython.cfunc |
| @cython.inline |
| @cython.locals( |
| i=cython.int, |
| j=cython.int, |
| # tolerance=cython.double, # https://github.com/fonttools/fonttools/issues/3282 |
| x=cython.double, |
| y=cython.double, |
| p=cython.double, |
| q=cython.double, |
| ) |
| @cython.returns(int) |
| def can_iup_in_between( |
| deltas: _DeltaSegment, |
| coords: _PointSegment, |
| i: Integral, |
| j: Integral, |
| tolerance: Real, |
| ): # -> bool: |
| """Return true if the deltas for points at `i` and `j` (`i < j`) can be |
| successfully used to interpolate deltas for points in between them within |
| provided error tolerance.""" |
| |
| assert j - i >= 2 |
| interp = iup_segment(coords[i + 1 : j], coords[i], deltas[i], coords[j], deltas[j]) |
| deltas = deltas[i + 1 : j] |
| |
| return all( |
| abs(complex(x - p, y - q)) <= tolerance |
| for (x, y), (p, q) in zip(deltas, interp) |
| ) |
| |
| |
| @cython.locals( |
| cj=cython.double, |
| dj=cython.double, |
| lcj=cython.double, |
| ldj=cython.double, |
| ncj=cython.double, |
| ndj=cython.double, |
| force=cython.int, |
| forced=set, |
| ) |
| def _iup_contour_bound_forced_set( |
| deltas: _DeltaSegment, coords: _PointSegment, tolerance: Real = 0 |
| ) -> set: |
| """The forced set is a conservative set of points on the contour that must be encoded |
| explicitly (ie. cannot be interpolated). Calculating this set allows for significantly |
| speeding up the dynamic-programming, as well as resolve circularity in DP. |
| |
| The set is precise; that is, if an index is in the returned set, then there is no way |
| that IUP can generate delta for that point, given `coords` and `deltas`. |
| """ |
| assert len(deltas) == len(coords) |
| |
| n = len(deltas) |
| forced = set() |
| # Track "last" and "next" points on the contour as we sweep. |
| for i in range(len(deltas) - 1, -1, -1): |
| ld, lc = deltas[i - 1], coords[i - 1] |
| d, c = deltas[i], coords[i] |
| nd, nc = deltas[i - n + 1], coords[i - n + 1] |
| |
| for j in (0, 1): # For X and for Y |
| cj = c[j] |
| dj = d[j] |
| lcj = lc[j] |
| ldj = ld[j] |
| ncj = nc[j] |
| ndj = nd[j] |
| |
| if lcj <= ncj: |
| c1, c2 = lcj, ncj |
| d1, d2 = ldj, ndj |
| else: |
| c1, c2 = ncj, lcj |
| d1, d2 = ndj, ldj |
| |
| force = False |
| |
| # If the two coordinates are the same, then the interpolation |
| # algorithm produces the same delta if both deltas are equal, |
| # and zero if they differ. |
| # |
| # This test has to be before the next one. |
| if c1 == c2: |
| if abs(d1 - d2) > tolerance and abs(dj) > tolerance: |
| force = True |
| |
| # If coordinate for current point is between coordinate of adjacent |
| # points on the two sides, but the delta for current point is NOT |
| # between delta for those adjacent points (considering tolerance |
| # allowance), then there is no way that current point can be IUP-ed. |
| # Mark it forced. |
| elif c1 <= cj <= c2: # and c1 != c2 |
| if not (min(d1, d2) - tolerance <= dj <= max(d1, d2) + tolerance): |
| force = True |
| |
| # Otherwise, the delta should either match the closest, or have the |
| # same sign as the interpolation of the two deltas. |
| else: # cj < c1 or c2 < cj |
| if d1 != d2: |
| if cj < c1: |
| if ( |
| abs(dj) > tolerance |
| and abs(dj - d1) > tolerance |
| and ((dj - tolerance < d1) != (d1 < d2)) |
| ): |
| force = True |
| else: # c2 < cj |
| if ( |
| abs(dj) > tolerance |
| and abs(dj - d2) > tolerance |
| and ((d2 < dj + tolerance) != (d1 < d2)) |
| ): |
| force = True |
| |
| if force: |
| forced.add(i) |
| break |
| |
| return forced |
| |
| |
| @cython.locals( |
| i=cython.int, |
| j=cython.int, |
| best_cost=cython.double, |
| best_j=cython.int, |
| cost=cython.double, |
| forced=set, |
| tolerance=cython.double, |
| ) |
| def _iup_contour_optimize_dp( |
| deltas: _DeltaSegment, |
| coords: _PointSegment, |
| forced=set(), |
| tolerance: Real = 0, |
| lookback: Integral = None, |
| ): |
| """Straightforward Dynamic-Programming. For each index i, find least-costly encoding of |
| points 0 to i where i is explicitly encoded. We find this by considering all previous |
| explicit points j and check whether interpolation can fill points between j and i. |
| |
| Note that solution always encodes last point explicitly. Higher-level is responsible |
| for removing that restriction. |
| |
| As major speedup, we stop looking further whenever we see a "forced" point.""" |
| |
| n = len(deltas) |
| if lookback is None: |
| lookback = n |
| lookback = min(lookback, MAX_LOOKBACK) |
| costs = {-1: 0} |
| chain = {-1: None} |
| for i in range(0, n): |
| best_cost = costs[i - 1] + 1 |
| |
| costs[i] = best_cost |
| chain[i] = i - 1 |
| |
| if i - 1 in forced: |
| continue |
| |
| for j in range(i - 2, max(i - lookback, -2), -1): |
| cost = costs[j] + 1 |
| |
| if cost < best_cost and can_iup_in_between(deltas, coords, j, i, tolerance): |
| costs[i] = best_cost = cost |
| chain[i] = j |
| |
| if j in forced: |
| break |
| |
| return chain, costs |
| |
| |
| def _rot_list(l: list, k: int): |
| """Rotate list by k items forward. Ie. item at position 0 will be |
| at position k in returned list. Negative k is allowed.""" |
| n = len(l) |
| k %= n |
| if not k: |
| return l |
| return l[n - k :] + l[: n - k] |
| |
| |
| def _rot_set(s: set, k: int, n: int): |
| k %= n |
| if not k: |
| return s |
| return {(v + k) % n for v in s} |
| |
| |
| def iup_contour_optimize( |
| deltas: _DeltaSegment, coords: _PointSegment, tolerance: Real = 0.0 |
| ) -> _DeltaOrNoneSegment: |
| """For contour with coordinates `coords`, optimize a set of delta |
| values `deltas` within error `tolerance`. |
| |
| Returns delta vector that has most number of None items instead of |
| the input delta. |
| """ |
| |
| n = len(deltas) |
| |
| # Get the easy cases out of the way: |
| |
| # If all are within tolerance distance of 0, encode nothing: |
| if all(abs(complex(*p)) <= tolerance for p in deltas): |
| return [None] * n |
| |
| # If there's exactly one point, return it: |
| if n == 1: |
| return deltas |
| |
| # If all deltas are exactly the same, return just one (the first one): |
| d0 = deltas[0] |
| if all(d0 == d for d in deltas): |
| return [d0] + [None] * (n - 1) |
| |
| # Else, solve the general problem using Dynamic Programming. |
| |
| forced = _iup_contour_bound_forced_set(deltas, coords, tolerance) |
| # The _iup_contour_optimize_dp() routine returns the optimal encoding |
| # solution given the constraint that the last point is always encoded. |
| # To remove this constraint, we use two different methods, depending on |
| # whether forced set is non-empty or not: |
| |
| # Debugging: Make the next if always take the second branch and observe |
| # if the font size changes (reduced); that would mean the forced-set |
| # has members it should not have. |
| if forced: |
| # Forced set is non-empty: rotate the contour start point |
| # such that the last point in the list is a forced point. |
| k = (n - 1) - max(forced) |
| assert k >= 0 |
| |
| deltas = _rot_list(deltas, k) |
| coords = _rot_list(coords, k) |
| forced = _rot_set(forced, k, n) |
| |
| # Debugging: Pass a set() instead of forced variable to the next call |
| # to exercise forced-set computation for under-counting. |
| chain, costs = _iup_contour_optimize_dp(deltas, coords, forced, tolerance) |
| |
| # Assemble solution. |
| solution = set() |
| i = n - 1 |
| while i is not None: |
| solution.add(i) |
| i = chain[i] |
| solution.remove(-1) |
| |
| # if not forced <= solution: |
| # print("coord", coords) |
| # print("deltas", deltas) |
| # print("len", len(deltas)) |
| assert forced <= solution, (forced, solution) |
| |
| deltas = [deltas[i] if i in solution else None for i in range(n)] |
| |
| deltas = _rot_list(deltas, -k) |
| else: |
| # Repeat the contour an extra time, solve the new case, then look for solutions of the |
| # circular n-length problem in the solution for new linear case. I cannot prove that |
| # this always produces the optimal solution... |
| chain, costs = _iup_contour_optimize_dp( |
| deltas + deltas, coords + coords, forced, tolerance, n |
| ) |
| best_sol, best_cost = None, n + 1 |
| |
| for start in range(n - 1, len(costs) - 1): |
| # Assemble solution. |
| solution = set() |
| i = start |
| while i > start - n: |
| solution.add(i % n) |
| i = chain[i] |
| if i == start - n: |
| cost = costs[start] - costs[start - n] |
| if cost <= best_cost: |
| best_sol, best_cost = solution, cost |
| |
| # if not forced <= best_sol: |
| # print("coord", coords) |
| # print("deltas", deltas) |
| # print("len", len(deltas)) |
| assert forced <= best_sol, (forced, best_sol) |
| |
| deltas = [deltas[i] if i in best_sol else None for i in range(n)] |
| |
| return deltas |
| |
| |
| def iup_delta_optimize( |
| deltas: _DeltaSegment, |
| coords: _PointSegment, |
| ends: _Endpoints, |
| tolerance: Real = 0.0, |
| ) -> _DeltaOrNoneSegment: |
| """For the outline given in `coords`, with contour endpoints given |
| in sorted increasing order in `ends`, optimize a set of delta |
| values `deltas` within error `tolerance`. |
| |
| Returns delta vector that has most number of None items instead of |
| the input delta. |
| """ |
| assert sorted(ends) == ends and len(coords) == (ends[-1] + 1 if ends else 0) + 4 |
| n = len(coords) |
| ends = ends + [n - 4, n - 3, n - 2, n - 1] |
| out = [] |
| start = 0 |
| for end in ends: |
| contour = iup_contour_optimize( |
| deltas[start : end + 1], coords[start : end + 1], tolerance |
| ) |
| assert len(contour) == end - start + 1 |
| out.extend(contour) |
| start = end + 1 |
| |
| return out |