| 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() |