[config] Add new config module and use it for GPOS compression level

This commit is contained in:
Jany Belluz 2022-04-14 15:23:02 +01:00
parent e530c2fa1c
commit abc0441957
6 changed files with 81 additions and 71 deletions

View File

@ -0,0 +1,41 @@
"""
Define all configuration options that can affect the working of fontTools
modules. E.g. optimization levels of varLib IUP, otlLib GPOS compression level,
etc. If this file gets too big, split it into smaller files per-module.
An instance of the Config class can be attached to a TTFont object, so that
the various modules can access their configuration options from it.
"""
from textwrap import dedent
from fontTools.misc.configTools import *
class Config(AbstractConfig):
options = Options()
OPTIONS = Config.options
Config.register_option(
name="fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
help=dedent(
"""\
GPOS Lookup type 2 (PairPos) compression level:
0 = do not attempt to compact PairPos lookups;
1 to 8 = create at most 1 to 8 new subtables for each existing
subtable, provided that it would yield a 50%% file size saving;
9 = create as many new subtables as needed to yield a file size saving.
Default: 0.
This compaction aims to save file size, by splitting large class
kerning subtables (Format 2) that contain many zero values into
smaller and denser subtables. It's a trade-off between the overhead
of several subtables versus the sparseness of one big subtable.
See the pull request: https://github.com/fonttools/fonttools/pull/2326
"""
),
default=0,
parse=int,
validate=lambda v: v in range(10),
)

View File

@ -11,11 +11,7 @@ from fontTools.ttLib.tables.otBase import (
)
from fontTools.ttLib.tables import otBase
from fontTools.feaLib.ast import STATNameStatement
from fontTools.otlLib.optimize.gpos import (
GPOS_COMPACT_MODE_DEFAULT,
GPOS_COMPACT_MODE_ENV_KEY,
compact_lookup,
)
from fontTools.otlLib.optimize.gpos import compact_lookup
from fontTools.otlLib.error import OpenTypeLibError
from functools import reduce
import logging
@ -1414,10 +1410,10 @@ class PairPosBuilder(LookupBuilder):
# Compact the lookup
# This is a good moment to do it because the compaction should create
# smaller subtables, which may prevent overflows from happening.
mode = os.environ.get(GPOS_COMPACT_MODE_ENV_KEY, GPOS_COMPACT_MODE_DEFAULT)
if mode and mode != "0":
level = self.font.cfg["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"]
if level and level != 0:
log.info("Compacting GPOS...")
compact_lookup(self.font, mode, lookup)
compact_lookup(self.font, level, lookup)
return lookup

View File

@ -1,39 +1,30 @@
from argparse import RawTextHelpFormatter
from textwrap import dedent
from fontTools.config import OPTIONS
from fontTools.otlLib.optimize.gpos import compact
from fontTools.ttLib import TTFont
from fontTools.otlLib.optimize.gpos import compact, GPOS_COMPACT_MODE_DEFAULT
def main(args=None):
"""Optimize the layout tables of an existing font."""
from argparse import ArgumentParser
from fontTools import configLogger
parser = ArgumentParser(prog="otlLib.optimize", description=main.__doc__, formatter_class=RawTextHelpFormatter)
parser = ArgumentParser(
prog="otlLib.optimize",
description=main.__doc__,
formatter_class=RawTextHelpFormatter,
)
parser.add_argument("font")
parser.add_argument(
"-o", metavar="OUTPUTFILE", dest="outfile", default=None, help="output file"
)
COMPRESSION_LEVEL = OPTIONS["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"]
parser.add_argument(
"--gpos-compact-mode",
help=dedent(
f"""\
GPOS Lookup type 2 (PairPos) compaction mode:
0 = do not attempt to compact PairPos lookups;
1 to 8 = create at most 1 to 8 new subtables for each existing
subtable, provided that it would yield a 50%% file size saving;
9 = create as many new subtables as needed to yield a file size saving.
Default: {GPOS_COMPACT_MODE_DEFAULT}.
This compaction aims to save file size, by splitting large class
kerning subtables (Format 2) that contain many zero values into
smaller and denser subtables. It's a trade-off between the overhead
of several subtables versus the sparseness of one big subtable.
See the pull request: https://github.com/fonttools/fonttools/pull/2326
"""
),
default=int(GPOS_COMPACT_MODE_DEFAULT),
"--gpos-compression-level",
help=COMPRESSION_LEVEL.help,
default=COMPRESSION_LEVEL.default,
choices=list(range(10)),
type=int,
)
@ -51,12 +42,10 @@ def main(args=None):
)
font = TTFont(options.font)
# TODO: switch everything to have type(mode) = int when using the Config class
compact(font, str(options.gpos_compact_mode))
compact(font, options.gpos_compression_level)
font.save(options.outfile or options.font)
if __name__ == "__main__":
import sys
@ -65,4 +54,3 @@ if __name__ == "__main__":
import doctest
sys.exit(doctest.testmod().failed)

View File

@ -9,16 +9,10 @@ from fontTools.misc.intTools import bit_count, bit_indices
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables import otBase, otTables
# NOTE: activating this optimization via the environment variable is
# experimental and may not be supported once an alternative mechanism
# is in place. See: https://github.com/fonttools/fonttools/issues/2349
GPOS_COMPACT_MODE_ENV_KEY = "FONTTOOLS_GPOS_COMPACT_MODE"
GPOS_COMPACT_MODE_DEFAULT = "0"
log = logging.getLogger("fontTools.otlLib.optimize.gpos")
def compact(font: TTFont, mode: str) -> TTFont:
def compact(font: TTFont, level: int) -> TTFont:
# Ideal plan:
# 1. Find lookups of Lookup Type 2: Pair Adjustment Positioning Subtable
# https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-2-pair-adjustment-positioning-subtable
@ -35,21 +29,21 @@ def compact(font: TTFont, mode: str) -> TTFont:
gpos = font["GPOS"]
for lookup in gpos.table.LookupList.Lookup:
if lookup.LookupType == 2:
compact_lookup(font, mode, lookup)
compact_lookup(font, level, lookup)
elif lookup.LookupType == 9 and lookup.SubTable[0].ExtensionLookupType == 2:
compact_ext_lookup(font, mode, lookup)
compact_ext_lookup(font, level, lookup)
return font
def compact_lookup(font: TTFont, mode: str, lookup: otTables.Lookup) -> None:
new_subtables = compact_pair_pos(font, mode, lookup.SubTable)
def compact_lookup(font: TTFont, level: int, lookup: otTables.Lookup) -> None:
new_subtables = compact_pair_pos(font, level, lookup.SubTable)
lookup.SubTable = new_subtables
lookup.SubTableCount = len(new_subtables)
def compact_ext_lookup(font: TTFont, mode: str, lookup: otTables.Lookup) -> None:
def compact_ext_lookup(font: TTFont, level: int, lookup: otTables.Lookup) -> None:
new_subtables = compact_pair_pos(
font, mode, [ext_subtable.ExtSubTable for ext_subtable in lookup.SubTable]
font, level, [ext_subtable.ExtSubTable for ext_subtable in lookup.SubTable]
)
new_ext_subtables = []
for subtable in new_subtables:
@ -62,7 +56,7 @@ def compact_ext_lookup(font: TTFont, mode: str, lookup: otTables.Lookup) -> None
def compact_pair_pos(
font: TTFont, mode: str, subtables: Sequence[otTables.PairPos]
font: TTFont, level: int, subtables: Sequence[otTables.PairPos]
) -> Sequence[otTables.PairPos]:
new_subtables = []
for subtable in subtables:
@ -70,12 +64,12 @@ def compact_pair_pos(
# Not doing anything to Format 1 (yet?)
new_subtables.append(subtable)
elif subtable.Format == 2:
new_subtables.extend(compact_class_pairs(font, mode, subtable))
new_subtables.extend(compact_class_pairs(font, level, subtable))
return new_subtables
def compact_class_pairs(
font: TTFont, mode: str, subtable: otTables.PairPos
font: TTFont, level: int, subtable: otTables.PairPos
) -> List[otTables.PairPos]:
from fontTools.otlLib.builder import buildPairPosClassesSubtable
@ -95,17 +89,9 @@ def compact_class_pairs(
getattr(class2, "Value1", None),
getattr(class2, "Value2", None),
)
if len(mode) == 1 and mode in "123456789":
grouped_pairs = cluster_pairs_by_class2_coverage_custom_cost(
font, all_pairs, int(mode)
)
for pairs in grouped_pairs:
subtables.append(
buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap())
)
else:
raise ValueError(f"Bad {GPOS_COMPACT_MODE_ENV_KEY}={mode}")
grouped_pairs = cluster_pairs_by_class2_coverage_custom_cost(font, all_pairs, level)
for pairs in grouped_pairs:
subtables.append(buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap()))
return subtables

View File

@ -1,4 +1,6 @@
from fontTools.config import Config
from fontTools.misc import xmlWriter
from fontTools.misc.configTools import AbstractConfig
from fontTools.misc.textTools import Tag, byteord, tostr
from fontTools.misc.loggingTools import deprecateArgument
from fontTools.ttLib import TTLibError
@ -89,7 +91,7 @@ class TTFont(object):
sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0,
verbose=None, recalcBBoxes=True, allowVID=NotImplemented, ignoreDecompileErrors=False,
recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None,
_tableCache=None):
_tableCache=None, cfg={}):
for name in ("verbose", "quiet"):
val = locals().get(name)
if val is not None:
@ -101,6 +103,7 @@ class TTFont(object):
self.recalcTimestamp = recalcTimestamp
self.tables = {}
self.reader = None
self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg)
self.ignoreDecompileErrors = ignoreDecompileErrors
if not file:

View File

@ -15,11 +15,7 @@ from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo
from fontTools.varLib.varStore import VarStoreInstancer
from functools import reduce
from fontTools.otlLib.builder import buildSinglePos
from fontTools.otlLib.optimize.gpos import (
compact_pair_pos,
GPOS_COMPACT_MODE_DEFAULT,
GPOS_COMPACT_MODE_ENV_KEY,
)
from fontTools.otlLib.optimize.gpos import compact_pair_pos
log = logging.getLogger("fontTools.varLib.merger")
@ -850,10 +846,10 @@ def merge(merger, self, lst):
# Compact the merged subtables
# This is a good moment to do it because the compaction should create
# smaller subtables, which may prevent overflows from happening.
mode = os.environ.get(GPOS_COMPACT_MODE_ENV_KEY, GPOS_COMPACT_MODE_DEFAULT)
if mode and mode != "0":
level = merger.font.cfg["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"]
if level and level != 0:
log.info("Compacting GPOS...")
self.SubTable = compact_pair_pos(merger.font, mode, self.SubTable)
self.SubTable = compact_pair_pos(merger.font, level, self.SubTable)
self.SubTableCount = len(self.SubTable)
elif isSinglePos and flattened: