diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fdb79be73..963111aad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ A more detailed list of changes is available in the corresponding milestones for - to implement a backwards compatibility mechanism ### New checks +#### Added to the Universal profile + - **EXPERIMENTAL - [base_has_width]:** Check base characters have non-zero advance width. (issue #4906) + #### Added to the OpenType profile - **[STAT/ital_axis]**: Replaces the old checks (**opentype/italic_axis_in_stat**, **opentype/italic_axis_in_stat_is_boolean** and **opentype/italic_axis_last**) from the same profile (issue #4865) diff --git a/Lib/fontbakery/checks/base_has_width.py b/Lib/fontbakery/checks/base_has_width.py new file mode 100644 index 0000000000..4922814cba --- /dev/null +++ b/Lib/fontbakery/checks/base_has_width.py @@ -0,0 +1,50 @@ +import unicodedata + +from fontbakery.prelude import check, Message, FAIL +from fontbakery.utils import bullet_list, mark_glyphs + + +def is_space(codepoint): + return unicodedata.category(chr(codepoint)) in [ + "Zs", # Space Separator + "Zl", # Line Separator + "Zp", # Paragraph Separator + "Cf", # Format + "Mn", # Nonspacing Mark + "Cc", # Control + ] + + +@check( + id="base_has_width", + rationale=""" + Base characters should have non-zero advance width. + """, + proposal="https://github.com/fonttools/fontbakery/issues/4906", + experimental="Since 2024/12/28", +) +def check_base_has_width(font, context): + """Check base characters have non-zero advance width.""" + + reversed_cmap = {v: k for k, v in font.ttFont.getBestCmap().items()} + + problems = [] + for gid, metric in font.ttFont["hmtx"].metrics.items(): + advance = metric[0] + + codepoint = reversed_cmap.get(gid) + if codepoint == 0 or codepoint is None: + continue + + if advance == 0 and not gid not in mark_glyphs(font.ttFont): + if is_space(codepoint): + continue + + problems.append(f"{gid} (U+{codepoint:04X})") + + if problems: + problems = bullet_list(context, problems) + yield FAIL, Message( + "zero-width-bases", + f"The following glyphs had zero advance width:\n{problems}", + ) diff --git a/Lib/fontbakery/checks/tabular_kerning.py b/Lib/fontbakery/checks/tabular_kerning.py index b398d0e91f..1955291810 100644 --- a/Lib/fontbakery/checks/tabular_kerning.py +++ b/Lib/fontbakery/checks/tabular_kerning.py @@ -1,4 +1,5 @@ from fontbakery.prelude import check, Message, FAIL, SKIP +from fontbakery.utils import mark_glyphs @check( @@ -159,16 +160,6 @@ def digraph_kerning(ttFont, glyph_list, expected_kerning): and get_kerning(glyph_list) == expected_kerning ) - def mark_glyphs(ttFont): - marks = [] - if "GDEF" in ttFont and ttFont["GDEF"].table.GlyphClassDef: - class_def = ttFont["GDEF"].table.GlyphClassDef.classDefs - glyphOrder = ttFont.getGlyphOrder() - for name in glyphOrder: - if name in class_def and class_def[name] == 3: - marks.append(name) - return marks - # Font has no numerals at all if not all([glyph_name_for_character(ttFont, c) for c in "0123456789"]): yield SKIP, "Font has no numerals at all" diff --git a/Lib/fontbakery/profiles/adobefonts.py b/Lib/fontbakery/profiles/adobefonts.py index 087fe059c4..c7060268b6 100644 --- a/Lib/fontbakery/profiles/adobefonts.py +++ b/Lib/fontbakery/profiles/adobefonts.py @@ -43,6 +43,7 @@ "alt_caron", "arabic_high_hamza", "arabic_spacing_symbols", + "base_has_width", "case_mapping", "cjk_chws_feature", # was temporarily removed "cjk_not_enough_glyphs", diff --git a/Lib/fontbakery/profiles/fontbureau.py b/Lib/fontbakery/profiles/fontbureau.py index 9a483d4a24..bb095e3367 100644 --- a/Lib/fontbakery/profiles/fontbureau.py +++ b/Lib/fontbakery/profiles/fontbureau.py @@ -4,6 +4,7 @@ "pending_review": [ "opentype/weight_class_fvar", "opentype/slant_direction", + "base_has_width", "cjk_not_enough_glyphs", "cmap/format_12", "color_cpal_brightness", diff --git a/Lib/fontbakery/profiles/fontwerk.py b/Lib/fontbakery/profiles/fontwerk.py index 7eec857ffb..f4658b9129 100644 --- a/Lib/fontbakery/profiles/fontwerk.py +++ b/Lib/fontbakery/profiles/fontwerk.py @@ -18,6 +18,7 @@ ], "pending_review": [ "epar", + "base_has_width", "googlefonts/axes_match", "overlapping_path_segments", "typographic_family_name", diff --git a/Lib/fontbakery/profiles/microsoft.py b/Lib/fontbakery/profiles/microsoft.py index 9bbcce6410..3254e3bc8c 100644 --- a/Lib/fontbakery/profiles/microsoft.py +++ b/Lib/fontbakery/profiles/microsoft.py @@ -11,6 +11,7 @@ "opentype/slant_direction", "opentype/weight_class_fvar", # + "base_has_width", "cjk_not_enough_glyphs", "cmap/format_12", "color_cpal_brightness", diff --git a/Lib/fontbakery/profiles/typenetwork.py b/Lib/fontbakery/profiles/typenetwork.py index e275dc2468..cce2ac6212 100644 --- a/Lib/fontbakery/profiles/typenetwork.py +++ b/Lib/fontbakery/profiles/typenetwork.py @@ -41,6 +41,7 @@ ], "pending_review": [ "epar", + "base_has_width", ], "sections": { "Type Network": [ diff --git a/Lib/fontbakery/profiles/universal.py b/Lib/fontbakery/profiles/universal.py index f0ff231140..21d0f11d14 100644 --- a/Lib/fontbakery/profiles/universal.py +++ b/Lib/fontbakery/profiles/universal.py @@ -24,6 +24,7 @@ "alt_caron", "arabic_high_hamza", "arabic_spacing_symbols", + "base_has_width", "caps_vertically_centered", "case_mapping", "cjk_chws_feature", diff --git a/Lib/fontbakery/utils.py b/Lib/fontbakery/utils.py index ce40fc535b..e93252e932 100644 --- a/Lib/fontbakery/utils.py +++ b/Lib/fontbakery/utils.py @@ -849,3 +849,14 @@ def close_but_not_on(value_expected, value_true, tolerance): if abs(value_expected - value_true) <= tolerance: return True return False + + +def mark_glyphs(ttFont): + marks = [] + if "GDEF" in ttFont and ttFont["GDEF"].table.GlyphClassDef: + class_def = ttFont["GDEF"].table.GlyphClassDef.classDefs + glyphOrder = ttFont.getGlyphOrder() + for name in glyphOrder: + if name in class_def and class_def[name] == 3: + marks.append(name) + return marks