From cf43ff5d2253e62fa9240c6643d1b45e61e06e81 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 15 Mar 2023 16:19:39 +0000 Subject: [PATCH] Apply avar to variable locations --- Lib/fontTools/feaLib/builder.py | 8 +++- Lib/fontTools/feaLib/variableScalar.py | 26 ++++++++---- Tests/feaLib/builder_test.py | 58 ++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index ec67bc76c..c785f2e76 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -1614,6 +1614,7 @@ class Builder(object): deviceX = otl.buildDevice(dict(anchor.xDeviceTable)) if anchor.yDeviceTable is not None: deviceY = otl.buildDevice(dict(anchor.yDeviceTable)) + avar = self.font.get("avar") for dim in ("x", "y"): if not isinstance(getattr(anchor, dim), VariableScalar): continue @@ -1627,7 +1628,9 @@ class Builder(object): ) varscalar = getattr(anchor, dim) varscalar.axes = self.axes - default, index = varscalar.add_to_variation_store(self.varstorebuilder) + default, index = varscalar.add_to_variation_store( + self.varstorebuilder, avar + ) setattr(anchor, dim, default) if index is not None and index != 0xFFFFFFFF: if dim == "x": @@ -1654,6 +1657,7 @@ class Builder(object): if not v: return None + avar = self.font.get("avar") vr = {} for astName, (otName, isDevice) in self._VALUEREC_ATTRS.items(): val = getattr(v, astName, None) @@ -1674,7 +1678,7 @@ class Builder(object): location, ) val.axes = self.axes - default, index = val.add_to_variation_store(self.varstorebuilder) + default, index = val.add_to_variation_store(self.varstorebuilder, avar) vr[otName] = default if index is not None and index != 0xFFFFFFFF: vr[otDeviceName] = buildVarDevTable(index) diff --git a/Lib/fontTools/feaLib/variableScalar.py b/Lib/fontTools/feaLib/variableScalar.py index 35847e9de..8cbc2d22a 100644 --- a/Lib/fontTools/feaLib/variableScalar.py +++ b/Lib/fontTools/feaLib/variableScalar.py @@ -1,4 +1,4 @@ -from fontTools.varLib.models import VariationModel, normalizeValue +from fontTools.varLib.models import VariationModel, normalizeValue, piecewiseLinearMap def Location(loc): @@ -83,29 +83,37 @@ class VariableScalar: # I *guess* we could interpolate one, but I don't know how. return self.values[key] - def value_at_location(self, location): + def value_at_location(self, location, avar=None): loc = location if loc in self.values.keys(): return self.values[loc] values = list(self.values.values()) - return self.model.interpolateFromMasters(loc, values) + return self.model(avar).interpolateFromMasters(loc, values) - @property - def model(self): + def model(self, avar=None): key = tuple(self.values.keys()) if key in self.model_pool: return self.model_pool[key] locations = [dict(self._normalized_location(k)) for k in self.values.keys()] + if avar is not None: + mapping = avar.segments + locations = [ + { + k: piecewiseLinearMap(v, mapping[k]) if k in mapping else v + for k, v in location.items() + } + for location in locations + ] m = VariationModel(locations) self.model_pool[key] = m return m - def get_deltas_and_supports(self): + def get_deltas_and_supports(self, avar=None): values = list(self.values.values()) - return self.model.getDeltasAndSupports(values) + return self.model(avar).getDeltasAndSupports(values) - def add_to_variation_store(self, store_builder): - deltas, supports = self.get_deltas_and_supports() + def add_to_variation_store(self, store_builder, avar=None): + deltas, supports = self.get_deltas_and_supports(avar) store_builder.setSupports(supports) index = store_builder.storeDeltas(deltas) return int(self.default), index diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index abf4965b1..5fbbfaf52 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -1034,6 +1034,64 @@ class BuilderTest(unittest.TestCase): assert condition_table[0].FilterRangeMinValue == 0.5 assert condition_table[1].FilterRangeMinValue == 0.7 + def test_variable_scalar_avar(self): + """Test that the `avar` table is consulted when normalizing user-space + values.""" + + features = """ + languagesystem DFLT dflt; + + feature kern { + pos cursive one ; + pos two <0 (wght=200:12 wght=900:22 wdth=150,wght=900:42) 0 0>; + } kern; + """ + + def make_mock_vf(): + font = makeTTFont() + font["name"] = newTable("name") + addFvar(font, self.VARFONT_AXES, []) + del font["name"] + return font + + def get_region(var_region_axis): + return ( + var_region_axis.StartCoord, + var_region_axis.PeakCoord, + var_region_axis.EndCoord, + ) + + # Without `avar` (wght=200, wdth=100 is the default location): + font = make_mock_vf() + addOpenTypeFeaturesFromString(font, features) + + var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList + var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0] + var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1] + assert get_region(var_region_axis_wght) == (0.0, 0.875, 0.875) + assert get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0) + var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0] + var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1] + assert get_region(var_region_axis_wght) == (0.0, 0.875, 0.875) + assert get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5) + + # With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to + # the right, but leaving the wdth axis alone: + font = make_mock_vf() + font["avar"] = newTable("avar") + font["avar"].segments = {"wght": {-1.0: -1.0, 0.0: 0.0, 0.5: 0.625, 1.0: 1.0}} + addOpenTypeFeaturesFromString(font, features) + + var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList + var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0] + var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1] + assert get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625) + assert get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0) + var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0] + var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1] + assert get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625) + assert get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5) + def generate_feature_file_test(name): return lambda self: self.check_feature_file(name)