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