From f7031959376a7e5a42486a321b44ba01ea46935f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Corr=C3=AAa=20da=20Silva=20Sanches?= Date: Thu, 18 Jan 2024 18:00:59 -0300 Subject: [PATCH] new check: ensure glyph case mapping Ensure that no glyph lacks its corresponding upper or lower counterpart (but only when unicode supports case-mapping). com.google.fonts/check/case_mapping (EXPERIMENTAL) Added to the Universal profile. (issue #3230) --- CHANGELOG.md | 1 + Lib/fontbakery/profiles/universal.py | 79 ++++++++++++++++++++++++++++ tests/profiles/universal_test.py | 21 ++++++++ 3 files changed, 101 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a8fb96ade..74bf1e2671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ A more detailed list of changes is available in the corresponding milestones for #### Added to the Universal Profile - **EXPERIMENTAL - [com.google.fonts/check/varfont/family_axis_ranges]:** Check that family axis ranges are indentical. (issue #4445) - **EXPERIMENTAL - [com.google.fonts/check/tabular_kerning]:** Check that tabular numerals and symbols have no kerning. (issue #4440) + - **EXPERIMENTAL - [com.google.fonts/check/case_mapping]:** Ensure that no glyph lacks its corresponding upper or lower counterpart (but only when unicode supports case-mapping). (issue #3230) #### Added to the Google Fonts Profile - **EXPERIMENTAL - [com.google.fonts/check/metadata/has_tags]:** Check that the font family appears in the tags spreadsheet. (issue #4465) diff --git a/Lib/fontbakery/profiles/universal.py b/Lib/fontbakery/profiles/universal.py index a20542e401..d4370852e8 100644 --- a/Lib/fontbakery/profiles/universal.py +++ b/Lib/fontbakery/profiles/universal.py @@ -73,6 +73,7 @@ "com.google.fonts/check/alt_caron", "com.google.fonts/check/arabic_spacing_symbols", "com.google.fonts/check/arabic_high_hamza", + "com.google.fonts/check/case_mapping", ] ) @@ -2585,5 +2586,83 @@ def com_google_fonts_check_alt_caron(ttFont): yield PASS, "Looks good!" +@check( + id="com.google.fonts/check/case_mapping", + rationale=""" + Ensure that no glyph lacks its corresponding upper or lower counterpart + (but only when unicode supports case-mapping). + """, + proposal="https://github.com/googlefonts/fontbakery/issues/3230", + experimental="Since 2024/Jan/19", + severity=10, # if a font shows tofu in caps but not in lowercase + # then it can be considered broken. +) +def com_google_fonts_check_case_mapping(ttFont): + """Ensure the font supports case swapping for all its glyphs.""" + import unicodedata + from fontbakery.utils import markdown_table + + # These are a selection of codepoints for which the corresponding case-swap + # glyphs are missing way too often on the Google Fonts library, + # so we'll ignore for now: + EXCEPTIONS = [ + 0x0192, # ƒ - Latin Small Letter F with Hook + 0x00B5, # µ - Micro Sign + 0x03C0, # π - Greek Small Letter Pi + 0x2126, # Ω - Ohm Sign + 0x03BC, # μ - Greek Small Letter Mu + 0x03A9, # Ω - Greek Capital Letter Omega + 0x0394, # Δ - Greek Capital Letter Delta + 0x0251, # ɑ - Latin Small Letter Alpha + 0x0261, # ɡ - Latin Small Letter Script G + 0x00FF, # ÿ - Latin Small Letter Y with Diaeresis + 0x0250, # ɐ - Latin Small Letter Turned A + 0x025C, # ɜ - Latin Small Letter Reversed Open E + 0x0252, # ɒ - Latin Small Letter Turned Alpha + 0x0271, # ɱ - Latin Small Letter M with Hook + 0x0282, # ʂ - Latin Small Letter S with Hook + 0x029E, # ʞ - Latin Small Letter Turned K + 0x0287, # ʇ - Latin Small Letter Turned T + 0x0127, # ħ - Latin Small Letter H with Stroke + 0x0140, # ŀ - Latin Small Letter L with Middle Dot + 0x023F, # ȿ - Latin Small Letter S with Swash Tail + 0x0240, # ɀ - Latin Small Letter Z with Swash Tail + 0x026B, # ɫ - Latin Small Letter L with Middle Tilde + ] + + missing_counterparts_table = [] + cmap = ttFont["cmap"].getBestCmap() + for codepoint in cmap: + if codepoint in EXCEPTIONS: + continue + + the_char = chr(codepoint) + swapped = the_char.swapcase() + + # skip cases like 'ß' => 'SS' + if len(swapped) > 1: + continue + + if the_char != swapped and ord(swapped) not in cmap: + name = unicodedata.name(the_char) + swapped_name = unicodedata.name(swapped) + row = { + "Glyph present in the font": f"U+{codepoint:04X}: {name}", + "Missing case-swapping counterpart": ( + f"U+{ord(swapped):04X}: {swapped_name}" + ), + } + missing_counterparts_table.append(row) + + if missing_counterparts_table: + yield FAIL, Message( + "missing-case-counterparts", + f"The following glyphs lack their case-swapping counterparts:\n\n" + f"{markdown_table(missing_counterparts_table)}\n\n", + ) + else: + yield PASS, "Looks good!" + + profile.auto_register(globals()) profile.test_expected_checks(UNIVERSAL_PROFILE_CHECKS, exclusive=True) diff --git a/tests/profiles/universal_test.py b/tests/profiles/universal_test.py index 9a8d45a345..46a916069e 100644 --- a/tests/profiles/universal_test.py +++ b/tests/profiles/universal_test.py @@ -1433,3 +1433,24 @@ def DISABLED_test_check_caps_vertically_centered(): ttFont = TTFont(TEST_FILE("cairo/CairoPlay-Italic.leftslanted.ttf")) assert_results_contain(check(ttFont), WARN, "vertical-metrics-not-centered") + + +def test_check_case_mapping(): + """Ensure the font supports case swapping for all its glyphs.""" + check = CheckTester(universal_profile, "com.google.fonts/check/case_mapping") + + ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf")) + # Glyph present in the font Missing case-swapping counterpart + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # U+01D3: LATIN CAPITAL LETTER U WITH CARON U+01D4: LATIN SMALL LETTER U WITH CARON + # U+01E6: LATIN CAPITAL LETTER G WITH CARON U+01E7: LATIN SMALL LETTER G WITH CARON + # U+01F4: LATIN CAPITAL LETTER G WITH ACUTE U+01F5: LATIN SMALL LETTER G WITH ACUTE + assert_results_contain(check(ttFont), FAIL, "missing-case-counterparts") + + # While we'd expect designers to draw the missing counterparts, + # for testing purposes we can simply delete the glyphs that lack a counterpart + # to make the check PASS: + _remove_cmap_entry(ttFont, 0x01D3) + _remove_cmap_entry(ttFont, 0x01E6) + _remove_cmap_entry(ttFont, 0x01F4) + assert_PASS(check(ttFont))