Skip to content

Commit

Permalink
Merge pull request #607 from googlefonts/fonttools-config
Browse files Browse the repository at this point in the history
add ftConfig parameter to set fontTools' TTFont.cfg options
  • Loading branch information
anthrotype authored Jan 11, 2024
2 parents add4fcc + 1ac9908 commit 702975e
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 33 deletions.
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))

0 comments on commit 702975e

Please sign in to comment.