Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ftConfig parameter to set fontTools' TTFont.cfg options #607

Merged
merged 4 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 45 additions & 31 deletions Lib/ufo2ft/_compilers/baseCompiler.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import logging
import os
from collections import defaultdict
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Callable, Optional, Type

from fontTools import varLib
from fontTools.designspaceLib.split import splitInterpolable, splitVariableFonts
from fontTools.misc.loggingTools import Timer
from fontTools.otlLib.optimize.gpos import GPOS_COMPACT_MODE_ENV_KEY
from fontTools.otlLib.optimize.gpos import COMPRESSION_LEVEL as GPOS_COMPRESSION_LEVEL

from ufo2ft.constants import MTI_FEATURES_PREFIX
from ufo2ft.errors import InvalidDesignSpaceData
Expand All @@ -22,6 +21,7 @@
_notdefGlyphFallback,
colrClipBoxQuantization,
ensure_all_sources_have_names,
getDefaultMasterFont,
location_to_string,
prune_unknown_kwargs,
)
Expand All @@ -47,6 +47,7 @@ class BaseCompiler:
colrClipBoxQuantization: Callable[[object], int] = colrClipBoxQuantization
feaIncludeDir: Optional[str] = None
skipFeatureCompilation: bool = False
ftConfig: dict = field(default_factory=dict)
_tables: Optional[list] = None

def __post_init__(self):
Expand Down Expand Up @@ -269,38 +270,51 @@ def _compileNeededSources(self, designSpaceDoc):
originalSources = {}
originalGlyphsets = {}

# Compile all needed sources in each interpolable subspace to make sure
# they're all compatible; that also ensures that sub-vfs within the same
# interpolable sub-space are compatible too.
for subDoc in interpolableSubDocs:
# Only keep the sources that we've identified earlier as need-to-compile
subDoc.sources = [s for s in subDoc.sources if s.name in sourcesToCompile]
if not subDoc.sources:
continue

# FIXME: Hack until we get a fontTools config module. Disable GPOS
# compaction while building masters because the compaction will be undone
# anyway by varLib merge and then done again on the VF
gpos_compact_value = os.environ.pop(GPOS_COMPACT_MODE_ENV_KEY, None)
save_production_names = self.useProductionNames
self.useProductionNames = False
save_postprocessor = self.postProcessorClass
self.postProcessorClass = None
self.skipFeatureCompilation = can_optimize_features
try:
# Disable GPOS compaction while building masters because the compaction
# will be undone anyway by varLib merge and then done again on the final VF
gpos_compact_value = self.ftConfig.pop(GPOS_COMPRESSION_LEVEL, None)
# we want to rename glyphs only on the final VF and skip postprocessing masters
save_production_names, self.useProductionNames = self.useProductionNames, False
save_postprocessor, self.postProcessorClass = self.postProcessorClass, None
# skip per-master feature compilation if we are building variable features
save_skip_features, self.skipFeatureCompilation = (
self.skipFeatureCompilation,
can_optimize_features,
)
try:
# Compile all needed sources in each interpolable subspace to make sure
# they're all compatible; that also ensures that sub-vfs within the same
# interpolable sub-space are compatible too.
for subDoc in interpolableSubDocs:
# Only keep the sources that we've identified earlier as need-to-compile
subDoc.sources = [
s for s in subDoc.sources if s.name in sourcesToCompile
]
if not subDoc.sources:
continue

ttfDesignSpace = self.compile_designspace(subDoc)
finally:

if gpos_compact_value is not None:
os.environ[GPOS_COMPACT_MODE_ENV_KEY] = gpos_compact_value
# the VF will inherit the config from the base TTF master
baseTtf = getDefaultMasterFont(ttfDesignSpace)
baseTtf.cfg[GPOS_COMPRESSION_LEVEL] = gpos_compact_value

# Stick TTFs back into original big DS
for ttfSource, glyphSet in zip(ttfDesignSpace.sources, self.glyphSets):
if can_optimize_features:
originalSources[ttfSource.name] = sourcesByName[
ttfSource.name
].font
sourcesByName[ttfSource.name].font = ttfSource.font
originalGlyphsets[ttfSource.name] = glyphSet
finally:
# can restore self to its original state
if gpos_compact_value is not None:
self.ftConfig[GPOS_COMPRESSION_LEVEL] = gpos_compact_value
self.postProcessorClass = save_postprocessor
self.useProductionNames = save_production_names

# Stick TTFs back into original big DS
for ttfSource, glyphSet in zip(ttfDesignSpace.sources, self.glyphSets):
if can_optimize_features:
originalSources[ttfSource.name] = sourcesByName[ttfSource.name].font
sourcesByName[ttfSource.name].font = ttfSource.font
originalGlyphsets[ttfSource.name] = glyphSet
self.skipFeatureCompilation = save_skip_features

return (
vfNameToBaseUfo,
Expand Down
8 changes: 7 additions & 1 deletion Lib/ufo2ft/outlineCompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def __init__(
colrLayerReuse=True,
colrAutoClipBoxes=True,
colrClipBoxQuantization=colrClipBoxQuantization,
ftConfig=None,
):
self.ufo = font
# use the previously filtered glyphSet, if any
Expand All @@ -126,6 +127,7 @@ def __init__(
self.colrLayerReuse = colrLayerReuse
self.colrAutoClipBoxes = colrAutoClipBoxes
self.colrClipBoxQuantization = colrClipBoxQuantization
self.ftConfig = ftConfig or {}
# cached values defined later on
self._glyphBoundingBoxes = None
self._fontBoundingBox = None
Expand All @@ -136,7 +138,7 @@ def compile(self):
"""
Compile the OpenType binary.
"""
self.otf = TTFont(sfntVersion=self.sfntVersion)
self.otf = TTFont(sfntVersion=self.sfntVersion, cfg=self.ftConfig)

# only compile vertical metrics tables if vhea metrics are defined
vertical_metrics = [
Expand Down Expand Up @@ -1104,6 +1106,7 @@ def __init__(
colrLayerReuse=True,
colrAutoClipBoxes=True,
colrClipBoxQuantization=colrClipBoxQuantization,
ftConfig=None,
):
if roundTolerance is not None:
self.roundTolerance = float(roundTolerance)
Expand All @@ -1119,6 +1122,7 @@ def __init__(
colrLayerReuse=colrLayerReuse,
colrAutoClipBoxes=colrAutoClipBoxes,
colrClipBoxQuantization=colrClipBoxQuantization,
ftConfig=ftConfig,
)
self.optimizeCFF = optimizeCFF
self._defaultAndNominalWidths = None
Expand Down Expand Up @@ -1440,6 +1444,7 @@ def __init__(
autoUseMyMetrics=True,
roundCoordinates=True,
glyphDataFormat=0,
ftConfig=None,
):
super().__init__(
font,
Expand All @@ -1450,6 +1455,7 @@ def __init__(
colrLayerReuse=colrLayerReuse,
colrAutoClipBoxes=colrAutoClipBoxes,
colrClipBoxQuantization=colrClipBoxQuantization,
ftConfig=ftConfig,
)
self.autoUseMyMetrics = autoUseMyMetrics
self.dropImpliedOnCurves = dropImpliedOnCurves
Expand Down
3 changes: 2 additions & 1 deletion Lib/ufo2ft/postProcessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,4 +404,5 @@ def _reloadFont(font: TTFont) -> TTFont:
stream = BytesIO()
font.save(stream)
stream.seek(0)
return TTFont(stream)
# keep the same Config (constructor will make a copy)
return TTFont(stream, cfg=font.cfg)
32 changes: 32 additions & 0 deletions tests/integration_test.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import difflib
import io
import logging
import os
import re
import sys
from pathlib import Path
from textwrap import dedent

import pytest
from fontTools.designspaceLib import DesignSpaceDocument
from fontTools.otlLib.optimize.gpos import COMPRESSION_LEVEL as GPOS_COMPRESSION_LEVEL
from fontTools.pens.boundsPen import BoundsPen
from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib.tables._g_l_y_f import (
Expand Down Expand Up @@ -547,6 +550,35 @@ def test_compileVariableCFF2_sparse_notdefGlyph(self, designspace):
tables=["CFF2", "hmtx", "HVAR"],
)

@pytest.mark.parametrize("compileMethod", [compileTTF, compileOTF])
@pytest.mark.parametrize("compression_level", [0, 9])
def test_compile_static_font_with_gpos_compression(
self, caplog, compileMethod, testufo, compression_level
):
with caplog.at_level(logging.INFO, logger="fontTools"):
compileMethod(testufo, ftConfig={GPOS_COMPRESSION_LEVEL: compression_level})
disabled = compression_level == 0
logged = "Compacting GPOS..." in caplog.text
assert logged ^ disabled

@pytest.mark.parametrize("compileMethod", [compileVariableTTF, compileVariableCFF2])
@pytest.mark.parametrize("variableFeatures", [True, False])
@pytest.mark.parametrize("compression_level", [0, 9])
def test_compile_variable_font_with_gpos_compression(
self, caplog, compileMethod, FontClass, variableFeatures, compression_level
):
designspace = DesignSpaceDocument.fromfile(getpath("TestVarfea.designspace"))
designspace.loadSourceFonts(FontClass)
with caplog.at_level(logging.INFO, logger="fontTools"):
compileMethod(
designspace,
ftConfig={GPOS_COMPRESSION_LEVEL: compression_level},
variableFeatures=variableFeatures,
)
disabled = compression_level == 0
logged = "Compacting GPOS..." in caplog.text
assert logged ^ disabled


if __name__ == "__main__":
sys.exit(pytest.main(sys.argv))
Loading