Skip to content

Commit

Permalink
Update 'missing_small_caps_glyphs' check:
Browse files Browse the repository at this point in the history
On the Universal profile:
  - missing_small_caps_glyphs: Rewrote it from scratch, marked it as **experimental**.

(issue #4713)
  • Loading branch information
yanone authored and felipesanches committed Sep 9, 2024
1 parent c0a53a7 commit 0c1f34b
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 53 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ A more detailed list of changes is available in the corresponding milestones for
## Upcoming release: 0.13.0 (2024-Sep-??)
### Changes to existing checks
#### On the Universal profile
- **[case_mapping]:** Dynamically exclude incomplete Greek glyphs (PR #4721)
- **[missing_small_caps_glyphs]:** Rewrote it from scratch, marked it as **experimental** (issue #4713)
- **[name/family_and_style_max_length"]:** Use nameID 16 (Typographic family name) to determine name length if it exists. (PR #4811)

### Migration of checks
Expand Down
126 changes: 84 additions & 42 deletions Lib/fontbakery/checks/glyphset.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from vharfbuzz import Vharfbuzz
import unicodedata

from fontbakery.constants import (
NameID,
PlatformID,
WindowsEncodingID,
WindowsLanguageID,
)
from fontbakery.prelude import check, Message, FAIL, WARN, PASS
from fontbakery.prelude import check, Message, FAIL, WARN, SKIP, PASS
from fontbakery.utils import bullet_list, glyph_has_ink


Expand All @@ -20,7 +23,6 @@
)
def 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
Expand Down Expand Up @@ -186,7 +188,6 @@ def check_family_control_chars(ttFonts):
)
def check_mandatory_glyphs(ttFont):
"""Font contains '.notdef' as its first glyph?"""
passed = True
NOTDEF = ".notdef"
glyph_order = ttFont.getGlyphOrder()

Expand All @@ -198,14 +199,12 @@ def check_mandatory_glyphs(ttFont):
return

if glyph_order[0] != NOTDEF:
passed = False
yield WARN, Message(
"notdef-not-first", f"The {NOTDEF!r} should be the font's first glyph."
)

cmap = ttFont.getBestCmap() # e.g. {65: 'A', 66: 'B', 67: 'C'} or None
if cmap and NOTDEF in cmap.values():
passed = False
rev_cmap = {name: val for val, name in reversed(sorted(cmap.items()))}
yield WARN, Message(
"notdef-has-codepoint",
Expand All @@ -214,66 +213,109 @@ def check_mandatory_glyphs(ttFont):
)

if not glyph_has_ink(ttFont, NOTDEF):
passed = False
yield FAIL, Message(
"notdef-is-blank",
f"The {NOTDEF!r} glyph should contain a drawing, but it is blank.",
)

if passed:
yield PASS, "OK"


@check(
id="missing_small_caps_glyphs",
rationale="""
Ensure small caps glyphs must be available if
Ensure small caps glyphs are available if
a font declares smcp or c2sc OT features.
If you believe that a certain character should not
be reported as missing, please add it to the
`exceptions_smcp` or `exceptions_c2sc` lists.
""",
proposal="https://github.com/fonttools/fontbakery/issues/3154",
experimental="Since 2024/May/15",
)
def check_missing_small_caps_glyphs(ttFont):
"""Ensure small caps glyphs are available."""
from fontbakery.utils import has_feature, characters_per_script

has_smcp = has_feature(ttFont, "smcp")
has_c2sc = has_feature(ttFont, "c2sc")

if not has_smcp and not has_c2sc:
yield SKIP, "Neither smcp nor c2sc features are declared in the font."
return

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

missing_smcp = []
missing_c2sc = []

exceptions_smcp = [
0x0192, # florin
0x00B5, # micro (common, not Greek)
0x2113, # liter sign
0xA78C, # saltillo
0x1FBE, # Greek prosgegrammeni
]
exceptions_c2sc = [
0xA78B, # Saltillo
0x2126, # Ohm (not Omega)
]

# Font has incomplete legacy Greek coverage, so ignore Greek dynamically
# (minimal Greek coverage is 2x24=48 characters, so we assume incomplete
# if coverage is less than half of 48)
if 0 < len(characters_per_script(ttFont, "Greek")) < 24:
exceptions_smcp.extend(characters_per_script(ttFont, "Greek", "Ll"))
exceptions_c2sc.extend(characters_per_script(ttFont, "Greek", "Lu"))

if "GSUB" in ttFont and ttFont["GSUB"].table.FeatureList is not None:
llist = ttFont["GSUB"].table.LookupList
for record in range(ttFont["GSUB"].table.FeatureList.FeatureCount):
feature = ttFont["GSUB"].table.FeatureList.FeatureRecord[record]
tag = feature.FeatureTag
if tag in ["smcp", "c2sc"]:
for index in feature.Feature.LookupListIndex:
subtable = llist.Lookup[index].SubTable[0]
if subtable.LookupType == 7:
# This is an Extension lookup
# used for reaching 32-bit offsets
# within the GSUB table.
subtable = subtable.ExtSubTable
if not hasattr(subtable, "mapping"):
continue
smcp_glyphs = set()
for value in subtable.mapping.values():
if isinstance(value, list):
for v in value:
smcp_glyphs.add(v)
else:
smcp_glyphs.add(value)
missing = smcp_glyphs - set(ttFont.getGlyphNames())
if missing:
missing = "\n\t - " + "\n\t - ".join(missing)
yield FAIL, Message(
"missing-glyphs",
f"These '{tag}' glyphs are missing:\n\n{missing}",
)
break
for codepoint in cmap:
char = chr(codepoint)

if (
has_smcp
and unicodedata.category(char) == "Ll"
and codepoint not in exceptions_smcp
):
if vhb.serialize_buf(vhb.shape(char)) == vhb.serialize_buf(
vhb.shape(char, {"features": {"smcp": True}})
):
missing_smcp.append(char)
if (
has_c2sc
and unicodedata.category(char) == "Lu"
and codepoint not in exceptions_c2sc
):
if vhb.serialize_buf(vhb.shape(char)) == vhb.serialize_buf(
vhb.shape(char, {"features": {"c2sc": True}})
):
missing_c2sc.append(char)

if missing_smcp:
missing_smcp = "\n\t - " + "\n\t - ".join(
[f"U+{ord(x):04X}: {unicodedata.name(x)}" for x in missing_smcp]
)
yield FAIL, Message(
"missing-smcp",
"'smcp' substitution target glyphs for these"
f" characters are missing:\n\n{missing_smcp}",
)

if missing_c2sc:
missing_c2sc = "\n\t - " + "\n\t - ".join(
[f"U+{ord(x):04X}: {unicodedata.name(x)}" for x in missing_c2sc]
)
yield FAIL, Message(
"missing-c2sc",
"'c2sc' substitution target glyphs for these"
f" characters are missing:\n\n{missing_c2sc}",
)


def can_shape(ttFont, text, parameters=None):
"""
Returns true if the font can render a text string without any
.notdef characters.
"""
from vharfbuzz import Vharfbuzz

filename = ttFont.reader.file.name
vharfbuzz = Vharfbuzz(filename)
buf = vharfbuzz.shape(text, parameters)
Expand Down
12 changes: 1 addition & 11 deletions Lib/fontbakery/checks/tabular_glyphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def check_tabular_kerning(ttFont):
from vharfbuzz import Vharfbuzz
import uharfbuzz as hb
import unicodedata
from fontbakery.utils import has_feature

EXCLUDE = [
"\u0600", # Arabic
Expand Down Expand Up @@ -194,17 +195,6 @@ def unique_combinations(list_1, list_2):

return unique_combinations

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

Expand Down
27 changes: 27 additions & 0 deletions Lib/fontbakery/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,3 +726,30 @@ def image_dimensions(filename):

else:
return None # some other file format


def has_feature(ttFont, featureTag):
"""Return whether a font has a certain OpenType feature"""
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 characters_per_script(ttFont, target_script, target_category=None):
"""Return the number of characters in a font for a given script"""
from unicodedataplus import script, category

characters = []
for codepoint in ttFont.getBestCmap().keys():
if script(chr(codepoint)) == target_script and (
not target_category or category(chr(codepoint)) == target_category
):
characters.append(codepoint)

return characters
Binary file not shown.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies = [
"ufolint",
"ufo2ft >= 2.25.2", # script lists for Unicode 14.0 were updated on v2.25.2
"uharfbuzz",
"unicodedataplus",
"vharfbuzz >= 0.2.0",
]

Expand Down
14 changes: 14 additions & 0 deletions tests/test_checks_universal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1423,3 +1423,17 @@ def test_check_gsub_smallcaps_before_ligatures():
smcp_feature.LookupListIndex = [1]
liga_feature.LookupListIndex = [0]
assert_results_contain(check(ttFont), FAIL, "feature-ordering")


def test_check_missing_small_caps_glyphs():
"""Check small caps glyphs are available."""
check = CheckTester("missing_small_caps_glyphs")

ttFont = TTFont(TEST_FILE("cormorantunicase/CormorantUnicase-Bold.ttf"))
assert_PASS(check(ttFont))

ttFont = TTFont(TEST_FILE("varfont/Georama[wdth,wght].ttf"))
assert_results_contain(check(ttFont), FAIL, "missing-smcp")

ttFont = TTFont(TEST_FILE("ubuntusans/UbuntuSans[wdth,wght].ttf"))
assert_results_contain(check(ttFont), FAIL, "missing-c2sc")

0 comments on commit 0c1f34b

Please sign in to comment.