iup.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. try:
  2. import cython
  3. COMPILED = cython.compiled
  4. except (AttributeError, ImportError):
  5. # if cython not installed, use mock module with no-op decorators and types
  6. from fontTools.misc import cython
  7. COMPILED = False
  8. from typing import (
  9. Sequence,
  10. Tuple,
  11. Union,
  12. )
  13. from numbers import Integral, Real
  14. _Point = Tuple[Real, Real]
  15. _Delta = Tuple[Real, Real]
  16. _PointSegment = Sequence[_Point]
  17. _DeltaSegment = Sequence[_Delta]
  18. _DeltaOrNone = Union[_Delta, None]
  19. _DeltaOrNoneSegment = Sequence[_DeltaOrNone]
  20. _Endpoints = Sequence[Integral]
  21. MAX_LOOKBACK = 8
  22. @cython.cfunc
  23. @cython.locals(
  24. j=cython.int,
  25. n=cython.int,
  26. x1=cython.double,
  27. x2=cython.double,
  28. d1=cython.double,
  29. d2=cython.double,
  30. scale=cython.double,
  31. x=cython.double,
  32. d=cython.double,
  33. )
  34. def iup_segment(
  35. coords: _PointSegment, rc1: _Point, rd1: _Delta, rc2: _Point, rd2: _Delta
  36. ): # -> _DeltaSegment:
  37. """Given two reference coordinates `rc1` & `rc2` and their respective
  38. delta vectors `rd1` & `rd2`, returns interpolated deltas for the set of
  39. coordinates `coords`."""
  40. # rc1 = reference coord 1
  41. # rd1 = reference delta 1
  42. out_arrays = [None, None]
  43. for j in 0, 1:
  44. out_arrays[j] = out = []
  45. x1, x2, d1, d2 = rc1[j], rc2[j], rd1[j], rd2[j]
  46. if x1 == x2:
  47. n = len(coords)
  48. if d1 == d2:
  49. out.extend([d1] * n)
  50. else:
  51. out.extend([0] * n)
  52. continue
  53. if x1 > x2:
  54. x1, x2 = x2, x1
  55. d1, d2 = d2, d1
  56. # x1 < x2
  57. scale = (d2 - d1) / (x2 - x1)
  58. for pair in coords:
  59. x = pair[j]
  60. if x <= x1:
  61. d = d1
  62. elif x >= x2:
  63. d = d2
  64. else:
  65. # Interpolate
  66. d = d1 + (x - x1) * scale
  67. out.append(d)
  68. return zip(*out_arrays)
  69. def iup_contour(deltas: _DeltaOrNoneSegment, coords: _PointSegment) -> _DeltaSegment:
  70. """For the contour given in `coords`, interpolate any missing
  71. delta values in delta vector `deltas`.
  72. Returns fully filled-out delta vector."""
  73. assert len(deltas) == len(coords)
  74. if None not in deltas:
  75. return deltas
  76. n = len(deltas)
  77. # indices of points with explicit deltas
  78. indices = [i for i, v in enumerate(deltas) if v is not None]
  79. if not indices:
  80. # All deltas are None. Return 0,0 for all.
  81. return [(0, 0)] * n
  82. out = []
  83. it = iter(indices)
  84. start = next(it)
  85. if start != 0:
  86. # Initial segment that wraps around
  87. i1, i2, ri1, ri2 = 0, start, start, indices[-1]
  88. out.extend(
  89. iup_segment(
  90. coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2]
  91. )
  92. )
  93. out.append(deltas[start])
  94. for end in it:
  95. if end - start > 1:
  96. i1, i2, ri1, ri2 = start + 1, end, start, end
  97. out.extend(
  98. iup_segment(
  99. coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2]
  100. )
  101. )
  102. out.append(deltas[end])
  103. start = end
  104. if start != n - 1:
  105. # Final segment that wraps around
  106. i1, i2, ri1, ri2 = start + 1, n, start, indices[0]
  107. out.extend(
  108. iup_segment(
  109. coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2]
  110. )
  111. )
  112. assert len(deltas) == len(out), (len(deltas), len(out))
  113. return out
  114. def iup_delta(
  115. deltas: _DeltaOrNoneSegment, coords: _PointSegment, ends: _Endpoints
  116. ) -> _DeltaSegment:
  117. """For the outline given in `coords`, with contour endpoints given
  118. in sorted increasing order in `ends`, interpolate any missing
  119. delta values in delta vector `deltas`.
  120. Returns fully filled-out delta vector."""
  121. assert sorted(ends) == ends and len(coords) == (ends[-1] + 1 if ends else 0) + 4
  122. n = len(coords)
  123. ends = ends + [n - 4, n - 3, n - 2, n - 1]
  124. out = []
  125. start = 0
  126. for end in ends:
  127. end += 1
  128. contour = iup_contour(deltas[start:end], coords[start:end])
  129. out.extend(contour)
  130. start = end
  131. return out
  132. # Optimizer
  133. @cython.cfunc
  134. @cython.inline
  135. @cython.locals(
  136. i=cython.int,
  137. j=cython.int,
  138. # tolerance=cython.double, # https://github.com/fonttools/fonttools/issues/3282
  139. x=cython.double,
  140. y=cython.double,
  141. p=cython.double,
  142. q=cython.double,
  143. )
  144. @cython.returns(int)
  145. def can_iup_in_between(
  146. deltas: _DeltaSegment,
  147. coords: _PointSegment,
  148. i: Integral,
  149. j: Integral,
  150. tolerance: Real,
  151. ): # -> bool:
  152. """Return true if the deltas for points at `i` and `j` (`i < j`) can be
  153. successfully used to interpolate deltas for points in between them within
  154. provided error tolerance."""
  155. assert j - i >= 2
  156. interp = iup_segment(coords[i + 1 : j], coords[i], deltas[i], coords[j], deltas[j])
  157. deltas = deltas[i + 1 : j]
  158. return all(
  159. abs(complex(x - p, y - q)) <= tolerance
  160. for (x, y), (p, q) in zip(deltas, interp)
  161. )
  162. @cython.locals(
  163. cj=cython.double,
  164. dj=cython.double,
  165. lcj=cython.double,
  166. ldj=cython.double,
  167. ncj=cython.double,
  168. ndj=cython.double,
  169. force=cython.int,
  170. forced=set,
  171. )
  172. def _iup_contour_bound_forced_set(
  173. deltas: _DeltaSegment, coords: _PointSegment, tolerance: Real = 0
  174. ) -> set:
  175. """The forced set is a conservative set of points on the contour that must be encoded
  176. explicitly (ie. cannot be interpolated). Calculating this set allows for significantly
  177. speeding up the dynamic-programming, as well as resolve circularity in DP.
  178. The set is precise; that is, if an index is in the returned set, then there is no way
  179. that IUP can generate delta for that point, given `coords` and `deltas`.
  180. """
  181. assert len(deltas) == len(coords)
  182. n = len(deltas)
  183. forced = set()
  184. # Track "last" and "next" points on the contour as we sweep.
  185. for i in range(len(deltas) - 1, -1, -1):
  186. ld, lc = deltas[i - 1], coords[i - 1]
  187. d, c = deltas[i], coords[i]
  188. nd, nc = deltas[i - n + 1], coords[i - n + 1]
  189. for j in (0, 1): # For X and for Y
  190. cj = c[j]
  191. dj = d[j]
  192. lcj = lc[j]
  193. ldj = ld[j]
  194. ncj = nc[j]
  195. ndj = nd[j]
  196. if lcj <= ncj:
  197. c1, c2 = lcj, ncj
  198. d1, d2 = ldj, ndj
  199. else:
  200. c1, c2 = ncj, lcj
  201. d1, d2 = ndj, ldj
  202. force = False
  203. # If the two coordinates are the same, then the interpolation
  204. # algorithm produces the same delta if both deltas are equal,
  205. # and zero if they differ.
  206. #
  207. # This test has to be before the next one.
  208. if c1 == c2:
  209. if abs(d1 - d2) > tolerance and abs(dj) > tolerance:
  210. force = True
  211. # If coordinate for current point is between coordinate of adjacent
  212. # points on the two sides, but the delta for current point is NOT
  213. # between delta for those adjacent points (considering tolerance
  214. # allowance), then there is no way that current point can be IUP-ed.
  215. # Mark it forced.
  216. elif c1 <= cj <= c2: # and c1 != c2
  217. if not (min(d1, d2) - tolerance <= dj <= max(d1, d2) + tolerance):
  218. force = True
  219. # Otherwise, the delta should either match the closest, or have the
  220. # same sign as the interpolation of the two deltas.
  221. else: # cj < c1 or c2 < cj
  222. if d1 != d2:
  223. if cj < c1:
  224. if (
  225. abs(dj) > tolerance
  226. and abs(dj - d1) > tolerance
  227. and ((dj - tolerance < d1) != (d1 < d2))
  228. ):
  229. force = True
  230. else: # c2 < cj
  231. if (
  232. abs(dj) > tolerance
  233. and abs(dj - d2) > tolerance
  234. and ((d2 < dj + tolerance) != (d1 < d2))
  235. ):
  236. force = True
  237. if force:
  238. forced.add(i)
  239. break
  240. return forced
  241. @cython.locals(
  242. i=cython.int,
  243. j=cython.int,
  244. best_cost=cython.double,
  245. best_j=cython.int,
  246. cost=cython.double,
  247. forced=set,
  248. tolerance=cython.double,
  249. )
  250. def _iup_contour_optimize_dp(
  251. deltas: _DeltaSegment,
  252. coords: _PointSegment,
  253. forced=set(),
  254. tolerance: Real = 0,
  255. lookback: Integral = None,
  256. ):
  257. """Straightforward Dynamic-Programming. For each index i, find least-costly encoding of
  258. points 0 to i where i is explicitly encoded. We find this by considering all previous
  259. explicit points j and check whether interpolation can fill points between j and i.
  260. Note that solution always encodes last point explicitly. Higher-level is responsible
  261. for removing that restriction.
  262. As major speedup, we stop looking further whenever we see a "forced" point."""
  263. n = len(deltas)
  264. if lookback is None:
  265. lookback = n
  266. lookback = min(lookback, MAX_LOOKBACK)
  267. costs = {-1: 0}
  268. chain = {-1: None}
  269. for i in range(0, n):
  270. best_cost = costs[i - 1] + 1
  271. costs[i] = best_cost
  272. chain[i] = i - 1
  273. if i - 1 in forced:
  274. continue
  275. for j in range(i - 2, max(i - lookback, -2), -1):
  276. cost = costs[j] + 1
  277. if cost < best_cost and can_iup_in_between(deltas, coords, j, i, tolerance):
  278. costs[i] = best_cost = cost
  279. chain[i] = j
  280. if j in forced:
  281. break
  282. return chain, costs
  283. def _rot_list(l: list, k: int):
  284. """Rotate list by k items forward. Ie. item at position 0 will be
  285. at position k in returned list. Negative k is allowed."""
  286. n = len(l)
  287. k %= n
  288. if not k:
  289. return l
  290. return l[n - k :] + l[: n - k]
  291. def _rot_set(s: set, k: int, n: int):
  292. k %= n
  293. if not k:
  294. return s
  295. return {(v + k) % n for v in s}
  296. def iup_contour_optimize(
  297. deltas: _DeltaSegment, coords: _PointSegment, tolerance: Real = 0.0
  298. ) -> _DeltaOrNoneSegment:
  299. """For contour with coordinates `coords`, optimize a set of delta
  300. values `deltas` within error `tolerance`.
  301. Returns delta vector that has most number of None items instead of
  302. the input delta.
  303. """
  304. n = len(deltas)
  305. # Get the easy cases out of the way:
  306. # If all are within tolerance distance of 0, encode nothing:
  307. if all(abs(complex(*p)) <= tolerance for p in deltas):
  308. return [None] * n
  309. # If there's exactly one point, return it:
  310. if n == 1:
  311. return deltas
  312. # If all deltas are exactly the same, return just one (the first one):
  313. d0 = deltas[0]
  314. if all(d0 == d for d in deltas):
  315. return [d0] + [None] * (n - 1)
  316. # Else, solve the general problem using Dynamic Programming.
  317. forced = _iup_contour_bound_forced_set(deltas, coords, tolerance)
  318. # The _iup_contour_optimize_dp() routine returns the optimal encoding
  319. # solution given the constraint that the last point is always encoded.
  320. # To remove this constraint, we use two different methods, depending on
  321. # whether forced set is non-empty or not:
  322. # Debugging: Make the next if always take the second branch and observe
  323. # if the font size changes (reduced); that would mean the forced-set
  324. # has members it should not have.
  325. if forced:
  326. # Forced set is non-empty: rotate the contour start point
  327. # such that the last point in the list is a forced point.
  328. k = (n - 1) - max(forced)
  329. assert k >= 0
  330. deltas = _rot_list(deltas, k)
  331. coords = _rot_list(coords, k)
  332. forced = _rot_set(forced, k, n)
  333. # Debugging: Pass a set() instead of forced variable to the next call
  334. # to exercise forced-set computation for under-counting.
  335. chain, costs = _iup_contour_optimize_dp(deltas, coords, forced, tolerance)
  336. # Assemble solution.
  337. solution = set()
  338. i = n - 1
  339. while i is not None:
  340. solution.add(i)
  341. i = chain[i]
  342. solution.remove(-1)
  343. # if not forced <= solution:
  344. # print("coord", coords)
  345. # print("deltas", deltas)
  346. # print("len", len(deltas))
  347. assert forced <= solution, (forced, solution)
  348. deltas = [deltas[i] if i in solution else None for i in range(n)]
  349. deltas = _rot_list(deltas, -k)
  350. else:
  351. # Repeat the contour an extra time, solve the new case, then look for solutions of the
  352. # circular n-length problem in the solution for new linear case. I cannot prove that
  353. # this always produces the optimal solution...
  354. chain, costs = _iup_contour_optimize_dp(
  355. deltas + deltas, coords + coords, forced, tolerance, n
  356. )
  357. best_sol, best_cost = None, n + 1
  358. for start in range(n - 1, len(costs) - 1):
  359. # Assemble solution.
  360. solution = set()
  361. i = start
  362. while i > start - n:
  363. solution.add(i % n)
  364. i = chain[i]
  365. if i == start - n:
  366. cost = costs[start] - costs[start - n]
  367. if cost <= best_cost:
  368. best_sol, best_cost = solution, cost
  369. # if not forced <= best_sol:
  370. # print("coord", coords)
  371. # print("deltas", deltas)
  372. # print("len", len(deltas))
  373. assert forced <= best_sol, (forced, best_sol)
  374. deltas = [deltas[i] if i in best_sol else None for i in range(n)]
  375. return deltas
  376. def iup_delta_optimize(
  377. deltas: _DeltaSegment,
  378. coords: _PointSegment,
  379. ends: _Endpoints,
  380. tolerance: Real = 0.0,
  381. ) -> _DeltaOrNoneSegment:
  382. """For the outline given in `coords`, with contour endpoints given
  383. in sorted increasing order in `ends`, optimize a set of delta
  384. values `deltas` within error `tolerance`.
  385. Returns delta vector that has most number of None items instead of
  386. the input delta.
  387. """
  388. assert sorted(ends) == ends and len(coords) == (ends[-1] + 1 if ends else 0) + 4
  389. n = len(coords)
  390. ends = ends + [n - 4, n - 3, n - 2, n - 1]
  391. out = []
  392. start = 0
  393. for end in ends:
  394. contour = iup_contour_optimize(
  395. deltas[start : end + 1], coords[start : end + 1], tolerance
  396. )
  397. assert len(contour) == end - start + 1
  398. out.extend(contour)
  399. start = end + 1
  400. return out