From 63f703ba605eba22bc2836cf23bba33e901ca3df Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 11 Jan 2024 16:50:58 +0000 Subject: [PATCH 1/4] add ftConfig parameter to set fontTools' TTFont.cfg options and use it to pass fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL instead of env var --- Lib/ufo2ft/_compilers/baseCompiler.py | 74 +++++++++++++++------------ Lib/ufo2ft/outlineCompiler.py | 8 ++- Lib/ufo2ft/postProcessor.py | 3 +- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index 4175cd0fc..8066bffe8 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -1,13 +1,13 @@ 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 +22,7 @@ _notdefGlyphFallback, colrClipBoxQuantization, ensure_all_sources_have_names, + getDefaultMasterFont, location_to_string, prune_unknown_kwargs, ) @@ -47,6 +48,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 +271,46 @@ 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) + 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 + + save_production_names = self.useProductionNames + self.useProductionNames = False + save_postprocessor = self.postProcessorClass + self.postProcessorClass = None + self.skipFeatureCompilation = can_optimize_features + ttfDesignSpace = self.compile_designspace(subDoc) - finally: + if gpos_compact_value is not None: - os.environ[GPOS_COMPACT_MODE_ENV_KEY] = 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 + baseTtf = getDefaultMasterFont(ttfDesignSpace) + baseTtf.cfg[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 + finally: + if gpos_compact_value is not None: + self.ftConfig[GPOS_COMPRESSION_LEVEL] = gpos_compact_value return ( vfNameToBaseUfo, diff --git a/Lib/ufo2ft/outlineCompiler.py b/Lib/ufo2ft/outlineCompiler.py index 5b6d8511a..e7856400f 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 56f2292cb..a7c25d184 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) From 5b4d18f5cd980b719c238c6635e23c883e2d50fc Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 11 Jan 2024 16:59:43 +0000 Subject: [PATCH 2/4] test passing ftConfig to enable GPOS compression --- tests/integration_test.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/integration_test.py b/tests/integration_test.py index 0eb2a038f..12e874579 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)) From c66430d3c10c08dd69aa10f3b9c3258c92071489 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 11 Jan 2024 17:03:27 +0000 Subject: [PATCH 3/4] flake8: fix unused import --- Lib/ufo2ft/_compilers/baseCompiler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index 8066bffe8..6d36dbef9 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -1,5 +1,4 @@ import logging -import os from collections import defaultdict from dataclasses import dataclass, field from typing import Callable, Optional, Type From 1ac9908d0496d1a5266d45494ab0053d8e9a576a Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 11 Jan 2024 17:17:57 +0000 Subject: [PATCH 4/4] move constant logic out of the for loop --- Lib/ufo2ft/_compilers/baseCompiler.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Lib/ufo2ft/_compilers/baseCompiler.py b/Lib/ufo2ft/_compilers/baseCompiler.py index 6d36dbef9..15a24379b 100644 --- a/Lib/ufo2ft/_compilers/baseCompiler.py +++ b/Lib/ufo2ft/_compilers/baseCompiler.py @@ -273,6 +273,14 @@ def _compileNeededSources(self, designSpaceDoc): # 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 @@ -285,19 +293,12 @@ def _compileNeededSources(self, designSpaceDoc): if not subDoc.sources: continue - save_production_names = self.useProductionNames - self.useProductionNames = False - save_postprocessor = self.postProcessorClass - self.postProcessorClass = None - self.skipFeatureCompilation = can_optimize_features - ttfDesignSpace = self.compile_designspace(subDoc) if gpos_compact_value is not None: + # the VF will inherit the config from the base TTF master baseTtf = getDefaultMasterFont(ttfDesignSpace) baseTtf.cfg[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): @@ -308,8 +309,12 @@ def _compileNeededSources(self, designSpaceDoc): 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 + self.skipFeatureCompilation = save_skip_features return ( vfNameToBaseUfo,