From a2f6f2ffed74dbfa43b1634e6dec91b41394030f Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 30 Nov 2023 18:06:19 +0000 Subject: [PATCH] [featureVars] allow to assign same subst lookup to several feature tags this can be useful to maximise layout engine compatibility or simply to have same feature variations substitutions be accessible via mutliple feature tags for whatever reason. Fixes https://github.com/fonttools/fonttools/issues/2050 Fixes https://github.com/fonttools/fonttools/issues/3004 --- Lib/fontTools/varLib/__init__.py | 23 ++++++--- Lib/fontTools/varLib/featureVars.py | 77 ++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 31 deletions(-) diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index b130d5b2a..81b7cef7d 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -52,7 +52,8 @@ from .errors import VarLibError, VarLibValidationError log = logging.getLogger("fontTools.varLib") # This is a lib key for the designspace document. The value should be -# an OpenType feature tag, to be used as the FeatureVariations feature. +# a comma-separated list of OpenType feature tag(s), to be used as the +# FeatureVariations feature. # If present, the DesignSpace flag is ignored. FEAVAR_FEATURETAG_LIB_KEY = "com.github.fonttools.varLib.featureVarsFeatureTag" @@ -781,7 +782,9 @@ def _merge_OTL(font, model, master_fonts, axisTags): font["GPOS"].table.remap_device_varidxes(varidx_map) -def _add_GSUB_feature_variations(font, axes, internal_axis_supports, rules, featureTag): +def _add_GSUB_feature_variations( + font, axes, internal_axis_supports, rules, featureTags +): def normalize(name, value): return models.normalizeLocation({name: value}, internal_axis_supports)[name] @@ -812,7 +815,7 @@ def _add_GSUB_feature_variations(font, axes, internal_axis_supports, rules, feat conditional_subs.append((region, subs)) - addFeatureVariations(font, conditional_subs, featureTag) + addFeatureVariations(font, conditional_subs, featureTags) _DesignSpaceData = namedtuple( @@ -1204,11 +1207,9 @@ def build( if "cvar" not in exclude and "glyf" in vf: _merge_TTHinting(vf, model, master_fonts) if "GSUB" not in exclude and ds.rules: - featureTag = ds.lib.get( - FEAVAR_FEATURETAG_LIB_KEY, "rclt" if ds.rulesProcessingLast else "rvrn" - ) + featureTags = _feature_variations_tags(ds) _add_GSUB_feature_variations( - vf, ds.axes, ds.internal_axis_supports, ds.rules, featureTag + vf, ds.axes, ds.internal_axis_supports, ds.rules, featureTags ) if "CFF2" not in exclude and ("CFF " in vf or "CFF2" in vf): _add_CFF2(vf, model, master_fonts) @@ -1299,6 +1300,14 @@ class MasterFinder(object): return os.path.normpath(path) +def _feature_variations_tags(ds): + raw_tags = ds.lib.get( + FEAVAR_FEATURETAG_LIB_KEY, + "rclt" if ds.rulesProcessingLast else "rvrn", + ) + return sorted({t.strip() for t in raw_tags.split(",")}) + + def main(args=None): """Build variable fonts from a designspace file and masters""" from argparse import ArgumentParser diff --git a/Lib/fontTools/varLib/featureVars.py b/Lib/fontTools/varLib/featureVars.py index f0403d76e..de473d976 100644 --- a/Lib/fontTools/varLib/featureVars.py +++ b/Lib/fontTools/varLib/featureVars.py @@ -43,9 +43,18 @@ def addFeatureVariations(font, conditionalSubstitutions, featureTag="rvrn"): # ... ] # >>> addFeatureVariations(f, condSubst) # >>> f.save(dstPath) + + The `featureTag` parameter takes either a str or a iterable of str (the single str + is kept for backwards compatibility), and defines which feature(s) will be + associated with the feature variations. + Note, if this is "rvrn", then the substitution lookup will be inserted at the + beginning of the lookup list so that it is processed before others, otherwise + for any other feature tags it will be appended last. """ - processLast = featureTag != "rvrn" + # process first when "rvrn" is the only listed tag + featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag) + processLast = "rvrn" not in featureTags or len(featureTags) > 1 _checkSubstitutionGlyphsExist( glyphNames=set(font.getGlyphOrder()), @@ -75,7 +84,7 @@ def addFeatureVariations(font, conditionalSubstitutions, featureTag="rvrn"): (conditionSet, [lookupMap[s] for s in substitutions]) ) - addFeatureVariationsRaw(font, font["GSUB"].table, conditionsAndLookups, featureTag) + addFeatureVariationsRaw(font, font["GSUB"].table, conditionsAndLookups, featureTags) def _checkSubstitutionGlyphsExist(glyphNames, substitutions): @@ -324,13 +333,16 @@ def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="r """Low level implementation of addFeatureVariations that directly models the possibilities of the FeatureVariations table.""" - processLast = featureTag != "rvrn" + featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag) + processLast = "rvrn" not in featureTags or len(featureTags) > 1 # - # if there is no feature: + # if a feature is not present: # make empty feature # sort features, get feature index # add feature to all scripts + # if a feature is present: + # reuse feature index # make lookups # add feature variations # @@ -339,31 +351,48 @@ def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="r table.FeatureVariations = None # delete any existing FeatureVariations - varFeatureIndices = [] - for index, feature in enumerate(table.FeatureList.FeatureRecord): - if feature.FeatureTag == featureTag: - varFeatureIndices.append(index) + varFeatureIndices = set() - if not varFeatureIndices: - varFeature = buildFeatureRecord(featureTag, []) - table.FeatureList.FeatureRecord.append(varFeature) + existingTags = { + feature.FeatureTag + for feature in table.FeatureList.FeatureRecord + if feature.FeatureTag in featureTags + } + + newTags = set(featureTags) - existingTags + if newTags: + varFeatures = [] + for featureTag in sorted(newTags): + varFeature = buildFeatureRecord(featureTag, []) + table.FeatureList.FeatureRecord.append(varFeature) + varFeatures.append(varFeature) table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord) sortFeatureList(table) - varFeatureIndex = table.FeatureList.FeatureRecord.index(varFeature) - for scriptRecord in table.ScriptList.ScriptRecord: - if scriptRecord.Script.DefaultLangSys is None: - raise VarLibError( - "Feature variations require that the script " - f"'{scriptRecord.ScriptTag}' defines a default language system." - ) - langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord] - for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems: - langSys.FeatureIndex.append(varFeatureIndex) - langSys.FeatureCount = len(langSys.FeatureIndex) + for varFeature in varFeatures: + varFeatureIndex = table.FeatureList.FeatureRecord.index(varFeature) - varFeatureIndices = [varFeatureIndex] + for scriptRecord in table.ScriptList.ScriptRecord: + if scriptRecord.Script.DefaultLangSys is None: + raise VarLibError( + "Feature variations require that the script " + f"'{scriptRecord.ScriptTag}' defines a default language system." + ) + langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord] + for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems: + langSys.FeatureIndex.append(varFeatureIndex) + langSys.FeatureCount = len(langSys.FeatureIndex) + varFeatureIndices.add(varFeatureIndex) + + if existingTags: + # indices may have changed if we inserted new features and sorted feature list + # so we must do this after the above + varFeatureIndices.update( + index + for index, feature in enumerate(table.FeatureList.FeatureRecord) + if feature.FeatureTag in existingTags + ) axisIndices = { axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes) @@ -380,7 +409,7 @@ def addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="r ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue) conditionTable.append(ct) records = [] - for varFeatureIndex in varFeatureIndices: + for varFeatureIndex in sorted(varFeatureIndices): existingLookupIndices = table.FeatureList.FeatureRecord[ varFeatureIndex ].Feature.LookupListIndex