fontcrunch.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. # Copyright 2014 Google Inc. All rights reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # Contributor: Raph Levien
  16. import os
  17. import sys
  18. sys.path.append(
  19. os.path.join(os.path.dirname(__file__), os.pardir, 'spiro', 'curves'))
  20. from fontTools import ttLib
  21. from fontTools.ttLib.tables import _g_l_y_f
  22. import fromcubic
  23. import tocubic
  24. import pcorn
  25. import math
  26. import md5
  27. def lerppt(t, p0, p1):
  28. return (p0[0] + t * (p1[0] - p0[0]), p0[1] + t * (p1[1] - p0[1]))
  29. def glyph_to_bzs(g):
  30. bzs = []
  31. for i in range(g.numberOfContours):
  32. beg = 0 if i == 0 else g.endPtsOfContours[i - 1] + 1
  33. end = g.endPtsOfContours[i] + 1
  34. n = end - beg
  35. pts = g.coordinates[beg:end]
  36. flags = g.flags[beg:end]
  37. bz = []
  38. for j in range(n):
  39. x1, y1 = pts[(j+1) % n]
  40. if flags[j] and flags[(j+1) % n]:
  41. bz.append((pts[j], (x1, y1)))
  42. elif not flags[j]:
  43. if flags[j - 1]:
  44. x0, y0 = pts[j - 1]
  45. else:
  46. x0, y0 = lerppt(0.5, pts[j - 1], pts[j])
  47. if not flags[(j+1) % n]:
  48. x1, y1 = lerppt(0.5, (x1, y1), pts[j])
  49. if pts[j] == (x0, y0) or pts[j] == (x1, y1):
  50. # degenerate quad, treat as line
  51. bz.append(((x0, y0), (x1, y1)))
  52. else:
  53. bz.append(((x0, y0), pts[j], (x1, y1)))
  54. bzs.append(bz)
  55. return bzs
  56. # convert all quadratics to cubics
  57. def raise_to_cubic(bzs):
  58. result = []
  59. for sp in bzs:
  60. r = []
  61. for bz in sp:
  62. if len(bz) == 3:
  63. r.append((bz[0], lerppt(2./3, bz[0], bz[1]), lerppt(2./3, bz[2], bz[1]), bz[2]))
  64. else:
  65. r.append(bz)
  66. result.append(r)
  67. return result
  68. def plot(bzs):
  69. tocubic.plot_prolog()
  70. print '/ss 1.5 def'
  71. print '/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def'
  72. fromcubic.plot_bzs(bzs, (100, 100), 0.25, fancy = True)
  73. print 'showpage'
  74. def getbreaks(curve):
  75. extrema = curve.find_extrema()
  76. extrema.extend(curve.find_breaks())
  77. extrema.append(0)
  78. extrema.append(curve.arclen)
  79. extrema.sort()
  80. result = []
  81. for i in range(len(extrema)):
  82. if i == 0 or extrema[i] > extrema[i-1] + 0.1:
  83. result.append(extrema[i])
  84. print result
  85. return result
  86. class Pt:
  87. def __init__(self, curve, s):
  88. self.s = s
  89. x, y = curve.xy(s)
  90. self.xy = (round(x), round(y))
  91. self.th = curve.th(s)
  92. class MiniState:
  93. def __init__(self, score, sp):
  94. self.score = score
  95. self.sp = sp
  96. def combine(self, score, bz):
  97. newscore = self.score + score + penalty * (len(bz) - 1)
  98. if len(bz) == 3 and len(self.sp):
  99. lastbz = self.sp[-1]
  100. if len(lastbz) == 3:
  101. if lerppt(0.5, lastbz[1], bz[1]) == bz[0]:
  102. newscore -= penalty
  103. return MiniState(newscore, self.sp + [bz])
  104. class State:
  105. def __init__(self, base):
  106. self.base = base # a MiniState
  107. self.map = {}
  108. penalty = 0.05
  109. def measure_bz(curve, s0, s1, bz):
  110. bz_arclen = tocubic.bz_arclength_rk4(bz)
  111. if bz_arclen == 0: return 1e9
  112. arclen_scale = (s1 - s0) / bz_arclen
  113. def th_fn(s):
  114. return curve.th(s0 + arclen_scale * s, s == 0)
  115. return tocubic.measure_bz_rk4(bz, bz_arclen, th_fn)
  116. def measure_line(curve, st, pt0, pt1):
  117. bz = (pt0.xy, pt1.xy)
  118. return st.combine(measure_bz(curve, pt0.s, pt1.s, bz), bz)
  119. def intersect(xy0, th0, xy1, th1):
  120. x0, y0 = xy0
  121. x1, y1 = xy1
  122. dx0 = math.cos(th0)
  123. dy0 = math.sin(th0)
  124. dx1 = math.cos(th1)
  125. dy1 = math.sin(th1)
  126. det = dx0 * dy1 - dy0 * dx1
  127. if abs(det) < 1e-6: return None
  128. det = 1 / det
  129. a = y0 * dx0 - x0 * dy0
  130. b = y1 * dx1 - x1 * dy1
  131. x = (a * dx1 - b * dx0) * det
  132. y = (a * dy1 - b * dy0) * det
  133. return (x, y)
  134. def measure_quad(curve, st, pt0, pt1):
  135. xy = intersect(pt0.xy, pt0.th, pt1.xy, pt1.th)
  136. if xy is None: return None
  137. x, y = xy
  138. x = round(x)
  139. y = round(y)
  140. bz = (pt0.xy, (x, y), pt1.xy)
  141. return st.combine(measure_bz(curve, pt0.s, pt1.s, bz), bz)
  142. class Thcache:
  143. mult = 1
  144. def __init__(self, curve, s0, s1):
  145. self.s0 = s0
  146. self.s1 = s1
  147. self.ths1 = curve.th(s1, False)
  148. self.vals = []
  149. scale = 1.0 / self.mult
  150. for i in range(int(self.mult * (s1 - s0)) + 2):
  151. s = min(s1, s0 + i * scale)
  152. self.vals.append(curve.th(s, i == 0))
  153. def th(self, s, ds):
  154. if s > self.s1: return self.ths1
  155. s = self.mult * (s - self.s0)
  156. bucket = int(s)
  157. v0 = self.vals[bucket]
  158. v1 = self.vals[bucket + 1]
  159. return v0 + (s - bucket) * (v1 - v0)
  160. # produce an optimized sequence of quadratics from s0 to s1 of the curve
  161. def optimize_run(curve, s0, s1):
  162. print s0, s1
  163. n = int(round(1 * (s1 - s0)))
  164. pts = []
  165. for i in range(n + 1):
  166. pts.append(Pt(curve, s0 + (s1 - s0) * i / n))
  167. cache = Thcache(curve, s0, s1)
  168. states = [MiniState(0, [])]
  169. newst = measure_line(cache, states[0], pts[0], pts[n])
  170. bestst = newst
  171. newst = measure_quad(cache, states[0], pts[0], pts[n])
  172. if newst and newst.score < bestst.score:
  173. bestst = newst
  174. if bestst.score <= 3 * penalty:
  175. return bestst.sp
  176. # Quick scan for two-quad sections
  177. # Note, could do line+quad and quad+line too, but less likely to win
  178. for i in range(1, n):
  179. st1 = measure_quad(cache, states[0], pts[0], pts[i])
  180. if st1:
  181. st2 = measure_quad(cache, st1, pts[i], pts[n])
  182. if st2 and st2.score < bestst.score:
  183. bestst = st2
  184. if bestst.score <= 4 * penalty:
  185. return bestst.sp
  186. for i in range(1, n + 1):
  187. best = 1e9
  188. badcount = 0
  189. for j in range(i - 1, -1, -1):
  190. newst = measure_line(cache, states[j], pts[j], pts[i])
  191. if newst and newst.score < best:
  192. best, bestst = newst.score, newst
  193. newst = measure_quad(cache, states[j], pts[j], pts[i])
  194. if newst and newst.score < best:
  195. best, bestst = newst.score, newst
  196. if newst is None or newst.score - states[j].score > 10 * penalty:
  197. badcount += 1
  198. if badcount == 20:
  199. break
  200. else:
  201. badcount = 0
  202. states.append(bestst)
  203. return states[n].sp
  204. def optimize(bzs):
  205. result = []
  206. for sp in fromcubic.bzs_to_pcorn(bzs):
  207. r = []
  208. curve = pcorn.Curve(sp)
  209. breaks = getbreaks(curve)
  210. for i in range(len(breaks) - 1):
  211. r.extend(optimize_run(curve, breaks[i], breaks[i + 1]))
  212. result.append(r)
  213. return result
  214. def plot_tt_raw(bzs, fancy = True):
  215. x0 = 100
  216. y0 = 100
  217. scale = 0.25
  218. fromcubic.plot_bzs(raise_to_cubic(bzs), (x0, y0), scale)
  219. if fancy:
  220. for sp in bzs:
  221. for i in range(len(sp)):
  222. lastbz = sp[i - 1]
  223. bz = sp[i]
  224. if len(bz) != 3 or len(lastbz) != 3 or lerppt(0.5, lastbz[1], bz[1]) != bz[0]:
  225. x, y = bz[0]
  226. print 'gsave %f %f translate circle fill grestore' % (x * scale + x0, y * scale + y0)
  227. if len(bz) == 3:
  228. x, y = bz[1]
  229. print 'gsave %f %f translate circle stroke grestore' % (x * scale + x0, y * scale + y0)
  230. def plot_tt(bzs, orig = None, style = 'redcyan'):
  231. tocubic.plot_prolog()
  232. print '/ss 2 def'
  233. print '/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def'
  234. if style == 'redcyan':
  235. print 'true setoverprint true setoverprintmode'
  236. x0 = 100
  237. y0 = 100
  238. scale = 0.25
  239. if orig:
  240. print '0 1 1 0 setcmykcolor'
  241. fancy = (style == 'redcyan')
  242. plot_tt_raw(orig, fancy)
  243. if style == 'redcyan':
  244. print '1 0 0 0 setcmykcolor'
  245. elif style == 'redblack':
  246. print '0 0 0 1 setcmykcolor'
  247. plot_tt_raw(bzs)
  248. print 'showpage'
  249. def segment_sp(sp):
  250. bks = set()
  251. # direction changes
  252. xsg = 0
  253. ysg = 0
  254. for i in range(2 * len(sp)):
  255. imod = i % len(sp)
  256. xsg1 = sp[imod][-1][0] - sp[imod][0][0]
  257. ysg1 = sp[imod][-1][1] - sp[imod][0][1]
  258. if xsg * xsg1 < 0 or ysg * ysg1 < 0:
  259. bks.add(imod)
  260. xsg = xsg1
  261. ysg = ysg1
  262. else:
  263. if xsg == 0: xsg = xsg1
  264. if ysg == 0: ysg = ysg1
  265. # angle breaks
  266. for i in range(len(sp)):
  267. dx0 = sp[i-1][-1][0] - sp[i-1][-2][0]
  268. dy0 = sp[i-1][-1][1] - sp[i-1][-2][1]
  269. dx1 = sp[i][1][0] - sp[i][0][0]
  270. dy1 = sp[i][1][1] - sp[i][0][1]
  271. bend = dx1 * dy0 - dx0 * dy1
  272. if (dx0 == 0 and dy0 == 0) or (dx1 == 0 and dy1 == 0):
  273. bks.add(i)
  274. else:
  275. bend = bend / (math.hypot(dx0, dy0) * math.hypot(dx1, dy1))
  276. # for small angles, bend is in units of radians
  277. if abs(bend) > 0.02:
  278. bks.add(i)
  279. return sorted(bks)
  280. def seg_to_string(sp, bk0, bk1):
  281. if bk1 < bk0:
  282. bk1 += len(sp)
  283. res = []
  284. for i in range(bk0, bk1):
  285. bz = sp[i % len(sp)]
  286. if len(bz) == 2:
  287. # just represent lines as quads
  288. bz = (bz[0], lerppt(0.5, bz[0], bz[1]), bz[1])
  289. res.append(' '.join(['%g' % z for xy in bz for z in xy]) + '\n')
  290. return ''.join(res)
  291. USE_SUBDIRS = True
  292. # get filename, ensuring directory exists
  293. def seg_fn(segstr):
  294. fn = md5.new(segstr).hexdigest()[:16]
  295. if USE_SUBDIRS:
  296. dirname = fn[:2]
  297. if not os.path.exists(dirname):
  298. os.mkdir(dirname)
  299. fn = dirname + '/' + fn[2:]
  300. fn += '.bez'
  301. return fn
  302. def gen_segs(glyph):
  303. bzs = glyph_to_bzs(glyph)
  304. for sp in bzs:
  305. bks = segment_sp(sp)
  306. for i in range(len(bks)):
  307. bk0, bk1 = bks[i], bks[(i + 1) % len(bks)]
  308. if bk1 != (bk0 + 1) % len(sp) or len(sp[bk0]) != 2:
  309. segstr = seg_to_string(sp, bk0, bk1)
  310. fn = seg_fn(segstr)
  311. file(fn, 'w').write(segstr)
  312. def generate(fn):
  313. f = ttLib.TTFont(fn)
  314. glyf = f['glyf']
  315. for name in glyf.keys():
  316. g = glyf[name]
  317. print 'generating', name
  318. gen_segs(g)
  319. def read_bzs(fn):
  320. result = []
  321. for l in file(fn):
  322. z = [float(z) for z in l.split()]
  323. bz = ((z[0], z[1]), (z[2], z[3]), (z[4], z[5]))
  324. if bz[1] == lerppt(0.5, bz[0], bz[2]):
  325. bz = (bz[0], bz[2])
  326. result.append(bz)
  327. return result
  328. def pt_to_int(pt):
  329. # todo: should investigate non-int points
  330. return (int(round(pt[0])), int(round(pt[1])))
  331. def bzs_to_glyph(bzs, glyph):
  332. coordinates = []
  333. flags = []
  334. endPtsOfContours = []
  335. for sp in bzs:
  336. for i in range(len(sp)):
  337. lastbz = sp[i - 1]
  338. bz = sp[i]
  339. if len(bz) != 3 or len(lastbz) != 3 or lerppt(0.5, lastbz[1], bz[1]) != bz[0]:
  340. coordinates.append(pt_to_int(bz[0]))
  341. flags.append(1)
  342. if len(bz) == 3:
  343. coordinates.append(pt_to_int(bz[1]))
  344. flags.append(0)
  345. endPtsOfContours.append(len(coordinates) - 1)
  346. glyph.coordinates = _g_l_y_f.GlyphCoordinates(coordinates)
  347. glyph.flags = flags
  348. glyph.endPtsOfContours = endPtsOfContours
  349. def repack_glyph(glyph):
  350. bzs = glyph_to_bzs(glyph)
  351. newbzs = []
  352. for sp in bzs:
  353. bks = segment_sp(sp)
  354. newsp = []
  355. for i in range(len(bks)):
  356. bk0, bk1 = bks[i], bks[(i + 1) % len(bks)]
  357. if bk1 != (bk0 + 1) % len(sp) or len(sp[bk0]) != 2:
  358. segstr = seg_to_string(sp, bk0, bk1)
  359. fn = seg_fn(segstr) + 'opt'
  360. newsp.extend(read_bzs(fn))
  361. else:
  362. newsp.append(sp[bk0])
  363. newbzs.append(newsp)
  364. bzs_to_glyph(newbzs, glyph)
  365. plot_tt(newbzs, bzs, style = 'redblack')
  366. def repack(fn, newfn):
  367. f = ttLib.TTFont(fn)
  368. glyf = f['glyf']
  369. for name in glyf.keys():
  370. g = glyf[name]
  371. if not g.isComposite():
  372. repack_glyph(g)
  373. if newfn:
  374. f.save(newfn)
  375. def main(argv):
  376. if argv[1] == 'gen':
  377. generate(sys.argv[2])
  378. elif argv[1] == 'pack':
  379. repack(sys.argv[2], sys.argv[3] if len(argv) >= 3 else None)
  380. main(sys.argv)