Repository: googlefonts/glyphsLib
Pull request #832 (2022-11-15 16:38:30)

Implement Rename Glyphs custom parameter
- Rename Glyphs custom parameter implementation
- Add tests for rename glyphs


anthrotype: Thanks Simon! For the records, this is going to fix https://github.com/googlefonts/glyphsLib/issues/735
anthrotype: Do we also need "Remove Glyphs" support to reach feature parity with the behaviour described in https://github.com/googlefonts/glyphsLib/issues/735#issuecomment-1270211329 ?
simoncozens: Looks like we still need quite a few more. Will work on them.

Patch:
diff --git a/Lib/glyphsLib/builder/custom_params.py b/Lib/glyphsLib/builder/custom_params.py
index 61a784db..bc8a59a5 100644
--- a/Lib/glyphsLib/builder/custom_params.py
+++ b/Lib/glyphsLib/builder/custom_params.py
@@ -996,6 +996,37 @@ class ReencodeGlyphsParamHandler(AbstractParamHandler):
 register(ReencodeGlyphsParamHandler())
 
 
+class RenameGlyphsParamHandler(AbstractParamHandler):
+    """The "Rename Glyphs" custom parameter contains a list of
+    'glyphname=glyphname' strings: e.g., ["a=b", "b=a"].
+    It only applies to specific instance (not to master or globally).
+
+    The glyph data is swapped, but the unicode assignments remain the
+    same.
+    """
+
+    def to_ufo(self, builder, glyphs, ufo):
+        rename_list = glyphs.get_custom_value("Rename Glyphs")
+        if not rename_list:
+            return
+        ufo = ufo._owner
+        for entry in rename_list:
+            oldname, newname = entry.split("=")
+            ufo[newname], ufo[oldname] = ufo[oldname], ufo[newname]
+            ufo[newname].unicodes, ufo[oldname].unicodes = (
+                ufo[oldname].unicodes,
+                ufo[newname].unicodes,
+            )
+
+    def to_glyphs(self, glyphs, ufo):
+        # The 'Reencode Glyphs' parameter only applies to instances, which
+        # are not meant to be roundtripped. No need to handle it here.
+        pass
+
+
+register(RenameGlyphsParamHandler())
+
+
 def to_ufo_custom_params(self, ufo, glyphs_object):
     # glyphs_module=None because we shouldn't instanciate any Glyphs classes
     glyphs_proxy = GlyphsObjectProxy(glyphs_object, glyphs_module=None)


Test patch:
diff --git a/tests/builder/instances_test.py b/tests/builder/instances_test.py
index e9df6346..8a4e5415 100644
--- a/tests/builder/instances_test.py
+++ b/tests/builder/instances_test.py
@@ -145,3 +145,29 @@ def test_glyphs3_instance_filtering():
 
     doc = glyphsLib.to_designspace(font, family_name="MyFamily 12pt")
     assert len(doc.instances) == 2
+
+
+def test_rename_glyphs(tmpdir):
+    font = glyphsLib.GSFont(os.path.join(DATA, "RenameGlyphsTest.glyphs"))
+    instance_dir = tmpdir.ensure_dir("instance_ufo")
+    designspace = glyphsLib.to_designspace(font, instance_dir=instance_dir)
+    path = str(tmpdir / (font.familyName + ".designspace"))
+    write_designspace_and_UFOs(designspace, path)
+
+    ufo_path = tmpdir / "RenameGlyphsTest-Regular.ufo"
+    ufo_path.copy(instance_dir.ensure_dir("RenameGlyphsTest-Straight.ufo"))
+    ufo_path.copy(instance_dir.ensure_dir("RenameGlyphsTest-Swapped.ufo"))
+
+    ufos = apply_instance_data(designspace.path)
+
+    assert len(ufos) == 2
+
+    assert len(ufos[0]["a"][0]) == 4  # Square
+    assert len(ufos[0]["b"][0]) == 12  # Circle
+    assert ufos[0]["a"].unicode == 0x0061
+    assert ufos[0]["b"].unicode == 0x0062
+
+    assert len(ufos[1]["a"][0]) == 12  # Circle
+    assert len(ufos[1]["b"][0]) == 4  # Square
+    assert ufos[0]["a"].unicode == 0x0061
+    assert ufos[0]["b"].unicode == 0x0062
diff --git a/tests/data/RenameGlyphsTest.glyphs b/tests/data/RenameGlyphsTest.glyphs
new file mode 100644
index 00000000..227543b7
--- /dev/null
+++ b/tests/data/RenameGlyphsTest.glyphs
@@ -0,0 +1,199 @@
+{
+.appVersion = "3109";
+.formatVersion = 3;
+DisplayStrings = (
+badc
+);
+date = "2022-11-15 15:16:33 +0000";
+familyName = "Rename Glyphs Test";
+features = (
+{
+code = "sub a' c by c;";
+tag = rlig;
+}
+);
+fontMaster = (
+{
+id = m01;
+metricValues = (
+{
+over = 16;
+pos = 800;
+},
+{
+over = 16;
+pos = 700;
+},
+{
+over = 16;
+pos = 500;
+},
+{
+over = -16;
+},
+{
+over = -16;
+pos = -200;
+},
+{
+over = -16;
+}
+);
+name = Regular;
+}
+);
+glyphs = (
+{
+glyphname = a;
+lastChange = "2022-11-15 15:16:52 +0000";
+layers = (
+{
+layerId = m01;
+shapes = (
+{
+closed = 1;
+nodes = (
+(167,113,l),
+(437,113,l),
+(437,383,l),
+(167,383,l)
+);
+}
+);
+width = 600;
+}
+);
+unicode = 97;
+},
+{
+glyphname = b;
+lastChange = "2022-11-15 15:17:07 +0000";
+layers = (
+{
+layerId = m01;
+shapes = (
+{
+closed = 1;
+nodes = (
+(367,116,o),
+(427,176,o),
+(427,251,cs),
+(427,326,o),
+(367,386,o),
+(292,386,cs),
+(217,386,o),
+(157,326,o),
+(157,251,cs),
+(157,176,o),
+(217,116,o),
+(292,116,cs)
+);
+}
+);
+width = 600;
+}
+);
+unicode = 98;
+},
+{
+glyphname = c;
+lastChange = "2022-11-15 15:24:23 +0000";
+layers = (
+{
+layerId = m01;
+shapes = (
+{
+closed = 1;
+nodes = (
+(155,1,l),
+(493,1,l),
+(330,339,l)
+);
+}
+);
+width = 600;
+}
+);
+unicode = 99;
+},
+{
+glyphname = d;
+lastChange = "2022-11-15 15:29:06 +0000";
+layers = (
+{
+layerId = m01;
+shapes = (
+{
+closed = 1;
+nodes = (
+(42,0,l),
+(132,0,l),
+(132,411,l),
+(544,411,l),
+(544,501,l),
+(44,501,l),
+(44,500,l),
+(42,500,l)
+);
+},
+{
+pos = (46,-56);
+ref = a;
+},
+{
+pos = (43,428);
+ref = b;
+}
+);
+width = 600;
+}
+);
+unicode = 100;
+}
+);
+instances = (
+{
+instanceInterpolations = {
+m01 = 1;
+};
+name = Straight;
+},
+{
+customParameters = (
+{
+name = "Rename Glyphs";
+value = (
+"a=b"
+);
+}
+);
+instanceInterpolations = {
+m01 = 1;
+};
+name = Swapped;
+}
+);
+metrics = (
+{
+type = ascender;
+},
+{
+type = "cap height";
+},
+{
+type = "x-height";
+},
+{
+type = baseline;
+},
+{
+type = descender;
+},
+{
+type = "italic angle";
+}
+);
+unitsPerEm = 1000;
+versionMajor = 1;
+versionMinor = 0;
+}