make USE_HARFBUZZ_REPACKER a 3-state option, defaults to auto

if explicitly enabled, it will raise ImportError if uharfbuzz is not found, and will propagate the uharfbuzz error instead of silently falling back to the pure-python serializer
This commit is contained in:
Cosimo Lupo 2022-04-21 18:11:20 +01:00
parent 7588062413
commit af6804bed5
6 changed files with 100 additions and 26 deletions

View File

@ -49,9 +49,11 @@ Config.register_option(
FontTools tries to use the HarfBuzz Repacker to serialize GPOS/GSUB tables FontTools tries to use the HarfBuzz Repacker to serialize GPOS/GSUB tables
if the uharfbuzz python bindings are importable, otherwise falls back to its if the uharfbuzz python bindings are importable, otherwise falls back to its
slower, less efficient serializer. Set to False to always use the latter. slower, less efficient serializer. Set to False to always use the latter.
Set to True to explicitly request the HarfBuzz Repacker (will raise an
error if uharfbuzz cannot be imported).
""" """
), ),
default=True, default=None,
parse=lambda s: str(s).lower() not in {"0", "no", "false"}, parse=Option.parse_optional_bool,
validate=lambda v: isinstance(v, bool), validate=Option.validate_optional_bool,
) )

View File

@ -100,6 +100,21 @@ class Option:
validate: Optional[Callable[[Any], bool]] = None validate: Optional[Callable[[Any], bool]] = None
"""Return true if the given value is an acceptable value.""" """Return true if the given value is an acceptable value."""
@staticmethod
def parse_optional_bool(v: str) -> Optional[bool]:
s = str(v).lower()
if s in {"0", "no", "false"}:
return False
if s in {"1", "yes", "true"}:
return True
if s in {"auto", "none"}:
return None
raise ValueError("invalid optional bool: {v!r}")
@staticmethod
def validate_optional_bool(v: Any) -> bool:
return v is None or isinstance(v, bool)
class Options(Mapping): class Options(Mapping):
"""Registry of available options for a given config system. """Registry of available options for a given config system.

View File

@ -146,9 +146,11 @@ Output options
The Zopfli Python bindings are available at: The Zopfli Python bindings are available at:
https://pypi.python.org/pypi/zopfli https://pypi.python.org/pypi/zopfli
--harfbuzz-repacker [default] --harfbuzz-repacker
Serialize GPOS/GSUB using the HarfBuzz Repacker if uharfbuzz can be By default, we serialize GPOS/GSUB using the HarfBuzz Repacker when
imported [default]. uharfbuzz can be imported and is successful, otherwise fall back to
the pure-python serializer. Set the option to force using the HarfBuzz
Repacker (raises an error if uharfbuzz can't be found or fails).
--no-harfbuzz-repacker --no-harfbuzz-repacker
Always use the pure-python serializer even if uharfbuzz is available. Always use the pure-python serializer even if uharfbuzz is available.

View File

@ -76,14 +76,21 @@ class BaseTTXConverter(DefaultTable):
# If a lookup subtable overflows an offset, we have to start all over. # If a lookup subtable overflows an offset, we have to start all over.
overflowRecord = None overflowRecord = None
# this is 3-state option: default (None) means automatically use hb.repack or
# silently fall back if it fails; True, use it and raise error if not possible
# or it errors out; False, don't use it, even if you can.
use_hb_repack = font.cfg[USE_HARFBUZZ_REPACKER] use_hb_repack = font.cfg[USE_HARFBUZZ_REPACKER]
if self.tableTag in ("GSUB", "GPOS"): if self.tableTag in ("GSUB", "GPOS"):
if not use_hb_repack: if use_hb_repack is False:
log.debug( log.debug(
"hb.repack disabled, compiling '%s' with pure-python serializer", "hb.repack disabled, compiling '%s' with pure-python serializer",
self.tableTag, self.tableTag,
) )
elif not have_uharfbuzz: elif not have_uharfbuzz:
if use_hb_repack is True:
raise ImportError("No module named 'uharfbuzz'")
else:
assert use_hb_repack is None
log.debug( log.debug(
"uharfbuzz not found, compiling '%s' with pure-python serializer", "uharfbuzz not found, compiling '%s' with pure-python serializer",
self.tableTag, self.tableTag,
@ -93,11 +100,16 @@ class BaseTTXConverter(DefaultTable):
try: try:
writer = OTTableWriter(tableTag=self.tableTag) writer = OTTableWriter(tableTag=self.tableTag)
self.table.compile(writer, font) self.table.compile(writer, font)
if use_hb_repack and have_uharfbuzz and self.tableTag in ("GSUB", "GPOS"): if (
use_hb_repack in (None, True)
and have_uharfbuzz
and self.tableTag in ("GSUB", "GPOS")
):
try: try:
log.debug("serializing '%s' with hb.repack", self.tableTag) log.debug("serializing '%s' with hb.repack", self.tableTag)
return writer.getAllDataUsingHarfbuzz() return writer.getAllDataUsingHarfbuzz()
except (ValueError, MemoryError, hb.RepackerError) as e: except (ValueError, MemoryError, hb.RepackerError) as e:
if use_hb_repack is None:
log.error( log.error(
"hb.repack failed to serialize '%s', reverting to " "hb.repack failed to serialize '%s', reverting to "
"pure-python serializer; the error message was: %s", "pure-python serializer; the error message was: %s",
@ -105,6 +117,8 @@ class BaseTTXConverter(DefaultTable):
e, e,
) )
return writer.getAllData(remove_duplicate=False) return writer.getAllData(remove_duplicate=False)
# let the error propagate if USE_HARFBUZZ_REPACKER is True
raise
return writer.getAllData() return writer.getAllData()
except OTLOffsetOverflowError as e: except OTLOffsetOverflowError as e:

View File

@ -57,3 +57,24 @@ def test_options_are_unique():
cfg.get(opt2) cfg.get(opt2)
with pytest.raises(ConfigUnknownOptionError): with pytest.raises(ConfigUnknownOptionError):
cfg.set(opt2, "bar") cfg.set(opt2, "bar")
def test_optional_bool():
for v in ("yes", "YES", "Yes", "1", "True", "true", "TRUE"):
assert Option.parse_optional_bool(v) is True
for v in ("no", "NO", "No", "0", "False", "false", "FALSE"):
assert Option.parse_optional_bool(v) is False
for v in ("auto", "AUTO", "Auto", "None", "none", "NONE"):
assert Option.parse_optional_bool(v) is None
with pytest.raises(ValueError, match="invalid optional bool"):
Option.parse_optional_bool("foobar")
assert Option.validate_optional_bool(True)
assert Option.validate_optional_bool(False)
assert Option.validate_optional_bool(None)
assert not Option.validate_optional_bool(1)
assert not Option.validate_optional_bool(0)
assert not Option.validate_optional_bool("1")

View File

@ -802,6 +802,8 @@ class SubsetTest:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"installed, enabled, ok", "installed, enabled, ok",
[ [
pytest.param(True, None, True, id="installed-auto-ok"),
pytest.param(True, None, True, id="installed-auto-fail"),
pytest.param(True, True, True, id="installed-enabled-ok"), pytest.param(True, True, True, id="installed-enabled-ok"),
pytest.param(True, True, False, id="installed-enabled-fail"), pytest.param(True, True, False, id="installed-enabled-fail"),
pytest.param(True, False, True, id="installed-disabled"), pytest.param(True, False, True, id="installed-disabled"),
@ -839,8 +841,24 @@ class SubsetTest:
"--layout-features=*", "--layout-features=*",
f"--output-file={subsetpath}", f"--output-file={subsetpath}",
] ]
if not enabled: if enabled is True:
args.append("--harfbuzz-repacker")
elif enabled is False:
args.append("--no-harfbuzz-repacker") args.append("--no-harfbuzz-repacker")
# elif enabled is None: ... is the default
if enabled is True:
if not installed:
# raise if enabled but not installed
with pytest.raises(ImportError, match="uharfbuzz"):
subset.main(args)
return
elif not ok:
# raise if enabled but fails
with pytest.raises(hb.RepackerError, match="mocking"):
subset.main(args)
return
with caplog.at_level(logging.DEBUG, "fontTools.ttLib.tables.otBase"): with caplog.at_level(logging.DEBUG, "fontTools.ttLib.tables.otBase"):
subset.main(args) subset.main(args)
@ -851,14 +869,16 @@ class SubsetTest:
subsetfont, self.getpath("expect_harfbuzz_repacker.ttx"), ["GSUB"] subsetfont, self.getpath("expect_harfbuzz_repacker.ttx"), ["GSUB"]
) )
if enabled: if enabled or enabled is None:
if installed: if installed:
assert "serializing 'GSUB' with hb.repack" in caplog.text assert "serializing 'GSUB' with hb.repack" in caplog.text
else:
if enabled is None and not installed:
assert ( assert (
"uharfbuzz not found, compiling 'GSUB' with pure-python serializer" "uharfbuzz not found, compiling 'GSUB' with pure-python serializer"
) in caplog.text ) in caplog.text
else:
if enabled is False:
assert ( assert (
"hb.repack disabled, compiling 'GSUB' with pure-python serializer" "hb.repack disabled, compiling 'GSUB' with pure-python serializer"
) in caplog.text ) in caplog.text