commit
fb56e7b7c9
@ -204,8 +204,8 @@ class AxisTriple(Sequence):
|
||||
default = None
|
||||
if n == 2:
|
||||
minimum, maximum = v
|
||||
elif n == 3:
|
||||
minimum, default, maximum = v
|
||||
elif n >= 3:
|
||||
return cls(*v)
|
||||
else:
|
||||
raise ValueError(f"expected sequence of 2 or 3; got {n}: {v!r}")
|
||||
return cls(minimum, default, maximum)
|
||||
@ -251,6 +251,70 @@ class NormalizedAxisTriple(AxisTriple):
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, order=True, repr=False)
|
||||
class NormalizedAxisTripleAndDistances(AxisTriple):
|
||||
"""A triple of (min, default, max) normalized axis values,
|
||||
with distances between min and default, and default and max,
|
||||
in the *pre-normalized* space."""
|
||||
|
||||
minimum: float
|
||||
default: float
|
||||
maximum: float
|
||||
distanceNegative: Optional[float] = 1
|
||||
distancePositive: Optional[float] = 1
|
||||
|
||||
def __post_init__(self):
|
||||
if self.default is None:
|
||||
object.__setattr__(self, "default", max(self.minimum, min(self.maximum, 0)))
|
||||
if not (-1.0 <= self.minimum <= self.default <= self.maximum <= 1.0):
|
||||
raise ValueError(
|
||||
"Normalized axis values not in -1..+1 range; got "
|
||||
f"minimum={self.minimum:g}, default={self.default:g}, maximum={self.maximum:g})"
|
||||
)
|
||||
|
||||
def reverse_negate(self):
|
||||
v = self
|
||||
return self.__class__(-v[2], -v[1], -v[0], v[4], v[3])
|
||||
|
||||
def renormalizeValue(self, v, extrapolate=True):
|
||||
"""Renormalizes a normalized value v to the range of this axis,
|
||||
considering the pre-normalized distances as well as the new
|
||||
axis limits."""
|
||||
|
||||
lower, default, upper, distanceNegative, distancePositive = self
|
||||
assert lower <= default <= upper
|
||||
|
||||
if not extrapolate:
|
||||
v = max(lower, min(upper, v))
|
||||
|
||||
if v == default:
|
||||
return 0
|
||||
|
||||
if default < 0:
|
||||
return -self.reverse_negate().renormalizeValue(-v, extrapolate=extrapolate)
|
||||
|
||||
# default >= 0 and v != default
|
||||
|
||||
if v > default:
|
||||
return (v - default) / (upper - default)
|
||||
|
||||
# v < default
|
||||
|
||||
if lower >= 0:
|
||||
return (v - default) / (default - lower)
|
||||
|
||||
# lower < 0 and v < default
|
||||
|
||||
totalDistance = distanceNegative * -lower + distancePositive * default
|
||||
|
||||
if v >= 0:
|
||||
vDistance = (default - v) * distancePositive
|
||||
else:
|
||||
vDistance = -v * distanceNegative + distancePositive * default
|
||||
|
||||
return -vDistance / totalDistance
|
||||
|
||||
|
||||
class _BaseAxisLimits(Mapping[str, AxisTriple]):
|
||||
def __getitem__(self, key: str) -> AxisTriple:
|
||||
return self._data[key]
|
||||
@ -334,8 +398,13 @@ class AxisLimits(_BaseAxisLimits):
|
||||
normalizedLimits = {}
|
||||
|
||||
for axis_tag, triple in axes.items():
|
||||
distanceNegative = triple[1] - triple[0]
|
||||
distancePositive = triple[2] - triple[1]
|
||||
|
||||
if self[axis_tag] is None:
|
||||
normalizedLimits[axis_tag] = NormalizedAxisTriple(0, 0, 0)
|
||||
normalizedLimits[axis_tag] = NormalizedAxisTripleAndDistances(
|
||||
0, 0, 0, distanceNegative, distancePositive
|
||||
)
|
||||
continue
|
||||
|
||||
minV, defaultV, maxV = self[axis_tag]
|
||||
@ -344,8 +413,10 @@ class AxisLimits(_BaseAxisLimits):
|
||||
defaultV = triple[1]
|
||||
|
||||
avarMapping = avarSegments.get(axis_tag, None)
|
||||
normalizedLimits[axis_tag] = NormalizedAxisTriple(
|
||||
*(normalize(v, triple, avarMapping) for v in (minV, defaultV, maxV))
|
||||
normalizedLimits[axis_tag] = NormalizedAxisTripleAndDistances(
|
||||
*(normalize(v, triple, avarMapping) for v in (minV, defaultV, maxV)),
|
||||
distanceNegative,
|
||||
distancePositive,
|
||||
)
|
||||
|
||||
return NormalizedAxisLimits(normalizedLimits)
|
||||
@ -358,7 +429,7 @@ class NormalizedAxisLimits(_BaseAxisLimits):
|
||||
self._data = data = {}
|
||||
for k, v in dict(*args, **kwargs).items():
|
||||
try:
|
||||
triple = NormalizedAxisTriple.expand(v)
|
||||
triple = NormalizedAxisTripleAndDistances.expand(v)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid axis limits for {k!r}: {v!r}") from e
|
||||
data[k] = triple
|
||||
@ -442,7 +513,7 @@ def changeTupleVariationsAxisLimits(variations, axisLimits):
|
||||
|
||||
|
||||
def changeTupleVariationAxisLimit(var, axisTag, axisLimit):
|
||||
assert isinstance(axisLimit, NormalizedAxisTriple)
|
||||
assert isinstance(axisLimit, NormalizedAxisTripleAndDistances)
|
||||
|
||||
# Skip when current axis is missing (i.e. doesn't participate),
|
||||
lower, peak, upper = var.axes.get(axisTag, (-1, 0, 1))
|
||||
@ -505,7 +576,7 @@ def _instantiateGvarGlyph(
|
||||
"Instancing accross VarComposite axes with variation is not supported."
|
||||
)
|
||||
limits = axisLimits[tag]
|
||||
loc = normalizeValue(loc, limits)
|
||||
loc = limits.renormalizeValue(loc, extrapolate=False)
|
||||
newLocation[tag] = loc
|
||||
component.location = newLocation
|
||||
|
||||
@ -925,15 +996,21 @@ def instantiateAvar(varfont, axisLimits):
|
||||
mappedMax = floatToFixedToFloat(
|
||||
piecewiseLinearMap(axisRange.maximum, mapping), 14
|
||||
)
|
||||
mappedAxisLimit = NormalizedAxisTripleAndDistances(
|
||||
mappedMin,
|
||||
mappedDef,
|
||||
mappedMax,
|
||||
axisRange.distanceNegative,
|
||||
axisRange.distancePositive,
|
||||
)
|
||||
newMapping = {}
|
||||
for fromCoord, toCoord in mapping.items():
|
||||
|
||||
if fromCoord < axisRange.minimum or fromCoord > axisRange.maximum:
|
||||
continue
|
||||
fromCoord = normalizeValue(fromCoord, axisRange)
|
||||
fromCoord = axisRange.renormalizeValue(fromCoord)
|
||||
|
||||
assert mappedMin <= toCoord <= mappedMax
|
||||
toCoord = normalizeValue(toCoord, (mappedMin, mappedDef, mappedMax))
|
||||
toCoord = mappedAxisLimit.renormalizeValue(toCoord)
|
||||
|
||||
fromCoord = floatToFixedToFloat(fromCoord, 14)
|
||||
toCoord = floatToFixedToFloat(toCoord, 14)
|
||||
|
@ -1,5 +1,4 @@
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.varLib.models import normalizeValue
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
|
||||
@ -41,7 +40,9 @@ def _limitFeatureVariationConditionRange(condition, axisLimit):
|
||||
# condition invalid or out of range
|
||||
return
|
||||
|
||||
return tuple(normalizeValue(v, axisLimit) for v in (minValue, maxValue))
|
||||
return tuple(
|
||||
axisLimit.renormalizeValue(v, extrapolate=False) for v in (minValue, maxValue)
|
||||
)
|
||||
|
||||
|
||||
def _instantiateFeatureVariationRecord(
|
||||
@ -50,9 +51,9 @@ def _instantiateFeatureVariationRecord(
|
||||
applies = True
|
||||
shouldKeep = False
|
||||
newConditions = []
|
||||
from fontTools.varLib.instancer import NormalizedAxisTriple
|
||||
from fontTools.varLib.instancer import NormalizedAxisTripleAndDistances
|
||||
|
||||
default_triple = NormalizedAxisTriple(-1, 0, +1)
|
||||
default_triple = NormalizedAxisTripleAndDistances(-1, 0, +1)
|
||||
for i, condition in enumerate(record.ConditionSet.ConditionTable):
|
||||
if condition.Format == 1:
|
||||
axisIdx = condition.AxisIndex
|
||||
|
@ -1,4 +1,4 @@
|
||||
from fontTools.varLib.models import supportScalar, normalizeValue
|
||||
from fontTools.varLib.models import supportScalar
|
||||
from fontTools.misc.fixedTools import MAX_F2DOT14
|
||||
from functools import lru_cache
|
||||
|
||||
@ -12,7 +12,7 @@ def _reverse_negate(v):
|
||||
|
||||
|
||||
def _solve(tent, axisLimit, negative=False):
|
||||
axisMin, axisDef, axisMax = axisLimit
|
||||
axisMin, axisDef, axisMax, _distanceNegative, _distancePositive = axisLimit
|
||||
lower, peak, upper = tent
|
||||
|
||||
# Mirror the problem such that axisDef <= peak
|
||||
@ -20,7 +20,9 @@ def _solve(tent, axisLimit, negative=False):
|
||||
return [
|
||||
(scalar, _reverse_negate(t) if t is not None else None)
|
||||
for scalar, t in _solve(
|
||||
_reverse_negate(tent), _reverse_negate(axisLimit), not negative
|
||||
_reverse_negate(tent),
|
||||
axisLimit.reverse_negate(),
|
||||
not negative,
|
||||
)
|
||||
]
|
||||
# axisDef <= peak
|
||||
@ -98,9 +100,8 @@ def _solve(tent, axisLimit, negative=False):
|
||||
# |
|
||||
# crossing
|
||||
if gain > outGain:
|
||||
|
||||
# Crossing point on the axis.
|
||||
crossing = peak + ((1 - gain) * (upper - peak) / (1 - outGain))
|
||||
crossing = peak + (1 - gain) * (upper - peak)
|
||||
|
||||
loc = (axisDef, peak, crossing)
|
||||
scalar = 1
|
||||
@ -116,7 +117,7 @@ def _solve(tent, axisLimit, negative=False):
|
||||
# the drawing above.
|
||||
if upper >= axisMax:
|
||||
loc = (crossing, axisMax, axisMax)
|
||||
scalar = supportScalar({"tag": axisMax}, {"tag": tent})
|
||||
scalar = outGain
|
||||
|
||||
out.append((scalar - gain, loc))
|
||||
|
||||
@ -147,84 +148,73 @@ def _solve(tent, axisLimit, negative=False):
|
||||
|
||||
# Eternity justify.
|
||||
loc2 = (upper, axisMax, axisMax)
|
||||
scalar2 = supportScalar({"tag": axisMax}, {"tag": tent})
|
||||
scalar2 = 0
|
||||
|
||||
out.append((scalar1 - gain, loc1))
|
||||
out.append((scalar2 - gain, loc2))
|
||||
|
||||
# Case 3: Outermost limit still fits within F2Dot14 bounds;
|
||||
# we keep deltas as is and only scale the axes bounds. Deltas beyond -1.0
|
||||
# or +1.0 will never be applied as implementations must clamp to that range.
|
||||
#
|
||||
# A second tent is needed for cases when gain is positive, though we add it
|
||||
# unconditionally and it will be dropped because scalar ends up 0.
|
||||
#
|
||||
# TODO: See if we can just move upper closer to adjust the slope, instead of
|
||||
# second tent.
|
||||
#
|
||||
# | peak |
|
||||
# 1.........|............o...|..................
|
||||
# | /x\ |
|
||||
# | /xxx\ |
|
||||
# | /xxxxx\|
|
||||
# | /xxxxxxx+
|
||||
# | /xxxxxxxx|\
|
||||
# 0---|-----|------oxxxxxxxxx|xo---------------1
|
||||
# axisMin | lower | upper
|
||||
# | |
|
||||
# axisDef axisMax
|
||||
#
|
||||
elif axisDef + (axisMax - axisDef) * 2 >= upper:
|
||||
|
||||
if not negative and axisDef + (axisMax - axisDef) * MAX_F2DOT14 < upper:
|
||||
# we clamp +2.0 to the max F2Dot14 (~1.99994) for convenience
|
||||
upper = axisDef + (axisMax - axisDef) * MAX_F2DOT14
|
||||
assert peak < upper
|
||||
|
||||
else:
|
||||
# Special-case if peak is at axisMax.
|
||||
if axisMax == peak:
|
||||
upper = peak
|
||||
|
||||
loc1 = (max(axisDef, lower), peak, upper)
|
||||
scalar1 = 1
|
||||
# Case 3:
|
||||
# We keep delta as is and only scale the axis upper to achieve
|
||||
# the desired new tent if feasible.
|
||||
#
|
||||
# peak
|
||||
# 1.....................o....................
|
||||
# / \_|
|
||||
# ..................../....+_.........outGain
|
||||
# / | \
|
||||
# gain..............+......|..+_.............
|
||||
# /| | | \
|
||||
# 0---|-----------o | | | o----------1
|
||||
# axisMin lower| | | upper
|
||||
# | | newUpper
|
||||
# axisDef axisMax
|
||||
#
|
||||
newUpper = peak + (1 - gain) * (upper - peak)
|
||||
assert axisMax <= newUpper # Because outGain >= gain
|
||||
if newUpper <= axisDef + (axisMax - axisDef) * 2:
|
||||
upper = newUpper
|
||||
if not negative and axisDef + (axisMax - axisDef) * MAX_F2DOT14 < upper:
|
||||
# we clamp +2.0 to the max F2Dot14 (~1.99994) for convenience
|
||||
upper = axisDef + (axisMax - axisDef) * MAX_F2DOT14
|
||||
assert peak < upper
|
||||
|
||||
loc2 = (peak, upper, upper)
|
||||
scalar2 = 0
|
||||
loc = (max(axisDef, lower), peak, upper)
|
||||
scalar = 1
|
||||
|
||||
out.append((scalar - gain, loc))
|
||||
|
||||
# Case 4: New limit doesn't fit; we need to chop into two tents,
|
||||
# because the shape of a triangle with part of one side cut off
|
||||
# cannot be represented as a triangle itself.
|
||||
#
|
||||
# | peak |
|
||||
# 1.........|......o.|....................
|
||||
# ..........|...../x\|.............outGain
|
||||
# | |xxy|\_
|
||||
# | /xxxy| \_
|
||||
# | |xxxxy| \_
|
||||
# | /xxxxy| \_
|
||||
# 0---|-----|-oxxxxxx| o----------1
|
||||
# axisMin | lower | upper
|
||||
# | |
|
||||
# axisDef axisMax
|
||||
#
|
||||
else:
|
||||
loc1 = (max(axisDef, lower), peak, axisMax)
|
||||
scalar1 = 1
|
||||
|
||||
loc2 = (peak, axisMax, axisMax)
|
||||
scalar2 = outGain
|
||||
|
||||
# Don't add a dirac delta!
|
||||
if axisDef < upper:
|
||||
out.append((scalar1 - gain, loc1))
|
||||
if peak < upper:
|
||||
out.append((scalar2 - gain, loc2))
|
||||
|
||||
# Case 4: New limit doesn't fit; we need to chop into two tents,
|
||||
# because the shape of a triangle with part of one side cut off
|
||||
# cannot be represented as a triangle itself.
|
||||
#
|
||||
# | peak |
|
||||
# 1.........|......o.|...................
|
||||
# | /x\|
|
||||
# | |xxy|\_
|
||||
# | /xxxy| \_
|
||||
# | |xxxxy| \_
|
||||
# | /xxxxy| \_
|
||||
# 0---|-----|-oxxxxxx| o----------1
|
||||
# axisMin | lower | upper
|
||||
# | |
|
||||
# axisDef axisMax
|
||||
#
|
||||
else:
|
||||
|
||||
loc1 = (max(axisDef, lower), peak, axisMax)
|
||||
scalar1 = 1
|
||||
|
||||
loc2 = (peak, axisMax, axisMax)
|
||||
scalar2 = supportScalar({"tag": axisMax}, {"tag": tent})
|
||||
|
||||
out.append((scalar1 - gain, loc1))
|
||||
# Don't add a dirac delta!
|
||||
if peak < axisMax:
|
||||
out.append((scalar2 - gain, loc2))
|
||||
# Don't add a dirac delta!
|
||||
if peak < axisMax:
|
||||
out.append((scalar2 - gain, loc2))
|
||||
|
||||
# Now, the negative side
|
||||
|
||||
@ -295,7 +285,7 @@ def rebaseTent(tent, axisLimit):
|
||||
If tent value is None, that is a special deltaset that should
|
||||
be always-enabled (called "gain")."""
|
||||
|
||||
axisMin, axisDef, axisMax = axisLimit
|
||||
axisMin, axisDef, axisMax, _distanceNegative, _distancePositive = axisLimit
|
||||
assert -1 <= axisMin <= axisDef <= axisMax <= +1
|
||||
|
||||
lower, peak, upper = tent
|
||||
@ -305,7 +295,7 @@ def rebaseTent(tent, axisLimit):
|
||||
|
||||
sols = _solve(tent, axisLimit)
|
||||
|
||||
n = lambda v: normalizeValue(v, axisLimit, extrapolate=True)
|
||||
n = lambda v: axisLimit.renormalizeValue(v)
|
||||
sols = [
|
||||
(scalar, (n(v[0]), n(v[1]), n(v[2])) if v is not None else None)
|
||||
for scalar, v in sols
|
||||
|
@ -1950,7 +1950,7 @@ class LimitTupleVariationAxisRangesTest:
|
||||
],
|
||||
)
|
||||
def test_positive_var(self, var, axisTag, newMax, expected):
|
||||
axisRange = instancer.NormalizedAxisTriple(0, 0, newMax)
|
||||
axisRange = instancer.NormalizedAxisTripleAndDistances(0, 0, newMax)
|
||||
self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -2029,7 +2029,7 @@ class LimitTupleVariationAxisRangesTest:
|
||||
],
|
||||
)
|
||||
def test_negative_var(self, var, axisTag, newMin, expected):
|
||||
axisRange = instancer.NormalizedAxisTriple(newMin, 0, 0)
|
||||
axisRange = instancer.NormalizedAxisTripleAndDistances(newMin, 0, 0, 1, 1)
|
||||
self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)
|
||||
|
||||
|
||||
@ -2052,7 +2052,7 @@ def test_limitFeatureVariationConditionRange(oldRange, newLimit, expected):
|
||||
condition = featureVars.buildConditionTable(0, *oldRange)
|
||||
|
||||
result = instancer.featureVars._limitFeatureVariationConditionRange(
|
||||
condition, instancer.NormalizedAxisTriple(*newLimit)
|
||||
condition, instancer.NormalizedAxisTripleAndDistances(*newLimit, 1, 1)
|
||||
)
|
||||
|
||||
assert result == expected
|
||||
@ -2094,9 +2094,10 @@ def test_parseLimits_invalid(limits):
|
||||
@pytest.mark.parametrize(
|
||||
"limits, expected",
|
||||
[
|
||||
({"wght": (100, 400)}, {"wght": (-1.0, 0, 0)}),
|
||||
({"wght": (100, 400, 400)}, {"wght": (-1.0, 0, 0)}),
|
||||
({"wght": (100, 300, 400)}, {"wght": (-1.0, -0.5, 0)}),
|
||||
# 300, 500 come from the font having 100,400,900 fvar axis limits.
|
||||
({"wght": (100, 400)}, {"wght": (-1.0, 0, 0, 300, 500)}),
|
||||
({"wght": (100, 400, 400)}, {"wght": (-1.0, 0, 0, 300, 500)}),
|
||||
({"wght": (100, 300, 400)}, {"wght": (-1.0, -0.5, 0, 300, 500)}),
|
||||
],
|
||||
)
|
||||
def test_normalizeAxisLimits(varfont, limits, expected):
|
||||
@ -2113,7 +2114,7 @@ def test_normalizeAxisLimits_no_avar(varfont):
|
||||
limits = instancer.AxisLimits(wght=(400, 400, 500))
|
||||
normalized = limits.normalize(varfont)
|
||||
|
||||
assert normalized["wght"] == pytest.approx((0, 0, 0.2), 1e-4)
|
||||
assert normalized["wght"] == pytest.approx((0, 0, 0.2, 300, 500), 1e-4)
|
||||
|
||||
|
||||
def test_normalizeAxisLimits_missing_from_fvar(varfont):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from fontTools.varLib.instancer import solver
|
||||
from fontTools.varLib.instancer import NormalizedAxisTripleAndDistances
|
||||
import pytest
|
||||
|
||||
|
||||
@ -91,6 +92,25 @@ class RebaseTentTest(object):
|
||||
(-1, (-1, -1, 0)),
|
||||
],
|
||||
),
|
||||
pytest.param(
|
||||
(0.0, 0.5, 1),
|
||||
(0, 0.5, 0.75),
|
||||
[
|
||||
(1, None),
|
||||
(-0.5, (0, 1, 1)),
|
||||
(-1, (-1, -1, 0)),
|
||||
],
|
||||
),
|
||||
pytest.param(
|
||||
(0.0, 0.5, 1),
|
||||
(0, 0.25, 0.8),
|
||||
[
|
||||
(0.5, None),
|
||||
(0.5, (0, 0.45454545, 0.9090909090)),
|
||||
(-0.1, (0.9090909090, 1.0, 1.0)),
|
||||
(-0.5, (-1, -1, 0)),
|
||||
],
|
||||
),
|
||||
# Case 3a/1neg
|
||||
pytest.param(
|
||||
(0.0, 0.5, 2),
|
||||
@ -117,8 +137,7 @@ class RebaseTentTest(object):
|
||||
(0.25, 0.25, 0.75),
|
||||
[
|
||||
(0.5, None),
|
||||
(0.5, (0, 0.5, 1.5)),
|
||||
(-0.5, (0.5, 1.5, 1.5)),
|
||||
(0.5, (0, 0.5, 1.0)),
|
||||
],
|
||||
),
|
||||
# Case 1neg
|
||||
@ -242,9 +261,26 @@ class RebaseTentTest(object):
|
||||
(-1, (-1, -1, -0.0001220703)),
|
||||
],
|
||||
),
|
||||
# https://github.com/fonttools/fonttools/issues/3177
|
||||
pytest.param(
|
||||
(0, 1, 1),
|
||||
(-1, -0.5, +1, 1, 1),
|
||||
[
|
||||
(1.0, (1 / 3, 1.0, 1.0)),
|
||||
],
|
||||
),
|
||||
pytest.param(
|
||||
(0, 1, 1),
|
||||
(-1, -0.5, +1, 2, 1),
|
||||
[
|
||||
(1.0, (0.5, 1.0, 1.0)),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_rebaseTent(self, tent, axisRange, expected):
|
||||
axisRange = NormalizedAxisTripleAndDistances(*axisRange)
|
||||
|
||||
sol = solver.rebaseTent(tent, axisRange)
|
||||
|
||||
a = pytest.approx
|
||||
|
Loading…
x
Reference in New Issue
Block a user