xref: /aosp_15_r20/external/fonttools/Snippets/compact_gpos.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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