xref: /aosp_15_r20/external/fonttools/Tests/otlLib/optimize_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughesimport contextlib
2*e1fe3e4aSElliott Hughesimport logging
3*e1fe3e4aSElliott Hughesimport os
4*e1fe3e4aSElliott Hughesfrom pathlib import Path
5*e1fe3e4aSElliott Hughesfrom subprocess import run
6*e1fe3e4aSElliott Hughesfrom typing import List, Optional, Tuple
7*e1fe3e4aSElliott Hughes
8*e1fe3e4aSElliott Hughesimport pytest
9*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.builder import addOpenTypeFeaturesFromString
10*e1fe3e4aSElliott Hughesfrom fontTools.fontBuilder import FontBuilder
11*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTFont
12*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables.otBase import OTTableWriter
13*e1fe3e4aSElliott Hughes
14*e1fe3e4aSElliott Hughes
15*e1fe3e4aSElliott Hughesdef test_main(tmpdir: Path):
16*e1fe3e4aSElliott Hughes    """Check that calling the main function on an input TTF works."""
17*e1fe3e4aSElliott Hughes    glyphs = ".notdef space A Aacute B D".split()
18*e1fe3e4aSElliott Hughes    features = """
19*e1fe3e4aSElliott Hughes    @A = [A Aacute];
20*e1fe3e4aSElliott Hughes    @B = [B D];
21*e1fe3e4aSElliott Hughes    feature kern {
22*e1fe3e4aSElliott Hughes        pos @A @B -50;
23*e1fe3e4aSElliott Hughes    } kern;
24*e1fe3e4aSElliott Hughes    """
25*e1fe3e4aSElliott Hughes    fb = FontBuilder(1000)
26*e1fe3e4aSElliott Hughes    fb.setupGlyphOrder(glyphs)
27*e1fe3e4aSElliott Hughes    addOpenTypeFeaturesFromString(fb.font, features)
28*e1fe3e4aSElliott Hughes    input = tmpdir / "in.ttf"
29*e1fe3e4aSElliott Hughes    fb.save(str(input))
30*e1fe3e4aSElliott Hughes    output = tmpdir / "out.ttf"
31*e1fe3e4aSElliott Hughes    run(
32*e1fe3e4aSElliott Hughes        [
33*e1fe3e4aSElliott Hughes            "fonttools",
34*e1fe3e4aSElliott Hughes            "otlLib.optimize",
35*e1fe3e4aSElliott Hughes            "--gpos-compression-level",
36*e1fe3e4aSElliott Hughes            "5",
37*e1fe3e4aSElliott Hughes            str(input),
38*e1fe3e4aSElliott Hughes            "-o",
39*e1fe3e4aSElliott Hughes            str(output),
40*e1fe3e4aSElliott Hughes        ],
41*e1fe3e4aSElliott Hughes        check=True,
42*e1fe3e4aSElliott Hughes    )
43*e1fe3e4aSElliott Hughes    assert output.exists()
44*e1fe3e4aSElliott Hughes
45*e1fe3e4aSElliott Hughes
46*e1fe3e4aSElliott Hughes# Copy-pasted from https://stackoverflow.com/questions/2059482/python-temporarily-modify-the-current-processs-environment
47*e1fe3e4aSElliott Hughes# TODO: remove when moving to the Config class
48*e1fe3e4aSElliott Hughes@contextlib.contextmanager
49*e1fe3e4aSElliott Hughesdef set_env(**environ):
50*e1fe3e4aSElliott Hughes    """
51*e1fe3e4aSElliott Hughes    Temporarily set the process environment variables.
52*e1fe3e4aSElliott Hughes
53*e1fe3e4aSElliott Hughes    >>> with set_env(PLUGINS_DIR=u'test/plugins'):
54*e1fe3e4aSElliott Hughes    ...   "PLUGINS_DIR" in os.environ
55*e1fe3e4aSElliott Hughes    True
56*e1fe3e4aSElliott Hughes
57*e1fe3e4aSElliott Hughes    >>> "PLUGINS_DIR" in os.environ
58*e1fe3e4aSElliott Hughes    False
59*e1fe3e4aSElliott Hughes
60*e1fe3e4aSElliott Hughes    :type environ: dict[str, unicode]
61*e1fe3e4aSElliott Hughes    :param environ: Environment variables to set
62*e1fe3e4aSElliott Hughes    """
63*e1fe3e4aSElliott Hughes    old_environ = dict(os.environ)
64*e1fe3e4aSElliott Hughes    os.environ.update(environ)
65*e1fe3e4aSElliott Hughes    try:
66*e1fe3e4aSElliott Hughes        yield
67*e1fe3e4aSElliott Hughes    finally:
68*e1fe3e4aSElliott Hughes        os.environ.clear()
69*e1fe3e4aSElliott Hughes        os.environ.update(old_environ)
70*e1fe3e4aSElliott Hughes
71*e1fe3e4aSElliott Hughes
72*e1fe3e4aSElliott Hughesdef count_pairpos_subtables(font: TTFont) -> int:
73*e1fe3e4aSElliott Hughes    subtables = 0
74*e1fe3e4aSElliott Hughes    for lookup in font["GPOS"].table.LookupList.Lookup:
75*e1fe3e4aSElliott Hughes        if lookup.LookupType == 2:
76*e1fe3e4aSElliott Hughes            subtables += len(lookup.SubTable)
77*e1fe3e4aSElliott Hughes        elif lookup.LookupType == 9:
78*e1fe3e4aSElliott Hughes            for subtable in lookup.SubTable:
79*e1fe3e4aSElliott Hughes                if subtable.ExtensionLookupType == 2:
80*e1fe3e4aSElliott Hughes                    subtables += 1
81*e1fe3e4aSElliott Hughes    return subtables
82*e1fe3e4aSElliott Hughes
83*e1fe3e4aSElliott Hughes
84*e1fe3e4aSElliott Hughesdef count_pairpos_bytes(font: TTFont) -> int:
85*e1fe3e4aSElliott Hughes    bytes = 0
86*e1fe3e4aSElliott Hughes    gpos = font["GPOS"]
87*e1fe3e4aSElliott Hughes    for lookup in font["GPOS"].table.LookupList.Lookup:
88*e1fe3e4aSElliott Hughes        if lookup.LookupType == 2:
89*e1fe3e4aSElliott Hughes            w = OTTableWriter(tableTag=gpos.tableTag)
90*e1fe3e4aSElliott Hughes            lookup.compile(w, font)
91*e1fe3e4aSElliott Hughes            bytes += len(w.getAllData())
92*e1fe3e4aSElliott Hughes        elif lookup.LookupType == 9:
93*e1fe3e4aSElliott Hughes            if any(subtable.ExtensionLookupType == 2 for subtable in lookup.SubTable):
94*e1fe3e4aSElliott Hughes                w = OTTableWriter(tableTag=gpos.tableTag)
95*e1fe3e4aSElliott Hughes                lookup.compile(w, font)
96*e1fe3e4aSElliott Hughes                bytes += len(w.getAllData())
97*e1fe3e4aSElliott Hughes    return bytes
98*e1fe3e4aSElliott Hughes
99*e1fe3e4aSElliott Hughes
100*e1fe3e4aSElliott Hughesdef get_kerning_by_blocks(blocks: List[Tuple[int, int]]) -> Tuple[List[str], str]:
101*e1fe3e4aSElliott Hughes    """Generate a highly compressible font by generating a bunch of rectangular
102*e1fe3e4aSElliott Hughes    blocks on the diagonal that can easily be sliced into subtables.
103*e1fe3e4aSElliott Hughes
104*e1fe3e4aSElliott Hughes    Returns the list of glyphs and feature code of the font.
105*e1fe3e4aSElliott Hughes    """
106*e1fe3e4aSElliott Hughes    value = 0
107*e1fe3e4aSElliott Hughes    glyphs: List[str] = []
108*e1fe3e4aSElliott Hughes    rules = []
109*e1fe3e4aSElliott Hughes    # Each block is like a script in a multi-script font
110*e1fe3e4aSElliott Hughes    for script, (width, height) in enumerate(blocks):
111*e1fe3e4aSElliott Hughes        glyphs.extend(f"g_{script}_{i}" for i in range(max(width, height)))
112*e1fe3e4aSElliott Hughes        for l in range(height):
113*e1fe3e4aSElliott Hughes            for r in range(width):
114*e1fe3e4aSElliott Hughes                value += 1
115*e1fe3e4aSElliott Hughes                rules.append((f"g_{script}_{l}", f"g_{script}_{r}", value))
116*e1fe3e4aSElliott Hughes    classes = "\n".join([f"@{g} = [{g}];" for g in glyphs])
117*e1fe3e4aSElliott Hughes    statements = "\n".join([f"pos @{l} @{r} {v};" for (l, r, v) in rules])
118*e1fe3e4aSElliott Hughes    features = f"""
119*e1fe3e4aSElliott Hughes        {classes}
120*e1fe3e4aSElliott Hughes        feature kern {{
121*e1fe3e4aSElliott Hughes            {statements}
122*e1fe3e4aSElliott Hughes        }} kern;
123*e1fe3e4aSElliott Hughes    """
124*e1fe3e4aSElliott Hughes    return glyphs, features
125*e1fe3e4aSElliott Hughes
126*e1fe3e4aSElliott Hughes
127*e1fe3e4aSElliott Hughes@pytest.mark.parametrize(
128*e1fe3e4aSElliott Hughes    ("blocks", "level", "expected_subtables", "expected_bytes"),
129*e1fe3e4aSElliott Hughes    [
130*e1fe3e4aSElliott Hughes        # Level = 0 = no optimization leads to 650 bytes of GPOS
131*e1fe3e4aSElliott Hughes        ([(15, 3), (2, 10)], None, 1, 602),
132*e1fe3e4aSElliott Hughes        # Optimization level 1 recognizes the 2 blocks and splits into 2
133*e1fe3e4aSElliott Hughes        # subtables = adds 1 subtable leading to a size reduction of
134*e1fe3e4aSElliott Hughes        # (602-298)/602 = 50%
135*e1fe3e4aSElliott Hughes        ([(15, 3), (2, 10)], 1, 2, 298),
136*e1fe3e4aSElliott Hughes        # On a bigger block configuration, we see that mode=5 doesn't create
137*e1fe3e4aSElliott Hughes        # as many subtables as it could, because of the stop criteria
138*e1fe3e4aSElliott Hughes        ([(4, 4) for _ in range(20)], 5, 14, 2042),
139*e1fe3e4aSElliott Hughes        # while level=9 creates as many subtables as there were blocks on the
140*e1fe3e4aSElliott Hughes        # diagonal and yields a better saving
141*e1fe3e4aSElliott Hughes        ([(4, 4) for _ in range(20)], 9, 20, 1886),
142*e1fe3e4aSElliott Hughes        # On a fully occupied kerning matrix, even the strategy 9 doesn't
143*e1fe3e4aSElliott Hughes        # split anything.
144*e1fe3e4aSElliott Hughes        ([(10, 10)], 9, 1, 304),
145*e1fe3e4aSElliott Hughes    ],
146*e1fe3e4aSElliott Hughes)
147*e1fe3e4aSElliott Hughesdef test_optimization_mode(
148*e1fe3e4aSElliott Hughes    caplog,
149*e1fe3e4aSElliott Hughes    blocks: List[Tuple[int, int]],
150*e1fe3e4aSElliott Hughes    level: Optional[int],
151*e1fe3e4aSElliott Hughes    expected_subtables: int,
152*e1fe3e4aSElliott Hughes    expected_bytes: int,
153*e1fe3e4aSElliott Hughes):
154*e1fe3e4aSElliott Hughes    """Check that the optimizations are off by default, and that increasing
155*e1fe3e4aSElliott Hughes    the optimization level creates more subtables and a smaller byte size.
156*e1fe3e4aSElliott Hughes    """
157*e1fe3e4aSElliott Hughes    caplog.set_level(logging.DEBUG)
158*e1fe3e4aSElliott Hughes
159*e1fe3e4aSElliott Hughes    glyphs, features = get_kerning_by_blocks(blocks)
160*e1fe3e4aSElliott Hughes    glyphs = [".notdef space"] + glyphs
161*e1fe3e4aSElliott Hughes
162*e1fe3e4aSElliott Hughes    fb = FontBuilder(1000)
163*e1fe3e4aSElliott Hughes    if level is not None:
164*e1fe3e4aSElliott Hughes        fb.font.cfg["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"] = level
165*e1fe3e4aSElliott Hughes    fb.setupGlyphOrder(glyphs)
166*e1fe3e4aSElliott Hughes    addOpenTypeFeaturesFromString(fb.font, features)
167*e1fe3e4aSElliott Hughes    assert expected_subtables == count_pairpos_subtables(fb.font)
168*e1fe3e4aSElliott Hughes    assert expected_bytes == count_pairpos_bytes(fb.font)
169