| from fontTools.varLib.models import ( |
| normalizeLocation, |
| supportScalar, |
| VariationModel, |
| VariationModelError, |
| ) |
| import pytest |
| |
| |
| def test_normalizeLocation(): |
| axes = {"wght": (100, 400, 900)} |
| assert normalizeLocation({"wght": 400}, axes) == {"wght": 0.0} |
| assert normalizeLocation({"wght": 100}, axes) == {"wght": -1.0} |
| assert normalizeLocation({"wght": 900}, axes) == {"wght": 1.0} |
| assert normalizeLocation({"wght": 650}, axes) == {"wght": 0.5} |
| assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0} |
| assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0} |
| |
| axes = {"wght": (0, 0, 1000)} |
| assert normalizeLocation({"wght": 0}, axes) == {"wght": 0.0} |
| assert normalizeLocation({"wght": -1}, axes) == {"wght": 0.0} |
| assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0} |
| assert normalizeLocation({"wght": 500}, axes) == {"wght": 0.5} |
| assert normalizeLocation({"wght": 1001}, axes) == {"wght": 1.0} |
| |
| axes = {"wght": (0, 1000, 1000)} |
| assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0} |
| assert normalizeLocation({"wght": -1}, axes) == {"wght": -1.0} |
| assert normalizeLocation({"wght": 500}, axes) == {"wght": -0.5} |
| assert normalizeLocation({"wght": 1000}, axes) == {"wght": 0.0} |
| assert normalizeLocation({"wght": 1001}, axes) == {"wght": 0.0} |
| |
| |
| @pytest.mark.parametrize( |
| "axes, location, expected", |
| [ |
| # lower != default != upper |
| ({"wght": (100, 400, 900)}, {"wght": 1000}, {"wght": 1.2}), |
| ({"wght": (100, 400, 900)}, {"wght": 900}, {"wght": 1.0}), |
| ({"wght": (100, 400, 900)}, {"wght": 650}, {"wght": 0.5}), |
| ({"wght": (100, 400, 900)}, {"wght": 400}, {"wght": 0.0}), |
| ({"wght": (100, 400, 900)}, {"wght": 250}, {"wght": -0.5}), |
| ({"wght": (100, 400, 900)}, {"wght": 100}, {"wght": -1.0}), |
| ({"wght": (100, 400, 900)}, {"wght": 25}, {"wght": -1.25}), |
| # lower == default != upper |
| ( |
| {"wght": (400, 400, 900), "wdth": (100, 100, 150)}, |
| {"wght": 1000, "wdth": 200}, |
| {"wght": 1.2, "wdth": 2.0}, |
| ), |
| ( |
| {"wght": (400, 400, 900), "wdth": (100, 100, 150)}, |
| {"wght": 25, "wdth": 25}, |
| {"wght": -0.75, "wdth": -1.5}, |
| ), |
| # lower != default == upper |
| ( |
| {"wght": (100, 400, 400), "wdth": (50, 100, 100)}, |
| {"wght": 700, "wdth": 150}, |
| {"wght": 1.0, "wdth": 1.0}, |
| ), |
| ( |
| {"wght": (100, 400, 400), "wdth": (50, 100, 100)}, |
| {"wght": -50, "wdth": 25}, |
| {"wght": -1.5, "wdth": -1.5}, |
| ), |
| # degenerate case with lower == default == upper, normalized location always 0 |
| ({"wght": (400, 400, 400)}, {"wght": 100}, {"wght": 0.0}), |
| ({"wght": (400, 400, 400)}, {"wght": 400}, {"wght": 0.0}), |
| ({"wght": (400, 400, 400)}, {"wght": 700}, {"wght": 0.0}), |
| ], |
| ) |
| def test_normalizeLocation_extrapolate(axes, location, expected): |
| assert normalizeLocation(location, axes, extrapolate=True) == expected |
| |
| |
| def test_supportScalar(): |
| assert supportScalar({}, {}) == 1.0 |
| 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": 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( |
| "numLocations, numSamples", |
| [ |
| pytest.param(127, 509, marks=pytest.mark.slow), |
| (31, 251), |
| ], |
| ) |
| def test_modeling_error(numLocations, numSamples): |
| # https://github.com/fonttools/fonttools/issues/2213 |
| locations = [{"axis": float(i) / numLocations} for i in range(numLocations)] |
| masterValues = [100.0 if i else 0.0 for i in range(numLocations)] |
| |
| model = VariationModel(locations) |
| |
| for i in range(numSamples): |
| loc = {"axis": float(i) / numSamples} |
| scalars = model.getScalars(loc) |
| |
| deltas_float = model.getDeltas(masterValues) |
| deltas_round = model.getDeltas(masterValues, round=round) |
| |
| expected = model.interpolateFromDeltasAndScalars(deltas_float, scalars) |
| actual = model.interpolateFromDeltasAndScalars(deltas_round, scalars) |
| |
| err = abs(actual - expected) |
| assert err <= 0.5, (i, err) |
| |
| # This is how NOT to round deltas. |
| # deltas_late_round = [round(d) for d in deltas_float] |
| # bad = model.interpolateFromDeltasAndScalars(deltas_late_round, scalars) |
| # err_bad = abs(bad - expected) |
| # if err != err_bad: |
| # print("{:d} {:.2} {:.2}".format(i, err, err_bad)) |
| |
| |
| locationsA = [{}, {"wght": 1}, {"wdth": 1}] |
| locationsB = [{}, {"wght": 1}, {"wdth": 1}, {"wght": 1, "wdth": 1}] |
| locationsC = [ |
| {}, |
| {"wght": 0.5}, |
| {"wght": 1}, |
| {"wdth": 1}, |
| {"wght": 1, "wdth": 1}, |
| ] |
| |
| |
| class VariationModelTest(object): |
| @pytest.mark.parametrize( |
| "locations, axisOrder, sortedLocs, supports, deltaWeights", |
| [ |
| ( |
| [ |
| {"wght": 0.55, "wdth": 0.0}, |
| {"wght": -0.55, "wdth": 0.0}, |
| {"wght": -1.0, "wdth": 0.0}, |
| {"wght": 0.0, "wdth": 1.0}, |
| {"wght": 0.66, "wdth": 1.0}, |
| {"wght": 0.66, "wdth": 0.66}, |
| {"wght": 0.0, "wdth": 0.0}, |
| {"wght": 1.0, "wdth": 1.0}, |
| {"wght": 1.0, "wdth": 0.0}, |
| ], |
| ["wght"], |
| [ |
| {}, |
| {"wght": -0.55}, |
| {"wght": -1.0}, |
| {"wght": 0.55}, |
| {"wght": 1.0}, |
| {"wdth": 1.0}, |
| {"wdth": 1.0, "wght": 1.0}, |
| {"wdth": 1.0, "wght": 0.66}, |
| {"wdth": 0.66, "wght": 0.66}, |
| ], |
| [ |
| {}, |
| {"wght": (-1.0, -0.55, 0)}, |
| {"wght": (-1.0, -1.0, -0.55)}, |
| {"wght": (0, 0.55, 1.0)}, |
| {"wght": (0.55, 1.0, 1.0)}, |
| {"wdth": (0, 1.0, 1.0)}, |
| {"wdth": (0, 1.0, 1.0), "wght": (0, 1.0, 1.0)}, |
| {"wdth": (0, 1.0, 1.0), "wght": (0, 0.66, 1.0)}, |
| {"wdth": (0, 0.66, 1.0), "wght": (0, 0.66, 1.0)}, |
| ], |
| [ |
| {}, |
| {0: 1.0}, |
| {0: 1.0}, |
| {0: 1.0}, |
| {0: 1.0}, |
| {0: 1.0}, |
| {0: 1.0, 4: 1.0, 5: 1.0}, |
| { |
| 0: 1.0, |
| 3: 0.7555555555555555, |
| 4: 0.24444444444444444, |
| 5: 1.0, |
| 6: 0.66, |
| }, |
| { |
| 0: 1.0, |
| 3: 0.7555555555555555, |
| 4: 0.24444444444444444, |
| 5: 0.66, |
| 6: 0.43560000000000004, |
| 7: 0.66, |
| }, |
| ], |
| ), |
| ( |
| [ |
| {}, |
| {"bar": 0.5}, |
| {"bar": 1.0}, |
| {"foo": 1.0}, |
| {"bar": 0.5, "foo": 1.0}, |
| {"bar": 1.0, "foo": 1.0}, |
| ], |
| None, |
| [ |
| {}, |
| {"bar": 0.5}, |
| {"bar": 1.0}, |
| {"foo": 1.0}, |
| {"bar": 0.5, "foo": 1.0}, |
| {"bar": 1.0, "foo": 1.0}, |
| ], |
| [ |
| {}, |
| {"bar": (0, 0.5, 1.0)}, |
| {"bar": (0.5, 1.0, 1.0)}, |
| {"foo": (0, 1.0, 1.0)}, |
| {"bar": (0, 0.5, 1.0), "foo": (0, 1.0, 1.0)}, |
| {"bar": (0.5, 1.0, 1.0), "foo": (0, 1.0, 1.0)}, |
| ], |
| [ |
| {}, |
| {0: 1.0}, |
| {0: 1.0}, |
| {0: 1.0}, |
| {0: 1.0, 1: 1.0, 3: 1.0}, |
| {0: 1.0, 2: 1.0, 3: 1.0}, |
| ], |
| ), |
| ( |
| [ |
| {}, |
| {"foo": 0.25}, |
| {"foo": 0.5}, |
| {"foo": 0.75}, |
| {"foo": 1.0}, |
| {"bar": 0.25}, |
| {"bar": 0.75}, |
| {"bar": 1.0}, |
| ], |
| None, |
| [ |
| {}, |
| {"bar": 0.25}, |
| {"bar": 0.75}, |
| {"bar": 1.0}, |
| {"foo": 0.25}, |
| {"foo": 0.5}, |
| {"foo": 0.75}, |
| {"foo": 1.0}, |
| ], |
| [ |
| {}, |
| {"bar": (0.0, 0.25, 1.0)}, |
| {"bar": (0.25, 0.75, 1.0)}, |
| {"bar": (0.75, 1.0, 1.0)}, |
| {"foo": (0.0, 0.25, 1.0)}, |
| {"foo": (0.25, 0.5, 1.0)}, |
| {"foo": (0.5, 0.75, 1.0)}, |
| {"foo": (0.75, 1.0, 1.0)}, |
| ], |
| [ |
| {}, |
| {0: 1.0}, |
| {0: 1.0, 1: 0.3333333333333333}, |
| {0: 1.0}, |
| {0: 1.0}, |
| {0: 1.0, 4: 0.6666666666666666}, |
| {0: 1.0, 4: 0.3333333333333333, 5: 0.5}, |
| {0: 1.0}, |
| ], |
| ), |
| ( |
| [ |
| {}, |
| {"foo": 0.25}, |
| {"foo": 0.5}, |
| {"foo": 0.75}, |
| {"foo": 1.0}, |
| {"bar": 0.25}, |
| {"bar": 0.75}, |
| {"bar": 1.0}, |
| ], |
| None, |
| [ |
| {}, |
| {"bar": 0.25}, |
| {"bar": 0.75}, |
| {"bar": 1.0}, |
| {"foo": 0.25}, |
| {"foo": 0.5}, |
| {"foo": 0.75}, |
| {"foo": 1.0}, |
| ], |
| [ |
| {}, |
| {"bar": (0, 0.25, 1.0)}, |
| {"bar": (0.25, 0.75, 1.0)}, |
| {"bar": (0.75, 1.0, 1.0)}, |
| {"foo": (0, 0.25, 1.0)}, |
| {"foo": (0.25, 0.5, 1.0)}, |
| {"foo": (0.5, 0.75, 1.0)}, |
| {"foo": (0.75, 1.0, 1.0)}, |
| ], |
| [ |
| {}, |
| {0: 1.0}, |
| {0: 1.0, 1: 0.3333333333333333}, |
| {0: 1.0}, |
| {0: 1.0}, |
| {0: 1.0, 4: 0.6666666666666666}, |
| {0: 1.0, 4: 0.3333333333333333, 5: 0.5}, |
| {0: 1.0}, |
| ], |
| ), |
| ], |
| ) |
| def test_init(self, locations, axisOrder, sortedLocs, supports, deltaWeights): |
| model = VariationModel(locations, axisOrder=axisOrder) |
| |
| assert model.locations == sortedLocs |
| assert model.supports == supports |
| assert model.deltaWeights == deltaWeights |
| |
| def test_init_duplicate_locations(self): |
| with pytest.raises(VariationModelError, match="Locations must be unique."): |
| VariationModel( |
| [ |
| {"foo": 0.0, "bar": 0.0}, |
| {"foo": 1.0, "bar": 1.0}, |
| {"bar": 1.0, "foo": 1.0}, |
| ] |
| ) |
| |
| @pytest.mark.parametrize( |
| "locations, axisOrder, masterValues, instanceLocation, expectedValue, masterScalars", |
| [ |
| ( |
| [ |
| {}, |
| {"axis_A": 1.0}, |
| {"axis_B": 1.0}, |
| {"axis_A": 1.0, "axis_B": 1.0}, |
| {"axis_A": 0.5, "axis_B": 1.0}, |
| {"axis_A": 1.0, "axis_B": 0.5}, |
| ], |
| ["axis_A", "axis_B"], |
| [ |
| 0, |
| 10, |
| 20, |
| 70, |
| 50, |
| 60, |
| ], |
| { |
| "axis_A": 0.5, |
| "axis_B": 0.5, |
| }, |
| 37.5, |
| [0.25, 0.0, 0.0, -0.25, 0.5, 0.5], |
| ), |
| ], |
| ) |
| def test_interpolation( |
| self, |
| locations, |
| axisOrder, |
| masterValues, |
| instanceLocation, |
| expectedValue, |
| masterScalars, |
| ): |
| model = VariationModel(locations, axisOrder=axisOrder) |
| |
| interpolatedValue = model.interpolateFromMasters(instanceLocation, masterValues) |
| assert interpolatedValue == expectedValue |
| |
| assert masterScalars == model.getMasterScalars(instanceLocation) |
| |
| assert model.interpolateFromValuesAndScalars( |
| masterValues, masterScalars |
| ) == pytest.approx(interpolatedValue) |
| |
| @pytest.mark.parametrize( |
| "masterLocations, location, expected", |
| [ |
| (locationsA, {"wght": 0, "wdth": 0}, [1, 0, 0]), |
| ( |
| locationsA, |
| {"wght": 0.5, "wdth": 0}, |
| [0.5, 0.5, 0], |
| ), |
| (locationsA, {"wght": 1, "wdth": 0}, [0, 1, 0]), |
| ( |
| locationsA, |
| {"wght": 0, "wdth": 0.5}, |
| [0.5, 0, 0.5], |
| ), |
| (locationsA, {"wght": 0, "wdth": 1}, [0, 0, 1]), |
| (locationsA, {"wght": 1, "wdth": 1}, [-1, 1, 1]), |
| ( |
| locationsA, |
| {"wght": 0.5, "wdth": 0.5}, |
| [0, 0.5, 0.5], |
| ), |
| ( |
| locationsA, |
| {"wght": 0.75, "wdth": 0.75}, |
| [-0.5, 0.75, 0.75], |
| ), |
| ( |
| locationsB, |
| {"wght": 1, "wdth": 1}, |
| [0, 0, 0, 1], |
| ), |
| ( |
| locationsB, |
| {"wght": 0.5, "wdth": 0}, |
| [0.5, 0.5, 0, 0], |
| ), |
| ( |
| locationsB, |
| {"wght": 1, "wdth": 0.5}, |
| [0, 0.5, 0, 0.5], |
| ), |
| ( |
| locationsB, |
| {"wght": 0.5, "wdth": 0.5}, |
| [0.25, 0.25, 0.25, 0.25], |
| ), |
| ( |
| locationsC, |
| {"wght": 0.5, "wdth": 0}, |
| [0, 1, 0, 0, 0], |
| ), |
| ( |
| locationsC, |
| {"wght": 0.25, "wdth": 0}, |
| [0.5, 0.5, 0, 0, 0], |
| ), |
| ( |
| locationsC, |
| {"wght": 0.75, "wdth": 0}, |
| [0, 0.5, 0.5, 0, 0], |
| ), |
| ( |
| locationsC, |
| {"wght": 0.5, "wdth": 1}, |
| [-0.5, 1, -0.5, 0.5, 0.5], |
| ), |
| ( |
| locationsC, |
| {"wght": 0.75, "wdth": 1}, |
| [-0.25, 0.5, -0.25, 0.25, 0.75], |
| ), |
| ], |
| ) |
| def test_getMasterScalars(self, masterLocations, location, expected): |
| model = VariationModel(masterLocations) |
| assert model.getMasterScalars(location) == expected |