You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			211 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			211 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Python
		
	
# -*- 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()
 |