#! /usr/bin/env python3 """ Sample script to use the otlLib.optimize.gpos functions to compact GPOS tables of existing fonts. This script takes one or more TTF files as arguments and will create compacted copies of the fonts using all available modes of the GPOS compaction algorithm. For each copy, it will measure the new size of the GPOS table and also the new size of the font in WOFF2 format. All results will be printed to stdout in CSV format, so the savings provided by the algorithm in each mode can be inspected. This was initially made to debug the algorithm but can also be used to choose a mode value for a specific font (trade-off between bytes saved in TTF format vs more bytes in WOFF2 format and more subtables). Run: python Snippets/compact_gpos.py MyFont.ttf > results.csv """ import argparse from collections import defaultdict import csv import time import sys from pathlib import Path from typing import Any, Iterable, List, Optional, Sequence, Tuple from fontTools.ttLib import TTFont from fontTools.otlLib.optimize import compact MODES = [str(c) for c in range(1, 10)] def main(args: Optional[List[str]] = None): parser = argparse.ArgumentParser() parser.add_argument("fonts", type=Path, nargs="+", help="Path to TTFs.") parsed_args = parser.parse_args(args) runtimes = defaultdict(list) rows = [] font_path: Path for font_path in parsed_args.fonts: font = TTFont(font_path) if "GPOS" not in font: print(f"No GPOS in {font_path.name}, skipping.", file=sys.stderr) continue size_orig = len(font.getTableData("GPOS")) / 1024 print(f"Measuring {font_path.name}...", file=sys.stderr) fonts = {} font_paths = {} sizes = {} for mode in MODES: print(f" Running mode={mode}", file=sys.stderr) fonts[mode] = TTFont(font_path) before = time.perf_counter() compact(fonts[mode], mode=str(mode)) runtimes[mode].append(time.perf_counter() - before) font_paths[mode] = ( font_path.parent / "compact" / (font_path.stem + f"_{mode}" + font_path.suffix) ) font_paths[mode].parent.mkdir(parents=True, exist_ok=True) fonts[mode].save(font_paths[mode]) fonts[mode] = TTFont(font_paths[mode]) sizes[mode] = len(fonts[mode].getTableData("GPOS")) / 1024 print(f" Runtimes:", file=sys.stderr) for mode, times in runtimes.items(): print( f" {mode:10} {' '.join(f'{t:5.2f}' for t in times)}", file=sys.stderr, ) # Bonus: measure WOFF2 file sizes. print(f" Measuring WOFF2 sizes", file=sys.stderr) size_woff_orig = woff_size(font, font_path) / 1024 sizes_woff = { mode: woff_size(fonts[mode], font_paths[mode]) / 1024 for mode in MODES } rows.append( ( font_path.name, size_orig, size_woff_orig, *flatten( ( sizes[mode], pct(sizes[mode], size_orig), sizes_woff[mode], pct(sizes_woff[mode], size_woff_orig), ) for mode in MODES ), ) ) write_csv(rows) def woff_size(font: TTFont, path: Path) -> int: font.flavor = "woff2" woff_path = path.with_suffix(".woff2") font.save(woff_path) return woff_path.stat().st_size def write_csv(rows: List[Tuple[Any]]) -> None: sys.stdout.reconfigure(encoding="utf-8") sys.stdout.write("\uFEFF") writer = csv.writer(sys.stdout, lineterminator="\n") writer.writerow( [ "File", "Original GPOS Size", "Original WOFF2 Size", *flatten( ( f"mode={mode}", f"Change {mode}", f"mode={mode} WOFF2 Size", f"Change {mode} WOFF2 Size", ) for mode in MODES ), ] ) for row in rows: writer.writerow(row) def pct(new: float, old: float) -> float: return -(1 - (new / old)) def flatten(seq_seq: Iterable[Iterable[Any]]) -> List[Any]: return [thing for seq in seq_seq for thing in seq] if __name__ == "__main__": main()