[instancer] Split featureVars code into own module

Towards fixing https://github.com/fonttools/fonttools/issues/2737
This commit is contained in:
Behdad Esfahbod 2022-11-03 12:51:59 -06:00
parent fe4bb77cda
commit 6e156a7f13
3 changed files with 171 additions and 160 deletions

View File

@ -100,6 +100,7 @@ from fontTools.varLib import builder
from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.merger import MutatorMerger from fontTools.varLib.merger import MutatorMerger
from fontTools.varLib.instancer import names from fontTools.varLib.instancer import names
from .featureVars import instantiateFeatureVariations
from fontTools.misc.cliTools import makeOutputFileName from fontTools.misc.cliTools import makeOutputFileName
from fontTools.varLib.instancer import solver from fontTools.varLib.instancer import solver
import collections import collections
@ -834,165 +835,6 @@ def instantiateOTL(varfont, axisLimits):
del varfont["GDEF"] del varfont["GDEF"]
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()
def _featureVariationRecordIsUnique(rec, seen):
conditionSet = []
for cond in rec.ConditionSet.ConditionTable:
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(normalizeValue(v, axisLimit) for v in (minValue, maxValue))
def _instantiateFeatureVariationRecord(
record, recIdx, axisLimits, fvarAxes, axisIndexMap
):
applies = True
newConditions = []
default_triple = NormalizedAxisTriple(-1, 0, +1)
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
# condition not met so 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]
newConditions.append(condition)
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:
record.ConditionSet.ConditionTable = newConditions
shouldKeep = True
else:
shouldKeep = False
return applies, shouldKeep
def _limitFeatureVariationRecord(record, axisLimits, axisOrder):
newConditions = []
for condition in record.ConditionSet.ConditionTable:
if condition.Format == 1:
axisIdx = condition.AxisIndex
axisTag = axisOrder[axisIdx]
if axisTag in axisLimits:
axisLimit = axisLimits[axisTag]
newRange = _limitFeatureVariationConditionRange(condition, axisLimit)
if newRange:
# keep condition with updated limits
minimum, maximum = newRange
condition.FilterRangeMinValue = minimum
condition.FilterRangeMaxValue = maximum
if minimum != -1 or maximum != +1:
newConditions.append(condition)
else:
# condition out of range, remove entire record
newConditions = None
break
else:
newConditions.append(condition)
else:
newConditions.append(condition)
record.ConditionSet.ConditionTable = newConditions
return newConditions is not None
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 = []
for i, record in enumerate(table.FeatureVariations.FeatureVariationRecord):
applies, shouldKeep = _instantiateFeatureVariationRecord(
record, i, axisLimits, fvarAxes, axisIndexMap
)
if shouldKeep:
shouldKeep = _limitFeatureVariationRecord(record, axisLimits, axisOrder)
if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords):
newRecords.append(record)
if applies and not featureVariationApplied:
assert record.FeatureTableSubstitution.Version == 0x00010000
for rec in record.FeatureTableSubstitution.SubstitutionRecord:
table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = deepcopy(
rec.Feature
)
# Set variations only once
featureVariationApplied = True
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 _isValidAvarSegmentMap(axisTag, segmentMap): def _isValidAvarSegmentMap(axisTag, segmentMap):
if not segmentMap: if not segmentMap:
return True return True

View File

@ -0,0 +1,168 @@
from fontTools.varLib.models import normalizeValue
from copy import deepcopy
import logging
log = logging.getLogger("fontTools.varLib.instancer.featureVars")
def _featureVariationRecordIsUnique(rec, seen):
conditionSet = []
for cond in rec.ConditionSet.ConditionTable:
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(normalizeValue(v, axisLimit) for v in (minValue, maxValue))
def _instantiateFeatureVariationRecord(
record, recIdx, axisLimits, fvarAxes, axisIndexMap
):
applies = True
newConditions = []
from fontTools.varLib.instancer import NormalizedAxisTriple
default_triple = NormalizedAxisTriple(-1, 0, +1)
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
# condition not met so 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]
newConditions.append(condition)
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:
record.ConditionSet.ConditionTable = newConditions
shouldKeep = True
else:
shouldKeep = False
return applies, shouldKeep
def _limitFeatureVariationRecord(record, axisLimits, axisOrder):
newConditions = []
for condition in record.ConditionSet.ConditionTable:
if condition.Format == 1:
axisIdx = condition.AxisIndex
axisTag = axisOrder[axisIdx]
if axisTag in axisLimits:
axisLimit = axisLimits[axisTag]
newRange = _limitFeatureVariationConditionRange(condition, axisLimit)
if newRange:
# keep condition with updated limits
minimum, maximum = newRange
condition.FilterRangeMinValue = minimum
condition.FilterRangeMaxValue = maximum
if minimum != -1 or maximum != +1:
newConditions.append(condition)
else:
# condition out of range, remove entire record
newConditions = None
break
else:
newConditions.append(condition)
else:
newConditions.append(condition)
record.ConditionSet.ConditionTable = newConditions
return newConditions is not None
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 = []
for i, record in enumerate(table.FeatureVariations.FeatureVariationRecord):
applies, shouldKeep = _instantiateFeatureVariationRecord(
record, i, axisLimits, fvarAxes, axisIndexMap
)
if shouldKeep:
shouldKeep = _limitFeatureVariationRecord(record, axisLimits, axisOrder)
if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords):
newRecords.append(record)
if applies and not featureVariationApplied:
assert record.FeatureTableSubstitution.Version == 0x00010000
for rec in record.FeatureTableSubstitution.SubstitutionRecord:
table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = deepcopy(
rec.Feature
)
# Set variations only once
featureVariationApplied = True
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()

View File

@ -9,6 +9,7 @@ from fontTools.ttLib.tables import otTables
from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools import varLib from fontTools import varLib
from fontTools.varLib import instancer from fontTools.varLib import instancer
from fontTools.varLib.instancer import featureVars as instancer_featureVars
from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib import builder from fontTools.varLib import builder
from fontTools.varLib import featureVars from fontTools.varLib import featureVars
@ -1975,7 +1976,7 @@ class LimitTupleVariationAxisRangesTest:
def test_limitFeatureVariationConditionRange(oldRange, newLimit, expected): def test_limitFeatureVariationConditionRange(oldRange, newLimit, expected):
condition = featureVars.buildConditionTable(0, *oldRange) condition = featureVars.buildConditionTable(0, *oldRange)
result = instancer._limitFeatureVariationConditionRange( result = instancer_featureVars._limitFeatureVariationConditionRange(
condition, instancer.NormalizedAxisTriple(*newLimit) condition, instancer.NormalizedAxisTriple(*newLimit)
) )