Merge pull request #2416 from daltonmaag/add-conf
Add config module as per #2349
This commit is contained in:
commit
afa0998be2
3
.gitignore
vendored
3
.gitignore
vendored
@ -58,3 +58,6 @@ Lib/**/*.c
|
||||
|
||||
# Ctags
|
||||
tags
|
||||
|
||||
# Documentation
|
||||
Doc/source/_build
|
||||
|
8
Doc/source/config.rst
Normal file
8
Doc/source/config.rst
Normal file
@ -0,0 +1,8 @@
|
||||
###########################
|
||||
config: configure fontTools
|
||||
###########################
|
||||
|
||||
.. automodule:: fontTools.config
|
||||
:inherited-members:
|
||||
:members:
|
||||
:undoc-members:
|
@ -69,6 +69,7 @@ libraries in the fontTools suite:
|
||||
- :py:mod:`fontTools.agl`: Access to the Adobe Glyph List
|
||||
- :py:mod:`fontTools.cffLib`: Read/write tools for Adobe CFF fonts
|
||||
- :py:mod:`fontTools.colorLib`: Module for handling colors in CPAL/COLR fonts
|
||||
- :py:mod:`fontTools.config`: Configure fontTools
|
||||
- :py:mod:`fontTools.cu2qu`: Module for cubic to quadratic conversion
|
||||
- :py:mod:`fontTools.designspaceLib`: Read and write designspace files
|
||||
- :py:mod:`fontTools.encodings`: Support for font-related character encodings
|
||||
@ -120,6 +121,7 @@ Table of Contents
|
||||
agl
|
||||
cffLib/index
|
||||
colorLib/index
|
||||
config
|
||||
cu2qu/index
|
||||
designspaceLib/index
|
||||
encodings/index
|
||||
|
8
Doc/source/misc/configTools.rst
Normal file
8
Doc/source/misc/configTools.rst
Normal file
@ -0,0 +1,8 @@
|
||||
###########
|
||||
configTools
|
||||
###########
|
||||
|
||||
.. automodule:: fontTools.misc.configTools
|
||||
:inherited-members:
|
||||
:members:
|
||||
:undoc-members:
|
@ -12,6 +12,7 @@ utilities by fontTools, but some of which may be more generally useful.
|
||||
bezierTools
|
||||
classifyTools
|
||||
cliTools
|
||||
configTools
|
||||
eexec
|
||||
encodingTools
|
||||
etree
|
||||
|
41
Lib/fontTools/config/__init__.py
Normal file
41
Lib/fontTools/config/__init__.py
Normal 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),
|
||||
)
|
274
Lib/fontTools/misc/configTools.py
Normal file
274
Lib/fontTools/misc/configTools.py
Normal file
@ -0,0 +1,274 @@
|
||||
"""
|
||||
Code of the config system; not related to fontTools or fonts in particular.
|
||||
|
||||
The options that are specific to fontTools are in :mod:`fontTools.config`.
|
||||
|
||||
To create your own config system, you need to create an instance of
|
||||
:class:`Options`, and a subclass of :class:`AbstractConfig` with its
|
||||
``options`` class variable set to your instance of Options.
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
Iterator,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
Union,
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"AbstractConfig",
|
||||
"ConfigAlreadyRegisteredError",
|
||||
"ConfigError",
|
||||
"ConfigUnknownOptionError",
|
||||
"ConfigValueParsingError",
|
||||
"ConfigValueValidationError",
|
||||
"Option",
|
||||
"Options",
|
||||
]
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
"""Base exception for the config module."""
|
||||
|
||||
|
||||
class ConfigAlreadyRegisteredError(ConfigError):
|
||||
"""Raised when a module tries to register a configuration option that
|
||||
already exists.
|
||||
|
||||
Should not be raised too much really, only when developing new fontTools
|
||||
modules.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Config option {name} is already registered.")
|
||||
|
||||
|
||||
class ConfigValueParsingError(ConfigError):
|
||||
"""Raised when a configuration value cannot be parsed."""
|
||||
|
||||
def __init__(self, name, value):
|
||||
super().__init__(
|
||||
f"Config option {name}: value cannot be parsed (given {repr(value)})"
|
||||
)
|
||||
|
||||
|
||||
class ConfigValueValidationError(ConfigError):
|
||||
"""Raised when a configuration value cannot be validated."""
|
||||
|
||||
def __init__(self, name, value):
|
||||
super().__init__(
|
||||
f"Config option {name}: value is invalid (given {repr(value)})"
|
||||
)
|
||||
|
||||
|
||||
_NO_VALUE = object()
|
||||
|
||||
|
||||
class ConfigUnknownOptionError(ConfigError):
|
||||
"""Raised when a configuration option is unknown."""
|
||||
|
||||
def __init__(self, name, value=_NO_VALUE):
|
||||
super().__init__(
|
||||
f"Config option {name} is unknown"
|
||||
+ ("" if value is _NO_VALUE else f" (with given value {repr(value)})")
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Option:
|
||||
help: str
|
||||
"""Help text for this option."""
|
||||
default: Any
|
||||
"""Default value for this option."""
|
||||
parse: Callable[[str], Any]
|
||||
"""Turn input (e.g. string) into proper type. Only when reading from file."""
|
||||
validate: Callable[[Any], bool]
|
||||
"""Return true if the given value is an acceptable value."""
|
||||
|
||||
|
||||
class Options(Mapping):
|
||||
"""Registry of available options for a given config system.
|
||||
|
||||
Define new options using the :meth:`register()` method.
|
||||
|
||||
Access existing options using the Mapping interface.
|
||||
"""
|
||||
|
||||
__options: Dict[str, Option]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.__options = {}
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
help: str,
|
||||
default: Any,
|
||||
parse: Callable[[str], Any],
|
||||
validate: Callable[[Any], bool],
|
||||
) -> Option:
|
||||
"""Register a new option."""
|
||||
return self.register_option(name, Option(help, default, parse, validate))
|
||||
|
||||
def register_option(self, name: str, option: Option) -> Option:
|
||||
"""Register a new option."""
|
||||
if name in self.__options:
|
||||
raise ConfigAlreadyRegisteredError(name)
|
||||
self.__options[name] = option
|
||||
return option
|
||||
|
||||
def __getitem__(self, __k: str) -> Option:
|
||||
return self.__options.__getitem__(__k)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return self.__options.__iter__()
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self.__options.__len__()
|
||||
|
||||
|
||||
_USE_GLOBAL_DEFAULT = object()
|
||||
|
||||
|
||||
class AbstractConfig(MutableMapping):
|
||||
"""
|
||||
Create a set of config values, optionally pre-filled with values from
|
||||
the given dictionary.
|
||||
|
||||
.. seealso:: :meth:`set()`
|
||||
|
||||
This config class is abstract because it needs its ``options`` class
|
||||
var to be set to an instance of :class:`Options` before it can be
|
||||
instanciated and used.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MyConfig(AbstractConfig):
|
||||
options = Options()
|
||||
|
||||
MyConfig.register_option( "test:option_name", "This is an option", 0, int, lambda v: isinstance(v, int))
|
||||
|
||||
cfg = MyConfig({"test:option_name": 10})
|
||||
"""
|
||||
|
||||
options: ClassVar[Options]
|
||||
|
||||
@classmethod
|
||||
def register_option(cls, *args, **kwargs) -> Option:
|
||||
"""Register an available option in this config system."""
|
||||
return cls.options.register(*args, **kwargs)
|
||||
|
||||
_values: Dict[str, Any]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
values: Union[AbstractConfig, Dict] = {},
|
||||
parse_values=False,
|
||||
skip_unknown=False,
|
||||
):
|
||||
self._values = {}
|
||||
values_dict = values._values if isinstance(values, AbstractConfig) else values
|
||||
for name, value in values_dict.items():
|
||||
self.set(name, value, parse_values, skip_unknown)
|
||||
|
||||
def set(self, name: str, value: Any, parse_values=False, skip_unknown=False):
|
||||
"""Set the value of an option.
|
||||
|
||||
Args:
|
||||
* `parse_values`: parse the configuration value from a string into
|
||||
its proper type, as per its `Option` object. The default
|
||||
behavior is to raise `ConfigValueValidationError` when the value
|
||||
is not of the right type. Useful when reading options from a
|
||||
file type that doesn't support as many types as Python.
|
||||
* `skip_unknown`: skip unknown configuration options. The default
|
||||
behaviour is to raise `ConfigUnknownOptionError`. Useful when
|
||||
reading options from a configuration file that has extra entries
|
||||
(e.g. for a later version of fontTools)
|
||||
"""
|
||||
try:
|
||||
option = self.options[name]
|
||||
except KeyError:
|
||||
if skip_unknown:
|
||||
log.debug(
|
||||
"Config option %s is unknown (with given value %r)", name, value
|
||||
)
|
||||
return
|
||||
else:
|
||||
raise ConfigUnknownOptionError(name, value)
|
||||
|
||||
# Can be useful if the values come from a source that doesn't have
|
||||
# strict typing (.ini file? Terminal input?)
|
||||
if parse_values:
|
||||
try:
|
||||
value = option.parse(value)
|
||||
except Exception as e:
|
||||
raise ConfigValueParsingError(name, value) from e
|
||||
|
||||
if not option.validate(value):
|
||||
raise ConfigValueValidationError(name, value)
|
||||
|
||||
self._values[name] = value
|
||||
|
||||
def get(self, name: str, default=_USE_GLOBAL_DEFAULT):
|
||||
"""
|
||||
Get the value of an option. The value which is returned is the first
|
||||
provided among:
|
||||
|
||||
1. a user-provided value in the options's ``self._values`` dict
|
||||
2. a caller-provided default value to this method call
|
||||
3. the global default for the option provided in ``fontTools.config``
|
||||
|
||||
This is to provide the ability to migrate progressively from config
|
||||
options passed as arguments to fontTools APIs to config options read
|
||||
from the current TTFont, e.g.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def fontToolsAPI(font, some_option):
|
||||
value = font.cfg.get("someLib.module:SOME_OPTION", some_option)
|
||||
# use value
|
||||
|
||||
That way, the function will work the same for users of the API that
|
||||
still pass the option to the function call, but will favour the new
|
||||
config mechanism if the given font specifies a value for that option.
|
||||
"""
|
||||
if name in self._values:
|
||||
return self._values[name]
|
||||
if default is not _USE_GLOBAL_DEFAULT:
|
||||
return default
|
||||
try:
|
||||
return self.options[name].default
|
||||
except KeyError as e:
|
||||
raise ConfigUnknownOptionError(name) from e
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self._values)
|
||||
|
||||
def __getitem__(self, name: str) -> Any:
|
||||
return self.get(name)
|
||||
|
||||
def __setitem__(self, name: str, value: Any) -> None:
|
||||
return self.set(name, value)
|
||||
|
||||
def __delitem__(self, name: str) -> None:
|
||||
del self._values[name]
|
||||
|
||||
def __iter__(self) -> Iterator:
|
||||
return self._values.__iter__()
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._values)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({repr(self._values)})"
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
)
|
||||
grouped_pairs = cluster_pairs_by_class2_coverage_custom_cost(font, all_pairs, level)
|
||||
for pairs in grouped_pairs:
|
||||
subtables.append(
|
||||
buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap())
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Bad {GPOS_COMPACT_MODE_ENV_KEY}={mode}")
|
||||
subtables.append(buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap()))
|
||||
return subtables
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
121
Tests/config_test.py
Normal file
121
Tests/config_test.py
Normal file
@ -0,0 +1,121 @@
|
||||
from fontTools.misc.configTools import AbstractConfig, Options
|
||||
import pytest
|
||||
from fontTools.config import (
|
||||
Config,
|
||||
ConfigUnknownOptionError,
|
||||
ConfigValueParsingError,
|
||||
ConfigValueValidationError,
|
||||
)
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
|
||||
def test_can_register_option():
|
||||
MY_OPTION = Config.register_option(
|
||||
name="tests:MY_OPTION",
|
||||
help="Test option, value should be True or False, default = True",
|
||||
default=True,
|
||||
parse=lambda v: v in ("True", "true", 1, True),
|
||||
validate=lambda v: v == True or v == False,
|
||||
)
|
||||
|
||||
assert (
|
||||
MY_OPTION.help == "Test option, value should be True or False, default = True"
|
||||
)
|
||||
assert MY_OPTION.default == True
|
||||
assert MY_OPTION.parse("true") == True
|
||||
assert MY_OPTION.validate("hello") == False
|
||||
|
||||
ttFont = TTFont(cfg={"tests:MY_OPTION": True})
|
||||
assert True == ttFont.cfg.get("tests:MY_OPTION")
|
||||
|
||||
|
||||
COMPRESSION_LEVEL = "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"
|
||||
|
||||
|
||||
def test_ttfont_has_config():
|
||||
ttFont = TTFont(cfg={COMPRESSION_LEVEL: 8})
|
||||
assert 8 == ttFont.cfg.get(COMPRESSION_LEVEL)
|
||||
|
||||
|
||||
def test_ttfont_can_take_superset_of_fonttools_config():
|
||||
# Create MyConfig with all options from fontTools.config plus some
|
||||
my_options = Options()
|
||||
for name, option in Config.options.items():
|
||||
my_options.register_option(name, option)
|
||||
|
||||
my_options.register("custom:my_option", "help", "default", str, any)
|
||||
|
||||
class MyConfig(AbstractConfig):
|
||||
options = my_options
|
||||
|
||||
ttFont = TTFont(cfg=MyConfig({"custom:my_option": "my_value"}))
|
||||
assert 0 == ttFont.cfg.get(COMPRESSION_LEVEL)
|
||||
assert "my_value" == ttFont.cfg.get("custom:my_option")
|
||||
|
||||
|
||||
def test_no_config_returns_default_values():
|
||||
ttFont = TTFont()
|
||||
assert 0 == ttFont.cfg.get(COMPRESSION_LEVEL)
|
||||
assert 3 == ttFont.cfg.get(COMPRESSION_LEVEL, 3)
|
||||
|
||||
|
||||
def test_can_set_config():
|
||||
ttFont = TTFont()
|
||||
ttFont.cfg.set(COMPRESSION_LEVEL, 5)
|
||||
assert 5 == ttFont.cfg.get(COMPRESSION_LEVEL)
|
||||
ttFont.cfg.set(COMPRESSION_LEVEL, 6)
|
||||
assert 6 == ttFont.cfg.get(COMPRESSION_LEVEL)
|
||||
|
||||
|
||||
def test_different_ttfonts_have_different_configs():
|
||||
cfg = Config({COMPRESSION_LEVEL: 5})
|
||||
ttFont1 = TTFont(cfg=cfg)
|
||||
ttFont2 = TTFont(cfg=cfg)
|
||||
ttFont2.cfg.set(COMPRESSION_LEVEL, 6)
|
||||
assert 5 == ttFont1.cfg.get(COMPRESSION_LEVEL)
|
||||
assert 6 == ttFont2.cfg.get(COMPRESSION_LEVEL)
|
||||
|
||||
|
||||
def test_cannot_set_inexistent_key():
|
||||
with pytest.raises(ConfigUnknownOptionError):
|
||||
TTFont(cfg={"notALib.notAModule.inexistent": 4})
|
||||
|
||||
|
||||
def test_value_not_parsed_by_default():
|
||||
# Note: value given as a string
|
||||
with pytest.raises(ConfigValueValidationError):
|
||||
TTFont(cfg={COMPRESSION_LEVEL: "8"})
|
||||
|
||||
|
||||
def test_value_gets_parsed_if_asked():
|
||||
# Note: value given as a string
|
||||
ttFont = TTFont(cfg=Config({COMPRESSION_LEVEL: "8"}, parse_values=True))
|
||||
assert 8 == ttFont.cfg.get(COMPRESSION_LEVEL)
|
||||
|
||||
|
||||
def test_value_parsing_can_error():
|
||||
with pytest.raises(ConfigValueParsingError):
|
||||
TTFont(
|
||||
cfg=Config(
|
||||
{COMPRESSION_LEVEL: "not an int"},
|
||||
parse_values=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_value_gets_validated():
|
||||
# Note: 12 is not a valid value for GPOS compression level (must be in 0-9)
|
||||
with pytest.raises(ConfigValueValidationError):
|
||||
TTFont(cfg={COMPRESSION_LEVEL: 12})
|
||||
|
||||
|
||||
def test_implements_mutable_mapping():
|
||||
cfg = Config()
|
||||
cfg[COMPRESSION_LEVEL] = 2
|
||||
assert 2 == cfg[COMPRESSION_LEVEL]
|
||||
assert [COMPRESSION_LEVEL] == list(iter(cfg))
|
||||
assert 1 == len(cfg)
|
||||
del cfg[COMPRESSION_LEVEL]
|
||||
assert 0 == cfg[COMPRESSION_LEVEL]
|
||||
assert [] == list(iter(cfg))
|
||||
assert 0 == len(cfg)
|
34
Tests/misc/configTools_test.py
Normal file
34
Tests/misc/configTools_test.py
Normal file
@ -0,0 +1,34 @@
|
||||
import pytest
|
||||
|
||||
from fontTools.misc.configTools import AbstractConfig, Options, ConfigUnknownOptionError
|
||||
|
||||
|
||||
def test_can_create_custom_config_system():
|
||||
class MyConfig(AbstractConfig):
|
||||
options = Options()
|
||||
|
||||
MyConfig.register_option(
|
||||
"test:option_name",
|
||||
"This is an option",
|
||||
0,
|
||||
int,
|
||||
lambda v: isinstance(v, int),
|
||||
)
|
||||
|
||||
cfg = MyConfig({"test:option_name": "10"}, parse_values=True)
|
||||
|
||||
assert 10 == cfg["test:option_name"]
|
||||
|
||||
# This config is independent from "the" fontTools config
|
||||
with pytest.raises(ConfigUnknownOptionError):
|
||||
MyConfig({"fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL": 4})
|
||||
|
||||
# Test the repr()
|
||||
assert repr(cfg) == "MyConfig({'test:option_name': 10})"
|
||||
|
||||
# Test the skip_unknown param: just check that the following does not raise
|
||||
MyConfig({"test:unknown": "whatever"}, skip_unknown=True)
|
||||
|
||||
# Test that it raises on unknown option
|
||||
with pytest.raises(ConfigUnknownOptionError):
|
||||
cfg.get("test:unknown")
|
@ -1,17 +1,15 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
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
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.ttLib.tables.otBase import OTTableWriter
|
||||
|
||||
|
||||
def test_main(tmpdir: Path):
|
||||
@ -34,7 +32,7 @@ def test_main(tmpdir: Path):
|
||||
[
|
||||
"fonttools",
|
||||
"otlLib.optimize",
|
||||
"--gpos-compact-mode",
|
||||
"--gpos-compression-level",
|
||||
"5",
|
||||
str(input),
|
||||
"-o",
|
||||
@ -127,9 +125,9 @@ def get_kerning_by_blocks(blocks: List[Tuple[int, int]]) -> Tuple[List[str], str
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("blocks", "mode", "expected_subtables", "expected_bytes"),
|
||||
("blocks", "level", "expected_subtables", "expected_bytes"),
|
||||
[
|
||||
# Mode = 0 = no optimization leads to 650 bytes of GPOS
|
||||
# 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
|
||||
@ -143,13 +141,13 @@ def get_kerning_by_blocks(blocks: List[Tuple[int, int]]) -> Tuple[List[str], str
|
||||
([(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)
|
||||
([(10, 10)], 9, 1, 304),
|
||||
],
|
||||
)
|
||||
def test_optimization_mode(
|
||||
caplog,
|
||||
blocks: List[Tuple[int, int]],
|
||||
mode: Optional[int],
|
||||
level: Optional[int],
|
||||
expected_subtables: int,
|
||||
expected_bytes: int,
|
||||
):
|
||||
@ -161,14 +159,9 @@ def test_optimization_mode(
|
||||
glyphs, features = get_kerning_by_blocks(blocks)
|
||||
glyphs = [".notdef space"] + glyphs
|
||||
|
||||
env = {}
|
||||
if mode is not None:
|
||||
# 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
|
||||
env["FONTTOOLS_GPOS_COMPACT_MODE"] = str(mode)
|
||||
with set_env(**env):
|
||||
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user