1*e1fe3e4aSElliott Hughes# -*- coding: utf-8 -*- 2*e1fe3e4aSElliott Hughes 3*e1fe3e4aSElliott Hughes"""T2CharString glyph width optimizer. 4*e1fe3e4aSElliott Hughes 5*e1fe3e4aSElliott HughesCFF glyphs whose width equals the CFF Private dictionary's ``defaultWidthX`` 6*e1fe3e4aSElliott Hughesvalue do not need to specify their width in their charstring, saving bytes. 7*e1fe3e4aSElliott HughesThis module determines the optimum ``defaultWidthX`` and ``nominalWidthX`` 8*e1fe3e4aSElliott Hughesvalues for a font, when provided with a list of glyph widths.""" 9*e1fe3e4aSElliott Hughes 10*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTFont 11*e1fe3e4aSElliott Hughesfrom collections import defaultdict 12*e1fe3e4aSElliott Hughesfrom operator import add 13*e1fe3e4aSElliott Hughesfrom functools import reduce 14*e1fe3e4aSElliott Hughes 15*e1fe3e4aSElliott Hughes 16*e1fe3e4aSElliott Hughesclass missingdict(dict): 17*e1fe3e4aSElliott Hughes def __init__(self, missing_func): 18*e1fe3e4aSElliott Hughes self.missing_func = missing_func 19*e1fe3e4aSElliott Hughes 20*e1fe3e4aSElliott Hughes def __missing__(self, v): 21*e1fe3e4aSElliott Hughes return self.missing_func(v) 22*e1fe3e4aSElliott Hughes 23*e1fe3e4aSElliott Hughes 24*e1fe3e4aSElliott Hughesdef cumSum(f, op=add, start=0, decreasing=False): 25*e1fe3e4aSElliott Hughes keys = sorted(f.keys()) 26*e1fe3e4aSElliott Hughes minx, maxx = keys[0], keys[-1] 27*e1fe3e4aSElliott Hughes 28*e1fe3e4aSElliott Hughes total = reduce(op, f.values(), start) 29*e1fe3e4aSElliott Hughes 30*e1fe3e4aSElliott Hughes if decreasing: 31*e1fe3e4aSElliott Hughes missing = lambda x: start if x > maxx else total 32*e1fe3e4aSElliott Hughes domain = range(maxx, minx - 1, -1) 33*e1fe3e4aSElliott Hughes else: 34*e1fe3e4aSElliott Hughes missing = lambda x: start if x < minx else total 35*e1fe3e4aSElliott Hughes domain = range(minx, maxx + 1) 36*e1fe3e4aSElliott Hughes 37*e1fe3e4aSElliott Hughes out = missingdict(missing) 38*e1fe3e4aSElliott Hughes 39*e1fe3e4aSElliott Hughes v = start 40*e1fe3e4aSElliott Hughes for x in domain: 41*e1fe3e4aSElliott Hughes v = op(v, f[x]) 42*e1fe3e4aSElliott Hughes out[x] = v 43*e1fe3e4aSElliott Hughes 44*e1fe3e4aSElliott Hughes return out 45*e1fe3e4aSElliott Hughes 46*e1fe3e4aSElliott Hughes 47*e1fe3e4aSElliott Hughesdef byteCost(widths, default, nominal): 48*e1fe3e4aSElliott Hughes if not hasattr(widths, "items"): 49*e1fe3e4aSElliott Hughes d = defaultdict(int) 50*e1fe3e4aSElliott Hughes for w in widths: 51*e1fe3e4aSElliott Hughes d[w] += 1 52*e1fe3e4aSElliott Hughes widths = d 53*e1fe3e4aSElliott Hughes 54*e1fe3e4aSElliott Hughes cost = 0 55*e1fe3e4aSElliott Hughes for w, freq in widths.items(): 56*e1fe3e4aSElliott Hughes if w == default: 57*e1fe3e4aSElliott Hughes continue 58*e1fe3e4aSElliott Hughes diff = abs(w - nominal) 59*e1fe3e4aSElliott Hughes if diff <= 107: 60*e1fe3e4aSElliott Hughes cost += freq 61*e1fe3e4aSElliott Hughes elif diff <= 1131: 62*e1fe3e4aSElliott Hughes cost += freq * 2 63*e1fe3e4aSElliott Hughes else: 64*e1fe3e4aSElliott Hughes cost += freq * 5 65*e1fe3e4aSElliott Hughes return cost 66*e1fe3e4aSElliott Hughes 67*e1fe3e4aSElliott Hughes 68*e1fe3e4aSElliott Hughesdef optimizeWidthsBruteforce(widths): 69*e1fe3e4aSElliott Hughes """Bruteforce version. Veeeeeeeeeeeeeeeeery slow. Only works for smallests of fonts.""" 70*e1fe3e4aSElliott Hughes 71*e1fe3e4aSElliott Hughes d = defaultdict(int) 72*e1fe3e4aSElliott Hughes for w in widths: 73*e1fe3e4aSElliott Hughes d[w] += 1 74*e1fe3e4aSElliott Hughes 75*e1fe3e4aSElliott Hughes # Maximum number of bytes using default can possibly save 76*e1fe3e4aSElliott Hughes maxDefaultAdvantage = 5 * max(d.values()) 77*e1fe3e4aSElliott Hughes 78*e1fe3e4aSElliott Hughes minw, maxw = min(widths), max(widths) 79*e1fe3e4aSElliott Hughes domain = list(range(minw, maxw + 1)) 80*e1fe3e4aSElliott Hughes 81*e1fe3e4aSElliott Hughes bestCostWithoutDefault = min(byteCost(widths, None, nominal) for nominal in domain) 82*e1fe3e4aSElliott Hughes 83*e1fe3e4aSElliott Hughes bestCost = len(widths) * 5 + 1 84*e1fe3e4aSElliott Hughes for nominal in domain: 85*e1fe3e4aSElliott Hughes if byteCost(widths, None, nominal) > bestCost + maxDefaultAdvantage: 86*e1fe3e4aSElliott Hughes continue 87*e1fe3e4aSElliott Hughes for default in domain: 88*e1fe3e4aSElliott Hughes cost = byteCost(widths, default, nominal) 89*e1fe3e4aSElliott Hughes if cost < bestCost: 90*e1fe3e4aSElliott Hughes bestCost = cost 91*e1fe3e4aSElliott Hughes bestDefault = default 92*e1fe3e4aSElliott Hughes bestNominal = nominal 93*e1fe3e4aSElliott Hughes 94*e1fe3e4aSElliott Hughes return bestDefault, bestNominal 95*e1fe3e4aSElliott Hughes 96*e1fe3e4aSElliott Hughes 97*e1fe3e4aSElliott Hughesdef optimizeWidths(widths): 98*e1fe3e4aSElliott Hughes """Given a list of glyph widths, or dictionary mapping glyph width to number of 99*e1fe3e4aSElliott Hughes glyphs having that, returns a tuple of best CFF default and nominal glyph widths. 100*e1fe3e4aSElliott Hughes 101*e1fe3e4aSElliott Hughes This algorithm is linear in UPEM+numGlyphs.""" 102*e1fe3e4aSElliott Hughes 103*e1fe3e4aSElliott Hughes if not hasattr(widths, "items"): 104*e1fe3e4aSElliott Hughes d = defaultdict(int) 105*e1fe3e4aSElliott Hughes for w in widths: 106*e1fe3e4aSElliott Hughes d[w] += 1 107*e1fe3e4aSElliott Hughes widths = d 108*e1fe3e4aSElliott Hughes 109*e1fe3e4aSElliott Hughes keys = sorted(widths.keys()) 110*e1fe3e4aSElliott Hughes minw, maxw = keys[0], keys[-1] 111*e1fe3e4aSElliott Hughes domain = list(range(minw, maxw + 1)) 112*e1fe3e4aSElliott Hughes 113*e1fe3e4aSElliott Hughes # Cumulative sum/max forward/backward. 114*e1fe3e4aSElliott Hughes cumFrqU = cumSum(widths, op=add) 115*e1fe3e4aSElliott Hughes cumMaxU = cumSum(widths, op=max) 116*e1fe3e4aSElliott Hughes cumFrqD = cumSum(widths, op=add, decreasing=True) 117*e1fe3e4aSElliott Hughes cumMaxD = cumSum(widths, op=max, decreasing=True) 118*e1fe3e4aSElliott Hughes 119*e1fe3e4aSElliott Hughes # Cost per nominal choice, without default consideration. 120*e1fe3e4aSElliott Hughes nomnCostU = missingdict( 121*e1fe3e4aSElliott Hughes lambda x: cumFrqU[x] + cumFrqU[x - 108] + cumFrqU[x - 1132] * 3 122*e1fe3e4aSElliott Hughes ) 123*e1fe3e4aSElliott Hughes nomnCostD = missingdict( 124*e1fe3e4aSElliott Hughes lambda x: cumFrqD[x] + cumFrqD[x + 108] + cumFrqD[x + 1132] * 3 125*e1fe3e4aSElliott Hughes ) 126*e1fe3e4aSElliott Hughes nomnCost = missingdict(lambda x: nomnCostU[x] + nomnCostD[x] - widths[x]) 127*e1fe3e4aSElliott Hughes 128*e1fe3e4aSElliott Hughes # Cost-saving per nominal choice, by best default choice. 129*e1fe3e4aSElliott Hughes dfltCostU = missingdict( 130*e1fe3e4aSElliott Hughes lambda x: max(cumMaxU[x], cumMaxU[x - 108] * 2, cumMaxU[x - 1132] * 5) 131*e1fe3e4aSElliott Hughes ) 132*e1fe3e4aSElliott Hughes dfltCostD = missingdict( 133*e1fe3e4aSElliott Hughes lambda x: max(cumMaxD[x], cumMaxD[x + 108] * 2, cumMaxD[x + 1132] * 5) 134*e1fe3e4aSElliott Hughes ) 135*e1fe3e4aSElliott Hughes dfltCost = missingdict(lambda x: max(dfltCostU[x], dfltCostD[x])) 136*e1fe3e4aSElliott Hughes 137*e1fe3e4aSElliott Hughes # Combined cost per nominal choice. 138*e1fe3e4aSElliott Hughes bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[x]) 139*e1fe3e4aSElliott Hughes 140*e1fe3e4aSElliott Hughes # Best nominal. 141*e1fe3e4aSElliott Hughes nominal = min(domain, key=lambda x: bestCost[x]) 142*e1fe3e4aSElliott Hughes 143*e1fe3e4aSElliott Hughes # Work back the best default. 144*e1fe3e4aSElliott Hughes bestC = bestCost[nominal] 145*e1fe3e4aSElliott Hughes dfltC = nomnCost[nominal] - bestCost[nominal] 146*e1fe3e4aSElliott Hughes ends = [] 147*e1fe3e4aSElliott Hughes if dfltC == dfltCostU[nominal]: 148*e1fe3e4aSElliott Hughes starts = [nominal, nominal - 108, nominal - 1132] 149*e1fe3e4aSElliott Hughes for start in starts: 150*e1fe3e4aSElliott Hughes while cumMaxU[start] and cumMaxU[start] == cumMaxU[start - 1]: 151*e1fe3e4aSElliott Hughes start -= 1 152*e1fe3e4aSElliott Hughes ends.append(start) 153*e1fe3e4aSElliott Hughes else: 154*e1fe3e4aSElliott Hughes starts = [nominal, nominal + 108, nominal + 1132] 155*e1fe3e4aSElliott Hughes for start in starts: 156*e1fe3e4aSElliott Hughes while cumMaxD[start] and cumMaxD[start] == cumMaxD[start + 1]: 157*e1fe3e4aSElliott Hughes start += 1 158*e1fe3e4aSElliott Hughes ends.append(start) 159*e1fe3e4aSElliott Hughes default = min(ends, key=lambda default: byteCost(widths, default, nominal)) 160*e1fe3e4aSElliott Hughes 161*e1fe3e4aSElliott Hughes return default, nominal 162*e1fe3e4aSElliott Hughes 163*e1fe3e4aSElliott Hughes 164*e1fe3e4aSElliott Hughesdef main(args=None): 165*e1fe3e4aSElliott Hughes """Calculate optimum defaultWidthX/nominalWidthX values""" 166*e1fe3e4aSElliott Hughes 167*e1fe3e4aSElliott Hughes import argparse 168*e1fe3e4aSElliott Hughes 169*e1fe3e4aSElliott Hughes parser = argparse.ArgumentParser( 170*e1fe3e4aSElliott Hughes "fonttools cffLib.width", 171*e1fe3e4aSElliott Hughes description=main.__doc__, 172*e1fe3e4aSElliott Hughes ) 173*e1fe3e4aSElliott Hughes parser.add_argument( 174*e1fe3e4aSElliott Hughes "inputs", metavar="FILE", type=str, nargs="+", help="Input TTF files" 175*e1fe3e4aSElliott Hughes ) 176*e1fe3e4aSElliott Hughes parser.add_argument( 177*e1fe3e4aSElliott Hughes "-b", 178*e1fe3e4aSElliott Hughes "--brute-force", 179*e1fe3e4aSElliott Hughes dest="brute", 180*e1fe3e4aSElliott Hughes action="store_true", 181*e1fe3e4aSElliott Hughes help="Use brute-force approach (VERY slow)", 182*e1fe3e4aSElliott Hughes ) 183*e1fe3e4aSElliott Hughes 184*e1fe3e4aSElliott Hughes args = parser.parse_args(args) 185*e1fe3e4aSElliott Hughes 186*e1fe3e4aSElliott Hughes for fontfile in args.inputs: 187*e1fe3e4aSElliott Hughes font = TTFont(fontfile) 188*e1fe3e4aSElliott Hughes hmtx = font["hmtx"] 189*e1fe3e4aSElliott Hughes widths = [m[0] for m in hmtx.metrics.values()] 190*e1fe3e4aSElliott Hughes if args.brute: 191*e1fe3e4aSElliott Hughes default, nominal = optimizeWidthsBruteforce(widths) 192*e1fe3e4aSElliott Hughes else: 193*e1fe3e4aSElliott Hughes default, nominal = optimizeWidths(widths) 194*e1fe3e4aSElliott Hughes print( 195*e1fe3e4aSElliott Hughes "glyphs=%d default=%d nominal=%d byteCost=%d" 196*e1fe3e4aSElliott Hughes % (len(widths), default, nominal, byteCost(widths, default, nominal)) 197*e1fe3e4aSElliott Hughes ) 198*e1fe3e4aSElliott Hughes 199*e1fe3e4aSElliott Hughes 200*e1fe3e4aSElliott Hughesif __name__ == "__main__": 201*e1fe3e4aSElliott Hughes import sys 202*e1fe3e4aSElliott Hughes 203*e1fe3e4aSElliott Hughes if len(sys.argv) == 1: 204*e1fe3e4aSElliott Hughes import doctest 205*e1fe3e4aSElliott Hughes 206*e1fe3e4aSElliott Hughes sys.exit(doctest.testmod().failed) 207*e1fe3e4aSElliott Hughes main() 208