featureVars.py 6.9 KB

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