aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/varLib/instancer/featureVars.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/varLib/instancer/featureVars.py')
-rw-r--r--Lib/fontTools/varLib/instancer/featureVars.py190
1 files changed, 190 insertions, 0 deletions
diff --git a/Lib/fontTools/varLib/instancer/featureVars.py b/Lib/fontTools/varLib/instancer/featureVars.py
new file mode 100644
index 00000000..d9370d9d
--- /dev/null
+++ b/Lib/fontTools/varLib/instancer/featureVars.py
@@ -0,0 +1,190 @@
+from fontTools.ttLib.tables import otTables as ot
+from copy import deepcopy
+import logging
+
+
+log = logging.getLogger("fontTools.varLib.instancer")
+
+
+def _featureVariationRecordIsUnique(rec, seen):
+ conditionSet = []
+ conditionSets = (
+ rec.ConditionSet.ConditionTable if rec.ConditionSet is not None else []
+ )
+ for cond in conditionSets:
+ 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(
+ axisLimit.renormalizeValue(v, extrapolate=False) for v in (minValue, maxValue)
+ )
+
+
+def _instantiateFeatureVariationRecord(
+ record, recIdx, axisLimits, fvarAxes, axisIndexMap
+):
+ applies = True
+ shouldKeep = False
+ newConditions = []
+ from fontTools.varLib.instancer import NormalizedAxisTripleAndDistances
+
+ default_triple = NormalizedAxisTripleAndDistances(-1, 0, +1)
+ if record.ConditionSet is None:
+ record.ConditionSet = ot.ConditionSet()
+ record.ConditionSet.ConditionTable = []
+ record.ConditionSet.ConditionCount = 0
+ 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
+
+ # if condition not met, 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]
+
+ # remap condition limits
+ newRange = _limitFeatureVariationConditionRange(condition, triple)
+ if newRange:
+ # keep condition with updated limits
+ minimum, maximum = newRange
+ condition.FilterRangeMinValue = minimum
+ condition.FilterRangeMaxValue = maximum
+ shouldKeep = True
+ if minimum != -1 or maximum != +1:
+ newConditions.append(condition)
+ else:
+ # condition out of range, remove entire record
+ newConditions = None
+ break
+
+ 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 is not None and shouldKeep:
+ record.ConditionSet.ConditionTable = newConditions
+ if not newConditions:
+ record.ConditionSet = None
+ shouldKeep = True
+ else:
+ shouldKeep = False
+
+ # Does this *always* apply?
+ universal = shouldKeep and not newConditions
+
+ return applies, shouldKeep, universal
+
+
+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 = []
+ defaultsSubsts = None
+
+ for i, record in enumerate(table.FeatureVariations.FeatureVariationRecord):
+ applies, shouldKeep, universal = _instantiateFeatureVariationRecord(
+ record, i, axisLimits, fvarAxes, axisIndexMap
+ )
+
+ if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords):
+ newRecords.append(record)
+
+ if applies and not featureVariationApplied:
+ assert record.FeatureTableSubstitution.Version == 0x00010000
+ defaultsSubsts = deepcopy(record.FeatureTableSubstitution)
+ for default, rec in zip(
+ defaultsSubsts.SubstitutionRecord,
+ record.FeatureTableSubstitution.SubstitutionRecord,
+ ):
+ default.Feature = deepcopy(
+ table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature
+ )
+ table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = deepcopy(
+ rec.Feature
+ )
+ # Set variations only once
+ featureVariationApplied = True
+
+ # Further records don't have a chance to apply after a universal record
+ if universal:
+ break
+
+ # Insert a catch-all record to reinstate the old features if necessary
+ if featureVariationApplied and newRecords and not universal:
+ defaultRecord = ot.FeatureVariationRecord()
+ defaultRecord.ConditionSet = ot.ConditionSet()
+ defaultRecord.ConditionSet.ConditionTable = []
+ defaultRecord.ConditionSet.ConditionCount = 0
+ defaultRecord.FeatureTableSubstitution = defaultsSubsts
+
+ newRecords.append(defaultRecord)
+
+ 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()