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