2021-07-05 15:12:40 +01:00
|
|
|
#! /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
|
|
|
|
"""
|
|
|
|
|
2021-05-28 16:46:20 +01:00
|
|
|
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()
|