xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/instancer/featureVars.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.ttLib.tables import otTables as ot
2from copy import deepcopy
3import logging
4
5
6log = logging.getLogger("fontTools.varLib.instancer")
7
8
9def _featureVariationRecordIsUnique(rec, seen):
10    conditionSet = []
11    conditionSets = (
12        rec.ConditionSet.ConditionTable if rec.ConditionSet is not None else []
13    )
14    for cond in conditionSets:
15        if cond.Format != 1:
16            # can't tell whether this is duplicate, assume is unique
17            return True
18        conditionSet.append(
19            (cond.AxisIndex, cond.FilterRangeMinValue, cond.FilterRangeMaxValue)
20        )
21    # besides the set of conditions, we also include the FeatureTableSubstitution
22    # version to identify unique FeatureVariationRecords, even though only one
23    # version is currently defined. It's theoretically possible that multiple
24    # records with same conditions but different substitution table version be
25    # present in the same font for backward compatibility.
26    recordKey = frozenset([rec.FeatureTableSubstitution.Version] + conditionSet)
27    if recordKey in seen:
28        return False
29    else:
30        seen.add(recordKey)  # side effect
31        return True
32
33
34def _limitFeatureVariationConditionRange(condition, axisLimit):
35    minValue = condition.FilterRangeMinValue
36    maxValue = condition.FilterRangeMaxValue
37
38    if (
39        minValue > maxValue
40        or minValue > axisLimit.maximum
41        or maxValue < axisLimit.minimum
42    ):
43        # condition invalid or out of range
44        return
45
46    return tuple(
47        axisLimit.renormalizeValue(v, extrapolate=False) for v in (minValue, maxValue)
48    )
49
50
51def _instantiateFeatureVariationRecord(
52    record, recIdx, axisLimits, fvarAxes, axisIndexMap
53):
54    applies = True
55    shouldKeep = False
56    newConditions = []
57    from fontTools.varLib.instancer import NormalizedAxisTripleAndDistances
58
59    default_triple = NormalizedAxisTripleAndDistances(-1, 0, +1)
60    if record.ConditionSet is None:
61        record.ConditionSet = ot.ConditionSet()
62        record.ConditionSet.ConditionTable = []
63        record.ConditionSet.ConditionCount = 0
64    for i, condition in enumerate(record.ConditionSet.ConditionTable):
65        if condition.Format == 1:
66            axisIdx = condition.AxisIndex
67            axisTag = fvarAxes[axisIdx].axisTag
68
69            minValue = condition.FilterRangeMinValue
70            maxValue = condition.FilterRangeMaxValue
71            triple = axisLimits.get(axisTag, default_triple)
72
73            if not (minValue <= triple.default <= maxValue):
74                applies = False
75
76            # if condition not met, remove entire record
77            if triple.minimum > maxValue or triple.maximum < minValue:
78                newConditions = None
79                break
80
81            if axisTag in axisIndexMap:
82                # remap axis index
83                condition.AxisIndex = axisIndexMap[axisTag]
84
85                # remap condition limits
86                newRange = _limitFeatureVariationConditionRange(condition, triple)
87                if newRange:
88                    # keep condition with updated limits
89                    minimum, maximum = newRange
90                    condition.FilterRangeMinValue = minimum
91                    condition.FilterRangeMaxValue = maximum
92                    shouldKeep = True
93                    if minimum != -1 or maximum != +1:
94                        newConditions.append(condition)
95                else:
96                    # condition out of range, remove entire record
97                    newConditions = None
98                    break
99
100        else:
101            log.warning(
102                "Condition table {0} of FeatureVariationRecord {1} has "
103                "unsupported format ({2}); ignored".format(i, recIdx, condition.Format)
104            )
105            applies = False
106            newConditions.append(condition)
107
108    if newConditions is not None and shouldKeep:
109        record.ConditionSet.ConditionTable = newConditions
110        if not newConditions:
111            record.ConditionSet = None
112        shouldKeep = True
113    else:
114        shouldKeep = False
115
116    # Does this *always* apply?
117    universal = shouldKeep and not newConditions
118
119    return applies, shouldKeep, universal
120
121
122def _instantiateFeatureVariations(table, fvarAxes, axisLimits):
123    pinnedAxes = set(axisLimits.pinnedLocation())
124    axisOrder = [axis.axisTag for axis in fvarAxes if axis.axisTag not in pinnedAxes]
125    axisIndexMap = {axisTag: axisOrder.index(axisTag) for axisTag in axisOrder}
126
127    featureVariationApplied = False
128    uniqueRecords = set()
129    newRecords = []
130    defaultsSubsts = None
131
132    for i, record in enumerate(table.FeatureVariations.FeatureVariationRecord):
133        applies, shouldKeep, universal = _instantiateFeatureVariationRecord(
134            record, i, axisLimits, fvarAxes, axisIndexMap
135        )
136
137        if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords):
138            newRecords.append(record)
139
140        if applies and not featureVariationApplied:
141            assert record.FeatureTableSubstitution.Version == 0x00010000
142            defaultsSubsts = deepcopy(record.FeatureTableSubstitution)
143            for default, rec in zip(
144                defaultsSubsts.SubstitutionRecord,
145                record.FeatureTableSubstitution.SubstitutionRecord,
146            ):
147                default.Feature = deepcopy(
148                    table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature
149                )
150                table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = deepcopy(
151                    rec.Feature
152                )
153            # Set variations only once
154            featureVariationApplied = True
155
156        # Further records don't have a chance to apply after a universal record
157        if universal:
158            break
159
160    # Insert a catch-all record to reinstate the old features if necessary
161    if featureVariationApplied and newRecords and not universal:
162        defaultRecord = ot.FeatureVariationRecord()
163        defaultRecord.ConditionSet = ot.ConditionSet()
164        defaultRecord.ConditionSet.ConditionTable = []
165        defaultRecord.ConditionSet.ConditionCount = 0
166        defaultRecord.FeatureTableSubstitution = defaultsSubsts
167
168        newRecords.append(defaultRecord)
169
170    if newRecords:
171        table.FeatureVariations.FeatureVariationRecord = newRecords
172        table.FeatureVariations.FeatureVariationCount = len(newRecords)
173    else:
174        del table.FeatureVariations
175        # downgrade table version if there are no FeatureVariations left
176        table.Version = 0x00010000
177
178
179def instantiateFeatureVariations(varfont, axisLimits):
180    for tableTag in ("GPOS", "GSUB"):
181        if tableTag not in varfont or not getattr(
182            varfont[tableTag].table, "FeatureVariations", None
183        ):
184            continue
185        log.info("Instantiating FeatureVariations of %s table", tableTag)
186        _instantiateFeatureVariations(
187            varfont[tableTag].table, varfont["fvar"].axes, axisLimits
188        )
189        # remove unreferenced lookups
190        varfont[tableTag].prune_lookups()
191