123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- # -*- 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
- 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()
|