Repository: googlefonts/ufo2ft
Pull request #881 (2024-10-12 16:00:19)

[outlineCompiler] Make space the 2nd glyph unless its order is explicitly set
* Fixes https://github.com/googlefonts/ufo2ft/issues/880

khaledhosny: The second commit is just in case someone wants this level of control, but I have no strong opinion here and I can do without it.
anthrotype: I would only move the space to second place if there is _not_ already an explicit glyphOrder in the UFO.
I don't see the point of having yet another custom parameter to "Keep GlyphOrder" in this situation. Either you don't care/define your own glyphOrder, in which case we do our best to place glyphs to maximise compatibility, or you do provide an explicit one and we just do what told.
anthrotype: as for glyphsLib, which always sets a glyphOrder in the exported master UFOs (whether implicit in the order of the glyphs in the source file, or with an explicit glyphOrder custom parameter), it can deal itself with those Glyphs-specific things. It may decide to always place the space in second place (like Glyphs does now?) regardless of what the stated glyphOrder is, unless the user uses "no, really I mean it" custom parameter..
khaledhosny: Done.
khaledhosny: This does not work. We don’t actually know if `public.glyphOrder` is set or not, we ask the font for glyphOrder and it gives us one (defcon at least will make up something if `public.glyphOrder` is not set).
khaledhosny: I think it is working now.
anthrotype: much better now, thanks!

Patch:
diff --git a/Lib/ufo2ft/util.py b/Lib/ufo2ft/util.py
index 17d4eff..a87b76e 100644
--- a/Lib/ufo2ft/util.py
+++ b/Lib/ufo2ft/util.py
@@ -32,15 +32,20 @@ def makeOfficialGlyphOrder(font, glyphOrder=None):
     If not explicit glyphOrder is defined, sort glyphs alphabetically.
 
     If ".notdef" glyph is present in the font, force this to always be
-    the first glyph (at index 0).
+    the first glyph (at index 0). Also, if "space" is present in the font and
+    missing from glyphOrder, force it to be the second glyph (at index 1).
     """
     if glyphOrder is None:
         glyphOrder = getattr(font, "glyphOrder", ())
+    reorderSpace = "space" not in glyphOrder
     names = set(font.keys())
     order = []
     if ".notdef" in names:
         names.remove(".notdef")
         order.append(".notdef")
+    if reorderSpace and "space" in names:
+        names.remove("space")
+        order.append("space")
     for name in glyphOrder:
         if name not in names:
             continue


Test patch:
diff --git a/tests/outlineCompiler_test.py b/tests/outlineCompiler_test.py
index 5755ce5..69a84f2 100644
--- a/tests/outlineCompiler_test.py
+++ b/tests/outlineCompiler_test.py
@@ -761,6 +761,67 @@ class GlyphOrderTest:
         compiler.compile()
         assert compiler.otf.getGlyphOrder() == EXPECTED_ORDER
 
+    def test_compile_reorder_space_glyph(self, quadufo):
+        """
+        Test that ufo2ft always puts .notdef first, and put space second if no
+        explicit glyph order is set.
+        """
+        EXPECTED_ORDER = [
+            ".notdef",
+            "space",
+            "a",
+            "b",
+            "c",
+            "d",
+            "e",
+            "f",
+            "g",
+            "h",
+            "i",
+            "j",
+            "k",
+            "l",
+        ]
+        # Check with no public.glyphOrder
+        del quadufo.lib["public.glyphOrder"]
+        assert not quadufo.glyphOrder
+        compiler = OutlineTTFCompiler(quadufo)
+        compiler.compile()
+        assert compiler.otf.getGlyphOrder() == EXPECTED_ORDER
+
+        # Empty glyphOrder is considered the same
+        quadufo.glyphOrder = []
+        compiler = OutlineTTFCompiler(quadufo)
+        compiler.compile()
+        assert compiler.otf.getGlyphOrder() == EXPECTED_ORDER
+
+        # Non-empty glyphOrder without "space" is considered the same
+        quadufo.glyphOrder = [n for n in EXPECTED_ORDER if n != "space"]
+        compiler = OutlineTTFCompiler(quadufo)
+        compiler.compile()
+        assert compiler.otf.getGlyphOrder() == EXPECTED_ORDER
+
+        EXPECTED_ORDER = [
+            ".notdef",
+            "a",
+            "b",
+            "c",
+            "d",
+            "space",
+            "e",
+            "f",
+            "g",
+            "h",
+            "i",
+            "j",
+            "k",
+            "l",
+        ]
+        quadufo.glyphOrder = EXPECTED_ORDER
+        compiler = OutlineTTFCompiler(quadufo)
+        compiler.compile()
+        assert compiler.otf.getGlyphOrder() == EXPECTED_ORDER
+
 
 class NamesTest:
     @pytest.mark.parametrize(
@@ -915,7 +976,7 @@ class NamesTest:
         assert long_name in result.getGlyphOrder()
 
     def test_duplicate_glyph_names(self, testufo):
-        order = ["ab", "ab.1", "a-b", "a/b", "ba"]
+        order = ["ab", "ab.1", "a-b", "a/b", "ba", "space"]
         testufo.lib["public.glyphOrder"] = order
         testufo.lib["public.postscriptNames"] = {"ba": "ab"}
         for name in order: