Merge pull request #1561 from googlefonts/refactor-instantiate-varstore
refactor instantiateItemVariationStore for better test-ability
This commit is contained in:
commit
3217f9e8d7
@ -17,9 +17,7 @@ from fontTools.varLib.models import supportScalar, normalizeValue, piecewiseLine
|
||||
from fontTools.varLib.iup import iup_delta
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
||||
from fontTools.varLib.varStore import VarStoreInstancer
|
||||
from fontTools.varLib.mvar import MVAR_ENTRIES
|
||||
import collections
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
import os
|
||||
@ -132,125 +130,106 @@ def instantiateCvar(varfont, location):
|
||||
del varfont["cvar"]
|
||||
|
||||
|
||||
def setMvarDeltas(varfont, location):
|
||||
def setMvarDeltas(varfont, deltaArray):
|
||||
log.info("Setting MVAR deltas")
|
||||
|
||||
mvar = varfont["MVAR"].table
|
||||
fvar = varfont["fvar"]
|
||||
varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, location)
|
||||
records = mvar.ValueRecord
|
||||
# accumulate applicable deltas as floats and only round at the end
|
||||
deltas = collections.defaultdict(float)
|
||||
for rec in records:
|
||||
mvarTag = rec.ValueTag
|
||||
if mvarTag not in MVAR_ENTRIES:
|
||||
continue
|
||||
tableTag, itemName = MVAR_ENTRIES[mvarTag]
|
||||
deltas[(tableTag, itemName)] += varStoreInstancer[rec.VarIdx]
|
||||
|
||||
for (tableTag, itemName), delta in deltas.items():
|
||||
setattr(
|
||||
varfont[tableTag],
|
||||
itemName,
|
||||
getattr(varfont[tableTag], itemName) + otRound(delta),
|
||||
)
|
||||
varDataIndex = rec.VarIdx >> 16
|
||||
itemIndex = rec.VarIdx & 0xFFFF
|
||||
deltaRow = deltaArray[varDataIndex][itemIndex]
|
||||
delta = sum(deltaRow)
|
||||
if delta != 0:
|
||||
setattr(
|
||||
varfont[tableTag],
|
||||
itemName,
|
||||
getattr(varfont[tableTag], itemName) + otRound(delta),
|
||||
)
|
||||
|
||||
|
||||
def instantiateMvar(varfont, location):
|
||||
log.info("Instantiating MVAR table")
|
||||
# First instantiate to new position without modifying MVAR table
|
||||
setMvarDeltas(varfont, location)
|
||||
|
||||
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:
|
||||
# Delete table if no more regions left.
|
||||
del varfont["MVAR"]
|
||||
|
||||
|
||||
def instantiateItemVariationStore(varStore, fvarAxes, location):
|
||||
regionsToBeRemoved = set()
|
||||
regionScalars = {}
|
||||
pinnedAxes = set(location.keys())
|
||||
fvarAxisIndices = {
|
||||
axis.axisTag: index
|
||||
for index, axis in enumerate(fvarAxes)
|
||||
if axis.axisTag in pinnedAxes
|
||||
def _getVarRegionAxes(region, fvarAxes):
|
||||
# map fvar axes tags to VarRegionAxis in VarStore region, excluding axes that
|
||||
# don't participate (peak == 0)
|
||||
axes = {}
|
||||
assert len(fvarAxes) == len(region.VarRegionAxis)
|
||||
for fvarAxis, regionAxis in zip(fvarAxes, region.VarRegionAxis):
|
||||
if regionAxis.PeakCoord != 0:
|
||||
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):
|
||||
# collect set of axisTags which have influence: peak != 0
|
||||
regionAxes = set(
|
||||
axis
|
||||
for axis, (start, peak, end) in region.get_support(fvarAxes).items()
|
||||
if peak != 0
|
||||
return supportScalar(location, pinnedAxes)
|
||||
|
||||
|
||||
def _scaleVarDataDeltas(varData, regionScalars):
|
||||
# multiply all varData deltas in-place by the corresponding region scalar
|
||||
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
|
||||
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
|
||||
return deltaSets
|
||||
|
||||
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 = []
|
||||
for vardata in varStore.VarData:
|
||||
varRegionIndex = vardata.VarRegionIndex
|
||||
if regionsToBeRemoved.issuperset(varRegionIndex):
|
||||
for varData in varStore.VarData:
|
||||
if regionIndices.isdisjoint(varData.VarRegionIndex):
|
||||
# drop VarData subtable if we remove all the regions referenced by it
|
||||
continue
|
||||
|
||||
# Apply scalars for regions to be retained.
|
||||
for item in vardata.Item:
|
||||
for column, delta in enumerate(item):
|
||||
regionIndex = varRegionIndex[column]
|
||||
if regionIndex in regionScalars:
|
||||
scalar = regionScalars[regionIndex]
|
||||
item[column] = otRound(delta * scalar)
|
||||
# only retain delta-set columns that correspond to the given regions
|
||||
varData.Item = _getVarDataDeltasForRegions(varData, regionIndices, rounded=True)
|
||||
varData.VarRegionIndex = [
|
||||
ri for ri in varData.VarRegionIndex if ri in regionIndices
|
||||
]
|
||||
varData.VarRegionCount = len(varData.VarRegionIndex)
|
||||
|
||||
if not regionsToBeRemoved.isdisjoint(varRegionIndex):
|
||||
# from each deltaset row, delete columns corresponding to the regions to
|
||||
# be deleted
|
||||
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)
|
||||
# recalculate NumShorts, reordering columns as necessary
|
||||
varData.optimize()
|
||||
newVarDatas.append(varData)
|
||||
|
||||
varStore.VarData = newVarDatas
|
||||
varStore.VarDataCount = len(varStore.VarData)
|
||||
@ -258,6 +237,45 @@ def instantiateItemVariationStore(varStore, fvarAxes, location):
|
||||
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):
|
||||
for tableTag in ("GPOS", "GSUB"):
|
||||
if tableTag not in varfont or not hasattr(
|
||||
|
@ -1,8 +1,10 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools import ttLib
|
||||
from fontTools.ttLib.tables import _f_v_a_r
|
||||
from fontTools.varLib import instancer
|
||||
from fontTools.varLib.mvar import MVAR_ENTRIES
|
||||
from fontTools.varLib import builder
|
||||
import os
|
||||
import pytest
|
||||
|
||||
@ -12,7 +14,7 @@ TESTDATA = os.path.join(os.path.dirname(__file__), "data")
|
||||
|
||||
@pytest.fixture
|
||||
def varfont():
|
||||
f = TTFont()
|
||||
f = ttLib.TTFont()
|
||||
f.importXML(os.path.join(TESTDATA, "PartialInstancerTest-VF.ttx"))
|
||||
return f
|
||||
|
||||
@ -211,3 +213,178 @@ class InstantiateMvarTest(object):
|
||||
assert getattr(varfont[table_tag], item_name) == expected_value
|
||||
|
||||
assert "MVAR" not in varfont
|
||||
|
||||
|
||||
class InstantiateItemVariationStoreTest(object):
|
||||
def test_getVarRegionAxes(self):
|
||||
axisOrder = ["wght", "wdth", "opsz"]
|
||||
regionAxes = {"wdth": (-1.0, -1.0, 0.0), "wght": (0.0, 1.0, 1.0)}
|
||||
region = builder.buildVarRegion(regionAxes, axisOrder)
|
||||
fvarAxes = [SimpleNamespace(axisTag=tag) for tag in axisOrder]
|
||||
|
||||
result = instancer._getVarRegionAxes(region, fvarAxes)
|
||||
|
||||
assert {
|
||||
axisTag: (axis.StartCoord, axis.PeakCoord, axis.EndCoord)
|
||||
for axisTag, axis in result.items()
|
||||
} == regionAxes
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"location, regionAxes, expected",
|
||||
[
|
||||
({"wght": 0.5}, {"wght": (0.0, 1.0, 1.0)}, 0.5),
|
||||
({"wght": 0.5}, {"wght": (0.0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0.0)}, 0.5),
|
||||
(
|
||||
{"wght": 0.5, "wdth": -0.5},
|
||||
{"wght": (0.0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0.0)},
|
||||
0.25,
|
||||
),
|
||||
({"wght": 0.5, "wdth": -0.5}, {"wght": (0.0, 1.0, 1.0)}, 0.5),
|
||||
({"wght": 0.5}, {"wdth": (-1.0, -1.0, 1.0)}, 1.0),
|
||||
],
|
||||
)
|
||||
def test_getVarRegionScalar(self, location, regionAxes, expected):
|
||||
varRegionAxes = {
|
||||
axisTag: builder.buildVarRegionAxis(support)
|
||||
for axisTag, support in regionAxes.items()
|
||||
}
|
||||
|
||||
assert instancer._getVarRegionScalar(location, varRegionAxes) == expected
|
||||
|
||||
def test_scaleVarDataDeltas(self):
|
||||
regionScalars = [0.0, 0.5, 1.0]
|
||||
varData = builder.buildVarData(
|
||||
[1, 0], [[100, 200], [-100, -200]], optimize=False
|
||||
)
|
||||
|
||||
instancer._scaleVarDataDeltas(varData, regionScalars)
|
||||
|
||||
assert varData.Item == [[50, 0], [-50, 0]]
|
||||
|
||||
def test_getVarDataDeltasForRegions(self):
|
||||
varData = builder.buildVarData(
|
||||
[1, 0], [[33.5, 67.9], [-100, -200]], optimize=False
|
||||
)
|
||||
|
||||
assert instancer._getVarDataDeltasForRegions(varData, {1}) == [[33.5], [-100]]
|
||||
assert instancer._getVarDataDeltasForRegions(varData, {0}) == [[67.9], [-200]]
|
||||
assert instancer._getVarDataDeltasForRegions(varData, set()) == [[], []]
|
||||
assert instancer._getVarDataDeltasForRegions(varData, {1}, rounded=True) == [
|
||||
[34],
|
||||
[-100],
|
||||
]
|
||||
|
||||
def test_subsetVarStoreRegions(self):
|
||||
regionList = builder.buildVarRegionList(
|
||||
[
|
||||
{"wght": (0, 0.5, 1)},
|
||||
{"wght": (0.5, 1, 1)},
|
||||
{"wdth": (-1, -1, 0)},
|
||||
{"wght": (0, 0.5, 1), "wdth": (-1, -1, 0)},
|
||||
{"wght": (0.5, 1, 1), "wdth": (-1, -1, 0)},
|
||||
],
|
||||
["wght", "wdth"],
|
||||
)
|
||||
varData1 = builder.buildVarData([0, 1, 2, 4], [[0, 1, 2, 3], [4, 5, 6, 7]])
|
||||
varData2 = builder.buildVarData([2, 3, 1], [[8, 9, 10], [11, 12, 13]])
|
||||
varStore = builder.buildVarStore(regionList, [varData1, varData2])
|
||||
|
||||
instancer._subsetVarStoreRegions(varStore, {0, 4})
|
||||
|
||||
assert (
|
||||
varStore.VarRegionList.RegionCount
|
||||
== len(varStore.VarRegionList.Region)
|
||||
== 2
|
||||
)
|
||||
axis00 = varStore.VarRegionList.Region[0].VarRegionAxis[0]
|
||||
assert (axis00.StartCoord, axis00.PeakCoord, axis00.EndCoord) == (0, 0.5, 1)
|
||||
axis01 = varStore.VarRegionList.Region[0].VarRegionAxis[1]
|
||||
assert (axis01.StartCoord, axis01.PeakCoord, axis01.EndCoord) == (0, 0, 0)
|
||||
axis10 = varStore.VarRegionList.Region[1].VarRegionAxis[0]
|
||||
assert (axis10.StartCoord, axis10.PeakCoord, axis10.EndCoord) == (0.5, 1, 1)
|
||||
axis11 = varStore.VarRegionList.Region[1].VarRegionAxis[1]
|
||||
assert (axis11.StartCoord, axis11.PeakCoord, axis11.EndCoord) == (-1, -1, 0)
|
||||
|
||||
assert varStore.VarDataCount == len(varStore.VarData) == 1
|
||||
assert varStore.VarData[0].VarRegionCount == 2
|
||||
assert varStore.VarData[0].VarRegionIndex == [0, 1]
|
||||
assert varStore.VarData[0].Item == [[0, 3], [4, 7]]
|
||||
assert varStore.VarData[0].NumShorts == 0
|
||||
|
||||
@pytest.fixture
|
||||
def fvarAxes(self):
|
||||
wght = _f_v_a_r.Axis()
|
||||
wght.axisTag = Tag("wght")
|
||||
wght.minValue = 100
|
||||
wght.defaultValue = 400
|
||||
wght.maxValue = 900
|
||||
wdth = _f_v_a_r.Axis()
|
||||
wdth.axisTag = Tag("wdth")
|
||||
wdth.minValue = 70
|
||||
wdth.defaultValue = 100
|
||||
wdth.maxValue = 100
|
||||
return [wght, wdth]
|
||||
|
||||
@pytest.fixture
|
||||
def varStore(self):
|
||||
return builder.buildVarStore(
|
||||
builder.buildVarRegionList(
|
||||
[
|
||||
{"wght": (-1.0, -1.0, 0)},
|
||||
{"wght": (0, 0.5, 1.0)},
|
||||
{"wght": (0.5, 1.0, 1.0)},
|
||||
{"wdth": (-1.0, -1.0, 0)},
|
||||
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
|
||||
{"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)},
|
||||
{"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
|
||||
],
|
||||
["wght", "wdth"],
|
||||
),
|
||||
[
|
||||
builder.buildVarData([0, 1, 2], [[100, 100, 100], [100, 100, 100]]),
|
||||
builder.buildVarData(
|
||||
[3, 4, 5, 6], [[100, 100, 100, 100], [100, 100, 100, 100]]
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"location, expected_deltas, num_regions, num_vardatas",
|
||||
[
|
||||
({"wght": 0}, [[[0, 0, 0], [0, 0, 0]], [[], []]], 1, 1),
|
||||
({"wght": 0.25}, [[[0, 50, 0], [0, 50, 0]], [[], []]], 2, 1),
|
||||
({"wdth": 0}, [[[], []], [[0], [0]]], 3, 1),
|
||||
({"wdth": -0.75}, [[[], []], [[75], [75]]], 6, 2),
|
||||
(
|
||||
{"wght": 0, "wdth": 0},
|
||||
[[[0, 0, 0], [0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0]]],
|
||||
0,
|
||||
0,
|
||||
),
|
||||
(
|
||||
{"wght": 0.25, "wdth": 0},
|
||||
[[[0, 50, 0], [0, 50, 0]], [[0, 0, 0, 0], [0, 0, 0, 0]]],
|
||||
0,
|
||||
0,
|
||||
),
|
||||
(
|
||||
{"wght": 0, "wdth": -0.75},
|
||||
[[[0, 0, 0], [0, 0, 0]], [[75, 0, 0, 0], [75, 0, 0, 0]]],
|
||||
0,
|
||||
0,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_instantiate_default_deltas(
|
||||
self, varStore, fvarAxes, location, expected_deltas, num_regions, num_vardatas
|
||||
):
|
||||
defaultDeltas = instancer.instantiateItemVariationStore(
|
||||
varStore, fvarAxes, location
|
||||
)
|
||||
|
||||
# from fontTools.misc.testTools import getXML
|
||||
# print("\n".join(getXML(varStore.toXML, ttFont=None)))
|
||||
|
||||
assert defaultDeltas == expected_deltas
|
||||
assert varStore.VarRegionList.RegionCount == num_regions
|
||||
assert varStore.VarDataCount == num_vardatas
|
||||
|
Loading…
x
Reference in New Issue
Block a user