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