diff --git a/Lib/glyphsLib/builder/constants.py b/Lib/glyphsLib/builder/constants.py index 815c7e74c..e8113efe8 100644 --- a/Lib/glyphsLib/builder/constants.py +++ b/Lib/glyphsLib/builder/constants.py @@ -53,6 +53,7 @@ UFO_ORIGINAL_KERNING_GROUPS_KEY = GLYPHLIB_PREFIX + "originalKerningGroups" UFO_GROUPS_NOT_IN_FEATURE_KEY = GLYPHLIB_PREFIX + "groupsNotInFeature" UFO_KERN_GROUP_PATTERN = re.compile("^public\\.kern([12])\\.(.*)$") +SORT_G3_RTL_KERNING_SUFFIX = ".G3RTLKERNINGCONVERSION" LOCKED_GUIDE_NAME_SUFFIX = " [locked]" diff --git a/Lib/glyphsLib/builder/groups.py b/Lib/glyphsLib/builder/groups.py index 01c4cdde7..04a2cf2d7 100644 --- a/Lib/glyphsLib/builder/groups.py +++ b/Lib/glyphsLib/builder/groups.py @@ -22,6 +22,7 @@ UFO_GROUPS_NOT_IN_FEATURE_KEY, UFO_KERN_GROUP_PATTERN, BRACKET_GLYPH_RE, + SORT_G3_RTL_KERNING_SUFFIX, ) @@ -71,14 +72,17 @@ def to_ufo_groups(self): recovered.add((glyph_name, int(side))) # Read modified grouping values + sides = [1, 2] for glyph in self.font.glyphs.values(): - for side in 1, 2: + for i, side in enumerate(sides): + other_side = sides[len(sides) - 1 - i] if (glyph.name, side) not in recovered: attr = _glyph_kerning_attr(glyph, side) - group = getattr(glyph, attr) - if group: - group = f"public.kern{side}.{group}" - groups[group].append(glyph.name) + glyph_group = getattr(glyph, attr) + if glyph_group: + _add_glyph_to_ufo_groups( + glyph, groups, glyph_group, side, other_side + ) # Update all UFOs with the same info for source in self._sources.values(): @@ -87,13 +91,40 @@ def to_ufo_groups(self): source.font.groups[name] = glyphs[:] +def _add_glyph_to_ufo_groups(glyph, groups, glyph_group, side, other_side): + if not glyph_group.endswith(SORT_G3_RTL_KERNING_SUFFIX): + # Traditional group + group = f"public.kern{side}.{glyph_group}" + if glyph.name not in groups[group]: + groups[group].append(glyph.name) + # Additional RTL group + # This will add lots of unused groups which we'll prune later + # but is necessary to implement G3 RTL kerning. See kerning.py + group = f"public.kern{other_side}.{glyph_group}{SORT_G3_RTL_KERNING_SUFFIX}" + if glyph.name not in groups[group]: + groups[group].append(glyph.name) + else: + # Additional RTL group + # This will add lots of unused groups which we'll prune later + # but is necessary to implement G3 RTL kerning. See kerning.py + group = f"public.kern{side}.{glyph_group}" + if glyph.name not in groups[group]: + groups[group].append(glyph.name) + + # Traditional group + glyph_group = glyph_group.replace(SORT_G3_RTL_KERNING_SUFFIX, "") + group = f"public.kern{other_side}.{glyph_group}" + if glyph.name not in groups[group]: + groups[group].append(glyph.name) + + def to_glyphs_groups(self): # Build the GSClasses from the groups of the first UFO. groups = [] for source in self._sources.values(): for name, glyphs in source.font.groups.items(): # Filter out all BRACKET glyphs first, as they are created at - # to_designspace time to inherit glyph kerning to their bracket + # to_designspace time to inherit to_ufo_groups # variants. They need to be removed because Glpyhs.app handles that # on its own. glyphs = [name for name in glyphs if not BRACKET_GLYPH_RE.match(name)] diff --git a/Lib/glyphsLib/builder/kerning.py b/Lib/glyphsLib/builder/kerning.py index b9e478b2b..38e9f9f2e 100644 --- a/Lib/glyphsLib/builder/kerning.py +++ b/Lib/glyphsLib/builder/kerning.py @@ -15,39 +15,123 @@ import re -from .constants import BRACKET_GLYPH_RE, UFO_KERN_GROUP_PATTERN +from .constants import ( + BRACKET_GLYPH_RE, + UFO_KERN_GROUP_PATTERN, + SORT_G3_RTL_KERNING_SUFFIX, +) def to_ufo_kerning(self): + used_groups = set() for master in self.font.masters: + ufo = self._sources[master.id].font 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) + if kerning_source.id in self.font.kerningLTR: + kerning = self.font.kerningLTR[kerning_source.id] + used_groups.update(_to_ufo_kerning(self, ufo, kerning)) + if kerning_source.id in self.font.kerningRTL: + kerning = self.font.kerningRTL[kerning_source.id] + used_groups.update(_to_ufo_kerning(self, ufo, kerning, "RTL")) + # Pruning as consequence of PR #838 + for master in self.font.masters: + ufo = self._sources[master.id].font + + # Prune kerning groups that are not used in kerning rules + # (added but unused .RTL groups, see groups.py) + for group in list(ufo.groups.keys()): + # Group exists in font but not in kerning rules + if group.startswith("public.kern") and group not in used_groups: + del ufo.groups[group] + + # Remove .RTL from groups + for group in list(ufo.groups.keys()): + if group.startswith("public.kern") and group.endswith( + SORT_G3_RTL_KERNING_SUFFIX + ): + ufo.groups[group.replace(SORT_G3_RTL_KERNING_SUFFIX, "")] = ufo.groups[ + group + ] + del ufo.groups[group] + + # Remove .RTL from kerning + for first, second in list(ufo.kerning.keys()): + if ( + first.startswith("public.kern") + and first.endswith(SORT_G3_RTL_KERNING_SUFFIX) + and second.startswith("public.kern") + and second.endswith(SORT_G3_RTL_KERNING_SUFFIX) + ): + ufo.kerning[ + first.replace(SORT_G3_RTL_KERNING_SUFFIX, ""), + second.replace(SORT_G3_RTL_KERNING_SUFFIX, ""), + ] = ufo.kerning[first, second] + del ufo.kerning[first, second] + elif first.startswith("public.kern") and first.endswith( + SORT_G3_RTL_KERNING_SUFFIX + ): + ufo.kerning[ + first.replace(SORT_G3_RTL_KERNING_SUFFIX, ""), second + ] = ufo.kerning[first, second] + del ufo.kerning[first, second] + elif second.startswith("public.kern") and second.endswith( + SORT_G3_RTL_KERNING_SUFFIX + ): + ufo.kerning[ + first, second.replace(SORT_G3_RTL_KERNING_SUFFIX, "") + ] = ufo.kerning[first, second] + del ufo.kerning[first, second] -def _to_ufo_kerning(self, ufo, kerning_data): + +def _to_ufo_kerning(self, ufo, kerning_data, direction="LTR"): """Add .glyphs kerning to an UFO.""" - warning_msg = "Non-existent glyph class %s found in kerning rules." - - for left, pairs in kerning_data.items(): - match = re.match(r"@MMK_L_(.+)", left) - left_is_class = bool(match) - if left_is_class: - left = "public.kern1.%s" % match.group(1) - if left not in ufo.groups: - self.logger.warning(warning_msg % left) - for right, kerning_val in pairs.items(): - match = re.match(r"@MMK_R_(.+)", right) - right_is_class = bool(match) - if right_is_class: - right = "public.kern2.%s" % match.group(1) - if right not in ufo.groups: - self.logger.warning(warning_msg % right) - ufo.kerning[left, right] = kerning_val + class_missing_msg = "Non-existent glyph class %s found in kerning rules." + overwriting_kerning_msg = "Overwriting kerning value for %s." + + used_groups = set() + + for first, pairs in kerning_data.items(): + first, is_class = _ufo_class_name(first, direction, 1) + if is_class: + used_groups.add(first) + if is_class and first not in ufo.groups: + self.logger.warning(class_missing_msg, first) + + for second, kerning_val in pairs.items(): + second, is_class = _ufo_class_name(second, direction, 2) + if is_class: + used_groups.add(second) + if is_class and second not in ufo.groups: + self.logger.warning(class_missing_msg, second) + + if (first, second) in ufo.kerning: + self.logger.warning(overwriting_kerning_msg, first, second) + ufo.kerning[first, second] = kerning_val + + return used_groups + + +def _ufo_class_name(name, direction, order): + """Return the UFO class name for a .glyphs class name.""" + if order == 1: + side = "L" if direction == "LTR" else "R" + else: + assert order == 2 + side = "R" if direction == "LTR" else "L" + + match = re.match(rf"@MMK_{side}_(.+)", name) + + name_is_class = bool(match) + if name_is_class: + name = f"public.kern{order}.{match.group(1)}" + if direction == "RTL": + name += SORT_G3_RTL_KERNING_SUFFIX + + return name, name_is_class def to_glyphs_kerning(self): diff --git a/README.rst b/README.rst index 78a983426..04dc8808b 100644 --- a/README.rst +++ b/README.rst @@ -145,6 +145,23 @@ In practice there are always a few diffs on things that don't really make a difference, like optional things being added/removed or whitespace changes or things getting reordered... +Kerning interaction between Glyphs 3 and UFO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Glyphs 3 introduced the attribute ``.kerningRTL`` for the storage of RTL kerning pairs +which breaks with the UFO spec of storing kerning as logical first/second pairs +regardless of writing direction. +As of `PR #838 `__ glyphsLib +reverts this separate Glyphs 3-style RTL kerning back to Glyphs 2/UFO-style kerning +upon conversion of a Glyphs object to a UFO object, *but it does not convert the kerning +back to Glyphs 3-style when converting a UFO object to a Glyphs object!* + +This means that if you convert a UFO to a Glyphs file and subsequently open that file +in Glyphs 3, the RTL kerning will initially not be visible in the UI, but be hidden +in the LTR kerning. This is identical to opening a Glyphs 2 file with RTL kerning +in Glyphs 3. It is in the responsibility of Glyphs 3 and the user to convert the kerning +back to Glyphs 3's separate RTL kerning. + Make a release ^^^^^^^^^^^^^^ diff --git a/tests/builder/to_glyphs_test.py b/tests/builder/to_glyphs_test.py index 09f869a58..c4d4d667b 100644 --- a/tests/builder/to_glyphs_test.py +++ b/tests/builder/to_glyphs_test.py @@ -163,8 +163,22 @@ def test_groups(ufo_module): (ufo,) = to_ufos(font) - # Check that nothing has changed - assert dict(ufo.groups) == groups_dict + # Prior to PR #838, we were checking for d1 and d2 to be equal. + # However, with PR #838, we are now pruning kerning groups that are not + # used in the kerning table. + # The groups in the assertion below got pruned. + d1 = dict(ufo.groups) + d2 = groups_dict + + assert set(d2) ^ set(d1) == { + "public.kern1.notInFont", + "public.kern1.hebrewLikeT", + "public.kern1.halfInFont", + "public.kern2.hebrewLikeO", + "public.kern2.oe", + "public.kern1.empty", + "public.kern1.T", + } # Check that changing the `left/rightKerningGroup` fields in Glyphs # updates the UFO kerning groups @@ -178,7 +192,20 @@ def test_groups(ufo_module): (ufo,) = to_ufos(font) - assert dict(ufo.groups) == groups_dict + # Checking for known pruned groups (PR #838) + d1 = dict(ufo.groups) + d2 = groups_dict + + assert set(d2) ^ set(d1) == { + "public.kern1.halfInFont", + "public.kern1.empty", + "public.kern1.newNameT", + "public.kern2.oe", + "public.kern2.hebrewLikeO", + "public.kern1.onItsOwnO", + "public.kern1.hebrewLikeT", + "public.kern1.notInFont", + } def test_guidelines(ufo_module): diff --git a/tests/data/RTL_kerning_v2.glyphs b/tests/data/RTL_kerning_v2.glyphs new file mode 100644 index 000000000..caec77ba4 --- /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 000000000..627a34c74 --- /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 872df46e6..1ea602a45 100644 --- a/tests/glyphs3_test.py +++ b/tests/glyphs3_test.py @@ -1,5 +1,6 @@ import glyphsLib from glyphsLib.classes import GSFont, GSFontMaster +from glyphsLib import to_designspace, to_glyphs def test_metrics(): @@ -12,7 +13,7 @@ def test_metrics(): def test_glyphs3_italic_angle(datadir): - with open(str(datadir.join("Italic-G3.glyphs"))) as f: + with open(str(datadir.join("Italic-G3.glyphs")), encoding="utf-8") as f: font = glyphsLib.load(f) assert font.masters[0].italicAngle == 11 @@ -22,3 +23,60 @@ def test_glyphspackage_load(datadir): font1.DisplayStrings = "" # glyphspackages, rather sensibly, don't store user state font2 = glyphsLib.load(str(datadir.join("GlyphsUnitTestSans3.glyphspackage"))) assert glyphsLib.dumps(font1) == glyphsLib.dumps(font2) + + +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 = 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 = to_designspace(original_glyphs_font, ufo_module=ufo_module) + first_derivative_ufos = [source.font for source in designspace.sources] + + 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 = to_glyphs(first_derivative_ufos) + + # Second conversion back to UFO + designspace = 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