cu2qu.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. # cython: language_level=3
  2. # distutils: define_macros=CYTHON_TRACE_NOGIL=1
  3. # Copyright 2015 Google Inc. All Rights Reserved.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. try:
  17. import cython
  18. except (AttributeError, ImportError):
  19. # if cython not installed, use mock module with no-op decorators and types
  20. from fontTools.misc import cython
  21. COMPILED = cython.compiled
  22. import math
  23. from .errors import Error as Cu2QuError, ApproxNotFoundError
  24. __all__ = ["curve_to_quadratic", "curves_to_quadratic"]
  25. MAX_N = 100
  26. NAN = float("NaN")
  27. @cython.cfunc
  28. @cython.inline
  29. @cython.returns(cython.double)
  30. @cython.locals(v1=cython.complex, v2=cython.complex)
  31. def dot(v1, v2):
  32. """Return the dot product of two vectors.
  33. Args:
  34. v1 (complex): First vector.
  35. v2 (complex): Second vector.
  36. Returns:
  37. double: Dot product.
  38. """
  39. return (v1 * v2.conjugate()).real
  40. @cython.cfunc
  41. @cython.inline
  42. @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
  43. @cython.locals(
  44. _1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex
  45. )
  46. def calc_cubic_points(a, b, c, d):
  47. _1 = d
  48. _2 = (c / 3.0) + d
  49. _3 = (b + c) / 3.0 + _2
  50. _4 = a + d + c + b
  51. return _1, _2, _3, _4
  52. @cython.cfunc
  53. @cython.inline
  54. @cython.locals(
  55. p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex
  56. )
  57. @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
  58. def calc_cubic_parameters(p0, p1, p2, p3):
  59. c = (p1 - p0) * 3.0
  60. b = (p2 - p1) * 3.0 - c
  61. d = p0
  62. a = p3 - d - c - b
  63. return a, b, c, d
  64. @cython.cfunc
  65. @cython.inline
  66. @cython.locals(
  67. p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex
  68. )
  69. def split_cubic_into_n_iter(p0, p1, p2, p3, n):
  70. """Split a cubic Bezier into n equal parts.
  71. Splits the curve into `n` equal parts by curve time.
  72. (t=0..1/n, t=1/n..2/n, ...)
  73. Args:
  74. p0 (complex): Start point of curve.
  75. p1 (complex): First handle of curve.
  76. p2 (complex): Second handle of curve.
  77. p3 (complex): End point of curve.
  78. Returns:
  79. An iterator yielding the control points (four complex values) of the
  80. subcurves.
  81. """
  82. # Hand-coded special-cases
  83. if n == 2:
  84. return iter(split_cubic_into_two(p0, p1, p2, p3))
  85. if n == 3:
  86. return iter(split_cubic_into_three(p0, p1, p2, p3))
  87. if n == 4:
  88. a, b = split_cubic_into_two(p0, p1, p2, p3)
  89. return iter(
  90. split_cubic_into_two(a[0], a[1], a[2], a[3])
  91. + split_cubic_into_two(b[0], b[1], b[2], b[3])
  92. )
  93. if n == 6:
  94. a, b = split_cubic_into_two(p0, p1, p2, p3)
  95. return iter(
  96. split_cubic_into_three(a[0], a[1], a[2], a[3])
  97. + split_cubic_into_three(b[0], b[1], b[2], b[3])
  98. )
  99. return _split_cubic_into_n_gen(p0, p1, p2, p3, n)
  100. @cython.locals(
  101. p0=cython.complex,
  102. p1=cython.complex,
  103. p2=cython.complex,
  104. p3=cython.complex,
  105. n=cython.int,
  106. )
  107. @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
  108. @cython.locals(
  109. dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int
  110. )
  111. @cython.locals(
  112. a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex
  113. )
  114. def _split_cubic_into_n_gen(p0, p1, p2, p3, n):
  115. a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3)
  116. dt = 1 / n
  117. delta_2 = dt * dt
  118. delta_3 = dt * delta_2
  119. for i in range(n):
  120. t1 = i * dt
  121. t1_2 = t1 * t1
  122. # calc new a, b, c and d
  123. a1 = a * delta_3
  124. b1 = (3 * a * t1 + b) * delta_2
  125. c1 = (2 * b * t1 + c + 3 * a * t1_2) * dt
  126. d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d
  127. yield calc_cubic_points(a1, b1, c1, d1)
  128. @cython.cfunc
  129. @cython.inline
  130. @cython.locals(
  131. p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex
  132. )
  133. @cython.locals(mid=cython.complex, deriv3=cython.complex)
  134. def split_cubic_into_two(p0, p1, p2, p3):
  135. """Split a cubic Bezier into two equal parts.
  136. Splits the curve into two equal parts at t = 0.5
  137. Args:
  138. p0 (complex): Start point of curve.
  139. p1 (complex): First handle of curve.
  140. p2 (complex): Second handle of curve.
  141. p3 (complex): End point of curve.
  142. Returns:
  143. tuple: Two cubic Beziers (each expressed as a tuple of four complex
  144. values).
  145. """
  146. mid = (p0 + 3 * (p1 + p2) + p3) * 0.125
  147. deriv3 = (p3 + p2 - p1 - p0) * 0.125
  148. return (
  149. (p0, (p0 + p1) * 0.5, mid - deriv3, mid),
  150. (mid, mid + deriv3, (p2 + p3) * 0.5, p3),
  151. )
  152. @cython.cfunc
  153. @cython.inline
  154. @cython.locals(
  155. p0=cython.complex,
  156. p1=cython.complex,
  157. p2=cython.complex,
  158. p3=cython.complex,
  159. )
  160. @cython.locals(
  161. mid1=cython.complex,
  162. deriv1=cython.complex,
  163. mid2=cython.complex,
  164. deriv2=cython.complex,
  165. )
  166. def split_cubic_into_three(p0, p1, p2, p3):
  167. """Split a cubic Bezier into three equal parts.
  168. Splits the curve into three equal parts at t = 1/3 and t = 2/3
  169. Args:
  170. p0 (complex): Start point of curve.
  171. p1 (complex): First handle of curve.
  172. p2 (complex): Second handle of curve.
  173. p3 (complex): End point of curve.
  174. Returns:
  175. tuple: Three cubic Beziers (each expressed as a tuple of four complex
  176. values).
  177. """
  178. mid1 = (8 * p0 + 12 * p1 + 6 * p2 + p3) * (1 / 27)
  179. deriv1 = (p3 + 3 * p2 - 4 * p0) * (1 / 27)
  180. mid2 = (p0 + 6 * p1 + 12 * p2 + 8 * p3) * (1 / 27)
  181. deriv2 = (4 * p3 - 3 * p1 - p0) * (1 / 27)
  182. return (
  183. (p0, (2 * p0 + p1) / 3.0, mid1 - deriv1, mid1),
  184. (mid1, mid1 + deriv1, mid2 - deriv2, mid2),
  185. (mid2, mid2 + deriv2, (p2 + 2 * p3) / 3.0, p3),
  186. )
  187. @cython.cfunc
  188. @cython.inline
  189. @cython.returns(cython.complex)
  190. @cython.locals(
  191. t=cython.double,
  192. p0=cython.complex,
  193. p1=cython.complex,
  194. p2=cython.complex,
  195. p3=cython.complex,
  196. )
  197. @cython.locals(_p1=cython.complex, _p2=cython.complex)
  198. def cubic_approx_control(t, p0, p1, p2, p3):
  199. """Approximate a cubic Bezier using a quadratic one.
  200. Args:
  201. t (double): Position of control point.
  202. p0 (complex): Start point of curve.
  203. p1 (complex): First handle of curve.
  204. p2 (complex): Second handle of curve.
  205. p3 (complex): End point of curve.
  206. Returns:
  207. complex: Location of candidate control point on quadratic curve.
  208. """
  209. _p1 = p0 + (p1 - p0) * 1.5
  210. _p2 = p3 + (p2 - p3) * 1.5
  211. return _p1 + (_p2 - _p1) * t
  212. @cython.cfunc
  213. @cython.inline
  214. @cython.returns(cython.complex)
  215. @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
  216. @cython.locals(ab=cython.complex, cd=cython.complex, p=cython.complex, h=cython.double)
  217. def calc_intersect(a, b, c, d):
  218. """Calculate the intersection of two lines.
  219. Args:
  220. a (complex): Start point of first line.
  221. b (complex): End point of first line.
  222. c (complex): Start point of second line.
  223. d (complex): End point of second line.
  224. Returns:
  225. complex: Location of intersection if one present, ``complex(NaN,NaN)``
  226. if no intersection was found.
  227. """
  228. ab = b - a
  229. cd = d - c
  230. p = ab * 1j
  231. try:
  232. h = dot(p, a - c) / dot(p, cd)
  233. except ZeroDivisionError:
  234. return complex(NAN, NAN)
  235. return c + cd * h
  236. @cython.cfunc
  237. @cython.returns(cython.int)
  238. @cython.locals(
  239. tolerance=cython.double,
  240. p0=cython.complex,
  241. p1=cython.complex,
  242. p2=cython.complex,
  243. p3=cython.complex,
  244. )
  245. @cython.locals(mid=cython.complex, deriv3=cython.complex)
  246. def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
  247. """Check if a cubic Bezier lies within a given distance of the origin.
  248. "Origin" means *the* origin (0,0), not the start of the curve. Note that no
  249. checks are made on the start and end positions of the curve; this function
  250. only checks the inside of the curve.
  251. Args:
  252. p0 (complex): Start point of curve.
  253. p1 (complex): First handle of curve.
  254. p2 (complex): Second handle of curve.
  255. p3 (complex): End point of curve.
  256. tolerance (double): Distance from origin.
  257. Returns:
  258. bool: True if the cubic Bezier ``p`` entirely lies within a distance
  259. ``tolerance`` of the origin, False otherwise.
  260. """
  261. # First check p2 then p1, as p2 has higher error early on.
  262. if abs(p2) <= tolerance and abs(p1) <= tolerance:
  263. return True
  264. # Split.
  265. mid = (p0 + 3 * (p1 + p2) + p3) * 0.125
  266. if abs(mid) > tolerance:
  267. return False
  268. deriv3 = (p3 + p2 - p1 - p0) * 0.125
  269. return cubic_farthest_fit_inside(
  270. p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance
  271. ) and cubic_farthest_fit_inside(mid, mid + deriv3, (p2 + p3) * 0.5, p3, tolerance)
  272. @cython.cfunc
  273. @cython.inline
  274. @cython.locals(tolerance=cython.double)
  275. @cython.locals(
  276. q1=cython.complex,
  277. c0=cython.complex,
  278. c1=cython.complex,
  279. c2=cython.complex,
  280. c3=cython.complex,
  281. )
  282. def cubic_approx_quadratic(cubic, tolerance):
  283. """Approximate a cubic Bezier with a single quadratic within a given tolerance.
  284. Args:
  285. cubic (sequence): Four complex numbers representing control points of
  286. the cubic Bezier curve.
  287. tolerance (double): Permitted deviation from the original curve.
  288. Returns:
  289. Three complex numbers representing control points of the quadratic
  290. curve if it fits within the given tolerance, or ``None`` if no suitable
  291. curve could be calculated.
  292. """
  293. q1 = calc_intersect(cubic[0], cubic[1], cubic[2], cubic[3])
  294. if math.isnan(q1.imag):
  295. return None
  296. c0 = cubic[0]
  297. c3 = cubic[3]
  298. c1 = c0 + (q1 - c0) * (2 / 3)
  299. c2 = c3 + (q1 - c3) * (2 / 3)
  300. if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance):
  301. return None
  302. return c0, q1, c3
  303. @cython.cfunc
  304. @cython.locals(n=cython.int, tolerance=cython.double)
  305. @cython.locals(i=cython.int)
  306. @cython.locals(all_quadratic=cython.int)
  307. @cython.locals(
  308. c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex
  309. )
  310. @cython.locals(
  311. q0=cython.complex,
  312. q1=cython.complex,
  313. next_q1=cython.complex,
  314. q2=cython.complex,
  315. d1=cython.complex,
  316. )
  317. def cubic_approx_spline(cubic, n, tolerance, all_quadratic):
  318. """Approximate a cubic Bezier curve with a spline of n quadratics.
  319. Args:
  320. cubic (sequence): Four complex numbers representing control points of
  321. the cubic Bezier curve.
  322. n (int): Number of quadratic Bezier curves in the spline.
  323. tolerance (double): Permitted deviation from the original curve.
  324. Returns:
  325. A list of ``n+2`` complex numbers, representing control points of the
  326. quadratic spline if it fits within the given tolerance, or ``None`` if
  327. no suitable spline could be calculated.
  328. """
  329. if n == 1:
  330. return cubic_approx_quadratic(cubic, tolerance)
  331. if n == 2 and all_quadratic == False:
  332. return cubic
  333. cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n)
  334. # calculate the spline of quadratics and check errors at the same time.
  335. next_cubic = next(cubics)
  336. next_q1 = cubic_approx_control(
  337. 0, next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3]
  338. )
  339. q2 = cubic[0]
  340. d1 = 0j
  341. spline = [cubic[0], next_q1]
  342. for i in range(1, n + 1):
  343. # Current cubic to convert
  344. c0, c1, c2, c3 = next_cubic
  345. # Current quadratic approximation of current cubic
  346. q0 = q2
  347. q1 = next_q1
  348. if i < n:
  349. next_cubic = next(cubics)
  350. next_q1 = cubic_approx_control(
  351. i / (n - 1), next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3]
  352. )
  353. spline.append(next_q1)
  354. q2 = (q1 + next_q1) * 0.5
  355. else:
  356. q2 = c3
  357. # End-point deltas
  358. d0 = d1
  359. d1 = q2 - c3
  360. if abs(d1) > tolerance or not cubic_farthest_fit_inside(
  361. d0,
  362. q0 + (q1 - q0) * (2 / 3) - c1,
  363. q2 + (q1 - q2) * (2 / 3) - c2,
  364. d1,
  365. tolerance,
  366. ):
  367. return None
  368. spline.append(cubic[3])
  369. return spline
  370. @cython.locals(max_err=cython.double)
  371. @cython.locals(n=cython.int)
  372. @cython.locals(all_quadratic=cython.int)
  373. def curve_to_quadratic(curve, max_err, all_quadratic=True):
  374. """Approximate a cubic Bezier curve with a spline of n quadratics.
  375. Args:
  376. cubic (sequence): Four 2D tuples representing control points of
  377. the cubic Bezier curve.
  378. max_err (double): Permitted deviation from the original curve.
  379. all_quadratic (bool): If True (default) returned value is a
  380. quadratic spline. If False, it's either a single quadratic
  381. curve or a single cubic curve.
  382. Returns:
  383. If all_quadratic is True: A list of 2D tuples, representing
  384. control points of the quadratic spline if it fits within the
  385. given tolerance, or ``None`` if no suitable spline could be
  386. calculated.
  387. If all_quadratic is False: Either a quadratic curve (if length
  388. of output is 3), or a cubic curve (if length of output is 4).
  389. """
  390. curve = [complex(*p) for p in curve]
  391. for n in range(1, MAX_N + 1):
  392. spline = cubic_approx_spline(curve, n, max_err, all_quadratic)
  393. if spline is not None:
  394. # done. go home
  395. return [(s.real, s.imag) for s in spline]
  396. raise ApproxNotFoundError(curve)
  397. @cython.locals(l=cython.int, last_i=cython.int, i=cython.int)
  398. @cython.locals(all_quadratic=cython.int)
  399. def curves_to_quadratic(curves, max_errors, all_quadratic=True):
  400. """Return quadratic Bezier splines approximating the input cubic Beziers.
  401. Args:
  402. curves: A sequence of *n* curves, each curve being a sequence of four
  403. 2D tuples.
  404. max_errors: A sequence of *n* floats representing the maximum permissible
  405. deviation from each of the cubic Bezier curves.
  406. all_quadratic (bool): If True (default) returned values are a
  407. quadratic spline. If False, they are either a single quadratic
  408. curve or a single cubic curve.
  409. Example::
  410. >>> curves_to_quadratic( [
  411. ... [ (50,50), (100,100), (150,100), (200,50) ],
  412. ... [ (75,50), (120,100), (150,75), (200,60) ]
  413. ... ], [1,1] )
  414. [[(50.0, 50.0), (75.0, 75.0), (125.0, 91.66666666666666), (175.0, 75.0), (200.0, 50.0)], [(75.0, 50.0), (97.5, 75.0), (135.41666666666666, 82.08333333333333), (175.0, 67.5), (200.0, 60.0)]]
  415. The returned splines have "implied oncurve points" suitable for use in
  416. TrueType ``glif`` outlines - i.e. in the first spline returned above,
  417. the first quadratic segment runs from (50,50) to
  418. ( (75 + 125)/2 , (120 + 91.666..)/2 ) = (100, 83.333...).
  419. Returns:
  420. If all_quadratic is True, a list of splines, each spline being a list
  421. of 2D tuples.
  422. If all_quadratic is False, a list of curves, each curve being a quadratic
  423. (length 3), or cubic (length 4).
  424. Raises:
  425. fontTools.cu2qu.Errors.ApproxNotFoundError: if no suitable approximation
  426. can be found for all curves with the given parameters.
  427. """
  428. curves = [[complex(*p) for p in curve] for curve in curves]
  429. assert len(max_errors) == len(curves)
  430. l = len(curves)
  431. splines = [None] * l
  432. last_i = i = 0
  433. n = 1
  434. while True:
  435. spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic)
  436. if spline is None:
  437. if n == MAX_N:
  438. break
  439. n += 1
  440. last_i = i
  441. continue
  442. splines[i] = spline
  443. i = (i + 1) % l
  444. if i == last_i:
  445. # done. go home
  446. return [[(s.real, s.imag) for s in spline] for spline in splines]
  447. raise ApproxNotFoundError(curves)