instancer: convert item to tuple varstore to reuse same partial istancing code

This commit is contained in:
Cosimo Lupo 2019-04-16 18:14:05 +01:00
parent 4a7ab3fee2
commit 8aa57fef81
No known key found for this signature in database
GPG Key ID: 20D4A261E4A0E642
3 changed files with 282 additions and 231 deletions

View File

@ -12,10 +12,12 @@ NOTE: The module is experimental and both the API and the CLI *will* change.
"""
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import floatToFixedToFloat, otRound
from fontTools.misc.fixedTools import floatToFixedToFloat
from fontTools.varLib.models import supportScalar, normalizeValue, piecewiseLinearMap
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
from fontTools.varLib import builder
from fontTools.varLib.mvar import MVAR_ENTRIES
import collections
from copy import deepcopy
@ -150,13 +152,12 @@ def setMvarDeltas(varfont, deltaArray):
tableTag, itemName = MVAR_ENTRIES[mvarTag]
varDataIndex = rec.VarIdx >> 16
itemIndex = rec.VarIdx & 0xFFFF
deltaRow = deltaArray[varDataIndex][itemIndex]
delta = sum(deltaRow)
delta = deltaArray[varDataIndex][itemIndex]
if delta != 0:
setattr(
varfont[tableTag],
itemName,
getattr(varfont[tableTag], itemName) + otRound(delta),
getattr(varfont[tableTag], itemName) + delta,
)
@ -178,116 +179,111 @@ def instantiateMvar(varfont, location):
del varfont["MVAR"]
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
class _TupleVarStoreAdapter(object):
def __init__(self, regions, axisOrder, tupleVarData, itemCounts):
self.regions = regions
self.axisOrder = axisOrder
self.tupleVarData = tupleVarData
self.itemCounts = itemCounts
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
}
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
]
)
return deltaSets
def _subsetVarStoreRegions(varStore, regionIndices):
# drop regions not in regionIndices
for varData in varStore.VarData:
if regionIndices.isdisjoint(varData.VarRegionIndex):
# empty VarData subtable if we remove all the regions referenced by it
varData.Item = [[] for _ in range(varData.ItemCount)]
varData.VarRegionIndex = []
varData.VarRegionCount = varData.NumShorts = 0
continue
# 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
@classmethod
def fromItemVarStore(cls, itemVarStore, fvarAxes):
axisOrder = [axis.axisTag for axis in fvarAxes]
regions = [
region.get_support(fvarAxes) for region in itemVarStore.VarRegionList.Region
]
varData.VarRegionCount = len(varData.VarRegionIndex)
tupleVarData = []
itemCounts = []
for varData in itemVarStore.VarData:
variations = []
varDataRegions = (regions[i] for i in varData.VarRegionIndex)
for axes, coordinates in zip(varDataRegions, zip(*varData.Item)):
variations.append(TupleVariation(axes, list(coordinates)))
tupleVarData.append(variations)
itemCounts.append(varData.ItemCount)
return cls(regions, axisOrder, tupleVarData, itemCounts)
# recalculate NumShorts, reordering columns as necessary
varData.optimize()
def dropAxes(self, axes):
prunedRegions = (
frozenset(
(axisTag, support)
for axisTag, support in region.items()
if axisTag not in axes
)
for region in self.regions
)
# dedup regions while keeping original order
uniqueRegions = collections.OrderedDict.fromkeys(prunedRegions)
self.regions = [dict(items) for items in uniqueRegions if items]
# TODO(anthrotype) uncomment this once we support subsetting fvar axes
# self.axisOrder = [
# axisTag for axisTag in self.axisOrder if axisTag not in axes
# ]
# remove unused regions from VarRegionList
varStore.prune_regions()
def instantiate(self, location):
defaultDeltaArray = []
for variations, itemCount in zip(self.tupleVarData, self.itemCounts):
defaultDeltas = instantiateTupleVariationStore(variations, location)
if not defaultDeltas:
defaultDeltas = [0] * itemCount
defaultDeltaArray.append(defaultDeltas)
# remove pinned axes from all the regions
self.dropAxes(location.keys())
return defaultDeltaArray
def asItemVarStore(self):
regionOrder = [frozenset(axes.items()) for axes in self.regions]
varDatas = []
for variations, itemCount in zip(self.tupleVarData, self.itemCounts):
if variations:
assert len(variations[0].coordinates) == itemCount
varRegionIndices = [
regionOrder.index(frozenset(var.axes.items())) for var in variations
]
varDataItems = list(zip(*(var.coordinates for var in variations)))
varDatas.append(
builder.buildVarData(varRegionIndices, varDataItems, optimize=False)
)
else:
varDatas.append(
builder.buildVarData([], [[] for _ in range(itemCount)])
)
regionList = builder.buildVarRegionList(self.regions, self.axisOrder)
itemVarStore = builder.buildVarStore(regionList, varDatas)
return itemVarStore
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)
def instantiateItemVariationStore(itemVarStore, fvarAxes, location):
""" Compute deltas at partial location, and update varStore in-place.
# 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
]
Remove regions in which all axes were instanced, and scale the deltas of
the remaining regions where only some of the axes were instanced.
# 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)
Args:
varStore: An otTables.VarStore object (Item Variation Store)
fvarAxes: list of fvar's Axis objects
location: Dict[str, float] mapping axis tags to normalized axis coordinates.
May not specify coordinates for all the fvar axes.
if varStore.VarRegionList.Region:
Returns:
defaultDeltaArray: the deltas to be added to the default instance (list of list
of integers, indexed by outer/inner VarIdx)
varIndexMapping: a mapping from old to new VarIdx after optimization (None if
varStore was fully instanced thus left empty).
"""
tupleVarStore = _TupleVarStoreAdapter.fromItemVarStore(itemVarStore, fvarAxes)
defaultDeltaArray = tupleVarStore.instantiate(location)
newItemVarStore = tupleVarStore.asItemVarStore()
itemVarStore.VarRegionList = newItemVarStore.VarRegionList
assert itemVarStore.VarDataCount == newItemVarStore.VarDataCount
itemVarStore.VarData = newItemVarStore.VarData
if itemVarStore.VarRegionList.Region:
# optimize VarStore, and get a map from old to new VarIdx after optimization
varIndexMapping = varStore.optimize()
varIndexMapping = itemVarStore.optimize()
else:
varIndexMapping = None # VarStore is empty

View File

@ -133,8 +133,11 @@ def VarData_addItem(self, deltas):
ot.VarData.addItem = VarData_addItem
def VarRegion_get_support(self, fvar_axes):
return {fvar_axes[i].axisTag: (reg.StartCoord,reg.PeakCoord,reg.EndCoord)
for i,reg in enumerate(self.VarRegionAxis)}
return {
fvar_axes[i].axisTag: (reg.StartCoord,reg.PeakCoord,reg.EndCoord)
for i, reg in enumerate(self.VarRegionAxis)
if reg.PeakCoord != 0
}
ot.VarRegion.get_support = VarRegion_get_support

View File

@ -2,6 +2,7 @@ from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from fontTools import ttLib
from fontTools.ttLib.tables import _f_v_a_r
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.varLib import instancer
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib import builder
@ -24,6 +25,21 @@ def optimize(request):
return request.param
@pytest.fixture
def fvarAxes():
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]
def _get_coordinates(varfont, glyphname):
# converts GlyphCoordinates to a list of (x, y) tuples, so that pytest's
# assert will give us a nicer diff
@ -256,118 +272,17 @@ class InstantiateMvarTest(object):
class InstantiateItemVariationStoreTest(object):
def test_getVarRegionAxes(self):
def test_VarRegion_get_support(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 len(region.VarRegionAxis) == 3
assert region.VarRegionAxis[2].PeakCoord == 0
assert {
axisTag: (axis.StartCoord, axis.PeakCoord, axis.EndCoord)
for axisTag, axis in result.items()
} == regionAxes
fvarAxes = [SimpleNamespace(axisTag=axisTag) for axisTag in axisOrder]
@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) == 2
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
assert varStore.VarData[1].VarRegionCount == 0
assert varStore.VarData[1].VarRegionIndex == []
assert varStore.VarData[1].Item == [[], []]
assert varStore.VarData[1].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]
assert region.get_support(fvarAxes) == regionAxes
@pytest.fixture
def varStore(self):
@ -395,25 +310,13 @@ class InstantiateItemVariationStoreTest(object):
@pytest.mark.parametrize(
"location, expected_deltas, num_regions",
[
({"wght": 0}, [[[0, 0, 0], [0, 0, 0]], [[], []]], 1),
({"wght": 0.25}, [[[0, 50, 0], [0, 50, 0]], [[], []]], 2),
({"wdth": 0}, [[[], []], [[0], [0]]], 3),
({"wdth": -0.75}, [[[], []], [[75], [75]]], 6),
(
{"wght": 0, "wdth": 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,
),
(
{"wght": 0, "wdth": -0.75},
[[[0, 0, 0], [0, 0, 0]], [[75, 0, 0, 0], [75, 0, 0, 0]]],
0,
),
({"wght": 0}, [[0, 0], [0, 0]], 1),
({"wght": 0.25}, [[50, 50], [0, 0]], 1),
({"wdth": 0}, [[0, 0], [0, 0]], 3),
({"wdth": -0.75}, [[0, 0], [75, 75]], 3),
({"wght": 0, "wdth": 0}, [[0, 0], [0, 0]], 0),
({"wght": 0.25, "wdth": 0}, [[50, 50], [0, 0]], 0),
({"wght": 0, "wdth": -0.75}, [[0, 0], [75, 75]], 0),
],
)
def test_instantiate_default_deltas(
@ -425,3 +328,152 @@ class InstantiateItemVariationStoreTest(object):
assert defaultDeltas == expected_deltas
assert varStore.VarRegionList.RegionCount == num_regions
class TupleVarStoreAdapterTest(object):
def test_instantiate(self):
regions = [
{"wght": (-1.0, -1.0, 0)},
{"wght": (0.0, 1.0, 1.0)},
{"wdth": (-1.0, -1.0, 0)},
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
{"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
]
axisOrder = ["wght", "wdth"]
tupleVarData = [
[
TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]),
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [30, 90]),
TupleVariation(
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100]
),
TupleVariation(
{"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120]
),
],
[
TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]),
TupleVariation(
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55]
),
TupleVariation(
{"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75]
),
],
]
adapter = instancer._TupleVarStoreAdapter(
regions, axisOrder, tupleVarData, itemCounts=[2, 2]
)
defaultDeltaArray = adapter.instantiate({"wght": 0.5})
assert defaultDeltaArray == [[15, 45], [0, 0]]
assert adapter.regions == [{"wdth": (-1.0, -1.0, 0)}]
assert adapter.tupleVarData == [
[TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-30, -60])],
[TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-12, 8])],
]
def test_dropAxes(self):
regions = [
{"wght": (-1.0, -1.0, 0)},
{"wght": (0.0, 1.0, 1.0)},
{"wdth": (-1.0, -1.0, 0)},
{"opsz": (0.0, 1.0, 1.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)},
]
axisOrder = ["wght", "wdth", "opsz"]
adapter = instancer._TupleVarStoreAdapter(regions, axisOrder, [], itemCounts=[])
adapter.dropAxes({"wdth"})
assert adapter.regions == [
{"wght": (-1.0, -1.0, 0)},
{"wght": (0.0, 1.0, 1.0)},
{"opsz": (0.0, 1.0, 1.0)},
{"wght": (0.0, 0.5, 1.0)},
{"wght": (0.5, 1.0, 1.0)},
]
adapter.dropAxes({"wght", "opsz"})
assert adapter.regions == []
def test_roundtrip(self, fvarAxes):
regions = [
{"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)},
]
axisOrder = [axis.axisTag for axis in fvarAxes]
itemVarStore = builder.buildVarStore(
builder.buildVarRegionList(regions, axisOrder),
[
builder.buildVarData(
[0, 1, 2, 4, 5, 6],
[[10, -20, 30, -40, 50, -60], [70, -80, 90, -100, 110, -120]],
),
builder.buildVarData(
[3, 4, 5, 6], [[5, -15, 25, -35], [45, -55, 65, -75]]
),
],
)
adapter = instancer._TupleVarStoreAdapter.fromItemVarStore(
itemVarStore, fvarAxes
)
assert adapter.tupleVarData == [
[
TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]),
TupleVariation({"wght": (0, 0.5, 1.0)}, [-20, -80]),
TupleVariation({"wght": (0.5, 1.0, 1.0)}, [30, 90]),
TupleVariation(
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100]
),
TupleVariation(
{"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [50, 110]
),
TupleVariation(
{"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120]
),
],
[
TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]),
TupleVariation(
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55]
),
TupleVariation(
{"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [25, 65]
),
TupleVariation(
{"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75]
),
],
]
assert adapter.itemCounts == [data.ItemCount for data in itemVarStore.VarData]
assert adapter.regions == regions
assert adapter.axisOrder == axisOrder
itemVarStore2 = adapter.asItemVarStore()
assert [
reg.get_support(fvarAxes)
for reg in itemVarStore2.VarRegionList.Region
] == regions
assert itemVarStore2.VarDataCount == 2
assert itemVarStore2.VarData[0].VarRegionIndex == [0, 1, 2, 4, 5, 6]
assert itemVarStore2.VarData[0].Item == [
[10, -20, 30, -40, 50, -60],
[70, -80, 90, -100, 110, -120],
]
assert itemVarStore2.VarData[1].VarRegionIndex == [3, 4, 5, 6]
assert itemVarStore2.VarData[1].Item == [[5, -15, 25, -35], [45, -55, 65, -75]]