Skip to content

Commit 0c1f34b

Browse files
yanonefelipesanches
authored andcommitted
Update 'missing_small_caps_glyphs' check:
On the Universal profile: - missing_small_caps_glyphs: Rewrote it from scratch, marked it as **experimental**. (issue #4713)
1 parent c0a53a7 commit 0c1f34b

File tree

7 files changed

+129
-53
lines changed

7 files changed

+129
-53
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ A more detailed list of changes is available in the corresponding milestones for
44
## Upcoming release: 0.13.0 (2024-Sep-??)
55
### Changes to existing checks
66
#### On the Universal profile
7+
- **[case_mapping]:** Dynamically exclude incomplete Greek glyphs (PR #4721)
8+
- **[missing_small_caps_glyphs]:** Rewrote it from scratch, marked it as **experimental** (issue #4713)
79
- **[name/family_and_style_max_length"]:** Use nameID 16 (Typographic family name) to determine name length if it exists. (PR #4811)
810

911
### Migration of checks

Lib/fontbakery/checks/glyphset.py

Lines changed: 84 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from vharfbuzz import Vharfbuzz
2+
import unicodedata
3+
14
from fontbakery.constants import (
25
NameID,
36
PlatformID,
47
WindowsEncodingID,
58
WindowsLanguageID,
69
)
7-
from fontbakery.prelude import check, Message, FAIL, WARN, PASS
10+
from fontbakery.prelude import check, Message, FAIL, WARN, SKIP, PASS
811
from fontbakery.utils import bullet_list, glyph_has_ink
912

1013

@@ -20,7 +23,6 @@
2023
)
2124
def check_case_mapping(ttFont):
2225
"""Ensure the font supports case swapping for all its glyphs."""
23-
import unicodedata
2426
from fontbakery.utils import markdown_table
2527

2628
# These are a selection of codepoints for which the corresponding case-swap
@@ -186,7 +188,6 @@ def check_family_control_chars(ttFonts):
186188
)
187189
def check_mandatory_glyphs(ttFont):
188190
"""Font contains '.notdef' as its first glyph?"""
189-
passed = True
190191
NOTDEF = ".notdef"
191192
glyph_order = ttFont.getGlyphOrder()
192193

@@ -198,14 +199,12 @@ def check_mandatory_glyphs(ttFont):
198199
return
199200

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

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

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

223-
if passed:
224-
yield PASS, "OK"
225-
226221

227222
@check(
228223
id="missing_small_caps_glyphs",
229224
rationale="""
230-
Ensure small caps glyphs must be available if
225+
Ensure small caps glyphs are available if
231226
a font declares smcp or c2sc OT features.
227+
228+
If you believe that a certain character should not
229+
be reported as missing, please add it to the
230+
`exceptions_smcp` or `exceptions_c2sc` lists.
232231
""",
233232
proposal="https://github.com/fonttools/fontbakery/issues/3154",
233+
experimental="Since 2024/May/15",
234234
)
235235
def check_missing_small_caps_glyphs(ttFont):
236236
"""Ensure small caps glyphs are available."""
237+
from fontbakery.utils import has_feature, characters_per_script
238+
239+
has_smcp = has_feature(ttFont, "smcp")
240+
has_c2sc = has_feature(ttFont, "c2sc")
241+
242+
if not has_smcp and not has_c2sc:
243+
yield SKIP, "Neither smcp nor c2sc features are declared in the font."
244+
return
245+
246+
vhb = Vharfbuzz(ttFont.reader.file.name)
247+
cmap = ttFont.getBestCmap()
248+
249+
missing_smcp = []
250+
missing_c2sc = []
251+
252+
exceptions_smcp = [
253+
0x0192, # florin
254+
0x00B5, # micro (common, not Greek)
255+
0x2113, # liter sign
256+
0xA78C, # saltillo
257+
0x1FBE, # Greek prosgegrammeni
258+
]
259+
exceptions_c2sc = [
260+
0xA78B, # Saltillo
261+
0x2126, # Ohm (not Omega)
262+
]
263+
264+
# Font has incomplete legacy Greek coverage, so ignore Greek dynamically
265+
# (minimal Greek coverage is 2x24=48 characters, so we assume incomplete
266+
# if coverage is less than half of 48)
267+
if 0 < len(characters_per_script(ttFont, "Greek")) < 24:
268+
exceptions_smcp.extend(characters_per_script(ttFont, "Greek", "Ll"))
269+
exceptions_c2sc.extend(characters_per_script(ttFont, "Greek", "Lu"))
237270

238-
if "GSUB" in ttFont and ttFont["GSUB"].table.FeatureList is not None:
239-
llist = ttFont["GSUB"].table.LookupList
240-
for record in range(ttFont["GSUB"].table.FeatureList.FeatureCount):
241-
feature = ttFont["GSUB"].table.FeatureList.FeatureRecord[record]
242-
tag = feature.FeatureTag
243-
if tag in ["smcp", "c2sc"]:
244-
for index in feature.Feature.LookupListIndex:
245-
subtable = llist.Lookup[index].SubTable[0]
246-
if subtable.LookupType == 7:
247-
# This is an Extension lookup
248-
# used for reaching 32-bit offsets
249-
# within the GSUB table.
250-
subtable = subtable.ExtSubTable
251-
if not hasattr(subtable, "mapping"):
252-
continue
253-
smcp_glyphs = set()
254-
for value in subtable.mapping.values():
255-
if isinstance(value, list):
256-
for v in value:
257-
smcp_glyphs.add(v)
258-
else:
259-
smcp_glyphs.add(value)
260-
missing = smcp_glyphs - set(ttFont.getGlyphNames())
261-
if missing:
262-
missing = "\n\t - " + "\n\t - ".join(missing)
263-
yield FAIL, Message(
264-
"missing-glyphs",
265-
f"These '{tag}' glyphs are missing:\n\n{missing}",
266-
)
267-
break
271+
for codepoint in cmap:
272+
char = chr(codepoint)
273+
274+
if (
275+
has_smcp
276+
and unicodedata.category(char) == "Ll"
277+
and codepoint not in exceptions_smcp
278+
):
279+
if vhb.serialize_buf(vhb.shape(char)) == vhb.serialize_buf(
280+
vhb.shape(char, {"features": {"smcp": True}})
281+
):
282+
missing_smcp.append(char)
283+
if (
284+
has_c2sc
285+
and unicodedata.category(char) == "Lu"
286+
and codepoint not in exceptions_c2sc
287+
):
288+
if vhb.serialize_buf(vhb.shape(char)) == vhb.serialize_buf(
289+
vhb.shape(char, {"features": {"c2sc": True}})
290+
):
291+
missing_c2sc.append(char)
292+
293+
if missing_smcp:
294+
missing_smcp = "\n\t - " + "\n\t - ".join(
295+
[f"U+{ord(x):04X}: {unicodedata.name(x)}" for x in missing_smcp]
296+
)
297+
yield FAIL, Message(
298+
"missing-smcp",
299+
"'smcp' substitution target glyphs for these"
300+
f" characters are missing:\n\n{missing_smcp}",
301+
)
302+
303+
if missing_c2sc:
304+
missing_c2sc = "\n\t - " + "\n\t - ".join(
305+
[f"U+{ord(x):04X}: {unicodedata.name(x)}" for x in missing_c2sc]
306+
)
307+
yield FAIL, Message(
308+
"missing-c2sc",
309+
"'c2sc' substitution target glyphs for these"
310+
f" characters are missing:\n\n{missing_c2sc}",
311+
)
268312

269313

270314
def can_shape(ttFont, text, parameters=None):
271315
"""
272316
Returns true if the font can render a text string without any
273317
.notdef characters.
274318
"""
275-
from vharfbuzz import Vharfbuzz
276-
277319
filename = ttFont.reader.file.name
278320
vharfbuzz = Vharfbuzz(filename)
279321
buf = vharfbuzz.shape(text, parameters)

Lib/fontbakery/checks/tabular_glyphs.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def check_tabular_kerning(ttFont):
145145
from vharfbuzz import Vharfbuzz
146146
import uharfbuzz as hb
147147
import unicodedata
148+
from fontbakery.utils import has_feature
148149

149150
EXCLUDE = [
150151
"\u0600", # Arabic
@@ -194,17 +195,6 @@ def unique_combinations(list_1, list_2):
194195

195196
return unique_combinations
196197

197-
def has_feature(ttFont, featureTag):
198-
if "GSUB" in ttFont and ttFont["GSUB"].table.FeatureList:
199-
for FeatureRecord in ttFont["GSUB"].table.FeatureList.FeatureRecord:
200-
if FeatureRecord.FeatureTag == featureTag:
201-
return True
202-
if "GPOS" in ttFont and ttFont["GPOS"].table.FeatureList:
203-
for FeatureRecord in ttFont["GPOS"].table.FeatureList.FeatureRecord:
204-
if FeatureRecord.FeatureTag == featureTag:
205-
return True
206-
return False
207-
208198
def buf_to_width(buf):
209199
x_cursor = 0
210200

Lib/fontbakery/utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,3 +726,30 @@ def image_dimensions(filename):
726726

727727
else:
728728
return None # some other file format
729+
730+
731+
def has_feature(ttFont, featureTag):
732+
"""Return whether a font has a certain OpenType feature"""
733+
if "GSUB" in ttFont and ttFont["GSUB"].table.FeatureList:
734+
for FeatureRecord in ttFont["GSUB"].table.FeatureList.FeatureRecord:
735+
if FeatureRecord.FeatureTag == featureTag:
736+
return True
737+
if "GPOS" in ttFont and ttFont["GPOS"].table.FeatureList:
738+
for FeatureRecord in ttFont["GPOS"].table.FeatureList.FeatureRecord:
739+
if FeatureRecord.FeatureTag == featureTag:
740+
return True
741+
return False
742+
743+
744+
def characters_per_script(ttFont, target_script, target_category=None):
745+
"""Return the number of characters in a font for a given script"""
746+
from unicodedataplus import script, category
747+
748+
characters = []
749+
for codepoint in ttFont.getBestCmap().keys():
750+
if script(chr(codepoint)) == target_script and (
751+
not target_category or category(chr(codepoint)) == target_category
752+
):
753+
characters.append(codepoint)
754+
755+
return characters
745 KB
Binary file not shown.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dependencies = [
4848
"ufolint",
4949
"ufo2ft >= 2.25.2", # script lists for Unicode 14.0 were updated on v2.25.2
5050
"uharfbuzz",
51+
"unicodedataplus",
5152
"vharfbuzz >= 0.2.0",
5253
]
5354

tests/test_checks_universal.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,3 +1423,17 @@ def test_check_gsub_smallcaps_before_ligatures():
14231423
smcp_feature.LookupListIndex = [1]
14241424
liga_feature.LookupListIndex = [0]
14251425
assert_results_contain(check(ttFont), FAIL, "feature-ordering")
1426+
1427+
1428+
def test_check_missing_small_caps_glyphs():
1429+
"""Check small caps glyphs are available."""
1430+
check = CheckTester("missing_small_caps_glyphs")
1431+
1432+
ttFont = TTFont(TEST_FILE("cormorantunicase/CormorantUnicase-Bold.ttf"))
1433+
assert_PASS(check(ttFont))
1434+
1435+
ttFont = TTFont(TEST_FILE("varfont/Georama[wdth,wght].ttf"))
1436+
assert_results_contain(check(ttFont), FAIL, "missing-smcp")
1437+
1438+
ttFont = TTFont(TEST_FILE("ubuntusans/UbuntuSans[wdth,wght].ttf"))
1439+
assert_results_contain(check(ttFont), FAIL, "missing-c2sc")

0 commit comments

Comments
 (0)