xref: /aosp_15_r20/external/fonttools/Lib/fontTools/cffLib/width.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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