diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index a8f0440f3..65ce2bbf0 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -100,6 +100,7 @@ from fontTools.varLib import builder from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib.merger import MutatorMerger from fontTools.varLib.instancer import names +from .featureVars import instantiateFeatureVariations from fontTools.misc.cliTools import makeOutputFileName from fontTools.varLib.instancer import solver import collections @@ -834,165 +835,6 @@ def instantiateOTL(varfont, axisLimits): del varfont["GDEF"] -def instantiateFeatureVariations(varfont, axisLimits): - for tableTag in ("GPOS", "GSUB"): - if tableTag not in varfont or not getattr( - varfont[tableTag].table, "FeatureVariations", None - ): - continue - log.info("Instantiating FeatureVariations of %s table", tableTag) - _instantiateFeatureVariations( - varfont[tableTag].table, varfont["fvar"].axes, axisLimits - ) - # remove unreferenced lookups - varfont[tableTag].prune_lookups() - - -def _featureVariationRecordIsUnique(rec, seen): - conditionSet = [] - for cond in rec.ConditionSet.ConditionTable: - if cond.Format != 1: - # can't tell whether this is duplicate, assume is unique - return True - conditionSet.append( - (cond.AxisIndex, cond.FilterRangeMinValue, cond.FilterRangeMaxValue) - ) - # besides the set of conditions, we also include the FeatureTableSubstitution - # version to identify unique FeatureVariationRecords, even though only one - # version is currently defined. It's theoretically possible that multiple - # records with same conditions but different substitution table version be - # present in the same font for backward compatibility. - recordKey = frozenset([rec.FeatureTableSubstitution.Version] + conditionSet) - if recordKey in seen: - return False - else: - seen.add(recordKey) # side effect - return True - - -def _limitFeatureVariationConditionRange(condition, axisLimit): - minValue = condition.FilterRangeMinValue - maxValue = condition.FilterRangeMaxValue - - if ( - minValue > maxValue - or minValue > axisLimit.maximum - or maxValue < axisLimit.minimum - ): - # condition invalid or out of range - return - - return tuple(normalizeValue(v, axisLimit) for v in (minValue, maxValue)) - - -def _instantiateFeatureVariationRecord( - record, recIdx, axisLimits, fvarAxes, axisIndexMap -): - applies = True - newConditions = [] - default_triple = NormalizedAxisTriple(-1, 0, +1) - for i, condition in enumerate(record.ConditionSet.ConditionTable): - if condition.Format == 1: - axisIdx = condition.AxisIndex - axisTag = fvarAxes[axisIdx].axisTag - - minValue = condition.FilterRangeMinValue - maxValue = condition.FilterRangeMaxValue - triple = axisLimits.get(axisTag, default_triple) - if not (minValue <= triple.default <= maxValue): - applies = False - # condition not met so remove entire record - if triple.minimum > maxValue or triple.maximum < minValue: - newConditions = None - break - - if axisTag in axisIndexMap: - # remap axis index - condition.AxisIndex = axisIndexMap[axisTag] - newConditions.append(condition) - else: - log.warning( - "Condition table {0} of FeatureVariationRecord {1} has " - "unsupported format ({2}); ignored".format(i, recIdx, condition.Format) - ) - applies = False - newConditions.append(condition) - - if newConditions: - record.ConditionSet.ConditionTable = newConditions - shouldKeep = True - else: - shouldKeep = False - - return applies, shouldKeep - - -def _limitFeatureVariationRecord(record, axisLimits, axisOrder): - newConditions = [] - for condition in record.ConditionSet.ConditionTable: - if condition.Format == 1: - axisIdx = condition.AxisIndex - axisTag = axisOrder[axisIdx] - if axisTag in axisLimits: - axisLimit = axisLimits[axisTag] - newRange = _limitFeatureVariationConditionRange(condition, axisLimit) - if newRange: - # keep condition with updated limits - minimum, maximum = newRange - condition.FilterRangeMinValue = minimum - condition.FilterRangeMaxValue = maximum - if minimum != -1 or maximum != +1: - newConditions.append(condition) - else: - # condition out of range, remove entire record - newConditions = None - break - else: - newConditions.append(condition) - else: - newConditions.append(condition) - - record.ConditionSet.ConditionTable = newConditions - return newConditions is not None - - -def _instantiateFeatureVariations(table, fvarAxes, axisLimits): - pinnedAxes = set(axisLimits.pinnedLocation()) - axisOrder = [axis.axisTag for axis in fvarAxes if axis.axisTag not in pinnedAxes] - axisIndexMap = {axisTag: axisOrder.index(axisTag) for axisTag in axisOrder} - - featureVariationApplied = False - uniqueRecords = set() - newRecords = [] - - for i, record in enumerate(table.FeatureVariations.FeatureVariationRecord): - applies, shouldKeep = _instantiateFeatureVariationRecord( - record, i, axisLimits, fvarAxes, axisIndexMap - ) - if shouldKeep: - shouldKeep = _limitFeatureVariationRecord(record, axisLimits, axisOrder) - - if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords): - newRecords.append(record) - - if applies and not featureVariationApplied: - assert record.FeatureTableSubstitution.Version == 0x00010000 - for rec in record.FeatureTableSubstitution.SubstitutionRecord: - table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = deepcopy( - rec.Feature - ) - # Set variations only once - featureVariationApplied = True - - if newRecords: - table.FeatureVariations.FeatureVariationRecord = newRecords - table.FeatureVariations.FeatureVariationCount = len(newRecords) - else: - del table.FeatureVariations - # downgrade table version if there are no FeatureVariations left - table.Version = 0x00010000 - - def _isValidAvarSegmentMap(axisTag, segmentMap): if not segmentMap: return True diff --git a/Lib/fontTools/varLib/instancer/featureVars.py b/Lib/fontTools/varLib/instancer/featureVars.py new file mode 100644 index 000000000..cdcfac659 --- /dev/null +++ b/Lib/fontTools/varLib/instancer/featureVars.py @@ -0,0 +1,168 @@ + +from fontTools.varLib.models import normalizeValue +from copy import deepcopy +import logging + + +log = logging.getLogger("fontTools.varLib.instancer.featureVars") + + +def _featureVariationRecordIsUnique(rec, seen): + conditionSet = [] + for cond in rec.ConditionSet.ConditionTable: + if cond.Format != 1: + # can't tell whether this is duplicate, assume is unique + return True + conditionSet.append( + (cond.AxisIndex, cond.FilterRangeMinValue, cond.FilterRangeMaxValue) + ) + # besides the set of conditions, we also include the FeatureTableSubstitution + # version to identify unique FeatureVariationRecords, even though only one + # version is currently defined. It's theoretically possible that multiple + # records with same conditions but different substitution table version be + # present in the same font for backward compatibility. + recordKey = frozenset([rec.FeatureTableSubstitution.Version] + conditionSet) + if recordKey in seen: + return False + else: + seen.add(recordKey) # side effect + return True + + +def _limitFeatureVariationConditionRange(condition, axisLimit): + minValue = condition.FilterRangeMinValue + maxValue = condition.FilterRangeMaxValue + + if ( + minValue > maxValue + or minValue > axisLimit.maximum + or maxValue < axisLimit.minimum + ): + # condition invalid or out of range + return + + return tuple(normalizeValue(v, axisLimit) for v in (minValue, maxValue)) + + +def _instantiateFeatureVariationRecord( + record, recIdx, axisLimits, fvarAxes, axisIndexMap +): + applies = True + newConditions = [] + from fontTools.varLib.instancer import NormalizedAxisTriple + default_triple = NormalizedAxisTriple(-1, 0, +1) + for i, condition in enumerate(record.ConditionSet.ConditionTable): + if condition.Format == 1: + axisIdx = condition.AxisIndex + axisTag = fvarAxes[axisIdx].axisTag + + minValue = condition.FilterRangeMinValue + maxValue = condition.FilterRangeMaxValue + triple = axisLimits.get(axisTag, default_triple) + if not (minValue <= triple.default <= maxValue): + applies = False + # condition not met so remove entire record + if triple.minimum > maxValue or triple.maximum < minValue: + newConditions = None + break + + if axisTag in axisIndexMap: + # remap axis index + condition.AxisIndex = axisIndexMap[axisTag] + newConditions.append(condition) + else: + log.warning( + "Condition table {0} of FeatureVariationRecord {1} has " + "unsupported format ({2}); ignored".format(i, recIdx, condition.Format) + ) + applies = False + newConditions.append(condition) + + if newConditions: + record.ConditionSet.ConditionTable = newConditions + shouldKeep = True + else: + shouldKeep = False + + return applies, shouldKeep + + +def _limitFeatureVariationRecord(record, axisLimits, axisOrder): + newConditions = [] + for condition in record.ConditionSet.ConditionTable: + if condition.Format == 1: + axisIdx = condition.AxisIndex + axisTag = axisOrder[axisIdx] + if axisTag in axisLimits: + axisLimit = axisLimits[axisTag] + newRange = _limitFeatureVariationConditionRange(condition, axisLimit) + if newRange: + # keep condition with updated limits + minimum, maximum = newRange + condition.FilterRangeMinValue = minimum + condition.FilterRangeMaxValue = maximum + if minimum != -1 or maximum != +1: + newConditions.append(condition) + else: + # condition out of range, remove entire record + newConditions = None + break + else: + newConditions.append(condition) + else: + newConditions.append(condition) + + record.ConditionSet.ConditionTable = newConditions + return newConditions is not None + + +def _instantiateFeatureVariations(table, fvarAxes, axisLimits): + pinnedAxes = set(axisLimits.pinnedLocation()) + axisOrder = [axis.axisTag for axis in fvarAxes if axis.axisTag not in pinnedAxes] + axisIndexMap = {axisTag: axisOrder.index(axisTag) for axisTag in axisOrder} + + featureVariationApplied = False + uniqueRecords = set() + newRecords = [] + + for i, record in enumerate(table.FeatureVariations.FeatureVariationRecord): + applies, shouldKeep = _instantiateFeatureVariationRecord( + record, i, axisLimits, fvarAxes, axisIndexMap + ) + if shouldKeep: + shouldKeep = _limitFeatureVariationRecord(record, axisLimits, axisOrder) + + if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords): + newRecords.append(record) + + if applies and not featureVariationApplied: + assert record.FeatureTableSubstitution.Version == 0x00010000 + for rec in record.FeatureTableSubstitution.SubstitutionRecord: + table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = deepcopy( + rec.Feature + ) + # Set variations only once + featureVariationApplied = True + + if newRecords: + table.FeatureVariations.FeatureVariationRecord = newRecords + table.FeatureVariations.FeatureVariationCount = len(newRecords) + else: + del table.FeatureVariations + # downgrade table version if there are no FeatureVariations left + table.Version = 0x00010000 + + +def instantiateFeatureVariations(varfont, axisLimits): + for tableTag in ("GPOS", "GSUB"): + if tableTag not in varfont or not getattr( + varfont[tableTag].table, "FeatureVariations", None + ): + continue + log.info("Instantiating FeatureVariations of %s table", tableTag) + _instantiateFeatureVariations( + varfont[tableTag].table, varfont["fvar"].axes, axisLimits + ) + # remove unreferenced lookups + varfont[tableTag].prune_lookups() + diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py index f4d266dd4..b3e40ebe7 100644 --- a/Tests/varLib/instancer/instancer_test.py +++ b/Tests/varLib/instancer/instancer_test.py @@ -9,6 +9,7 @@ from fontTools.ttLib.tables import otTables from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools import varLib from fontTools.varLib import instancer +from fontTools.varLib.instancer import featureVars as instancer_featureVars from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib import builder from fontTools.varLib import featureVars @@ -1975,7 +1976,7 @@ class LimitTupleVariationAxisRangesTest: def test_limitFeatureVariationConditionRange(oldRange, newLimit, expected): condition = featureVars.buildConditionTable(0, *oldRange) - result = instancer._limitFeatureVariationConditionRange( + result = instancer_featureVars._limitFeatureVariationConditionRange( condition, instancer.NormalizedAxisTriple(*newLimit) )