Repository: googlefonts/glyphsLib
Pull request #865 (2023-02-23 16:27:02)

3rd attempt at fixing RTL kerning export
This is adapted from @simoncozens' https://github.com/simoncozens/dumbifier/blob/main/Lib/dumbifier/rtl_kerning.py

see discussion in https://github.com/google/fonts/pull/5924#issuecomment-1441925247

~~The tests _don't_ work though yet...~~ (edit: they do now)


Patch:
diff --git a/Lib/glyphsLib/builder/groups.py b/Lib/glyphsLib/builder/groups.py
index 01c4cdde..520216f2 100644
--- a/Lib/glyphsLib/builder/groups.py
+++ b/Lib/glyphsLib/builder/groups.py
@@ -25,6 +25,43 @@ from .constants import (
 )
 
 
+def _get_glyphs_with_rtl_kerning(font):
+    # Return a set of all glyph names that are referenced from font.kerningRTL,
+    # either directly as single glyphs or as part of kerning groups.
+
+    rtl_glyphs = set()
+    if not font.kerningRTL:
+        return rtl_glyphs
+
+    # sets of group names keyed by {left,right}KerningGroup
+    rtl_groups = defaultdict(set)
+    glyph_kerning_attr = {"R": "leftKerningGroup", "L": "rightKerningGroup"}
+
+    def mark_as_rtl(s, side):
+        if s.startswith(f"@MMK_{side}_"):
+            rtl_groups[glyph_kerning_attr[side]].add(s[7:])
+        else:  # single glyph
+            assert not s.startswith("@MMK_"), f"unexpected key in kerningRTL: {s}"
+            rtl_glyphs.add(s)
+
+    for kerning_id in {
+        m.id if m.metricsSource is None else m.metricsSource.id for m in font.masters
+    }:
+        for kern1, subtable in font.kerningRTL.get(kerning_id, {}).items():
+            mark_as_rtl(kern1, side="R")
+            for kern2 in subtable.keys():
+                mark_as_rtl(kern2, side="L")
+
+    for glyph in font.glyphs.values():
+        if glyph.name not in rtl_glyphs and any(
+            getattr(glyph, attr) in rtl_groups[attr]
+            for attr in glyph_kerning_attr.values()
+        ):
+            rtl_glyphs.add(glyph.name)
+
+    return rtl_glyphs
+
+
 def to_ufo_groups(self):
     # Build groups once and then apply to all UFOs.
     groups = defaultdict(list)
@@ -60,21 +97,26 @@ def to_ufo_groups(self):
                 side = match.group(1)
                 group_name = match.group(2)
                 glyph = self.font.glyphs[glyph_name]
-                if (
-                    not glyph
-                    or getattr(glyph, _glyph_kerning_attr(glyph, side)) == group_name
-                ):
+                if not glyph or getattr(glyph, _glyph_kerning_attr(side)) == group_name:
                     # The original grouping is still valid
                     groups[group].append(glyph_name)
                     # Remember not to add this glyph again later
                     # Thus the original position in the list is preserved
                     recovered.add((glyph_name, int(side)))
 
-    # Read modified grouping values
+    # Read new/modified grouping values.
+    # For glyphs that are used in Glyphs3's kerningRTL dict, take the opposite side:
+    # NOTE: Not only this breaks Glyphs<=>UFO round-tripping, but also it makes
+    # impossible for the same glyph to be kerned in both LTR and RTL dictionaries.
+    # While this is unfortunate, we believe it's better than completely ignoring
+    # all Glyphs3's RTL kerning.
+    # For more info: https://github.com/googlefonts/glyphsLib/pull/778
+    rtl_glyphs = _get_glyphs_with_rtl_kerning(self.font)
     for glyph in self.font.glyphs.values():
+        is_rtl = glyph.name in rtl_glyphs
         for side in 1, 2:
             if (glyph.name, side) not in recovered:
-                attr = _glyph_kerning_attr(glyph, side)
+                attr = _glyph_kerning_attr(side, is_rtl)
                 group = getattr(glyph, attr)
                 if group:
                     group = f"public.kern{side}.{group}"
@@ -134,14 +176,20 @@ def _to_glyphs_kerning_group(self, name, glyphs):
     for glyph_name in glyphs:
         glyph = self.font.glyphs[glyph_name]
         if glyph:
-            setattr(glyph, _glyph_kerning_attr(glyph, side), group_name)
+            setattr(glyph, _glyph_kerning_attr(side), group_name)
 
 
-def _glyph_kerning_attr(glyph, side):
-    """Return leftKerningGroup or rightKerningGroup depending on the UFO
+def _glyph_kerning_attr(side, is_rtl=False):
+    """Return rightKerningGroup or leftKerningGroup depending on the UFO
     group's side (1 or 2).
+
+    Flip values for RTL kerning.
     """
-    if int(side) == 1:
+    side = int(side)
+    assert side in (1, 2), f"invalid kerning side: {side}"
+    if is_rtl:
+        side = 2 if side == 1 else 1
+    if side == 1:
         return "rightKerningGroup"
     else:
         return "leftKerningGroup"
diff --git a/Lib/glyphsLib/builder/kerning.py b/Lib/glyphsLib/builder/kerning.py
index b9e478b2..ca4cf389 100644
--- a/Lib/glyphsLib/builder/kerning.py
+++ b/Lib/glyphsLib/builder/kerning.py
@@ -14,18 +14,40 @@
 
 
 import re
+from collections import OrderedDict
+from copy import deepcopy
 
 from .constants import BRACKET_GLYPH_RE, UFO_KERN_GROUP_PATTERN
 
 
+def flip_class_side(s):
+    if s.startswith("@MMK_L_"):
+        return f"@MMK_R_{s[7:]}"
+    elif s.startswith("@MMK_R_"):
+        return f"@MMK_L_{s[7:]}"
+    return s
+
+
 def to_ufo_kerning(self):
     for master in self.font.masters:
         kerning_source = master.metricsSource  # Maybe be a linked master
         if kerning_source is None:
             kerning_source = master
-        if kerning_source.id in self.font.kerning:
-            kerning = self.font.kerning[kerning_source.id]
-            _to_ufo_kerning(self, self._sources[master.id].font, kerning)
+        both_directions = (
+            kerning_source.id in self.font.kerningLTR
+            and kerning_source.id in self.font.kerningRTL
+        )
+        combined_kerning = OrderedDict()
+        if kerning_source.id in self.font.kerningLTR:
+            kerning = self.font.kerningLTR[kerning_source.id]
+            combined_kerning = deepcopy(kerning) if both_directions else kerning
+        if kerning_source.id in self.font.kerningRTL:
+            for kern1, subtable in self.font.kerningRTL[kerning_source.id].items():
+                combined_kerning[flip_class_side(kern1)] = {
+                    flip_class_side(kern2): v for kern2, v in subtable.items()
+                }
+        if combined_kerning:
+            _to_ufo_kerning(self, self._sources[master.id].font, combined_kerning)
 
 
 def _to_ufo_kerning(self, ufo, kerning_data):
diff --git a/Lib/glyphsLib/classes.py b/Lib/glyphsLib/classes.py
index 1b8b5e64..7cb13674 100755
--- a/Lib/glyphsLib/classes.py
+++ b/Lib/glyphsLib/classes.py
@@ -2567,10 +2567,10 @@ class GSComponent(GSBase):
         if self.smartComponentValues:
             writer.writeKeyValue("piece", self.smartComponentValues)
         if writer.format_version > 2:
-            writer.writeObjectKeyValue(self, "rotation", keyName="angle", default=0)
             writer.writeObjectKeyValue(
                 self, "position", keyName="pos", default=Point(0, 0)
             )
+            writer.writeObjectKeyValue(self, "rotation", keyName="angle", default=0)
             writer.writeObjectKeyValue(self, "name", keyName="ref")
             if self.scale != (1, 1):
                 writer.writeKeyValue("scale", Point(list(self.scale)))
@@ -2806,16 +2806,10 @@ GSAnchor._add_parsers(
 
 class GSHint(GSBase):
     def _serialize_to_plist(self, writer):
-        if writer.format_version > 2:
-            writer.writeObjectKeyValue(self, "name", condition="if_true")
-        for field in ["horizontal", "options"]:
-            writer.writeObjectKeyValue(self, field, condition="if_true")
+        writer.writeObjectKeyValue(self, "horizontal", "if_true")
+
         for field in ["origin", "place"]:
             writer.writeObjectKeyValue(self, field)
-        if writer.format_version > 2:
-            writer.writeObjectKeyValue(self, "stem", self.stem != -2)
-        for field in ["other1", "other2", "scale"]:
-            writer.writeObjectKeyValue(self, field)
         if writer.format_version == 2:
             writer.writeObjectKeyValue(self, "target")
         elif self.target:
@@ -2825,11 +2819,12 @@ class GSHint(GSBase):
             else:
                 writer.writeValue(self.target)
             writer.file.write(";\n")
-        for field in ["settings", "type"]:
+        for field in ["other1", "other2", "scale"]:
+            writer.writeObjectKeyValue(self, field)
+        writer.writeObjectKeyValue(self, "stem", self.stem != -2)
+
+        for field in ["type", "name", "options", "settings"]:
             writer.writeObjectKeyValue(self, field, condition="if_true")
-        if writer.format_version == 2:
-            writer.writeObjectKeyValue(self, "stem", self.stem != -2)
-            writer.writeObjectKeyValue(self, "name", condition="if_true")
 
     _defaultsForName = {
         # TODO: (jany) check defaults in glyphs
@@ -3011,8 +3006,8 @@ GSHint._add_parsers(
 class GSFeature(GSBase):
     def _serialize_to_plist(self, writer):
         writer.writeObjectKeyValue(self, "automatic", "if_true")
-        writer.writeObjectKeyValue(self, "code", True)
         writer.writeObjectKeyValue(self, "disabled", "if_true")
+        writer.writeObjectKeyValue(self, "code", True)
         if writer.format_version == 3:
             writer.writeObjectKeyValue(self, "labels", "if_true")
             writer.writeKeyValue("tag", self.name)
@@ -3066,8 +3061,8 @@ GSFeature._add_parsers(
 class GSClass(GSFeature):
     def _serialize_to_plist(self, writer):
         writer.writeObjectKeyValue(self, "automatic", "if_true")
-        writer.writeObjectKeyValue(self, "code", True)
         writer.writeObjectKeyValue(self, "disabled", "if_true")
+        writer.writeObjectKeyValue(self, "code", True)
         writer.writeKeyValue("name", self.name)
 
     pass
@@ -3195,8 +3190,6 @@ class GSInstance(GSBase):
         writer.writeObjectKeyValue(self, "linkStyle", "if_true")
         writer.writeObjectKeyValue(self, "manualInterpolation", "if_true")
         writer.writeObjectKeyValue(self, "name")
-        if writer.format_version > 2:
-            writer.writeObjectKeyValue(self, "properties", condition="if_true")
         writer.writeObjectKeyValue(
             self, "weight", default="Regular", keyName="weightClass"
         )
@@ -3580,8 +3573,8 @@ class GSLayer(GSBase):
         writer.writeObjectKeyValue(self, "layerId", "if_true")
         if writer.format_version == 2:
             writer.writeObjectKeyValue(self, "metricLeft", keyName="leftMetricsKey")
-            writer.writeObjectKeyValue(self, "metricRight", keyName="rightMetricsKey")
             writer.writeObjectKeyValue(self, "metricWidth", keyName="widthMetricsKey")
+            writer.writeObjectKeyValue(self, "metricRight", keyName="rightMetricsKey")
         else:
             writer.writeObjectKeyValue(self, "metricLeft")
             writer.writeObjectKeyValue(self, "metricRight")
@@ -4053,7 +4046,6 @@ class GSGlyph(GSBase):
         writer.writeObjectKeyValue(self, "layers", "if_true")
         if writer.format_version > 2:
             writer.writeObjectKeyValue(self, "metricLeft", "if_true")
-            writer.writeObjectKeyValue(self, "metricRight", "if_true")
             writer.writeObjectKeyValue(self, "metricWidth", "if_true")
         else:
             writer.writeObjectKeyValue(self, "leftKerningGroup", "if_true")
@@ -4063,14 +4055,17 @@ class GSGlyph(GSBase):
             writer.writeObjectKeyValue(
                 self, "metricWidth", "if_true", keyName="widthMetricsKey"
             )
-            writer.writeObjectKeyValue(self, "rightKerningGroup", "if_true")
-            writer.writeObjectKeyValue(
-                self, "metricRight", "if_true", keyName="rightMetricsKey"
-            )
             writer.writeObjectKeyValue(
                 self, "metricVertWidth", "if_true", keyName="vertWidthMetricsKey"
             )
         writer.writeObjectKeyValue(self, "note")
+        if writer.format_version > 2:
+            writer.writeObjectKeyValue(self, "metricRight", "if_true")
+        else:
+            writer.writeObjectKeyValue(self, "rightKerningGroup", "if_true")
+            writer.writeObjectKeyValue(
+                self, "metricRight", "if_true", keyName="rightMetricsKey"
+            )
         writer.writeObjectKeyValue(self, "topKerningGroup", "if_true")
         writer.writeObjectKeyValue(self, "topMetricsKey", "if_true")
         writer.writeObjectKeyValue(self, "bottomKerningGroup", "if_true")


Test patch:
diff --git a/tests/builder/instances_test.py b/tests/builder/instances_test.py
index e7920164..8a4e5415 100644
--- a/tests/builder/instances_test.py
+++ b/tests/builder/instances_test.py
@@ -147,22 +147,6 @@ def test_glyphs3_instance_filtering():
     assert len(doc.instances) == 2
 
 
-def test_glyphs3_instance_properties(tmpdir):
-    expected_num_properties = [0, 0, 1, 1, 1, 1]
-
-    file = "InstanceFamilyName-G3.glyphs"
-    font = glyphsLib.GSFont(os.path.join(DATA, file))
-
-    for instance, num in zip(font.instances, expected_num_properties):
-        assert len(instance.properties) == num
-
-    font.save(tmpdir / file)
-    font = glyphsLib.GSFont(tmpdir / file)
-
-    for instance, num in zip(font.instances, expected_num_properties):
-        assert len(instance.properties) == num
-
-
 def test_rename_glyphs(tmpdir):
     font = glyphsLib.GSFont(os.path.join(DATA, "RenameGlyphsTest.glyphs"))
     instance_dir = tmpdir.ensure_dir("instance_ufo")
diff --git a/tests/data/RTL_kerning_v2.glyphs b/tests/data/RTL_kerning_v2.glyphs
new file mode 100644
index 00000000..caec77ba
--- /dev/null
+++ b/tests/data/RTL_kerning_v2.glyphs
@@ -0,0 +1,183 @@
+{
+.appVersion = "1352";
+DisplayStrings = (
+"/alef-hb/bet-hb",
+"/reh-ar/hah-ar.init.swsh/hah-ar.init"
+);
+date = "2022-02-09 20:07:11 +0000";
+familyName = "RTL kerning";
+fontMaster = (
+{
+ascender = 800;
+capHeight = 700;
+descender = -200;
+id = m01;
+xHeight = 500;
+}
+);
+glyphs = (
+{
+glyphname = A;
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 0041;
+},
+{
+glyphname = B;
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 0042;
+},
+{
+glyphname = C;
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 0043;
+},
+{
+glyphname = "hah-ar.init";
+lastChange = "2022-02-21 17:09:37 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+leftKerningGroup = "hah-ar.init";
+unicode = FEA3;
+},
+{
+glyphname = "hah-ar.init.swsh";
+lastChange = "2022-02-21 17:25:40 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+leftKerningGroup = "hah-ar.init.swsh";
+},
+{
+glyphname = "reh-ar";
+lastChange = "2022-02-21 17:09:13 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+rightKerningGroup = "reh-ar";
+unicode = 0631;
+},
+{
+glyphname = "alef-hb";
+lastChange = "2022-02-21 11:17:22 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+rightKerningGroup = leftAlef;
+unicode = 05D0;
+},
+{
+glyphname = "bet-hb";
+lastChange = "2022-02-21 11:17:26 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+leftKerningGroup = rightBet;
+unicode = 05D1;
+},
+{
+glyphname = "he-hb";
+lastChange = "2022-02-17 09:36:21 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 05D4;
+},
+{
+glyphname = "one-ar";
+lastChange = "2022-02-21 14:41:56 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+leftKerningGroup = "left-one-ar";
+unicode = 0661;
+},
+{
+glyphname = "one-ar.wide.ss16.calt";
+lastChange = "2022-02-21 14:59:56 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+leftKerningGroup = "left-one-ar";
+},
+{
+glyphname = space;
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 0020;
+}
+);
+instances = (
+{
+instanceInterpolations = {
+m01 = 1;
+};
+name = Regular;
+}
+);
+kerning = {
+m01 = {
+"@MMK_L_leftAlef" = {
+"@MMK_R_rightBet" = -20;
+"he-hb" = 4;
+};
+"@MMK_L_leftBet" = {
+"@MMK_R_rightAlef" = 20;
+};
+"@MMK_L_reh-ar" = {
+"@MMK_R_hah-ar.init" = -50;
+"@MMK_R_hah-ar.init.swsh" = -50;
+};
+"he-hb" = {
+"@MMK_R_rightAlef" = -2;
+"he-hb" = -21;
+};
+};
+};
+unitsPerEm = 1000;
+versionMajor = 1;
+versionMinor = 0;
+}
diff --git a/tests/data/RTL_kerning_v3.glyphs b/tests/data/RTL_kerning_v3.glyphs
new file mode 100644
index 00000000..627a34c7
--- /dev/null
+++ b/tests/data/RTL_kerning_v3.glyphs
@@ -0,0 +1,292 @@
+{
+.appVersion = "3149";
+.formatVersion = 3;
+DisplayStrings = (
+"אב",
+"/hah-ar.init ر/hah-ar.init.swsh"
+);
+axes = (
+{
+name = Weight;
+tag = wght;
+}
+);
+date = "2022-02-09 20:07:11 +0000";
+familyName = "RTL kerning";
+fontMaster = (
+{
+axesValues = (
+1
+);
+id = m01;
+metricValues = (
+{
+pos = 800;
+},
+{
+pos = 700;
+},
+{
+pos = 500;
+},
+{
+},
+{
+pos = -200;
+},
+{
+}
+);
+name = Regular;
+},
+{
+axesValues = (
+100
+);
+id = "B0D53B35-34A4-475E-9EF4-52C3D10908C6";
+metricValues = (
+{
+pos = 800;
+},
+{
+pos = 700;
+},
+{
+pos = 500;
+},
+{
+},
+{
+pos = -200;
+},
+{
+}
+);
+name = Bold;
+}
+);
+glyphs = (
+{
+glyphname = A;
+lastChange = "2022-12-13 14:06:47 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+},
+{
+layerId = "B0D53B35-34A4-475E-9EF4-52C3D10908C6";
+width = 600;
+}
+);
+unicode = 65;
+},
+{
+glyphname = B;
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 66;
+},
+{
+glyphname = C;
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 67;
+},
+{
+glyphname = "hah-ar.init";
+kernRight = "hah-ar.init";
+lastChange = "2022-12-13 14:06:50 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+},
+{
+layerId = "B0D53B35-34A4-475E-9EF4-52C3D10908C6";
+width = 600;
+}
+);
+},
+{
+glyphname = "hah-ar.init.swsh";
+kernRight = "hah-ar.init.swsh";
+lastChange = "2022-12-13 14:07:09 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+},
+{
+layerId = "B0D53B35-34A4-475E-9EF4-52C3D10908C6";
+width = 600;
+}
+);
+},
+{
+glyphname = "reh-ar";
+kernLeft = "reh-ar";
+lastChange = "2022-12-13 14:06:34 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+},
+{
+layerId = "B0D53B35-34A4-475E-9EF4-52C3D10908C6";
+width = 600;
+}
+);
+unicode = 1585;
+},
+{
+glyphname = "alef-hb";
+kernLeft = leftAlef;
+lastChange = "2022-12-13 14:06:34 +0000";
+layers = (
+{
+layerId = m01;
+width = 700;
+},
+{
+layerId = "B0D53B35-34A4-475E-9EF4-52C3D10908C6";
+width = 600;
+}
+);
+metricLeft = "=50";
+metricRight = "=50";
+unicode = 1488;
+},
+{
+glyphname = "bet-hb";
+kernRight = rightBet;
+lastChange = "2022-02-21 11:18:27 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 1489;
+},
+{
+glyphname = "he-hb";
+lastChange = "2022-02-17 09:36:21 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 1492;
+},
+{
+glyphname = "one-ar";
+kernLeft = "left-one-ar";
+lastChange = "2022-02-21 14:41:31 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 1633;
+},
+{
+glyphname = "one-ar.wide";
+kernLeft = "left-one-ar";
+lastChange = "2022-02-21 14:56:37 +0000";
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+},
+{
+glyphname = one;
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 49;
+},
+{
+glyphname = space;
+layers = (
+{
+layerId = m01;
+width = 600;
+}
+);
+unicode = 32;
+}
+);
+instances = (
+{
+axesValues = (
+100
+);
+instanceInterpolations = {
+"B0D53B35-34A4-475E-9EF4-52C3D10908C6" = 1;
+};
+name = Regular;
+}
+);
+kerningRTL = {
+m01 = {
+"@MMK_R_leftAlef" = {
+"@MMK_L_rightBet" = -20;
+"he-hb" = 4;
+};
+"@MMK_R_leftBet" = {
+"@MMK_L_rightAlef" = 20;
+};
+"@MMK_R_reh-ar" = {
+"@MMK_L_hah-ar.init.swsh" = -50;
+};
+"he-hb" = {
+"@MMK_L_rightAlef" = -2;
+"he-hb" = -21;
+};
+};
+"B0D53B35-34A4-475E-9EF4-52C3D10908C6" = {
+"@MMK_R_reh-ar" = {
+"@MMK_L_hah-ar.init.swsh" = -30;
+};
+};
+};
+metrics = (
+{
+type = ascender;
+},
+{
+type = "cap height";
+},
+{
+type = "x-height";
+},
+{
+type = baseline;
+},
+{
+type = descender;
+},
+{
+type = "italic angle";
+}
+);
+unitsPerEm = 1000;
+versionMajor = 1;
+versionMinor = 0;
+}
diff --git a/tests/glyphs3_test.py b/tests/glyphs3_test.py
index 0abc79fe..cbb167f8 100644
--- a/tests/glyphs3_test.py
+++ b/tests/glyphs3_test.py
@@ -85,3 +85,63 @@ def test_glyphs3_stems(datadir):
 
     assert master.verticalStems == [17, 19]
     assert master.horizontalStems == [16, 16, 18]
+
+
+def test_glyphs2_rtl_kerning(datadir, ufo_module):
+    file = "RTL_kerning_v2.glyphs"
+    with open(str(datadir.join(file)), encoding="utf-8") as f:
+        font = glyphsLib.load(f)
+
+    designspace = glyphsLib.to_designspace(font, ufo_module=ufo_module)
+    ufos = [source.font for source in designspace.sources]
+    print(file, ufos[0].groups)
+    assert ufos[0].groups["public.kern1.reh-ar"] == ["reh-ar"]
+    assert ufos[0].groups["public.kern2.hah-ar.init.swsh"] == ["hah-ar.init.swsh"]
+    assert (
+        ufos[0].kerning[("public.kern1.reh-ar", "public.kern2.hah-ar.init.swsh")] == -50
+    )
+
+    assert ufos[0].kerning[("he-hb", "he-hb")] == -21
+
+
+def test_glyphs3_rtl_kerning(datadir, ufo_module):
+    file = "RTL_kerning_v3.glyphs"
+    with open(str(datadir.join(file)), encoding="utf-8") as f:
+        original_glyphs_font = glyphsLib.load(f)
+
+    # First conversion to UFO
+    designspace = glyphsLib.to_designspace(original_glyphs_font, ufo_module=ufo_module)
+    first_derivative_ufos = [source.font for source in designspace.sources]
+
+    print(first_derivative_ufos[0].groups)
+    assert first_derivative_ufos[0].groups["public.kern1.reh-ar"] == ["reh-ar"]
+    assert first_derivative_ufos[0].groups["public.kern2.hah-ar.init.swsh"] == [
+        "hah-ar.init.swsh"
+    ]
+    assert (
+        first_derivative_ufos[0].kerning[
+            ("public.kern1.reh-ar", "public.kern2.hah-ar.init.swsh")
+        ]
+        == -50
+    )
+    assert first_derivative_ufos[0].kerning[("he-hb", "he-hb")] == -21
+
+    # Round-tripping back to Glyphs
+    round_tripped_glyphs_font = glyphsLib.to_glyphs(first_derivative_ufos)
+
+    # Second conversion back to UFO
+    designspace = glyphsLib.to_designspace(
+        round_tripped_glyphs_font, ufo_module=ufo_module
+    )
+    second_derivative_ufos = [source.font for source in designspace.sources]
+
+    # Comparing kerning between first and second derivative UFOs:
+    # Round-tripped RTL kernining ends up as LTR kerning, but at least it's lossless
+    # and produces correct results.
+    assert first_derivative_ufos[0].groups == second_derivative_ufos[0].groups
+    assert first_derivative_ufos[0].kerning == second_derivative_ufos[0].kerning
+    assert first_derivative_ufos[1].groups == second_derivative_ufos[1].groups
+    assert first_derivative_ufos[1].kerning == second_derivative_ufos[1].kerning
+    # Check that groups within one font are identical after pruning
+    assert first_derivative_ufos[0].groups == first_derivative_ufos[1].groups
+    assert second_derivative_ufos[0].groups == second_derivative_ufos[1].groups
diff --git a/tests/writer_test.py b/tests/writer_test.py
index 2b98a5e2..9a504616 100644
--- a/tests/writer_test.py
+++ b/tests/writer_test.py
@@ -678,9 +678,9 @@ class WriterTest(unittest.TestCase, test_helpers.AssertLinesEqual):
             leftKerningGroup = A;
             leftMetricsKey = A;
             widthMetricsKey = A;
+            note = "Stunning one-bedroom A with renovated acute accent";
             rightKerningGroup = A;
             rightMetricsKey = A;
-            note = "Stunning one-bedroom A with renovated acute accent";
             unicode = 00C1;
             script = latin;
             category = Letter;
@@ -849,8 +849,8 @@ class WriterTest(unittest.TestCase, test_helpers.AssertLinesEqual):
             );
             layerId = L1;
             leftMetricsKey = A;
-            rightMetricsKey = A;
             widthMetricsKey = A;
+            rightMetricsKey = A;
             name = "{125, 100}";
             paths = (
             {
@@ -1120,13 +1120,13 @@ rememberToDownloadARealRemindersApp = 1;}"',
                 """\
             {
             horizontal = 1;
-            options = 128;
             origin = "{0, 0}";
+            target = "{0, 1}";
             other1 = "{0, 2}";
             other2 = "{1, 0}";
-            target = "{0, 1}";
             type = 16;
             name = "My favourite hint";
+            options = 128;
             }
         """
             ),
@@ -1176,14 +1176,14 @@ rememberToDownloadARealRemindersApp = 1;}"',
             dedent(
                 """\
              {
-             name = "My favourite hint";
              horizontal = 1;
-             options = 128;
              origin = (0,0);
+             target = (0,1);
              other1 = (0,2);
              other2 = (1,0);
-             target = (0,1);
              type = 16;
+             name = "My favourite hint";
+             options = 128;
              }
          """,
             ),