Merge pull request #1561 from googlefonts/refactor-instantiate-varstore

refactor instantiateItemVariationStore for better test-ability
This commit is contained in:
Cosimo Lupo 2019-04-03 16:58:06 +01:00 committed by GitHub
commit 3217f9e8d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 288 additions and 93 deletions

View File

@ -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(

View File

@ -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