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

View File

@ -1,39 +1,30 @@
from argparse import RawTextHelpFormatter 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.ttLib import TTFont
from fontTools.otlLib.optimize.gpos import compact, GPOS_COMPACT_MODE_DEFAULT
def main(args=None): def main(args=None):
"""Optimize the layout tables of an existing font.""" """Optimize the layout tables of an existing font."""
from argparse import ArgumentParser from argparse import ArgumentParser
from fontTools import configLogger 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("font")
parser.add_argument( parser.add_argument(
"-o", metavar="OUTPUTFILE", dest="outfile", default=None, help="output file" "-o", metavar="OUTPUTFILE", dest="outfile", default=None, help="output file"
) )
COMPRESSION_LEVEL = OPTIONS["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"]
parser.add_argument( parser.add_argument(
"--gpos-compact-mode", "--gpos-compression-level",
help=dedent( help=COMPRESSION_LEVEL.help,
f"""\ default=COMPRESSION_LEVEL.default,
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),
choices=list(range(10)), choices=list(range(10)),
type=int, type=int,
) )
@ -51,12 +42,10 @@ def main(args=None):
) )
font = TTFont(options.font) font = TTFont(options.font)
# TODO: switch everything to have type(mode) = int when using the Config class compact(font, options.gpos_compression_level)
compact(font, str(options.gpos_compact_mode))
font.save(options.outfile or options.font) font.save(options.outfile or options.font)
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
@ -65,4 +54,3 @@ if __name__ == "__main__":
import doctest import doctest
sys.exit(doctest.testmod().failed) 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 import TTFont
from fontTools.ttLib.tables import otBase, otTables 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") log = logging.getLogger("fontTools.otlLib.optimize.gpos")
def compact(font: TTFont, mode: str) -> TTFont: def compact(font: TTFont, level: int) -> TTFont:
# Ideal plan: # Ideal plan:
# 1. Find lookups of Lookup Type 2: Pair Adjustment Positioning Subtable # 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 # 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"] gpos = font["GPOS"]
for lookup in gpos.table.LookupList.Lookup: for lookup in gpos.table.LookupList.Lookup:
if lookup.LookupType == 2: if lookup.LookupType == 2:
compact_lookup(font, mode, lookup) compact_lookup(font, level, lookup)
elif lookup.LookupType == 9 and lookup.SubTable[0].ExtensionLookupType == 2: elif lookup.LookupType == 9 and lookup.SubTable[0].ExtensionLookupType == 2:
compact_ext_lookup(font, mode, lookup) compact_ext_lookup(font, level, lookup)
return font return font
def compact_lookup(font: TTFont, mode: str, lookup: otTables.Lookup) -> None: def compact_lookup(font: TTFont, level: int, lookup: otTables.Lookup) -> None:
new_subtables = compact_pair_pos(font, mode, lookup.SubTable) new_subtables = compact_pair_pos(font, level, lookup.SubTable)
lookup.SubTable = new_subtables lookup.SubTable = new_subtables
lookup.SubTableCount = len(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( 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 = [] new_ext_subtables = []
for subtable in new_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( def compact_pair_pos(
font: TTFont, mode: str, subtables: Sequence[otTables.PairPos] font: TTFont, level: int, subtables: Sequence[otTables.PairPos]
) -> Sequence[otTables.PairPos]: ) -> Sequence[otTables.PairPos]:
new_subtables = [] new_subtables = []
for subtable in subtables: for subtable in subtables:
@ -70,12 +64,12 @@ def compact_pair_pos(
# Not doing anything to Format 1 (yet?) # Not doing anything to Format 1 (yet?)
new_subtables.append(subtable) new_subtables.append(subtable)
elif subtable.Format == 2: 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 return new_subtables
def compact_class_pairs( def compact_class_pairs(
font: TTFont, mode: str, subtable: otTables.PairPos font: TTFont, level: int, subtable: otTables.PairPos
) -> List[otTables.PairPos]: ) -> List[otTables.PairPos]:
from fontTools.otlLib.builder import buildPairPosClassesSubtable from fontTools.otlLib.builder import buildPairPosClassesSubtable
@ -95,17 +89,9 @@ def compact_class_pairs(
getattr(class2, "Value1", None), getattr(class2, "Value1", None),
getattr(class2, "Value2", None), getattr(class2, "Value2", None),
) )
grouped_pairs = cluster_pairs_by_class2_coverage_custom_cost(font, all_pairs, level)
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: for pairs in grouped_pairs:
subtables.append( subtables.append(buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap()))
buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap())
)
else:
raise ValueError(f"Bad {GPOS_COMPACT_MODE_ENV_KEY}={mode}")
return subtables return subtables

View File

@ -1,4 +1,6 @@
from fontTools.config import Config
from fontTools.misc import xmlWriter from fontTools.misc import xmlWriter
from fontTools.misc.configTools import AbstractConfig
from fontTools.misc.textTools import Tag, byteord, tostr from fontTools.misc.textTools import Tag, byteord, tostr
from fontTools.misc.loggingTools import deprecateArgument from fontTools.misc.loggingTools import deprecateArgument
from fontTools.ttLib import TTLibError from fontTools.ttLib import TTLibError
@ -89,7 +91,7 @@ class TTFont(object):
sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0, sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0,
verbose=None, recalcBBoxes=True, allowVID=NotImplemented, ignoreDecompileErrors=False, verbose=None, recalcBBoxes=True, allowVID=NotImplemented, ignoreDecompileErrors=False,
recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None, recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None,
_tableCache=None): _tableCache=None, cfg={}):
for name in ("verbose", "quiet"): for name in ("verbose", "quiet"):
val = locals().get(name) val = locals().get(name)
if val is not None: if val is not None:
@ -101,6 +103,7 @@ class TTFont(object):
self.recalcTimestamp = recalcTimestamp self.recalcTimestamp = recalcTimestamp
self.tables = {} self.tables = {}
self.reader = None self.reader = None
self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg)
self.ignoreDecompileErrors = ignoreDecompileErrors self.ignoreDecompileErrors = ignoreDecompileErrors
if not file: 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 fontTools.varLib.varStore import VarStoreInstancer
from functools import reduce from functools import reduce
from fontTools.otlLib.builder import buildSinglePos from fontTools.otlLib.builder import buildSinglePos
from fontTools.otlLib.optimize.gpos import ( from fontTools.otlLib.optimize.gpos import compact_pair_pos
compact_pair_pos,
GPOS_COMPACT_MODE_DEFAULT,
GPOS_COMPACT_MODE_ENV_KEY,
)
log = logging.getLogger("fontTools.varLib.merger") log = logging.getLogger("fontTools.varLib.merger")
@ -850,10 +846,10 @@ def merge(merger, self, lst):
# Compact the merged subtables # Compact the merged subtables
# This is a good moment to do it because the compaction should create # This is a good moment to do it because the compaction should create
# smaller subtables, which may prevent overflows from happening. # smaller subtables, which may prevent overflows from happening.
mode = os.environ.get(GPOS_COMPACT_MODE_ENV_KEY, GPOS_COMPACT_MODE_DEFAULT) level = merger.font.cfg["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"]
if mode and mode != "0": if level and level != 0:
log.info("Compacting GPOS...") 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) self.SubTableCount = len(self.SubTable)
elif isSinglePos and flattened: elif isSinglePos and flattened: