diff options
Diffstat (limited to 'third_party/fontcrunch/fontcrunch.py')
-rw-r--r-- | third_party/fontcrunch/fontcrunch.py | 412 |
1 files changed, 412 insertions, 0 deletions
diff --git a/third_party/fontcrunch/fontcrunch.py b/third_party/fontcrunch/fontcrunch.py new file mode 100644 index 0000000..d129452 --- /dev/null +++ b/third_party/fontcrunch/fontcrunch.py @@ -0,0 +1,412 @@ +# 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 + +from fontTools import ttLib +from fontTools.ttLib.tables import _g_l_y_f +import fromcubic +import tocubic +import pcorn +import math +import md5 + +import sys +import os + +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 += '.bz' + 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, g in glyf.glyphs.iteritems(): + 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, g in glyf.glyphs.iteritems(): + 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) |