123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- # Copyright 2014 Google Inc. All rights reserved.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- #
- # Contributor: Raph Levien
- import os
- import sys
- sys.path.append(
- os.path.join(os.path.dirname(__file__), os.pardir, 'spiro', 'curves'))
- from fontTools import ttLib
- from fontTools.ttLib.tables import _g_l_y_f
- import fromcubic
- import tocubic
- import pcorn
- import math
- import md5
- def lerppt(t, p0, p1):
- return (p0[0] + t * (p1[0] - p0[0]), p0[1] + t * (p1[1] - p0[1]))
- def glyph_to_bzs(g):
- bzs = []
- for i in range(g.numberOfContours):
- beg = 0 if i == 0 else g.endPtsOfContours[i - 1] + 1
- end = g.endPtsOfContours[i] + 1
- n = end - beg
- pts = g.coordinates[beg:end]
- flags = g.flags[beg:end]
- bz = []
- for j in range(n):
- x1, y1 = pts[(j+1) % n]
- if flags[j] and flags[(j+1) % n]:
- bz.append((pts[j], (x1, y1)))
- elif not flags[j]:
- if flags[j - 1]:
- x0, y0 = pts[j - 1]
- else:
- x0, y0 = lerppt(0.5, pts[j - 1], pts[j])
- if not flags[(j+1) % n]:
- x1, y1 = lerppt(0.5, (x1, y1), pts[j])
- if pts[j] == (x0, y0) or pts[j] == (x1, y1):
- # degenerate quad, treat as line
- bz.append(((x0, y0), (x1, y1)))
- else:
- bz.append(((x0, y0), pts[j], (x1, y1)))
- bzs.append(bz)
- return bzs
- # convert all quadratics to cubics
- def raise_to_cubic(bzs):
- result = []
- for sp in bzs:
- r = []
- for bz in sp:
- if len(bz) == 3:
- r.append((bz[0], lerppt(2./3, bz[0], bz[1]), lerppt(2./3, bz[2], bz[1]), bz[2]))
- else:
- r.append(bz)
- result.append(r)
- return result
- def plot(bzs):
- tocubic.plot_prolog()
- print '/ss 1.5 def'
- print '/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def'
- fromcubic.plot_bzs(bzs, (100, 100), 0.25, fancy = True)
- print 'showpage'
- def getbreaks(curve):
- extrema = curve.find_extrema()
- extrema.extend(curve.find_breaks())
- extrema.append(0)
- extrema.append(curve.arclen)
- extrema.sort()
- result = []
- for i in range(len(extrema)):
- if i == 0 or extrema[i] > extrema[i-1] + 0.1:
- result.append(extrema[i])
- print result
- return result
- class Pt:
- def __init__(self, curve, s):
- self.s = s
- x, y = curve.xy(s)
- self.xy = (round(x), round(y))
- self.th = curve.th(s)
- class MiniState:
- def __init__(self, score, sp):
- self.score = score
- self.sp = sp
- def combine(self, score, bz):
- newscore = self.score + score + penalty * (len(bz) - 1)
- if len(bz) == 3 and len(self.sp):
- lastbz = self.sp[-1]
- if len(lastbz) == 3:
- if lerppt(0.5, lastbz[1], bz[1]) == bz[0]:
- newscore -= penalty
- return MiniState(newscore, self.sp + [bz])
- class State:
- def __init__(self, base):
- self.base = base # a MiniState
- self.map = {}
- penalty = 0.05
- def measure_bz(curve, s0, s1, bz):
- bz_arclen = tocubic.bz_arclength_rk4(bz)
- if bz_arclen == 0: return 1e9
- arclen_scale = (s1 - s0) / bz_arclen
- def th_fn(s):
- return curve.th(s0 + arclen_scale * s, s == 0)
- return tocubic.measure_bz_rk4(bz, bz_arclen, th_fn)
- def measure_line(curve, st, pt0, pt1):
- bz = (pt0.xy, pt1.xy)
- return st.combine(measure_bz(curve, pt0.s, pt1.s, bz), bz)
- def intersect(xy0, th0, xy1, th1):
- x0, y0 = xy0
- x1, y1 = xy1
- dx0 = math.cos(th0)
- dy0 = math.sin(th0)
- dx1 = math.cos(th1)
- dy1 = math.sin(th1)
- det = dx0 * dy1 - dy0 * dx1
- if abs(det) < 1e-6: return None
- det = 1 / det
- a = y0 * dx0 - x0 * dy0
- b = y1 * dx1 - x1 * dy1
- x = (a * dx1 - b * dx0) * det
- y = (a * dy1 - b * dy0) * det
- return (x, y)
- def measure_quad(curve, st, pt0, pt1):
- xy = intersect(pt0.xy, pt0.th, pt1.xy, pt1.th)
- if xy is None: return None
- x, y = xy
- x = round(x)
- y = round(y)
- bz = (pt0.xy, (x, y), pt1.xy)
- return st.combine(measure_bz(curve, pt0.s, pt1.s, bz), bz)
- class Thcache:
- mult = 1
- def __init__(self, curve, s0, s1):
- self.s0 = s0
- self.s1 = s1
- self.ths1 = curve.th(s1, False)
- self.vals = []
- scale = 1.0 / self.mult
- for i in range(int(self.mult * (s1 - s0)) + 2):
- s = min(s1, s0 + i * scale)
- self.vals.append(curve.th(s, i == 0))
- def th(self, s, ds):
- if s > self.s1: return self.ths1
- s = self.mult * (s - self.s0)
- bucket = int(s)
- v0 = self.vals[bucket]
- v1 = self.vals[bucket + 1]
- return v0 + (s - bucket) * (v1 - v0)
- # produce an optimized sequence of quadratics from s0 to s1 of the curve
- def optimize_run(curve, s0, s1):
- print s0, s1
- n = int(round(1 * (s1 - s0)))
- pts = []
- for i in range(n + 1):
- pts.append(Pt(curve, s0 + (s1 - s0) * i / n))
- cache = Thcache(curve, s0, s1)
- states = [MiniState(0, [])]
- newst = measure_line(cache, states[0], pts[0], pts[n])
- bestst = newst
- newst = measure_quad(cache, states[0], pts[0], pts[n])
- if newst and newst.score < bestst.score:
- bestst = newst
- if bestst.score <= 3 * penalty:
- return bestst.sp
- # Quick scan for two-quad sections
- # Note, could do line+quad and quad+line too, but less likely to win
- for i in range(1, n):
- st1 = measure_quad(cache, states[0], pts[0], pts[i])
- if st1:
- st2 = measure_quad(cache, st1, pts[i], pts[n])
- if st2 and st2.score < bestst.score:
- bestst = st2
- if bestst.score <= 4 * penalty:
- return bestst.sp
- for i in range(1, n + 1):
- best = 1e9
- badcount = 0
- for j in range(i - 1, -1, -1):
- newst = measure_line(cache, states[j], pts[j], pts[i])
- if newst and newst.score < best:
- best, bestst = newst.score, newst
- newst = measure_quad(cache, states[j], pts[j], pts[i])
- if newst and newst.score < best:
- best, bestst = newst.score, newst
- if newst is None or newst.score - states[j].score > 10 * penalty:
- badcount += 1
- if badcount == 20:
- break
- else:
- badcount = 0
- states.append(bestst)
- return states[n].sp
- def optimize(bzs):
- result = []
- for sp in fromcubic.bzs_to_pcorn(bzs):
- r = []
- curve = pcorn.Curve(sp)
- breaks = getbreaks(curve)
- for i in range(len(breaks) - 1):
- r.extend(optimize_run(curve, breaks[i], breaks[i + 1]))
- result.append(r)
- return result
- def plot_tt_raw(bzs, fancy = True):
- x0 = 100
- y0 = 100
- scale = 0.25
- fromcubic.plot_bzs(raise_to_cubic(bzs), (x0, y0), scale)
- if fancy:
- for sp in bzs:
- for i in range(len(sp)):
- lastbz = sp[i - 1]
- bz = sp[i]
- if len(bz) != 3 or len(lastbz) != 3 or lerppt(0.5, lastbz[1], bz[1]) != bz[0]:
- x, y = bz[0]
- print 'gsave %f %f translate circle fill grestore' % (x * scale + x0, y * scale + y0)
- if len(bz) == 3:
- x, y = bz[1]
- print 'gsave %f %f translate circle stroke grestore' % (x * scale + x0, y * scale + y0)
- def plot_tt(bzs, orig = None, style = 'redcyan'):
- tocubic.plot_prolog()
- print '/ss 2 def'
- print '/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def'
- if style == 'redcyan':
- print 'true setoverprint true setoverprintmode'
- x0 = 100
- y0 = 100
- scale = 0.25
- if orig:
- print '0 1 1 0 setcmykcolor'
- fancy = (style == 'redcyan')
- plot_tt_raw(orig, fancy)
- if style == 'redcyan':
- print '1 0 0 0 setcmykcolor'
- elif style == 'redblack':
- print '0 0 0 1 setcmykcolor'
- plot_tt_raw(bzs)
- print 'showpage'
- def segment_sp(sp):
- bks = set()
- # direction changes
- xsg = 0
- ysg = 0
- for i in range(2 * len(sp)):
- imod = i % len(sp)
- xsg1 = sp[imod][-1][0] - sp[imod][0][0]
- ysg1 = sp[imod][-1][1] - sp[imod][0][1]
- if xsg * xsg1 < 0 or ysg * ysg1 < 0:
- bks.add(imod)
- xsg = xsg1
- ysg = ysg1
- else:
- if xsg == 0: xsg = xsg1
- if ysg == 0: ysg = ysg1
- # angle breaks
- for i in range(len(sp)):
- dx0 = sp[i-1][-1][0] - sp[i-1][-2][0]
- dy0 = sp[i-1][-1][1] - sp[i-1][-2][1]
- dx1 = sp[i][1][0] - sp[i][0][0]
- dy1 = sp[i][1][1] - sp[i][0][1]
- bend = dx1 * dy0 - dx0 * dy1
- if (dx0 == 0 and dy0 == 0) or (dx1 == 0 and dy1 == 0):
- bks.add(i)
- else:
- bend = bend / (math.hypot(dx0, dy0) * math.hypot(dx1, dy1))
- # for small angles, bend is in units of radians
- if abs(bend) > 0.02:
- bks.add(i)
- return sorted(bks)
- def seg_to_string(sp, bk0, bk1):
- if bk1 < bk0:
- bk1 += len(sp)
- res = []
- for i in range(bk0, bk1):
- bz = sp[i % len(sp)]
- if len(bz) == 2:
- # just represent lines as quads
- bz = (bz[0], lerppt(0.5, bz[0], bz[1]), bz[1])
- res.append(' '.join(['%g' % z for xy in bz for z in xy]) + '\n')
- return ''.join(res)
- USE_SUBDIRS = True
- # get filename, ensuring directory exists
- def seg_fn(segstr):
- fn = md5.new(segstr).hexdigest()[:16]
- if USE_SUBDIRS:
- dirname = fn[:2]
- if not os.path.exists(dirname):
- os.mkdir(dirname)
- fn = dirname + '/' + fn[2:]
- fn += '.bez'
- return fn
- def gen_segs(glyph):
- bzs = glyph_to_bzs(glyph)
- for sp in bzs:
- bks = segment_sp(sp)
- for i in range(len(bks)):
- bk0, bk1 = bks[i], bks[(i + 1) % len(bks)]
- if bk1 != (bk0 + 1) % len(sp) or len(sp[bk0]) != 2:
- segstr = seg_to_string(sp, bk0, bk1)
- fn = seg_fn(segstr)
- file(fn, 'w').write(segstr)
- def generate(fn):
- f = ttLib.TTFont(fn)
- glyf = f['glyf']
- for name in glyf.keys():
- g = glyf[name]
- print 'generating', name
- gen_segs(g)
- def read_bzs(fn):
- result = []
- for l in file(fn):
- z = [float(z) for z in l.split()]
- bz = ((z[0], z[1]), (z[2], z[3]), (z[4], z[5]))
- if bz[1] == lerppt(0.5, bz[0], bz[2]):
- bz = (bz[0], bz[2])
- result.append(bz)
- return result
- def pt_to_int(pt):
- # todo: should investigate non-int points
- return (int(round(pt[0])), int(round(pt[1])))
- def bzs_to_glyph(bzs, glyph):
- coordinates = []
- flags = []
- endPtsOfContours = []
- for sp in bzs:
- for i in range(len(sp)):
- lastbz = sp[i - 1]
- bz = sp[i]
- if len(bz) != 3 or len(lastbz) != 3 or lerppt(0.5, lastbz[1], bz[1]) != bz[0]:
- coordinates.append(pt_to_int(bz[0]))
- flags.append(1)
- if len(bz) == 3:
- coordinates.append(pt_to_int(bz[1]))
- flags.append(0)
- endPtsOfContours.append(len(coordinates) - 1)
- glyph.coordinates = _g_l_y_f.GlyphCoordinates(coordinates)
- glyph.flags = flags
- glyph.endPtsOfContours = endPtsOfContours
- def repack_glyph(glyph):
- bzs = glyph_to_bzs(glyph)
- newbzs = []
- for sp in bzs:
- bks = segment_sp(sp)
- newsp = []
- for i in range(len(bks)):
- bk0, bk1 = bks[i], bks[(i + 1) % len(bks)]
- if bk1 != (bk0 + 1) % len(sp) or len(sp[bk0]) != 2:
- segstr = seg_to_string(sp, bk0, bk1)
- fn = seg_fn(segstr) + 'opt'
- newsp.extend(read_bzs(fn))
- else:
- newsp.append(sp[bk0])
- newbzs.append(newsp)
- bzs_to_glyph(newbzs, glyph)
- plot_tt(newbzs, bzs, style = 'redblack')
- def repack(fn, newfn):
- f = ttLib.TTFont(fn)
- glyf = f['glyf']
- for name in glyf.keys():
- g = glyf[name]
- if not g.isComposite():
- repack_glyph(g)
- if newfn:
- f.save(newfn)
- def main(argv):
- if argv[1] == 'gen':
- generate(sys.argv[2])
- elif argv[1] == 'pack':
- repack(sys.argv[2], sys.argv[3] if len(argv) >= 3 else None)
- main(sys.argv)
|