Skip to content

Commit

Permalink
new check: ensure glyph case mapping
Browse files Browse the repository at this point in the history
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 fonttools#3230)
  • Loading branch information
felipesanches committed Feb 1, 2024
1 parent 5dd9ce1 commit f703195
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
79 changes: 79 additions & 0 deletions Lib/fontbakery/profiles/universal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
)

Expand Down Expand Up @@ -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)
21 changes: 21 additions & 0 deletions tests/profiles/universal_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

0 comments on commit f703195

Please sign in to comment.