diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index 4175cd0f..15a24379 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -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 @@ -22,6 +21,7 @@ _notdefGlyphFallback, colrClipBoxQuantization, ensure_all_sources_have_names, + getDefaultMasterFont, location_to_string, prune_unknown_kwargs, ) @@ -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): @@ -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, diff --git a/Lib/ufo2ft/outlineCompiler.py b/Lib/ufo2ft/outlineCompiler.py index 5b6d8511..e7856400 100644 --- a/Lib/ufo2ft/outlineCompiler.py +++ b/Lib/ufo2ft/outlineCompiler.py @@ -108,6 +108,7 @@ def __init__( colrLayerReuse=True, colrAutoClipBoxes=True, colrClipBoxQuantization=colrClipBoxQuantization, + ftConfig=None, ): self.ufo = font # use the previously filtered glyphSet, if any @@ -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 @@ -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 = [ @@ -1104,6 +1106,7 @@ def __init__( colrLayerReuse=True, colrAutoClipBoxes=True, colrClipBoxQuantization=colrClipBoxQuantization, + ftConfig=None, ): if roundTolerance is not None: self.roundTolerance = float(roundTolerance) @@ -1119,6 +1122,7 @@ def __init__( colrLayerReuse=colrLayerReuse, colrAutoClipBoxes=colrAutoClipBoxes, colrClipBoxQuantization=colrClipBoxQuantization, + ftConfig=ftConfig, ) self.optimizeCFF = optimizeCFF self._defaultAndNominalWidths = None @@ -1440,6 +1444,7 @@ def __init__( autoUseMyMetrics=True, roundCoordinates=True, glyphDataFormat=0, + ftConfig=None, ): super().__init__( font, @@ -1450,6 +1455,7 @@ def __init__( colrLayerReuse=colrLayerReuse, colrAutoClipBoxes=colrAutoClipBoxes, colrClipBoxQuantization=colrClipBoxQuantization, + ftConfig=ftConfig, ) self.autoUseMyMetrics = autoUseMyMetrics self.dropImpliedOnCurves = dropImpliedOnCurves diff --git a/Lib/ufo2ft/postProcessor.py b/Lib/ufo2ft/postProcessor.py index 56f2292c..a7c25d18 100644 --- a/Lib/ufo2ft/postProcessor.py +++ b/Lib/ufo2ft/postProcessor.py @@ -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) diff --git a/tests/integration_test.py b/tests/integration_test.py index 0eb2a038..12e87457 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -1,5 +1,6 @@ import difflib import io +import logging import os import re import sys @@ -7,6 +8,8 @@ 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 ( @@ -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))