aboutsummaryrefslogtreecommitdiff
path: root/Snippets/compact_gpos.py
diff options
context:
space:
mode:
Diffstat (limited to 'Snippets/compact_gpos.py')
-rw-r--r--Snippets/compact_gpos.py144
1 files changed, 144 insertions, 0 deletions
diff --git a/Snippets/compact_gpos.py b/Snippets/compact_gpos.py
new file mode 100644
index 00000000..a5bd2f8b
--- /dev/null
+++ b/Snippets/compact_gpos.py
@@ -0,0 +1,144 @@
+#! /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()