|
| 1 | +import unicodedata |
| 2 | +from vharfbuzz import Vharfbuzz |
| 3 | + |
1 | 4 | from fontbakery.constants import ( |
2 | 5 | NameID, |
3 | 6 | PlatformID, |
4 | 7 | WindowsEncodingID, |
5 | 8 | WindowsLanguageID, |
6 | 9 | ) |
7 | | -from fontbakery.prelude import check, Message, FAIL, WARN, PASS |
| 10 | +from fontbakery.prelude import check, Message, FAIL, WARN, SKIP, PASS |
8 | 11 | from fontbakery.utils import bullet_list, glyph_has_ink |
9 | 12 |
|
10 | 13 |
|
|
20 | 23 | ) |
21 | 24 | def check_case_mapping(ttFont): |
22 | 25 | """Ensure the font supports case swapping for all its glyphs.""" |
23 | | - import unicodedata |
24 | 26 | from fontbakery.utils import markdown_table |
25 | 27 |
|
26 | 28 | # These are a selection of codepoints for which the corresponding case-swap |
@@ -222,51 +224,98 @@ def check_mandatory_glyphs(ttFont): |
222 | 224 | rationale=""" |
223 | 225 | Ensure small caps glyphs are available if |
224 | 226 | 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. |
225 | 231 | """, |
226 | 232 | proposal="https://github.com/fonttools/fontbakery/issues/3154", |
| 233 | + experimental="Since 2024/May/15", |
227 | 234 | ) |
228 | 235 | def check_missing_small_caps_glyphs(ttFont): |
229 | 236 | """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() |
230 | 248 |
|
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 | + ) |
261 | 312 |
|
262 | 313 |
|
263 | 314 | def can_shape(ttFont, text, parameters=None): |
264 | 315 | """ |
265 | 316 | Returns true if the font can render a text string without any |
266 | 317 | .notdef characters. |
267 | 318 | """ |
268 | | - from vharfbuzz import Vharfbuzz |
269 | | - |
270 | 319 | filename = ttFont.reader.file.name |
271 | 320 | vharfbuzz = Vharfbuzz(filename) |
272 | 321 | buf = vharfbuzz.shape(text, parameters) |
|
0 commit comments