Spaces:
Sleeping
Sleeping
| # -*- coding: utf-8 -*- | |
| """T2CharString glyph width optimizer. | |
| CFF glyphs whose width equals the CFF Private dictionary's ``defaultWidthX`` | |
| value do not need to specify their width in their charstring, saving bytes. | |
| This module determines the optimum ``defaultWidthX`` and ``nominalWidthX`` | |
| values for a font, when provided with a list of glyph widths.""" | |
| from fontTools.ttLib import TTFont | |
| from collections import defaultdict | |
| from operator import add | |
| from functools import reduce | |
| __all__ = ["optimizeWidths", "main"] | |
| class missingdict(dict): | |
| def __init__(self, missing_func): | |
| self.missing_func = missing_func | |
| def __missing__(self, v): | |
| return self.missing_func(v) | |
| def cumSum(f, op=add, start=0, decreasing=False): | |
| keys = sorted(f.keys()) | |
| minx, maxx = keys[0], keys[-1] | |
| total = reduce(op, f.values(), start) | |
| if decreasing: | |
| missing = lambda x: start if x > maxx else total | |
| domain = range(maxx, minx - 1, -1) | |
| else: | |
| missing = lambda x: start if x < minx else total | |
| domain = range(minx, maxx + 1) | |
| out = missingdict(missing) | |
| v = start | |
| for x in domain: | |
| v = op(v, f[x]) | |
| out[x] = v | |
| return out | |
| def byteCost(widths, default, nominal): | |
| if not hasattr(widths, "items"): | |
| d = defaultdict(int) | |
| for w in widths: | |
| d[w] += 1 | |
| widths = d | |
| cost = 0 | |
| for w, freq in widths.items(): | |
| if w == default: | |
| continue | |
| diff = abs(w - nominal) | |
| if diff <= 107: | |
| cost += freq | |
| elif diff <= 1131: | |
| cost += freq * 2 | |
| else: | |
| cost += freq * 5 | |
| return cost | |
| def optimizeWidthsBruteforce(widths): | |
| """Bruteforce version. Veeeeeeeeeeeeeeeeery slow. Only works for smallests of fonts.""" | |
| d = defaultdict(int) | |
| for w in widths: | |
| d[w] += 1 | |
| # Maximum number of bytes using default can possibly save | |
| maxDefaultAdvantage = 5 * max(d.values()) | |
| minw, maxw = min(widths), max(widths) | |
| domain = list(range(minw, maxw + 1)) | |
| bestCostWithoutDefault = min(byteCost(widths, None, nominal) for nominal in domain) | |
| bestCost = len(widths) * 5 + 1 | |
| for nominal in domain: | |
| if byteCost(widths, None, nominal) > bestCost + maxDefaultAdvantage: | |
| continue | |
| for default in domain: | |
| cost = byteCost(widths, default, nominal) | |
| if cost < bestCost: | |
| bestCost = cost | |
| bestDefault = default | |
| bestNominal = nominal | |
| return bestDefault, bestNominal | |
| def optimizeWidths(widths): | |
| """Given a list of glyph widths, or dictionary mapping glyph width to number of | |
| glyphs having that, returns a tuple of best CFF default and nominal glyph widths. | |
| This algorithm is linear in UPEM+numGlyphs.""" | |
| if not hasattr(widths, "items"): | |
| d = defaultdict(int) | |
| for w in widths: | |
| d[w] += 1 | |
| widths = d | |
| keys = sorted(widths.keys()) | |
| minw, maxw = keys[0], keys[-1] | |
| domain = list(range(minw, maxw + 1)) | |
| # Cumulative sum/max forward/backward. | |
| cumFrqU = cumSum(widths, op=add) | |
| cumMaxU = cumSum(widths, op=max) | |
| cumFrqD = cumSum(widths, op=add, decreasing=True) | |
| cumMaxD = cumSum(widths, op=max, decreasing=True) | |
| # Cost per nominal choice, without default consideration. | |
| nomnCostU = missingdict( | |
| lambda x: cumFrqU[x] + cumFrqU[x - 108] + cumFrqU[x - 1132] * 3 | |
| ) | |
| nomnCostD = missingdict( | |
| lambda x: cumFrqD[x] + cumFrqD[x + 108] + cumFrqD[x + 1132] * 3 | |
| ) | |
| nomnCost = missingdict(lambda x: nomnCostU[x] + nomnCostD[x] - widths[x]) | |
| # Cost-saving per nominal choice, by best default choice. | |
| dfltCostU = missingdict( | |
| lambda x: max(cumMaxU[x], cumMaxU[x - 108] * 2, cumMaxU[x - 1132] * 5) | |
| ) | |
| dfltCostD = missingdict( | |
| lambda x: max(cumMaxD[x], cumMaxD[x + 108] * 2, cumMaxD[x + 1132] * 5) | |
| ) | |
| dfltCost = missingdict(lambda x: max(dfltCostU[x], dfltCostD[x])) | |
| # Combined cost per nominal choice. | |
| bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[x]) | |
| # Best nominal. | |
| nominal = min(domain, key=lambda x: bestCost[x]) | |
| # Work back the best default. | |
| bestC = bestCost[nominal] | |
| dfltC = nomnCost[nominal] - bestCost[nominal] | |
| ends = [] | |
| if dfltC == dfltCostU[nominal]: | |
| starts = [nominal, nominal - 108, nominal - 1132] | |
| for start in starts: | |
| while cumMaxU[start] and cumMaxU[start] == cumMaxU[start - 1]: | |
| start -= 1 | |
| ends.append(start) | |
| else: | |
| starts = [nominal, nominal + 108, nominal + 1132] | |
| for start in starts: | |
| while cumMaxD[start] and cumMaxD[start] == cumMaxD[start + 1]: | |
| start += 1 | |
| ends.append(start) | |
| default = min(ends, key=lambda default: byteCost(widths, default, nominal)) | |
| return default, nominal | |
| def main(args=None): | |
| """Calculate optimum defaultWidthX/nominalWidthX values""" | |
| import argparse | |
| parser = argparse.ArgumentParser( | |
| "fonttools cffLib.width", | |
| description=main.__doc__, | |
| ) | |
| parser.add_argument( | |
| "inputs", metavar="FILE", type=str, nargs="+", help="Input TTF files" | |
| ) | |
| parser.add_argument( | |
| "-b", | |
| "--brute-force", | |
| dest="brute", | |
| action="store_true", | |
| help="Use brute-force approach (VERY slow)", | |
| ) | |
| args = parser.parse_args(args) | |
| for fontfile in args.inputs: | |
| font = TTFont(fontfile) | |
| hmtx = font["hmtx"] | |
| widths = [m[0] for m in hmtx.metrics.values()] | |
| if args.brute: | |
| default, nominal = optimizeWidthsBruteforce(widths) | |
| else: | |
| default, nominal = optimizeWidths(widths) | |
| print( | |
| "glyphs=%d default=%d nominal=%d byteCost=%d" | |
| % (len(widths), default, nominal, byteCost(widths, default, nominal)) | |
| ) | |
| if __name__ == "__main__": | |
| import sys | |
| if len(sys.argv) == 1: | |
| import doctest | |
| sys.exit(doctest.testmod().failed) | |
| main() | |