Skip to content

Commit b54cfe9

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 ba5427c commit b54cfe9

File tree

7 files changed

+127
-45
lines changed

7 files changed

+127
-45
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ 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+
- **[missing_small_caps_glyphs]:** Rewrote it from scratch, marked it as **experimental** (issue #4713)
78
- **[name/family_and_style_max_length"]:** Use nameID 16 (Typographic family name) to determine name length if it exists. (PR #4811)
89

910
### Migration of checks

Lib/fontbakery/checks/glyphset.py

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import unicodedata
2+
from vharfbuzz import Vharfbuzz
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
@@ -222,51 +224,98 @@ def check_mandatory_glyphs(ttFont):
222224
rationale="""
223225
Ensure small caps glyphs are available if
224226
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.
225231
""",
226232
proposal="https://github.com/fonttools/fontbakery/issues/3154",
233+
experimental="Since 2024/May/15",
227234
)
228235
def check_missing_small_caps_glyphs(ttFont):
229236
"""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()
230248

231-
if "GSUB" in ttFont and ttFont["GSUB"].table.FeatureList is not None:
232-
llist = ttFont["GSUB"].table.LookupList
233-
for record in range(ttFont["GSUB"].table.FeatureList.FeatureCount):
234-
feature = ttFont["GSUB"].table.FeatureList.FeatureRecord[record]
235-
tag = feature.FeatureTag
236-
if tag in ["smcp", "c2sc"]:
237-
for index in feature.Feature.LookupListIndex:
238-
subtable = llist.Lookup[index].SubTable[0]
239-
if subtable.LookupType == 7:
240-
# This is an Extension lookup
241-
# used for reaching 32-bit offsets
242-
# within the GSUB table.
243-
subtable = subtable.ExtSubTable
244-
if not hasattr(subtable, "mapping"):
245-
continue
246-
smcp_glyphs = set()
247-
for value in subtable.mapping.values():
248-
if isinstance(value, list):
249-
for v in value:
250-
smcp_glyphs.add(v)
251-
else:
252-
smcp_glyphs.add(value)
253-
missing = smcp_glyphs - set(ttFont.getGlyphNames())
254-
if missing:
255-
missing = "\n\t - " + "\n\t - ".join(missing)
256-
yield FAIL, Message(
257-
"missing-glyphs",
258-
f"These '{tag}' glyphs are missing:\n\n{missing}",
259-
)
260-
break
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"))
270+
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+
)
261312

262313

263314
def can_shape(ttFont, text, parameters=None):
264315
"""
265316
Returns true if the font can render a text string without any
266317
.notdef characters.
267318
"""
268-
from vharfbuzz import Vharfbuzz
269-
270319
filename = ttFont.reader.file.name
271320
vharfbuzz = Vharfbuzz(filename)
272321
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 >= 15.0.0",
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)