Repository: googlefonts/ufo2ft
Pull request #841 (2024-05-20 13:37:41)

Add variable kerning to old kern writer
This upgrades the old kern writer to be able to output variable kerning for when you compile features once for a VF.


Patch:
diff --git a/Lib/ufo2ft/featureWriters/kernFeatureWriter.py b/Lib/ufo2ft/featureWriters/kernFeatureWriter.py
index 524c4dd..aa4695f 100644
--- a/Lib/ufo2ft/featureWriters/kernFeatureWriter.py
+++ b/Lib/ufo2ft/featureWriters/kernFeatureWriter.py
@@ -402,7 +402,13 @@ class KernFeatureWriter(BaseFeatureWriter):
         side2Classes: Mapping[str, tuple[str, ...]],
     ) -> list[KerningPair]:
         if self.context.isVariable:
-            return self.getVariableKerningPairs(side1Classes, side2Classes)
+            return self.getVariableKerningPairs(
+                self.context.font,
+                side1Classes,
+                side2Classes,
+                self.context.glyphSet,
+                self.options,
+            )
 
         glyphSet = self.context.glyphSet
         font = self.context.font
@@ -432,14 +438,15 @@ class KernFeatureWriter(BaseFeatureWriter):
 
         return result
 
+    @staticmethod
     def getVariableKerningPairs(
-        self,
+        designspace: DesignSpaceDocument,
         side1Classes: Mapping[str, tuple[str, ...]],
         side2Classes: Mapping[str, tuple[str, ...]],
+        glyphSet: Mapping[str, str],
+        options: SimpleNamespace,
     ) -> list[KerningPair]:
-        designspace: DesignSpaceDocument = self.context.font
-        glyphSet = self.context.glyphSet
-        quantization = self.options.quantization
+        quantization = options.quantization
 
         # Gather utility variables for faster kerning lookups.
         # TODO: Do we construct these in code elsewhere?
diff --git a/Lib/ufo2ft/featureWriters/kernFeatureWriter2.py b/Lib/ufo2ft/featureWriters/kernFeatureWriter2.py
index 82fab4d..13c8c95 100644
--- a/Lib/ufo2ft/featureWriters/kernFeatureWriter2.py
+++ b/Lib/ufo2ft/featureWriters/kernFeatureWriter2.py
@@ -1,11 +1,18 @@
 """Old implementation of KernFeatureWriter as of ufo2ft v2.30.0 for backward compat."""
 
+from __future__ import annotations
+
 from types import SimpleNamespace
+from typing import Mapping
 
 from fontTools import unicodedata
+from fontTools.designspaceLib import DesignSpaceDocument
 
 from ufo2ft.constants import INDIC_SCRIPTS, USE_SCRIPTS
 from ufo2ft.featureWriters import BaseFeatureWriter, ast
+from ufo2ft.featureWriters.kernFeatureWriter import (
+    KernFeatureWriter as NewKernFeatureWriter,
+)
 from ufo2ft.util import classifyGlyphs, quantize, unicodeScriptDirection
 
 SIDE1_PREFIX = "public.kern1."
@@ -113,7 +120,9 @@ class KernFeatureWriter(BaseFeatureWriter):
     def setContext(self, font, feaFile, compiler=None):
         ctx = super().setContext(font, feaFile, compiler=compiler)
         ctx.gdefClasses = self.getGDEFGlyphClasses()
-        ctx.kerning = self.getKerningData(font, feaFile, self.getOrderedGlyphSet())
+        ctx.kerning = self.getKerningData(
+            font, self.options, feaFile, self.getOrderedGlyphSet()
+        )
 
         feaScripts = ast.getScriptLanguageSystems(feaFile)
         ctx.scriptGroups = self._groupScriptsByTagAndDirection(feaScripts)
@@ -168,9 +177,9 @@ class KernFeatureWriter(BaseFeatureWriter):
         return True
 
     @classmethod
-    def getKerningData(cls, font, feaFile=None, glyphSet=None):
+    def getKerningData(cls, font, options, feaFile=None, glyphSet=None):
         side1Classes, side2Classes = cls.getKerningClasses(font, feaFile, glyphSet)
-        pairs = cls.getKerningPairs(font, side1Classes, side2Classes, glyphSet)
+        pairs = cls.getKerningPairs(font, side1Classes, side2Classes, glyphSet, options)
         return SimpleNamespace(
             side1Classes=side1Classes, side2Classes=side2Classes, pairs=pairs
         )
@@ -183,6 +192,13 @@ class KernFeatureWriter(BaseFeatureWriter):
             allGlyphs = set(font.keys())
         side1Groups = {}
         side2Groups = {}
+
+        if isinstance(font, DesignSpaceDocument):
+            default_font = font.findDefault()
+            assert default_font is not None
+            font = default_font.font
+            assert font is not None
+
         for name, members in font.groups.items():
             # prune non-existent or skipped glyphs
             members = [g for g in members if g in allGlyphs]
@@ -208,7 +224,35 @@ class KernFeatureWriter(BaseFeatureWriter):
         return side1Classes, side2Classes
 
     @staticmethod
-    def getKerningPairs(font, side1Classes, side2Classes, glyphSet=None):
+    def getKerningPairs(font, side1Classes, side2Classes, glyphSet=None, options=None):
+        if isinstance(font, DesignSpaceDocument):
+            # Reuse the newer kern writers variable kerning extractor. Repack
+            # some arguments and the return type for this.
+            side1ClassesRaw: Mapping[str, tuple[str, ...]] = {
+                group_name: tuple(
+                    glyph
+                    for glyphs in glyph_defs.glyphSet()
+                    for glyph in glyphs.glyphSet()
+                )
+                for group_name, glyph_defs in side1Classes.items()
+            }
+            side2ClassesRaw: Mapping[str, tuple[str, ...]] = {
+                group_name: tuple(
+                    glyph
+                    for glyphs in glyph_defs.glyphSet()
+                    for glyph in glyphs.glyphSet()
+                )
+                for group_name, glyph_defs in side2Classes.items()
+            }
+            pairs = NewKernFeatureWriter.getVariableKerningPairs(
+                font,
+                side1ClassesRaw,
+                side2ClassesRaw,
+                glyphSet or {},
+                options or SimpleNamespace(**KernFeatureWriter.options),
+            )
+            return [KerningPair(pair.side1, pair.side2, pair.value) for pair in pairs]
+
         if glyphSet:
             allGlyphs = set(glyphSet.keys())
         else:


Test patch:
diff --git a/tests/featureWriters/variableFeatureWriter_test.py b/tests/featureWriters/variableFeatureWriter_test.py
index 5ce5577..077fd4f 100644
--- a/tests/featureWriters/variableFeatureWriter_test.py
+++ b/tests/featureWriters/variableFeatureWriter_test.py
@@ -59,3 +59,76 @@ def test_variable_features(FontClass):
         } curs;
 """  # noqa: B950
     )
+
+
+def test_variable_features_old_kern_writer(FontClass):
+    tmp = io.StringIO()
+    designspace = designspaceLib.DesignSpaceDocument.fromfile(
+        "tests/data/TestVarfea.designspace"
+    )
+    designspace.loadSourceFonts(FontClass)
+
+    default_source = designspace.findDefault()
+    assert default_source is not None
+    default_ufo = default_source.font
+    assert default_ufo is not None
+    default_ufo.lib["com.github.googlei18n.ufo2ft.featureWriters"] = [
+        {
+            "module": "ufo2ft.featureWriters.kernFeatureWriter2",
+            "class": "KernFeatureWriter",
+        },
+        {
+            "module": "ufo2ft.featureWriters.markFeatureWriter",
+            "class": "MarkFeatureWriter",
+        },
+        {
+            "module": "ufo2ft.featureWriters.gdefFeatureWriter",
+            "class": "GdefFeatureWriter",
+        },
+        {
+            "module": "ufo2ft.featureWriters.cursFeatureWriter",
+            "class": "CursFeatureWriter",
+        },
+    ]
+
+    _ = compileVariableTTF(designspace, debugFeatureFile=tmp)
+
+    assert dedent("\n" + tmp.getvalue()) == dedent(
+        """
+        markClass dotabove-ar <anchor (wght=100:100 wght=1000:125) (wght=100:320 wght=1000:416)> @MC_top;
+        markClass gravecmb <anchor 250 400> @MC_top;
+
+        lookup kern_rtl {
+            lookupflag IgnoreMarks;
+            pos alef-ar.fina alef-ar.fina <(wght=100:15 wght=1000:35) 0 (wght=100:15 wght=1000:35) 0>;
+        } kern_rtl;
+
+        feature kern {
+            lookup kern_rtl;
+        } kern;
+
+        feature mark {
+            lookup mark2base {
+                pos base alef-ar.fina
+                    <anchor (wght=100:211 wght=1000:214) (wght=100:730 wght=1000:797)> mark @MC_top;
+                pos base a
+                    <anchor 250 400> mark @MC_top;
+            } mark2base;
+
+        } mark;
+
+        table GDEF {
+            LigatureCaretByPos peh-ar.init 100;
+        } GDEF;
+
+        feature curs {
+            lookup curs_rtl {
+                lookupflag RightToLeft IgnoreMarks;
+                pos cursive alef-ar.fina <anchor (wght=100:299 wght=1000:330) (wght=100:97 wght=1000:115)> <anchor NULL>;
+                pos cursive peh-ar.init <anchor NULL> <anchor (wght=100:161 wght=1000:73) (wght=100:54 wght=1000:89)>;
+                pos cursive peh-ar.init.BRACKET.varAlt01 <anchor NULL> <anchor (wght=100:89 wght=1000:73) (wght=100:53 wght=1000:85)>;
+            } curs_rtl;
+
+        } curs;
+"""  # noqa: B950
+    )