1*e1fe3e4aSElliott Hughes#! /usr/bin/env python3 2*e1fe3e4aSElliott Hughes 3*e1fe3e4aSElliott Hughes""" 4*e1fe3e4aSElliott HughesSample script to use the otlLib.optimize.gpos functions to compact GPOS tables 5*e1fe3e4aSElliott Hughesof existing fonts. This script takes one or more TTF files as arguments and 6*e1fe3e4aSElliott Hugheswill create compacted copies of the fonts using all available modes of the GPOS 7*e1fe3e4aSElliott Hughescompaction algorithm. For each copy, it will measure the new size of the GPOS 8*e1fe3e4aSElliott Hughestable and also the new size of the font in WOFF2 format. All results will be 9*e1fe3e4aSElliott Hughesprinted to stdout in CSV format, so the savings provided by the algorithm in 10*e1fe3e4aSElliott Hugheseach mode can be inspected. 11*e1fe3e4aSElliott Hughes 12*e1fe3e4aSElliott HughesThis was initially made to debug the algorithm but can also be used to choose 13*e1fe3e4aSElliott Hughesa mode value for a specific font (trade-off between bytes saved in TTF format 14*e1fe3e4aSElliott Hughesvs more bytes in WOFF2 format and more subtables). 15*e1fe3e4aSElliott Hughes 16*e1fe3e4aSElliott HughesRun: 17*e1fe3e4aSElliott Hughes 18*e1fe3e4aSElliott Hughespython Snippets/compact_gpos.py MyFont.ttf > results.csv 19*e1fe3e4aSElliott Hughes""" 20*e1fe3e4aSElliott Hughes 21*e1fe3e4aSElliott Hughesimport argparse 22*e1fe3e4aSElliott Hughesfrom collections import defaultdict 23*e1fe3e4aSElliott Hughesimport csv 24*e1fe3e4aSElliott Hughesimport time 25*e1fe3e4aSElliott Hughesimport sys 26*e1fe3e4aSElliott Hughesfrom pathlib import Path 27*e1fe3e4aSElliott Hughesfrom typing import Any, Iterable, List, Optional, Sequence, Tuple 28*e1fe3e4aSElliott Hughes 29*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTFont 30*e1fe3e4aSElliott Hughesfrom fontTools.otlLib.optimize import compact 31*e1fe3e4aSElliott Hughes 32*e1fe3e4aSElliott HughesMODES = [str(c) for c in range(1, 10)] 33*e1fe3e4aSElliott Hughes 34*e1fe3e4aSElliott Hughes 35*e1fe3e4aSElliott Hughesdef main(args: Optional[List[str]] = None): 36*e1fe3e4aSElliott Hughes parser = argparse.ArgumentParser() 37*e1fe3e4aSElliott Hughes parser.add_argument("fonts", type=Path, nargs="+", help="Path to TTFs.") 38*e1fe3e4aSElliott Hughes parsed_args = parser.parse_args(args) 39*e1fe3e4aSElliott Hughes 40*e1fe3e4aSElliott Hughes runtimes = defaultdict(list) 41*e1fe3e4aSElliott Hughes rows = [] 42*e1fe3e4aSElliott Hughes font_path: Path 43*e1fe3e4aSElliott Hughes for font_path in parsed_args.fonts: 44*e1fe3e4aSElliott Hughes font = TTFont(font_path) 45*e1fe3e4aSElliott Hughes if "GPOS" not in font: 46*e1fe3e4aSElliott Hughes print(f"No GPOS in {font_path.name}, skipping.", file=sys.stderr) 47*e1fe3e4aSElliott Hughes continue 48*e1fe3e4aSElliott Hughes size_orig = len(font.getTableData("GPOS")) / 1024 49*e1fe3e4aSElliott Hughes print(f"Measuring {font_path.name}...", file=sys.stderr) 50*e1fe3e4aSElliott Hughes 51*e1fe3e4aSElliott Hughes fonts = {} 52*e1fe3e4aSElliott Hughes font_paths = {} 53*e1fe3e4aSElliott Hughes sizes = {} 54*e1fe3e4aSElliott Hughes for mode in MODES: 55*e1fe3e4aSElliott Hughes print(f" Running mode={mode}", file=sys.stderr) 56*e1fe3e4aSElliott Hughes fonts[mode] = TTFont(font_path) 57*e1fe3e4aSElliott Hughes before = time.perf_counter() 58*e1fe3e4aSElliott Hughes compact(fonts[mode], mode=str(mode)) 59*e1fe3e4aSElliott Hughes runtimes[mode].append(time.perf_counter() - before) 60*e1fe3e4aSElliott Hughes font_paths[mode] = ( 61*e1fe3e4aSElliott Hughes font_path.parent 62*e1fe3e4aSElliott Hughes / "compact" 63*e1fe3e4aSElliott Hughes / (font_path.stem + f"_{mode}" + font_path.suffix) 64*e1fe3e4aSElliott Hughes ) 65*e1fe3e4aSElliott Hughes font_paths[mode].parent.mkdir(parents=True, exist_ok=True) 66*e1fe3e4aSElliott Hughes fonts[mode].save(font_paths[mode]) 67*e1fe3e4aSElliott Hughes fonts[mode] = TTFont(font_paths[mode]) 68*e1fe3e4aSElliott Hughes sizes[mode] = len(fonts[mode].getTableData("GPOS")) / 1024 69*e1fe3e4aSElliott Hughes 70*e1fe3e4aSElliott Hughes print(f" Runtimes:", file=sys.stderr) 71*e1fe3e4aSElliott Hughes for mode, times in runtimes.items(): 72*e1fe3e4aSElliott Hughes print( 73*e1fe3e4aSElliott Hughes f" {mode:10} {' '.join(f'{t:5.2f}' for t in times)}", 74*e1fe3e4aSElliott Hughes file=sys.stderr, 75*e1fe3e4aSElliott Hughes ) 76*e1fe3e4aSElliott Hughes 77*e1fe3e4aSElliott Hughes # Bonus: measure WOFF2 file sizes. 78*e1fe3e4aSElliott Hughes print(f" Measuring WOFF2 sizes", file=sys.stderr) 79*e1fe3e4aSElliott Hughes size_woff_orig = woff_size(font, font_path) / 1024 80*e1fe3e4aSElliott Hughes sizes_woff = { 81*e1fe3e4aSElliott Hughes mode: woff_size(fonts[mode], font_paths[mode]) / 1024 for mode in MODES 82*e1fe3e4aSElliott Hughes } 83*e1fe3e4aSElliott Hughes 84*e1fe3e4aSElliott Hughes rows.append( 85*e1fe3e4aSElliott Hughes ( 86*e1fe3e4aSElliott Hughes font_path.name, 87*e1fe3e4aSElliott Hughes size_orig, 88*e1fe3e4aSElliott Hughes size_woff_orig, 89*e1fe3e4aSElliott Hughes *flatten( 90*e1fe3e4aSElliott Hughes ( 91*e1fe3e4aSElliott Hughes sizes[mode], 92*e1fe3e4aSElliott Hughes pct(sizes[mode], size_orig), 93*e1fe3e4aSElliott Hughes sizes_woff[mode], 94*e1fe3e4aSElliott Hughes pct(sizes_woff[mode], size_woff_orig), 95*e1fe3e4aSElliott Hughes ) 96*e1fe3e4aSElliott Hughes for mode in MODES 97*e1fe3e4aSElliott Hughes ), 98*e1fe3e4aSElliott Hughes ) 99*e1fe3e4aSElliott Hughes ) 100*e1fe3e4aSElliott Hughes 101*e1fe3e4aSElliott Hughes write_csv(rows) 102*e1fe3e4aSElliott Hughes 103*e1fe3e4aSElliott Hughes 104*e1fe3e4aSElliott Hughesdef woff_size(font: TTFont, path: Path) -> int: 105*e1fe3e4aSElliott Hughes font.flavor = "woff2" 106*e1fe3e4aSElliott Hughes woff_path = path.with_suffix(".woff2") 107*e1fe3e4aSElliott Hughes font.save(woff_path) 108*e1fe3e4aSElliott Hughes return woff_path.stat().st_size 109*e1fe3e4aSElliott Hughes 110*e1fe3e4aSElliott Hughes 111*e1fe3e4aSElliott Hughesdef write_csv(rows: List[Tuple[Any]]) -> None: 112*e1fe3e4aSElliott Hughes sys.stdout.reconfigure(encoding="utf-8") 113*e1fe3e4aSElliott Hughes sys.stdout.write("\uFEFF") 114*e1fe3e4aSElliott Hughes writer = csv.writer(sys.stdout, lineterminator="\n") 115*e1fe3e4aSElliott Hughes writer.writerow( 116*e1fe3e4aSElliott Hughes [ 117*e1fe3e4aSElliott Hughes "File", 118*e1fe3e4aSElliott Hughes "Original GPOS Size", 119*e1fe3e4aSElliott Hughes "Original WOFF2 Size", 120*e1fe3e4aSElliott Hughes *flatten( 121*e1fe3e4aSElliott Hughes ( 122*e1fe3e4aSElliott Hughes f"mode={mode}", 123*e1fe3e4aSElliott Hughes f"Change {mode}", 124*e1fe3e4aSElliott Hughes f"mode={mode} WOFF2 Size", 125*e1fe3e4aSElliott Hughes f"Change {mode} WOFF2 Size", 126*e1fe3e4aSElliott Hughes ) 127*e1fe3e4aSElliott Hughes for mode in MODES 128*e1fe3e4aSElliott Hughes ), 129*e1fe3e4aSElliott Hughes ] 130*e1fe3e4aSElliott Hughes ) 131*e1fe3e4aSElliott Hughes for row in rows: 132*e1fe3e4aSElliott Hughes writer.writerow(row) 133*e1fe3e4aSElliott Hughes 134*e1fe3e4aSElliott Hughes 135*e1fe3e4aSElliott Hughesdef pct(new: float, old: float) -> float: 136*e1fe3e4aSElliott Hughes return -(1 - (new / old)) 137*e1fe3e4aSElliott Hughes 138*e1fe3e4aSElliott Hughes 139*e1fe3e4aSElliott Hughesdef flatten(seq_seq: Iterable[Iterable[Any]]) -> List[Any]: 140*e1fe3e4aSElliott Hughes return [thing for seq in seq_seq for thing in seq] 141*e1fe3e4aSElliott Hughes 142*e1fe3e4aSElliott Hughes 143*e1fe3e4aSElliott Hughesif __name__ == "__main__": 144*e1fe3e4aSElliott Hughes main() 145