From 204532aee3ddd6f79f2b62d034d39f3bce48184a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 21 Jun 2023 14:47:52 -0600 Subject: [PATCH] [instancer/L4] Misc fixes and fix tests --- Lib/fontTools/varLib/instancer/__init__.py | 35 ++++++++++++------- Lib/fontTools/varLib/instancer/featureVars.py | 9 ++--- Tests/varLib/instancer/instancer_test.py | 14 ++++---- Tests/varLib/instancer/solver_test.py | 3 ++ 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py index f26822896..8e9e21ee1 100644 --- a/Lib/fontTools/varLib/instancer/__init__.py +++ b/Lib/fontTools/varLib/instancer/__init__.py @@ -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) @@ -234,14 +234,14 @@ class AxisTriple(Sequence): @dataclasses.dataclass(frozen=True, order=True, repr=False) -class NormalizedAxisTriple(AxisTriple): +class NormalizedAxisTripleAndDistances(AxisTriple): """A triple of (min, default, max) normalized axis values.""" minimum: float default: float maximum: float - distanceNegative: float - distancePositive: float + distanceNegative: Optional[float] = 1 + distancePositive: Optional[float] = 1 def __post_init__(self): if self.default is None: @@ -256,15 +256,18 @@ class NormalizedAxisTriple(AxisTriple): v = self return self.__class__(-v[2], -v[1], -v[0], v[4], v[3]) - def normalizeValue(self, v): + def normalizeValue(self, v, extrapolate=True): 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().normalizeValue(-v) + return -self.reverse_negate().normalizeValue(-v, extrapolate=extrapolate) # default >= 0 and v != default @@ -285,6 +288,12 @@ class NormalizedAxisTriple(AxisTriple): else: vDistance = -v * distanceNegative + distancePositive * default + if totalDistance == 0: + # This happens + if default == 0: + return -v / lower + return 0 # Shouldn't happen + return -vDistance / totalDistance @@ -375,7 +384,7 @@ class AxisLimits(_BaseAxisLimits): distancePositive = triple[2] - triple[1] if self[axis_tag] is None: - normalizedLimits[axis_tag] = NormalizedAxisTriple( + normalizedLimits[axis_tag] = NormalizedAxisTripleAndDistances( 0, 0, 0, distanceNegative, distancePositive ) continue @@ -386,7 +395,7 @@ class AxisLimits(_BaseAxisLimits): defaultV = triple[1] avarMapping = avarSegments.get(axis_tag, None) - normalizedLimits[axis_tag] = NormalizedAxisTriple( + normalizedLimits[axis_tag] = NormalizedAxisTripleAndDistances( *(normalize(v, triple, avarMapping) for v in (minV, defaultV, maxV)), distanceNegative, distancePositive, @@ -402,7 +411,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 @@ -486,7 +495,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)) @@ -549,7 +558,7 @@ def _instantiateGvarGlyph( "Instancing accross VarComposite axes with variation is not supported." ) limits = axisLimits[tag] - loc = limits.normalizeValue(loc) + loc = limits.normalizeValue(loc, extrapolate=False) newLocation[tag] = loc component.location = newLocation @@ -969,7 +978,7 @@ def instantiateAvar(varfont, axisLimits): mappedMax = floatToFixedToFloat( piecewiseLinearMap(axisRange.maximum, mapping), 14 ) - mappedAxisLimit = NormalizedAxisTriple( + mappedAxisLimit = NormalizedAxisTripleAndDistances( mappedMin, mappedDef, mappedMax, diff --git a/Lib/fontTools/varLib/instancer/featureVars.py b/Lib/fontTools/varLib/instancer/featureVars.py index 950724756..3433577c1 100644 --- a/Lib/fontTools/varLib/instancer/featureVars.py +++ b/Lib/fontTools/varLib/instancer/featureVars.py @@ -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(axisLimit.normalizeValue(v) for v in (minValue, maxValue)) + return tuple( + axisLimit.normalizeValue(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, 0, 0) + default_triple = NormalizedAxisTripleAndDistances(-1, 0, +1, 0, 0) for i, condition in enumerate(record.ConditionSet.ConditionTable): if condition.Format == 1: axisIdx = condition.AxisIndex diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py index ab64c379d..f722ceb67 100644 --- a/Tests/varLib/instancer/instancer_test.py +++ b/Tests/varLib/instancer/instancer_test.py @@ -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, 1, 1) 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,9 @@ 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)}), + ({"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 +2113,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): diff --git a/Tests/varLib/instancer/solver_test.py b/Tests/varLib/instancer/solver_test.py index 8b2a8fc47..205405d52 100644 --- a/Tests/varLib/instancer/solver_test.py +++ b/Tests/varLib/instancer/solver_test.py @@ -1,4 +1,5 @@ from fontTools.varLib.instancer import solver +from fontTools.varLib.instancer import NormalizedAxisTripleAndDistances import pytest @@ -264,6 +265,8 @@ class RebaseTentTest(object): ], ) def test_rebaseTent(self, tent, axisRange, expected): + axisRange = NormalizedAxisTripleAndDistances(*axisRange) + sol = solver.rebaseTent(tent, axisRange) a = pytest.approx