1+ from vharfbuzz import Vharfbuzz
2+ import unicodedata
3+
14from 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
811from fontbakery .utils import bullet_list , glyph_has_ink
912
1013
2023)
2124def 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)
187189def 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)
235235def 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
270314def 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 )
0 commit comments