From abc04419577e198908236235fd6375df1a443063 Mon Sep 17 00:00:00 2001 From: Jany Belluz Date: Thu, 14 Apr 2022 15:23:02 +0100 Subject: [PATCH] [config] Add new config module and use it for GPOS compression level --- Lib/fontTools/config/__init__.py | 41 +++++++++++++++++++++++ Lib/fontTools/otlLib/builder.py | 12 +++---- Lib/fontTools/otlLib/optimize/__init__.py | 40 ++++++++-------------- Lib/fontTools/otlLib/optimize/gpos.py | 40 +++++++--------------- Lib/fontTools/ttLib/ttFont.py | 7 ++-- Lib/fontTools/varLib/merger.py | 12 +++---- 6 files changed, 81 insertions(+), 71 deletions(-) create mode 100644 Lib/fontTools/config/__init__.py diff --git a/Lib/fontTools/config/__init__.py b/Lib/fontTools/config/__init__.py new file mode 100644 index 000000000..b2834cdd2 --- /dev/null +++ b/Lib/fontTools/config/__init__.py @@ -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), +) diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index cec1ec317..0e2de3778 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -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 diff --git a/Lib/fontTools/otlLib/optimize/__init__.py b/Lib/fontTools/otlLib/optimize/__init__.py index 5c007e891..38c02815b 100644 --- a/Lib/fontTools/otlLib/optimize/__init__.py +++ b/Lib/fontTools/otlLib/optimize/__init__.py @@ -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) - diff --git a/Lib/fontTools/otlLib/optimize/gpos.py b/Lib/fontTools/otlLib/optimize/gpos.py index 79873fadb..eb0456f29 100644 --- a/Lib/fontTools/otlLib/optimize/gpos.py +++ b/Lib/fontTools/otlLib/optimize/gpos.py @@ -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 diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py index 9c779243a..d7f7ef835 100644 --- a/Lib/fontTools/ttLib/ttFont.py +++ b/Lib/fontTools/ttLib/ttFont.py @@ -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 @@ -49,7 +51,7 @@ class TTFont(object): >> tt2.importXML("afont.ttx") >> tt2['maxp'].numGlyphs 242 - + The TTFont object may be used as a context manager; this will cause the file reader to be closed after the context ``with`` block is exited:: @@ -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: diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py index 5a3a4f343..515e66b1a 100644 --- a/Lib/fontTools/varLib/merger.py +++ b/Lib/fontTools/varLib/merger.py @@ -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: