Add tests that optimize block matrices
This commit is contained in:
parent
ef67839fdb
commit
7860dd5fe8
@ -128,7 +128,9 @@ def _getClassRanges(glyphIDs: Iterable[int]):
|
||||
|
||||
# Adapted from https://github.com/fonttools/fonttools/blob/f64f0b42f2d1163b2d85194e0979def539f5dca3/Lib/fontTools/ttLib/tables/otTables.py#L960-L989
|
||||
def _classDef_bytes(
|
||||
class_data: List[Tuple[List[Tuple[int, int]], int, int]], class_ids: List[int], coverage=False
|
||||
class_data: List[Tuple[List[Tuple[int, int]], int, int]],
|
||||
class_ids: List[int],
|
||||
coverage=False,
|
||||
):
|
||||
if not class_ids:
|
||||
return 0
|
||||
@ -152,6 +154,10 @@ def cluster_pairs_by_class2_coverage_custom_cost(
|
||||
pairs: Pairs,
|
||||
compression: int = 5,
|
||||
) -> List[Pairs]:
|
||||
if not pairs:
|
||||
# The subtable was actually empty?
|
||||
return [pairs]
|
||||
|
||||
# Sorted for reproducibility/determinism
|
||||
all_class1 = sorted(set(pair[0] for pair in pairs))
|
||||
all_class2 = sorted(set(pair[1] for pair in pairs))
|
||||
@ -229,10 +235,13 @@ def cluster_pairs_by_class2_coverage_custom_cost(
|
||||
@property
|
||||
def cost(self):
|
||||
if self._cost is None:
|
||||
# From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment
|
||||
self._cost = (
|
||||
# uint16 posFormat Format identifier: format = 2
|
||||
# 2 bytes to store the offset to this subtable in the Lookup table above
|
||||
2
|
||||
# Contents of the subtable
|
||||
# From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment
|
||||
# uint16 posFormat Format identifier: format = 2
|
||||
+ 2
|
||||
# Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable.
|
||||
+ 2
|
||||
+ self.coverage_bytes
|
||||
@ -267,7 +276,9 @@ def cluster_pairs_by_class2_coverage_custom_cost(
|
||||
# uint16 glyphArray[glyphCount] Array of glyph IDs — in numerical order
|
||||
+ sum(len(all_class1[i]) for i in self.indices) * 2
|
||||
)
|
||||
ranges = sorted(chain.from_iterable(all_class1_data[i][0] for i in self.indices))
|
||||
ranges = sorted(
|
||||
chain.from_iterable(all_class1_data[i][0] for i in self.indices)
|
||||
)
|
||||
merged_range_count = 0
|
||||
last = None
|
||||
for (start, end) in ranges:
|
||||
|
@ -1,9 +1,18 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
import contextlib
|
||||
import os
|
||||
from typing import List, Optional, Tuple
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
import pytest
|
||||
|
||||
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
|
||||
from fontTools.fontBuilder import FontBuilder
|
||||
|
||||
from fontTools.ttLib.tables.otBase import OTTableWriter, ValueRecord
|
||||
|
||||
|
||||
def test_main(tmpdir: Path):
|
||||
"""Check that calling the main function on an input TTF works."""
|
||||
@ -36,19 +45,128 @@ def test_main(tmpdir: Path):
|
||||
assert output.exists()
|
||||
|
||||
|
||||
def test_off_by_default(tmpdir: Path):
|
||||
"""Check that calling the main function on an input TTF works."""
|
||||
glyphs = ".notdef space A B".split()
|
||||
features = """
|
||||
feature kern {
|
||||
pos A B -50;
|
||||
} kern;
|
||||
# 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):
|
||||
"""
|
||||
fb = FontBuilder(1000)
|
||||
fb.setupGlyphOrder(glyphs)
|
||||
addOpenTypeFeaturesFromString(fb.font, features)
|
||||
input = tmpdir / "in.ttf"
|
||||
fb.save(str(input))
|
||||
output = tmpdir / "out.ttf"
|
||||
run(["fonttools", "otlLib.optimize", str(input), "-o", str(output)], check=True)
|
||||
assert output.exists()
|
||||
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", "mode", "expected_subtables", "expected_bytes"),
|
||||
[
|
||||
# Mode = 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]],
|
||||
mode: 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
|
||||
|
||||
env = {}
|
||||
if mode is not None:
|
||||
env["FONTTOOLS_GPOS_COMPACT_MODE"] = str(mode)
|
||||
with set_env(**env):
|
||||
fb = FontBuilder(1000)
|
||||
fb.setupGlyphOrder(glyphs)
|
||||
addOpenTypeFeaturesFromString(fb.font, features)
|
||||
assert expected_subtables == count_pairpos_subtables(fb.font)
|
||||
assert expected_bytes == count_pairpos_bytes(fb.font)
|
||||
|
Loading…
x
Reference in New Issue
Block a user