Repository: googlefonts/ufo2ft
Pull request #897 (2025-02-14 15:55:31)

fix InfoCompiler overwriting the default master's codepage range bits
When a designspace `<variable-font>` contains a `public.fontInfo` lib key with overrides for the default master's fontinfo.plist to be applied on the VF binary font, and this does not define explicit `openTypeOS2CodePageRanges`, the way the InfoCompiler works (as a sublcass of the BaseOutlineCompiler) means that the codepage range bits computed on its empty temporary TTFont (without any glyphs nor cmap) will overwrite the good codepage bits that were previously computed on the default master TTFont.

This PR will make sure that this doesn't happen. Also we can use the `calcCodePageRanges` function that is in fonttools now (added by @khaledhosny last year) instead of keeping our own duplicate in ufo2ft.util.




Patch:
diff --git a/Lib/ufo2ft/outlineCompiler.py b/Lib/ufo2ft/outlineCompiler.py
index 03f07d6..bffb5eb 100644
--- a/Lib/ufo2ft/outlineCompiler.py
+++ b/Lib/ufo2ft/outlineCompiler.py
@@ -51,7 +51,6 @@ from ufo2ft.instructionCompiler import InstructionCompiler
 from ufo2ft.util import (
     _copyGlyph,
     _getNewGlyphFactory,
-    calcCodePageRanges,
     colrClipBoxQuantization,
     getMaxComponentDepth,
     makeOfficialGlyphOrder,
@@ -665,11 +664,11 @@ class BaseOutlineCompiler:
 
         # codepage ranges
         codepageRanges = getAttrWithFallback(font.info, "openTypeOS2CodePageRanges")
-        if codepageRanges is None:
-            unicodes = self.unicodeToGlyphNameMapping.keys()
-            codepageRanges = calcCodePageRanges(unicodes)
-        os2.ulCodePageRange1 = intListToNum(codepageRanges, 0, 32)
-        os2.ulCodePageRange2 = intListToNum(codepageRanges, 32, 32)
+        if codepageRanges is not None:
+            os2.ulCodePageRange1 = intListToNum(codepageRanges, 0, 32)
+            os2.ulCodePageRange2 = intListToNum(codepageRanges, 32, 32)
+        elif "cmap" in self.otf:
+            os2.recalcCodePageRanges(self.otf)
 
         # vendor id, padded with spaces if < 4 bytes
         os2.achVendID = getAttrWithFallback(font.info, "openTypeOS2VendorID").ljust(4)
@@ -1066,8 +1065,7 @@ class BaseOutlineCompiler:
                     isinstance(string, str) for string in value
                 ):
                     raise TypeError(
-                        f"public.openTypeMeta '{key}' value should "
-                        "be a list of strings"
+                        f"public.openTypeMeta '{key}' value should be a list of strings"
                     )
                 meta.data[key] = ",".join(value)
             elif key in ["appl", "bild"]:
diff --git a/Lib/ufo2ft/util.py b/Lib/ufo2ft/util.py
index 17d4eff..18e3acd 100644
--- a/Lib/ufo2ft/util.py
+++ b/Lib/ufo2ft/util.py
@@ -18,6 +18,10 @@ from fontTools.pens.filterPen import DecomposingFilterPointPen
 from fontTools.pens.reverseContourPen import ReverseContourPen
 from fontTools.pens.transformPen import TransformPen
 
+# we keep this unused import in case some external code relied on the
+# old location of this function, which now lives in fontTools
+from fontTools.ttLib.tables.O_S_2f_2 import calcCodePageRanges  # noqa: F401
+
 from ufo2ft.constants import OPENTYPE_CATEGORIES_KEY, UNICODE_SCRIPT_ALIASES
 from ufo2ft.errors import InvalidDesignSpaceData, InvalidFontData
 from ufo2ft.fontInfoData import getAttrWithFallback
@@ -373,101 +377,6 @@ def unicodeScriptDirection(uv):
     return unicodedata.script_horizontal_direction(sc, "LTR")
 
 
-def calcCodePageRanges(unicodes):
-    """Given a set of Unicode codepoints (integers), calculate the
-    corresponding OS/2 CodePage range bits.
-    This is a direct translation of FontForge implementation:
-    https://github.com/fontforge/fontforge/blob/7b2c074/fontforge/tottf.c#L3158
-    """
-    codepageRanges = set()
-
-    chars = [chr(u) for u in unicodes]
-
-    hasAscii = set(range(0x20, 0x7E)).issubset(unicodes)
-    hasLineart = "┤" in chars
-
-    for char in chars:
-        if char == "Þ" and hasAscii:
-            codepageRanges.add(0)  # Latin 1
-        elif char == "Ľ" and hasAscii:
-            codepageRanges.add(1)  # Latin 2: Eastern Europe
-            if hasLineart:
-                codepageRanges.add(58)  # Latin 2
-        elif char == "Б":
-            codepageRanges.add(2)  # Cyrillic
-            if "Ѕ" in chars and hasLineart:
-                codepageRanges.add(57)  # IBM Cyrillic
-            if "╜" in chars and hasLineart:
-                codepageRanges.add(49)  # MS-DOS Russian
-        elif char == "Ά":
-            codepageRanges.add(3)  # Greek
-            if hasLineart and "½" in chars:
-                codepageRanges.add(48)  # IBM Greek
-            if hasLineart and "√" in chars:
-                codepageRanges.add(60)  # Greek, former 437 G
-        elif char == "İ" and hasAscii:
-            codepageRanges.add(4)  # Turkish
-            if hasLineart:
-                codepageRanges.add(56)  # IBM turkish
-        elif char == "א":
-            codepageRanges.add(5)  # Hebrew
-            if hasLineart and "√" in chars:
-                codepageRanges.add(53)  # Hebrew
-        elif char == "ر":
-            codepageRanges.add(6)  # Arabic
-            if "√" in chars:
-                codepageRanges.add(51)  # Arabic
-            if hasLineart:
-                codepageRanges.add(61)  # Arabic; ASMO 708
-        elif char == "ŗ" and hasAscii:
-            codepageRanges.add(7)  # Windows Baltic
-            if hasLineart:
-                codepageRanges.add(59)  # MS-DOS Baltic
-        elif char == "₫" and hasAscii:
-            codepageRanges.add(8)  # Vietnamese
-        elif char == "ๅ":
-            codepageRanges.add(16)  # Thai
-        elif char == "エ":
-            codepageRanges.add(17)  # JIS/Japan
-        elif char == "ㄅ":
-            codepageRanges.add(18)  # Chinese: Simplified chars
-        elif char == "ㄱ":
-            codepageRanges.add(19)  # Korean wansung
-        elif char == "央":
-            codepageRanges.add(20)  # Chinese: Traditional chars
-        elif char == "곴":
-            codepageRanges.add(21)  # Korean Johab
-        elif char == "♥" and hasAscii:
-            codepageRanges.add(30)  # OEM Character Set
-        # TODO: Symbol bit has a special meaning (check the spec), we need
-        # to confirm if this is wanted by default.
-        # elif chr(0xF000) <= char <= chr(0xF0FF):
-        #    codepageRanges.add(31)          # Symbol Character Set
-        elif char == "þ" and hasAscii and hasLineart:
-            codepageRanges.add(54)  # MS-DOS Icelandic
-        elif char == "╚" and hasAscii:
-            codepageRanges.add(62)  # WE/Latin 1
-            codepageRanges.add(63)  # US
-        elif hasAscii and hasLineart and "√" in chars:
-            if char == "Å":
-                codepageRanges.add(50)  # MS-DOS Nordic
-            elif char == "é":
-                codepageRanges.add(52)  # MS-DOS Canadian French
-            elif char == "õ":
-                codepageRanges.add(55)  # MS-DOS Portuguese
-
-    if hasAscii and "‰" in chars and "∑" in chars:
-        codepageRanges.add(29)  # Macintosh Character Set (US Roman)
-
-    # when no codepage ranges can be enabled, fall back to enabling bit 0
-    # (Latin 1) so that the font works in MS Word:
-    # https://github.com/googlei18n/fontmake/issues/468
-    if not codepageRanges:
-        codepageRanges.add(0)
-
-    return codepageRanges
-
-
 class _LazyFontName:
     def __init__(self, font):
         self.font = font


Test patch:
diff --git a/tests/infoCompiler_test.py b/tests/infoCompiler_test.py
index b35fa6e..e3e6117 100644
--- a/tests/infoCompiler_test.py
+++ b/tests/infoCompiler_test.py
@@ -50,11 +50,30 @@ class InfoCompilerTest:
         assert ttf["name"].getDebugName(6) == "TestFontOverride-Italic"
 
     def test_OS2(self, testttf, testufo):
-        info = {"openTypeOS2TypoAscender": 100, "openTypeOS2TypoDescender": -200}
+        info = {
+            "openTypeOS2TypoAscender": 100,
+            "openTypeOS2TypoDescender": -200,
+            "openTypeOS2CodePageRanges": [0, 1, 2, 3, 32, 32 + 1, 32 + 2, 32 + 3],
+        }
         compiler = InfoCompiler(testttf, testufo, info)
         ttf = compiler.compile()
         assert ttf["OS/2"].sTypoAscender == 100
         assert ttf["OS/2"].sTypoDescender == -200
+        assert ttf["OS/2"].ulCodePageRange1 == 0b1111
+        assert ttf["OS/2"].ulCodePageRange2 == 0b1111
+
+    def test_OS2_dont_overwrite_codePageRanges(self, testttf, testufo):
+        # if the variable-font's 'public.fontInfo' lib key does not override the
+        # openTypeOS2CodePageRanges, we should keep the original values as defined
+        # or computed for the default master TTF.
+        ulCodePageRange1 = testttf["OS/2"].ulCodePageRange1
+        ulCodePageRange2 = testttf["OS/2"].ulCodePageRange2
+        testufo.info.openTypeOS2CodePageRanges = None
+        info = {}
+        compiler = InfoCompiler(testttf, testufo, info)
+        ttf = compiler.compile()
+        assert ttf["OS/2"].ulCodePageRange1 == ulCodePageRange1
+        assert ttf["OS/2"].ulCodePageRange2 == ulCodePageRange2
 
     def test_post(self, testttf, testufo):
         info = {"italicAngle": 30.6}