instancer: never drop STAT DesignAxes; only prune out-of-range AxisValues

The current method for L1 and L2 partial instacing of STAT table --
i.e. drop all pinned axes are respective axis values -- was incorrect.
STAT design axis are a superset of the fvar axes, they describe the relations
between members of a font family in which some aspects may be implemented as
variation axes within a single VF, others as multiple discrete fonts.

When we remove an axis from fvar, we still want to keep the STAT's DesignAxis,
as well as the single AxisValue table along that design axis which describes
the position of the new instance within the family's stylistic attributes.

This means, intantiateAvar will never drop any DesignAxis, but will only drops
AxisValue tables when: 1) we're pinning an axis and the desired instance
coordinate doesn't exactly equal any of the existing AxisValue records;
2) we're restricting an axis range, and the (nominal) AxisValue falls
outside of the desired range.

We never add new AxisValue records, as that's a design decision that
is outside of the scope of the partial instancer.
This commit is contained in:
Cosimo Lupo 2019-10-17 19:02:26 +01:00
parent b8500ac97c
commit 3c6ddb0ef8
No known key found for this signature in database
GPG Key ID: 20D4A261E4A0E642

View File

@ -981,61 +981,57 @@ def instantiateFvar(varfont, axisLimits):
def instantiateSTAT(varfont, axisLimits): def instantiateSTAT(varfont, axisLimits):
# 'axisLimits' dict must contain user-space (non-normalized) coordinates # 'axisLimits' dict must contain user-space (non-normalized) coordinates
# XXX do something with axisRanges stat = varfont["STAT"].table
if not stat.DesignAxisRecord or not (
stat.AxisValueArray and stat.AxisValueArray.AxisValue
):
return # STAT table empty, nothing to do
location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange) location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange)
pinnedAxes = set(location.keys()) def isAxisValueOutsideLimits(axisTag, axisValue):
if axisTag in location and axisValue != location[axisTag]:
stat = varfont["STAT"].table return True
if not stat.DesignAxisRecord: elif axisTag in axisRanges:
return # skip empty STAT table axisRange = axisRanges[axisTag]
if axisValue < axisRange.minimum or axisValue > axisRange.maximum:
designAxes = stat.DesignAxisRecord.Axis return True
pinnedAxisIndices = { return False
i for i, axis in enumerate(designAxes) if axis.AxisTag in pinnedAxes
}
if len(pinnedAxisIndices) == len(designAxes):
log.info("Dropping STAT table")
del varfont["STAT"]
return
log.info("Instantiating STAT table") log.info("Instantiating STAT table")
# only keep DesignAxis that were not instanced, and build a mapping from old # only keep AxisValues whose axis is not pinned nor restricted, or is pinned at the
# to new axis indices # exact (nominal) value, or is restricted but the value is within the new range
newDesignAxes = [] designAxes = stat.DesignAxisRecord.Axis
axisIndexMap = {} newAxisValueTables = []
for i, axis in enumerate(designAxes): for axisValueTable in stat.AxisValueArray.AxisValue:
if i not in pinnedAxisIndices: axisValueFormat = axisValueTable.Format
axisIndexMap[i] = len(newDesignAxes) if axisValueFormat in (1, 2, 3):
newDesignAxes.append(axis) axisTag = designAxes[axisValueTable.AxisIndex].AxisTag
if axisValueFormat == 2:
if stat.AxisValueArray and stat.AxisValueArray.AxisValue: axisValue = axisValueTable.NominalValue
# drop all AxisValue tables that reference any of the pinned axes
newAxisValueTables = []
for axisValueTable in stat.AxisValueArray.AxisValue:
if axisValueTable.Format in (1, 2, 3):
if axisValueTable.AxisIndex in pinnedAxisIndices:
continue
axisValueTable.AxisIndex = axisIndexMap[axisValueTable.AxisIndex]
newAxisValueTables.append(axisValueTable)
elif axisValueTable.Format == 4:
if any(
rec.AxisIndex in pinnedAxisIndices
for rec in axisValueTable.AxisValueRecord
):
continue
for rec in axisValueTable.AxisValueRecord:
rec.AxisIndex = axisIndexMap[rec.AxisIndex]
newAxisValueTables.append(axisValueTable)
else: else:
raise NotImplementedError(axisValueTable.Format) axisValue = axisValueTable.Value
stat.AxisValueArray.AxisValue = newAxisValueTables if isAxisValueOutsideLimits(axisTag, axisValue):
stat.AxisValueCount = len(stat.AxisValueArray.AxisValue) continue
elif axisValueFormat == 4:
# drop 'non-analytic' AxisValue if _any_ AxisValueRecord doesn't match
# the pinned location or is outside range
dropAxisValueTable = False
for rec in axisValueTable.AxisValueRecord:
axisTag = designAxes[rec.AxisIndex].AxisTag
axisValue = rec.Value
if isAxisValueOutsideLimits(axisTag, axisValue):
dropAxisValueTable = True
break
if dropAxisValueTable:
continue
else:
log.warn("Unknown AxisValue table format (%s); ignored", axisValueFormat)
newAxisValueTables.append(axisValueTable)
stat.DesignAxisRecord.Axis[:] = newDesignAxes stat.AxisValueArray.AxisValue = newAxisValueTables
stat.DesignAxisCount = len(stat.DesignAxisRecord.Axis) stat.AxisValueCount = len(stat.AxisValueArray.AxisValue)
def getVariationNameIDs(varfont): def getVariationNameIDs(varfont):