refactor instantiateItemVariationStore for better test-ability

The function now takes a VarStore instance, the fvar axes and a partial
location, and returns an array of delta-sets to be applied to the
default instance.

The algorithm is now more similar to the one used for instantiating the
tuple variation store.

Tests are coming soon.
This commit is contained in:
Cosimo Lupo 2019-03-28 17:41:58 +00:00
parent 3699f5b08c
commit 5f083bdf2e
No known key found for this signature in database
GPG Key ID: 179A8F0895A02F4F

View File

@ -17,9 +17,7 @@ from fontTools.varLib.models import supportScalar, normalizeValue, piecewiseLine
from fontTools.varLib.iup import iup_delta from fontTools.varLib.iup import iup_delta
from fontTools.ttLib import TTFont from fontTools.ttLib import TTFont
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
from fontTools.varLib.varStore import VarStoreInstancer
from fontTools.varLib.mvar import MVAR_ENTRIES from fontTools.varLib.mvar import MVAR_ENTRIES
import collections
from copy import deepcopy from copy import deepcopy
import logging import logging
import os import os
@ -132,23 +130,21 @@ def instantiateCvar(varfont, location):
del varfont["cvar"] del varfont["cvar"]
def setMvarDeltas(varfont, location): def setMvarDeltas(varfont, deltaArray):
log.info("Setting MVAR deltas") log.info("Setting MVAR deltas")
mvar = varfont["MVAR"].table mvar = varfont["MVAR"].table
fvar = varfont["fvar"]
varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, location)
records = mvar.ValueRecord records = mvar.ValueRecord
# accumulate applicable deltas as floats and only round at the end
deltas = collections.defaultdict(float)
for rec in records: for rec in records:
mvarTag = rec.ValueTag mvarTag = rec.ValueTag
if mvarTag not in MVAR_ENTRIES: if mvarTag not in MVAR_ENTRIES:
continue continue
tableTag, itemName = MVAR_ENTRIES[mvarTag] tableTag, itemName = MVAR_ENTRIES[mvarTag]
deltas[(tableTag, itemName)] += varStoreInstancer[rec.VarIdx] varDataIndex = rec.VarIdx >> 16
itemIndex = rec.VarIdx & 0xFFFF
for (tableTag, itemName), delta in deltas.items(): deltaRow = deltaArray[varDataIndex][itemIndex]
delta = sum(deltaRow)
if delta != 0:
setattr( setattr(
varfont[tableTag], varfont[tableTag],
itemName, itemName,
@ -158,99 +154,82 @@ def setMvarDeltas(varfont, location):
def instantiateMvar(varfont, location): def instantiateMvar(varfont, location):
log.info("Instantiating MVAR table") log.info("Instantiating MVAR table")
# First instantiate to new position without modifying MVAR table
setMvarDeltas(varfont, location)
varStore = varfont["MVAR"].table.VarStore varStore = varfont["MVAR"].table.VarStore
instantiateItemVariationStore(varStore, varfont["fvar"].axes, location) fvarAxes = varfont["fvar"].axes
defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, location)
setMvarDeltas(varfont, defaultDeltas)
if not varStore.VarRegionList.Region: if not varStore.VarRegionList.Region:
# Delete table if no more regions left. # Delete table if no more regions left.
del varfont["MVAR"] del varfont["MVAR"]
def instantiateItemVariationStore(varStore, fvarAxes, location): def _getVarRegionAxes(region, fvarAxes):
regionsToBeRemoved = set() # map fvar axes tags to VarRegionAxis in VarStore region, excluding axes that
regionScalars = {} # don't participate (peak == 0)
pinnedAxes = set(location.keys()) axes = {}
fvarAxisIndices = { assert len(fvarAxes) == len(region.VarRegionAxis)
axis.axisTag: index for fvarAxis, regionAxis in zip(fvarAxes, region.VarRegionAxis):
for index, axis in enumerate(fvarAxes) if regionAxis.PeakCoord != 0:
if axis.axisTag in pinnedAxes axes[fvarAxis.axisTag] = regionAxis
return axes
def _getVarRegionScalar(location, regionAxes):
# compute partial product of per-axis scalars at location, excluding the axes
# that are not pinned
pinnedAxes = {
axisTag: (axis.StartCoord, axis.PeakCoord, axis.EndCoord)
for axisTag, axis in regionAxes.items()
if axisTag in location
} }
for regionIndex, region in enumerate(varStore.VarRegionList.Region): return supportScalar(location, pinnedAxes)
# collect set of axisTags which have influence: peak != 0
regionAxes = set(
axis def _scaleVarDataDeltas(varData, regionScalars):
for axis, (start, peak, end) in region.get_support(fvarAxes).items() # multiply all varData deltas in-place by the corresponding region scalar
if peak != 0 varRegionCount = len(varData.VarRegionIndex)
scalars = [regionScalars[regionIndex] for regionIndex in varData.VarRegionIndex]
for item in varData.Item:
assert len(item) == varRegionCount
item[:] = [delta * scalar for delta, scalar in zip(item, scalars)]
def _getVarDataDeltasForRegions(varData, regionIndices, rounded=False):
# Get only the deltas that correspond to the given regions (optionally, rounded).
# Returns: list of lists of float
varRegionIndices = varData.VarRegionIndex
deltaSets = []
for item in varData.Item:
deltaSets.append(
[
delta if not rounded else otRound(delta)
for regionIndex, delta in zip(varRegionIndices, item)
if regionIndex in regionIndices
]
) )
pinnedRegionAxes = regionAxes & pinnedAxes return deltaSets
if not pinnedRegionAxes:
# A region where none of the axes having effect are pinned
continue
if len(pinnedRegionAxes) == len(regionAxes):
# All the axes having effect in this region are being pinned so
# remove it
regionsToBeRemoved.add(regionIndex)
else:
# compute the scalar support of the axes to be pinned
pinnedSupport = {
axis: support
for axis, support in region.get_support(fvarAxes).items()
if axis in pinnedRegionAxes
}
pinnedScalar = supportScalar(location, pinnedSupport)
if pinnedScalar == 0.0:
# no influence, drop this region
regionsToBeRemoved.add(regionIndex)
continue
elif pinnedScalar != 1.0:
# This region will be retained but the deltas will be scaled
regionScalars[regionIndex] = pinnedScalar
for axisTag in pinnedRegionAxes:
# For all pinnedRegionAxes make their influence null by setting
# PeakCoord to 0.
axis = region.VarRegionAxis[fvarAxisIndices[axisTag]]
axis.StartCoord, axis.PeakCoord, axis.EndCoord = (0, 0, 0)
def _subsetVarStoreRegions(varStore, regionIndices):
# drop regions not in regionIndices
newVarDatas = [] newVarDatas = []
for vardata in varStore.VarData: for varData in varStore.VarData:
varRegionIndex = vardata.VarRegionIndex if regionIndices.isdisjoint(varData.VarRegionIndex):
if regionsToBeRemoved.issuperset(varRegionIndex):
# drop VarData subtable if we remove all the regions referenced by it # drop VarData subtable if we remove all the regions referenced by it
continue continue
# Apply scalars for regions to be retained. # only retain delta-set columns that correspond to the given regions
for item in vardata.Item: varData.Item = _getVarDataDeltasForRegions(varData, regionIndices, rounded=True)
for column, delta in enumerate(item): varData.VarRegionIndex = [
regionIndex = varRegionIndex[column] ri for ri in varData.VarRegionIndex if ri in regionIndices
if regionIndex in regionScalars: ]
scalar = regionScalars[regionIndex] varData.VarRegionCount = len(varData.VarRegionIndex)
item[column] = otRound(delta * scalar)
if not regionsToBeRemoved.isdisjoint(varRegionIndex): # recalculate NumShorts, reordering columns as necessary
# from each deltaset row, delete columns corresponding to the regions to varData.optimize()
# be deleted newVarDatas.append(varData)
newItems = []
for item in vardata.Item:
newItems.append(
[
delta
for column, delta in enumerate(item)
if varRegionIndex[column] not in regionsToBeRemoved
]
)
vardata.Item = newItems
vardata.ItemCount = len(newItems)
# prune VarRegionIndex from the regions to be deleted
vardata.VarRegionIndex = [
ri for ri in vardata.VarRegionIndex if ri not in regionsToBeRemoved
]
vardata.VarRegionCount = len(vardata.VarRegionIndex)
vardata.calculateNumShorts()
newVarDatas.append(vardata)
varStore.VarData = newVarDatas varStore.VarData = newVarDatas
varStore.VarDataCount = len(varStore.VarData) varStore.VarDataCount = len(varStore.VarData)
@ -258,6 +237,45 @@ def instantiateItemVariationStore(varStore, fvarAxes, location):
varStore.prune_regions() varStore.prune_regions()
def instantiateItemVariationStore(varStore, fvarAxes, location):
regions = [
_getVarRegionAxes(reg, fvarAxes) for reg in varStore.VarRegionList.Region
]
# for each region, compute the scalar support of the axes to be pinned at the
# desired location, and scale the deltas accordingly
regionScalars = [_getVarRegionScalar(location, axes) for axes in regions]
for varData in varStore.VarData:
_scaleVarDataDeltas(varData, regionScalars)
# disable the pinned axes by setting PeakCoord to 0
for axes in regions:
for axisTag, axis in axes.items():
if axisTag in location:
axis.StartCoord, axis.PeakCoord, axis.EndCoord = (0, 0, 0)
# If all axes in a region are pinned, its deltas are added to the default instance
defaultRegionIndices = {
regionIndex
for regionIndex, axes in enumerate(regions)
if all(axis.PeakCoord == 0 for axis in axes.values())
}
# Collect the default deltas into a two-dimension array, with outer/inner indices
# corresponding to a VarData subtable and a deltaset row within that table.
defaultDeltaArray = [
_getVarDataDeltasForRegions(varData, defaultRegionIndices)
for varData in varStore.VarData
]
# drop default regions, or those whose influence at the pinned location is 0
newRegionIndices = {
regionIndex
for regionIndex in range(len(varStore.VarRegionList.Region))
if regionIndex not in defaultRegionIndices and regionScalars[regionIndex] != 0
}
_subsetVarStoreRegions(varStore, newRegionIndices)
return defaultDeltaArray
def instantiateFeatureVariations(varfont, location): def instantiateFeatureVariations(varfont, location):
for tableTag in ("GPOS", "GSUB"): for tableTag in ("GPOS", "GSUB"):
if tableTag not in varfont or not hasattr( if tableTag not in varfont or not hasattr(