I typically run tests like: $ python setup.py build_ext -i && PYTHONPATH=Lib pytest Previously, this particular test and only this, required that a `pip install -e .` has had happened. Not anymore.
167 lines
5.3 KiB
Python
167 lines
5.3 KiB
Python
import contextlib
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import List, Optional, Tuple
|
|
|
|
import pytest
|
|
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
|
|
from fontTools.fontBuilder import FontBuilder
|
|
from fontTools.ttLib import TTFont
|
|
from fontTools.ttLib.tables.otBase import OTTableWriter
|
|
|
|
|
|
def test_main(tmpdir: Path):
|
|
"""Check that calling the main function on an input TTF works."""
|
|
glyphs = ".notdef space A Aacute B D".split()
|
|
features = """
|
|
@A = [A Aacute];
|
|
@B = [B D];
|
|
feature kern {
|
|
pos @A @B -50;
|
|
} kern;
|
|
"""
|
|
fb = FontBuilder(1000)
|
|
fb.setupGlyphOrder(glyphs)
|
|
addOpenTypeFeaturesFromString(fb.font, features)
|
|
input = tmpdir / "in.ttf"
|
|
fb.save(str(input))
|
|
output = tmpdir / "out.ttf"
|
|
args = [
|
|
"--gpos-compression-level",
|
|
"5",
|
|
str(input),
|
|
"-o",
|
|
str(output),
|
|
]
|
|
from fontTools.otlLib.optimize import main
|
|
|
|
ret = main(args)
|
|
assert ret in (0, None)
|
|
assert output.exists()
|
|
|
|
|
|
# Copy-pasted from https://stackoverflow.com/questions/2059482/python-temporarily-modify-the-current-processs-environment
|
|
# TODO: remove when moving to the Config class
|
|
@contextlib.contextmanager
|
|
def set_env(**environ):
|
|
"""
|
|
Temporarily set the process environment variables.
|
|
|
|
>>> with set_env(PLUGINS_DIR=u'test/plugins'):
|
|
... "PLUGINS_DIR" in os.environ
|
|
True
|
|
|
|
>>> "PLUGINS_DIR" in os.environ
|
|
False
|
|
|
|
:type environ: dict[str, unicode]
|
|
:param environ: Environment variables to set
|
|
"""
|
|
old_environ = dict(os.environ)
|
|
os.environ.update(environ)
|
|
try:
|
|
yield
|
|
finally:
|
|
os.environ.clear()
|
|
os.environ.update(old_environ)
|
|
|
|
|
|
def count_pairpos_subtables(font: TTFont) -> int:
|
|
subtables = 0
|
|
for lookup in font["GPOS"].table.LookupList.Lookup:
|
|
if lookup.LookupType == 2:
|
|
subtables += len(lookup.SubTable)
|
|
elif lookup.LookupType == 9:
|
|
for subtable in lookup.SubTable:
|
|
if subtable.ExtensionLookupType == 2:
|
|
subtables += 1
|
|
return subtables
|
|
|
|
|
|
def count_pairpos_bytes(font: TTFont) -> int:
|
|
bytes = 0
|
|
gpos = font["GPOS"]
|
|
for lookup in font["GPOS"].table.LookupList.Lookup:
|
|
if lookup.LookupType == 2:
|
|
w = OTTableWriter(tableTag=gpos.tableTag)
|
|
lookup.compile(w, font)
|
|
bytes += len(w.getAllData())
|
|
elif lookup.LookupType == 9:
|
|
if any(subtable.ExtensionLookupType == 2 for subtable in lookup.SubTable):
|
|
w = OTTableWriter(tableTag=gpos.tableTag)
|
|
lookup.compile(w, font)
|
|
bytes += len(w.getAllData())
|
|
return bytes
|
|
|
|
|
|
def get_kerning_by_blocks(blocks: List[Tuple[int, int]]) -> Tuple[List[str], str]:
|
|
"""Generate a highly compressible font by generating a bunch of rectangular
|
|
blocks on the diagonal that can easily be sliced into subtables.
|
|
|
|
Returns the list of glyphs and feature code of the font.
|
|
"""
|
|
value = 0
|
|
glyphs: List[str] = []
|
|
rules = []
|
|
# Each block is like a script in a multi-script font
|
|
for script, (width, height) in enumerate(blocks):
|
|
glyphs.extend(f"g_{script}_{i}" for i in range(max(width, height)))
|
|
for l in range(height):
|
|
for r in range(width):
|
|
value += 1
|
|
rules.append((f"g_{script}_{l}", f"g_{script}_{r}", value))
|
|
classes = "\n".join([f"@{g} = [{g}];" for g in glyphs])
|
|
statements = "\n".join([f"pos @{l} @{r} {v};" for (l, r, v) in rules])
|
|
features = f"""
|
|
{classes}
|
|
feature kern {{
|
|
{statements}
|
|
}} kern;
|
|
"""
|
|
return glyphs, features
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("blocks", "level", "expected_subtables", "expected_bytes"),
|
|
[
|
|
# Level = 0 = no optimization leads to 650 bytes of GPOS
|
|
([(15, 3), (2, 10)], None, 1, 602),
|
|
# Optimization level 1 recognizes the 2 blocks and splits into 2
|
|
# subtables = adds 1 subtable leading to a size reduction of
|
|
# (602-298)/602 = 50%
|
|
([(15, 3), (2, 10)], 1, 2, 298),
|
|
# On a bigger block configuration, we see that mode=5 doesn't create
|
|
# as many subtables as it could, because of the stop criteria
|
|
([(4, 4) for _ in range(20)], 5, 14, 2042),
|
|
# while level=9 creates as many subtables as there were blocks on the
|
|
# diagonal and yields a better saving
|
|
([(4, 4) for _ in range(20)], 9, 20, 1886),
|
|
# On a fully occupied kerning matrix, even the strategy 9 doesn't
|
|
# split anything.
|
|
([(10, 10)], 9, 1, 304),
|
|
],
|
|
)
|
|
def test_optimization_mode(
|
|
caplog,
|
|
blocks: List[Tuple[int, int]],
|
|
level: Optional[int],
|
|
expected_subtables: int,
|
|
expected_bytes: int,
|
|
):
|
|
"""Check that the optimizations are off by default, and that increasing
|
|
the optimization level creates more subtables and a smaller byte size.
|
|
"""
|
|
caplog.set_level(logging.DEBUG)
|
|
|
|
glyphs, features = get_kerning_by_blocks(blocks)
|
|
glyphs = [".notdef space"] + glyphs
|
|
|
|
fb = FontBuilder(1000)
|
|
if level is not None:
|
|
fb.font.cfg["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"] = level
|
|
fb.setupGlyphOrder(glyphs)
|
|
addOpenTypeFeaturesFromString(fb.font, features)
|
|
assert expected_subtables == count_pairpos_subtables(fb.font)
|
|
assert expected_bytes == count_pairpos_bytes(fb.font)
|