Merge pull request #2846 from fonttools/issue2843

[varLib.models] Attempt to fix #2843 by computing the axis ranges
This commit is contained in:
Just van Rossum 2022-10-13 17:15:11 +02:00 committed by GitHub
commit d102b7a9fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 75 additions and 20 deletions

View File

@ -115,12 +115,15 @@ def normalizeLocation(location, axes):
return out
def supportScalar(location, support, ot=True, extrapolate=False):
def supportScalar(location, support, ot=True, extrapolate=False, axisRanges=None):
"""Returns the scalar multiplier at location, for a master
with support. If ot is True, then a peak value of zero
for support of an axis means "axis does not participate". That
is how OpenType Variation Font technology works.
If extrapolate is True, axisRanges must be a dict that maps axis
names to (axisMin, axisMax) tuples.
>>> supportScalar({}, {})
1.0
>>> supportScalar({'wght':.2}, {})
@ -137,11 +140,17 @@ def supportScalar(location, support, ot=True, extrapolate=False):
0.75
>>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
0.75
>>> supportScalar({'wght':4}, {'wght':(0,2,3)}, extrapolate=True)
2.0
>>> supportScalar({'wght':4}, {'wght':(0,2,2)}, extrapolate=True)
2.0
>>> supportScalar({'wght':3}, {'wght':(0,1,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
-1.0
>>> supportScalar({'wght':-1}, {'wght':(0,1,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
-1.0
>>> supportScalar({'wght':3}, {'wght':(0,2,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
1.5
>>> supportScalar({'wght':-1}, {'wght':(0,2,2)}, extrapolate=True, axisRanges={'wght':(0, 2)})
-0.5
"""
if extrapolate and axisRanges is None:
raise TypeError("axisRanges must be passed when extrapolate is True")
scalar = 1.0
for axis, (lower, peak, upper) in support.items():
if ot:
@ -160,18 +169,19 @@ def supportScalar(location, support, ot=True, extrapolate=False):
continue
if extrapolate:
if v < -1 and lower <= -1:
if peak <= -1 and peak < upper:
axisMin, axisMax = axisRanges[axis]
if v < axisMin and lower <= axisMin:
if peak <= axisMin and peak < upper:
scalar *= (v - upper) / (peak - upper)
continue
elif -1 < peak:
elif axisMin < peak:
scalar *= (v - lower) / (peak - lower)
continue
elif +1 < v and +1 <= upper:
if +1 <= peak and lower < peak:
elif axisMax < v and axisMax <= upper:
if axisMax <= peak and lower < peak:
scalar *= (v - lower) / (peak - lower)
continue
elif peak < +1:
elif peak < axisMax:
scalar *= (v - upper) / (peak - upper)
continue
@ -189,9 +199,8 @@ def supportScalar(location, support, ot=True, extrapolate=False):
class VariationModel(object):
"""Locations must have the base master at the origin (ie. 0).
If the extrapolate argument is set to True, then location values are
interpretted in the normalized space, ie. in the [-1,+1] range, and
values are extrapolated outside this range.
If the extrapolate argument is set to True, then values are extrapolated
outside the axis range.
>>> from pprint import pprint
>>> locations = [ \
@ -241,6 +250,7 @@ class VariationModel(object):
self.origLocations = locations
self.axisOrder = axisOrder if axisOrder is not None else []
self.extrapolate = extrapolate
self.axisRanges = self.computeAxisRanges(locations) if extrapolate else None
locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations]
keyFunc = self.getMasterLocationsSortKeyFunc(
@ -265,6 +275,17 @@ class VariationModel(object):
self._subModels[key] = subModel
return subModel, subList(key, items)
@staticmethod
def computeAxisRanges(locations):
axisRanges = {}
allAxes = {axis for loc in locations for axis in loc.keys()}
for loc in locations:
for axis in allAxes:
value = loc.get(axis, 0)
axisMin, axisMax = axisRanges.get(axis, (value, value))
axisRanges[axis] = min(value, axisMin), max(value, axisMax)
return axisRanges
@staticmethod
def getMasterLocationsSortKeyFunc(locations, axisOrder=[]):
if {} not in locations:
@ -439,8 +460,12 @@ class VariationModel(object):
return model.getDeltas(items, round=round), model.supports
def getScalars(self, loc):
return [supportScalar(loc, support, extrapolate=self.extrapolate)
for support in self.supports]
return [
supportScalar(
loc, support, extrapolate=self.extrapolate, axisRanges=self.axisRanges
)
for support in self.supports
]
@staticmethod
def interpolateFromDeltasAndScalars(deltas, scalars):

View File

@ -36,10 +36,40 @@ def test_supportScalar():
assert supportScalar({"wght": 0.2}, {}) == 1.0
assert supportScalar({"wght": 0.2}, {"wght": (0, 2, 3)}) == 0.1
assert supportScalar({"wght": 2.5}, {"wght": (0, 2, 4)}) == 0.75
assert supportScalar({"wght": 4}, {"wght": (0, 2, 2)}) == 0.0
assert supportScalar({"wght": 4}, {"wght": (0, 2, 2)}, extrapolate=True) == 2.0
assert supportScalar({"wght": 4}, {"wght": (0, 2, 3)}, extrapolate=True) == 2.0
assert supportScalar({"wght": 2}, {"wght": (0, 0.75, 1)}, extrapolate=True) == -4.0
assert supportScalar({"wght": 3}, {"wght": (0, 2, 2)}) == 0.0
assert supportScalar({"wght": 3}, {"wght": (0, 2, 2)}, extrapolate=True, axisRanges={"wght": (0, 2)}) == 1.5
assert supportScalar({"wght": -1}, {"wght": (0, 2, 2)}, extrapolate=True, axisRanges={"wght": (0, 2)}) == -0.5
assert supportScalar({"wght": 3}, {"wght": (0, 1, 2)}, extrapolate=True, axisRanges={"wght": (0, 2)}) == -1.0
assert supportScalar({"wght": -1}, {"wght": (0, 1, 2)}, extrapolate=True, axisRanges={"wght": (0, 2)}) == -1.0
assert supportScalar({"wght": 2}, {"wght": (0, 0.75, 1)}, extrapolate=True, axisRanges={"wght": (0, 1)}) == -4.0
with pytest.raises(TypeError):
supportScalar({"wght": 2}, {"wght": (0, 0.75, 1)}, extrapolate=True, axisRanges=None)
def test_model_extrapolate():
locations = [{}, {"a": 1}, {"b": 1}, {"a": 1, "b": 1}]
model = VariationModel(locations, extrapolate=True)
masterValues = [100, 200, 300, 400]
testLocsAndValues = [
({"a": -1, "b": -1}, -200),
({"a": -1, "b": 0}, 0),
({"a": -1, "b": 1}, 200),
({"a": -1, "b": 2}, 400),
({"a": 0, "b": -1}, -100),
({"a": 0, "b": 0}, 100),
({"a": 0, "b": 1}, 300),
({"a": 0, "b": 2}, 500),
({"a": 1, "b": -1}, 0),
({"a": 1, "b": 0}, 200),
({"a": 1, "b": 1}, 400),
({"a": 1, "b": 2}, 600),
({"a": 2, "b": -1}, 100),
({"a": 2, "b": 0}, 300),
({"a": 2, "b": 1}, 500),
({"a": 2, "b": 2}, 700),
]
for loc, expectedValue in testLocsAndValues:
assert expectedValue == model.interpolateFromMasters(loc, masterValues)
@pytest.mark.parametrize(