Merge pull request #2880 from fonttools/instancer-featureVars
Instancer feature vars
This commit is contained in:
commit
82b90236f8
@ -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
|
||||||
|
178
Lib/fontTools/varLib/instancer/featureVars.py
Normal file
178
Lib/fontTools/varLib/instancer/featureVars.py
Normal 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()
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user