iup.py 15 KB

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