Skip to content

Commit

Permalink
New check: tabular_kerning
Browse files Browse the repository at this point in the history
Check that tabular numerals and symbols have no kerning.

Added to the Universal Profile
EXPERIMENTAL - com.google.fonts/check/tabular_kerning

(issue fonttools#4440)
  • Loading branch information
yanone authored and felipesanches committed Jan 30, 2024
1 parent e4c9adf commit 8f607c4
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ A more detailed list of changes is available in the corresponding milestones for
### New checks
#### On the Universal Profile
- **EXPERIMENTAL - [com.google.fonts/check/varfont/family_axis_ranges]:** Check that family axis ranges are indentical. (issue #4445)

- **[com.google.fonts/check/tabular_kerning]:** Check that tabular numerals and symbols have no kerning

### Noteworthy code-changes
- The babelfont dependency has been dropped. (PR #4416)
- Fix a crash when no matching checks are found during a multi-processing run. Also, do not freeze indefinitely. Instead, terminate the program emitting a process error code -1 and giving the user some guidance. (issue #4420)
Expand Down
184 changes: 184 additions & 0 deletions Lib/fontbakery/profiles/universal.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"com.google.fonts/check/whitespace_widths",
"com.google.fonts/check/interpolation_issues",
"com.google.fonts/check/math_signs_width",
"com.google.fonts/check/tabular_kerning",
"com.google.fonts/check/linegaps",
"com.google.fonts/check/STAT_in_statics",
"com.google.fonts/check/alt_caron",
Expand Down Expand Up @@ -2212,6 +2213,189 @@ def com_google_fonts_check_math_signs_width(ttFont):
yield PASS, "Looks good."


@check(
id="com.google.fonts/check/tabular_kerning",
rationale="""
Tabular glyphs should not have kerning, as they are meant to be used in tables.
This check looks for kerning in:
- all glyphs in a font in combination with tabular numerals
- tabular symbols in combination with tabular numerals
"Tabular symbols" is defined as:
- for fonts with a "tnum" feature, all "tnum" substitution target glyphs
- for fonts without a "tnum" feature, all glyphs that have the same width
as the tabular numerals, but limited to numbers, math and currency symbols.
This check may produce false positives for fonts with no "tnum" feature
and with equal-width numerals (and other same-width symbols) that are
not intended to be used as tabular numerals.
""",
proposal="https://github.com/fonttools/fontbakery/issues/4440",
)
def com_google_fonts_check_tabular_kerning(ttFont):
"""Check tabular widths don't have kerning."""
from vharfbuzz import Vharfbuzz
import uharfbuzz as hb
import unicodedata

vhb = Vharfbuzz(ttFont.reader.file.name)

def glyph_width(ttFont, glyph_name):
return ttFont["hmtx"].metrics[glyph_name][0]

def glyph_name_for_character(ttFont, character):
if ttFont.getBestCmap().get(ord(character)):
return ttFont.getBestCmap().get(ord(character))

def unique_combinations(list_1, list_2):
unique_combinations = []

for i in range(len(list_1)):
for j in range(len(list_2)):
unique_combinations.append((list_1[i], list_2[j]))

return unique_combinations

def GID_for_glyph(ttFont, glyph_name):
return ttFont.getReverseGlyphMap()[glyph_name]

def unicode_for_glyph(ttFont, glyph_name):
for table in ttFont["cmap"].tables:
if table.isUnicode():
for codepoint, glyph in table.cmap.items():
if glyph == glyph_name:
return codepoint

def has_feature(ttFont, featureTag):
if "GSUB" in ttFont and ttFont["GSUB"].table.FeatureList:
for FeatureRecord in ttFont["GSUB"].table.FeatureList.FeatureRecord:
if FeatureRecord.FeatureTag == featureTag:
return True
if "GPOS" in ttFont and ttFont["GPOS"].table.FeatureList:
for FeatureRecord in ttFont["GPOS"].table.FeatureList.FeatureRecord:
if FeatureRecord.FeatureTag == featureTag:
return True
return False

def buf_to_width(buf):
x_cursor = 0

for _info, pos in zip(buf.glyph_infos, buf.glyph_positions):
x_cursor += pos.x_advance

return x_cursor

def get_substitutions(
ttFont,
featureTag,
lookupType=1,
):
substitutions = {}
if "GSUB" in ttFont:
for FeatureRecord in ttFont["GSUB"].table.FeatureList.FeatureRecord:
if FeatureRecord.FeatureTag == featureTag:
for LookupIndex in FeatureRecord.Feature.LookupListIndex:
for SubTable in (
ttFont["GSUB"].table.LookupList.Lookup[LookupIndex].SubTable
):
if SubTable.LookupType == lookupType:
substitutions.update(SubTable.mapping)
return substitutions

def nominal_glyph_func(font, code_point, data):
return code_point

def buffer_for_GIDs(GIDs, features):
buf = hb.Buffer()
buf.add_codepoints(GIDs)
buf.guess_segment_properties()
funcs = hb.FontFuncs.create()
funcs.set_nominal_glyph_func(nominal_glyph_func)
vhb.hbfont.funcs = funcs
hb.shape(vhb.hbfont, buf, features)
return buf

def kerning(ttFont, glyph_list):
GID_list = [GID_for_glyph(ttFont, glyph) for glyph in glyph_list]
return buf_to_width(
buffer_for_GIDs(
GID_list,
{"kern": True},
)
) - buf_to_width(
buffer_for_GIDs(
GID_list,
{"kern": False},
)
)

all_glyphs = list(ttFont.getGlyphOrder())
tabular_glyphs = []
tabular_numerals = []

# Fonts with tnum feautre
if has_feature(ttFont, "tnum"):
tabular_glyphs = list(get_substitutions(ttFont, "tnum").values())
buf = vhb.shape("0123456789", {"features": {"tnum": True}})
tabular_numerals = vhb.serialize_buf(buf, glyphsonly=True).split("|")

# Without tnum feature
else:
# Find out if font maybe has tabular numerals by default
glyph_widths = [
glyph_width(ttFont, glyph_name_for_character(ttFont, str(i)))
for i in range(10)
]
if len(set(glyph_widths)) == 1:
tabular_width = glyph_width(ttFont, glyph_name_for_character(ttFont, "0"))
tabular_numerals = [
glyph_name_for_character(ttFont, c) for c in "0123456789"
]
# Collect all glyphs with the same width as the tabular numerals
for glyph in all_glyphs:
unicode = unicode_for_glyph(ttFont, glyph)
if (
glyph_width(ttFont, glyph) == tabular_width
and unicode
and unicodedata.category(chr(unicode))
in (
"Nd", # Decimal number
"No", # Other number
"Nl", # Letter-like number
"Sm", # Math symbol
"Sc", # Currency symbol
)
):
tabular_glyphs.append(glyph)

passed = True

# Actually check for kerning
if tabular_numerals and has_feature(ttFont, "kern"):
for sets in (
(all_glyphs, tabular_numerals),
(tabular_numerals, tabular_glyphs),
):
combinations = unique_combinations(sets[0], sets[1])
for a, b in combinations:
if kerning(ttFont, [a, b]) != 0:
yield FAIL, Message(
"has-tabular-kerning",
f"Kerning between {a} and {b} is not zero",
)
passed = False
if kerning(ttFont, [b, a]) != 0:
yield FAIL, Message(
"has-tabular-kerning",
f"Kerning between {b} and {a} is not zero",
)
passed = False

if passed:
yield PASS, "No kerning found for tabular glyphs"


@check(
id="com.google.fonts/check/linegaps",
rationale="""
Expand Down
11 changes: 11 additions & 0 deletions tests/profiles/universal_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,6 +1332,17 @@ def test_check_math_signs_width():
assert_results_contain(check(font), WARN, "width-outliers")


def test_check_math_tabular_kerning():
"""Check tabular widths don't have kerning."""
check = CheckTester(universal_profile, "com.google.fonts/check/tabular_kerning")

font = TEST_FILE("montserrat/Montserrat-Regular.ttf")
assert_PASS(check(font))

font = TEST_FILE("hinting/Roboto-VF.ttf")
assert_results_contain(check(font), FAIL, "has-tabular-kerning")


def test_check_linegaps():
"""Checking Vertical Metric Linegaps."""
check = CheckTester(universal_profile, "com.google.fonts/check/linegaps")
Expand Down

0 comments on commit 8f607c4

Please sign in to comment.