From 26590d3d6e2dd33a37a987474aaef1aae3206146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=AE=D8=A7=D9=84=D8=AF=20=D8=AD=D8=B3=D9=86=D9=8A=20=28K?= =?UTF-8?q?haled=20Hosny=29?= Date: Thu, 5 Dec 2024 14:09:44 +0200 Subject: [PATCH] =?UTF-8?q?[feaLib]=20Don=E2=80=99t=20modify=20variable=20?= =?UTF-8?q?anchors=20in=20place=20(#3717)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When passing a parsed feature file that has variable anchors to addOpenTypeFeatures(), builder would modify the anchors in place and discard the variations, which break any subsequent use of the feature file. I encountered this building a font that has variable cursing anchors with ufo2ft. The cursFeatureWriter would write the variable anchors, but then when kernFeatreWriter compiles the file to get GSUB closure, the variation would be dropped from the anchors, and later when when the feature data is compiled into the font, the anchors would be compiled without variations. --- Lib/fontTools/feaLib/builder.py | 39 ++++++++++++++------------------- Tests/feaLib/builder_test.py | 17 ++++++++++++++ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index bda855e1e..81aa8c2e2 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -1658,38 +1658,31 @@ class Builder(object): return default, device + def makeAnchorPos(self, varscalar, deviceTable, location): + device = None + if not isinstance(varscalar, VariableScalar): + if deviceTable is not None: + device = otl.buildDevice(dict(deviceTable)) + return varscalar, device + default, device = self.makeVariablePos(location, varscalar) + if device is not None and deviceTable is not None: + raise FeatureLibError( + "Can't define a device coordinate and variable scalar", location + ) + return default, device + def makeOpenTypeAnchor(self, location, anchor): """ast.Anchor --> otTables.Anchor""" if anchor is None: return None - variable = False deviceX, deviceY = None, None if anchor.xDeviceTable is not None: deviceX = otl.buildDevice(dict(anchor.xDeviceTable)) if anchor.yDeviceTable is not None: deviceY = otl.buildDevice(dict(anchor.yDeviceTable)) - for dim in ("x", "y"): - varscalar = getattr(anchor, dim) - if not isinstance(varscalar, VariableScalar): - continue - if getattr(anchor, dim + "DeviceTable") is not None: - raise FeatureLibError( - "Can't define a device coordinate and variable scalar", location - ) - default, device = self.makeVariablePos(location, varscalar) - setattr(anchor, dim, default) - if device is not None: - if dim == "x": - deviceX = device - else: - deviceY = device - variable = True - - otlanchor = otl.buildAnchor( - anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY - ) - if variable: - otlanchor.Format = 3 + x, deviceX = self.makeAnchorPos(anchor.x, anchor.xDeviceTable, location) + y, deviceY = self.makeAnchorPos(anchor.y, anchor.yDeviceTable, location) + otlanchor = otl.buildAnchor(x, y, anchor.contourpoint, deviceX, deviceY) return otlanchor _VALUEREC_ATTRS = { diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 6c1fdab24..434acdce9 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -12,6 +12,7 @@ from fontTools.feaLib.lexer import Lexer from fontTools.fontBuilder import addFvar import difflib from io import StringIO +from textwrap import dedent import os import re import shutil @@ -1160,6 +1161,22 @@ class BuilderTest(unittest.TestCase): var_region_axis = var_region_list.Region[0].VarRegionAxis[0] assert self.get_region(var_region_axis) == (0.0, 0.875, 1.0) + def test_variable_anchors_round_trip(self): + """Test that calling `addOpenTypeFeatures` with parsed feature file does + not discard variations from variable anchors.""" + features = """\ + feature curs { + pos cursive one ; + } curs; + """ + + f = StringIO(features) + feafile = Parser(f).parse() + + font = self.make_mock_vf() + addOpenTypeFeatures(font, feafile) + assert dedent(str(feafile)) == dedent(features) + def generate_feature_file_test(name): return lambda self: self.check_feature_file(name)