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