Merge pull request #2880 from fonttools/instancer-featureVars

Instancer feature vars
This commit is contained in:
Behdad Esfahbod 2022-11-04 11:56:18 -06:00 committed by GitHub
commit 82b90236f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 207 additions and 163 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,178 @@
from fontTools.ttLib.tables import otTables as ot
from fontTools.varLib.models import normalizeValue
from copy import deepcopy
import logging
log = logging.getLogger("fontTools.varLib.instancer")
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
shouldKeep = False
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
# 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
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()

View File

@ -1587,13 +1587,13 @@ class InstantiateFeatureVariationsTest(object):
"location, appliedSubs, expectedRecords", "location, appliedSubs, expectedRecords",
[ [
({"wght": 0}, {}, [({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"})]), ({"wght": 0}, {}, [({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"})]),
# Buggy. See: https://github.com/fonttools/fonttools/issues/2737
( (
{"wght": -1.0}, {"wght": -1.0},
{"uni0061": "uni0041"}, {"uni0061": "uni0041"},
[ [
({"cntr": (0, 0.25)}, {"uni0061": "uni0041"}), ({"cntr": (0, 0.25)}, {"uni0061": "uni0041"}),
({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"}), ({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"}),
({}, {}),
], ],
), ),
( (
@ -1603,7 +1603,8 @@ class InstantiateFeatureVariationsTest(object):
( (
{"cntr": (0.75, 1.0)}, {"cntr": (0.75, 1.0)},
{"uni0024": "uni0024.nostroke", "uni0041": "uni0061"}, {"uni0024": "uni0024.nostroke", "uni0041": "uni0061"},
) ),
({}, {})
], ],
), ),
( (
@ -1621,7 +1622,8 @@ class InstantiateFeatureVariationsTest(object):
( (
{"wght": (0.20886, 1.0)}, {"wght": (0.20886, 1.0)},
{"uni0024": "uni0024.nostroke", "uni0041": "uni0061"}, {"uni0024": "uni0024.nostroke", "uni0041": "uni0061"},
) ),
({}, {})
], ],
), ),
( (
@ -1660,6 +1662,28 @@ class InstantiateFeatureVariationsTest(object):
), ),
], ],
), ),
(
{"cntr": (0.7, 0.9, 1.0)},
{"uni0041": "uni0061"},
[
(
{'cntr': (-0.7499999999999999, 1.0), 'wght': (0.20886, 1.0)},
{"uni0024": "uni0024.nostroke", "uni0041": "uni0061"},
),
(
{'cntr': (-0.7499999999999999, 1.0)},
{"uni0041": "uni0061"},
),
(
{"wght": (0.20886, 1.0)},
{"uni0024": "uni0024.nostroke"},
),
(
{},
{},
),
],
),
], ],
) )
def test_partial_instance(self, location, appliedSubs, expectedRecords): def test_partial_instance(self, location, appliedSubs, expectedRecords):
@ -1975,7 +1999,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)
) )