test_general.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. from fontbakery.callable import check
  2. from fontbakery.callable import condition
  3. from fontbakery.checkrunner import Section, PASS, FAIL, WARN
  4. from fontbakery.fonts_profile import profile_factory
  5. from fontbakery.profiles.universal import UNIVERSAL_PROFILE_CHECKS
  6. from fontbakery.profiles.googlefonts_conditions import (
  7. style,
  8. expected_style,
  9. familyname_with_spaces,
  10. familyname,
  11. )
  12. from fontbakery.profiles.googlefonts import (
  13. com_google_fonts_check_usweightclass,
  14. com_google_fonts_check_fsselection,
  15. com_google_fonts_check_name_familyname,
  16. )
  17. from nototools.unittests import layout
  18. # These checks fail in V2. If we try and make these checks pass, we
  19. # will cause regressions. This isn't acceptable for a family which is
  20. # requested 40 billion times a week.
  21. REMOVE_CHECKS = [
  22. "com.google.fonts/check/required_tables",
  23. "com.google.fonts/check/family/win_ascent_and_descent",
  24. "com.google.fonts/check/os2_metrics_match_hhea",
  25. "com.google.fonts/check/ftxvalidator_is_available",
  26. "com.google.fonts/check/dsig",
  27. "com.google.fonts/check/fontbakery_version",
  28. "com.google.fonts/check/outline_semi_vertical",
  29. "com.google.fonts/check/outline_jaggy_segments",
  30. "com.google.fonts/check/outline_colinear_vectors",
  31. "com.google.fonts/check/outline_short_segments",
  32. "com.google.fonts/check/outline_alignment_miss",
  33. "com.adobe.fonts/check/varfont/valid_default_instance_nameids",
  34. ]
  35. def filter_checks(_, check_id, __):
  36. if check_id in REMOVE_CHECKS:
  37. return False
  38. return True
  39. GOOGLEFONTS_PROFILE_CHECKS = [
  40. 'com.google.fonts/check/usweightclass',
  41. 'com.google.fonts/check/fsselection',
  42. 'com.google.fonts/check/name/familyname',
  43. ]
  44. ROBOTO_GENERAL_CHECKS = [c for c in UNIVERSAL_PROFILE_CHECKS + GOOGLEFONTS_PROFILE_CHECKS
  45. if c not in REMOVE_CHECKS]
  46. ROBOTO_GENERAL_CHECKS += [
  47. "com.roboto.fonts/check/italic_angle",
  48. "com.roboto.fonts/check/fs_type",
  49. "com.roboto.fonts/check/vendorid",
  50. "com.roboto.fonts/check/charset_coverage",
  51. "com.roboto.fonts/check/digit_widths",
  52. "com.roboto.fonts/check/numr_mapped_to_supr",
  53. "com.roboto.fonts/check/name_copyright",
  54. "com.roboto.fonts/check/name_unique_id",
  55. "com.roboto.fonts/check/vertical_metrics",
  56. "com.roboto.fonts/check/cmap4",
  57. "com.roboto.fonts/check/features",
  58. ]
  59. profile_imports = ('fontbakery.profiles.universal',)
  60. profile = profile_factory(default_section=Section("Roboto v3 general"))
  61. # Checks ported from https://github.com/googlefonts/roboto/blob/master/scripts/run_general_tests.py
  62. @condition
  63. def is_vf(ttFont):
  64. return True if "fvar" in ttFont else False
  65. @condition
  66. def font_features(ttFont):
  67. if "GSUB" not in ttFont:
  68. return []
  69. gsub = set(f.FeatureTag for f in ttFont["GSUB"].table.FeatureList.FeatureRecord)
  70. if "GPOS" not in ttFont:
  71. return gsub
  72. gpos = set(f.FeatureTag for f in ttFont["GPOS"].table.FeatureList.FeatureRecord)
  73. return gsub | gpos
  74. @condition
  75. def include_features():
  76. return frozenset(
  77. [
  78. 'frac',
  79. 'subs',
  80. 'salt',
  81. 'numr',
  82. 'sups',
  83. 'unic',
  84. 'ccmp',
  85. 'c2sc',
  86. 'smcp',
  87. 'dnom',
  88. 'dlig',
  89. 'onum',
  90. 'lnum',
  91. 'tnum',
  92. 'ss06',
  93. 'ss07',
  94. 'ss02',
  95. 'ss01',
  96. 'ss04',
  97. 'liga',
  98. 'locl',
  99. 'ss05',
  100. 'pnum',
  101. 'ss03'
  102. ]
  103. )
  104. def font_style(ttFont):
  105. subfamily_name = ttFont['name'].getName(2, 3, 1, 1033)
  106. typo_subfamily_name = ttFont['name'].getName(17, 3, 1, 1033)
  107. if typo_subfamily_name:
  108. return typo_subfamily_name.toUnicode()
  109. return subfamily_name.toUnicode()
  110. def font_family(ttFont):
  111. family_name = ttFont['name'].getName(1, 3, 1, 1033)
  112. typo_family_name = ttFont['name'].getName(16, 3, 1, 1033)
  113. if typo_family_name:
  114. return typo_family_name.toUnicode()
  115. return family_name.toUnicode()
  116. @check(
  117. id="com.roboto.fonts/check/italic_angle",
  118. conditions = ["is_italic"]
  119. )
  120. def com_roboto_fonts_check_italic_angle(ttFont):
  121. """Check italic fonts have correct italic angle"""
  122. failed = False
  123. if ttFont['post'].italicAngle != -12:
  124. yield FAIL, "post.italicAngle must be set to -12"
  125. else:
  126. yield PASS, "post.italicAngle is set correctly"
  127. @check(
  128. id="com.roboto.fonts/check/fs_type",
  129. )
  130. def com_roboto_fonts_check_fs_type(ttFont):
  131. """Check metadata is correct"""
  132. failed = False
  133. if ttFont['OS/2'].fsType != 0:
  134. yield FAIL, "OS/2.fsType must be 0"
  135. else:
  136. yield PASS, "OS/2.fsType is set correctly"
  137. @check(
  138. id="com.roboto.fonts/check/vendorid",
  139. )
  140. def com_roboto_fonts_check_vendorid(ttFont):
  141. """Check vendorID is correct"""
  142. if ttFont["OS/2"].achVendID != "GOOG":
  143. yield FAIL, "OS/2.achVendID must be set to 'GOOG'"
  144. else:
  145. yield PASS, "OS/2.achVendID is set corrrectly"
  146. @check(
  147. id="com.roboto.fonts/check/name_copyright",
  148. )
  149. def com_roboto_fonts_check_copyright(ttFont):
  150. """Check font copyright is correct"""
  151. expected_copyright = "Copyright 2011 Google Inc. All Rights Reserved."
  152. copyright_record = ttFont['name'].getName(0, 3, 1, 1033).toUnicode()
  153. if copyright_record == expected_copyright:
  154. yield PASS, "Copyright is correct"
  155. else:
  156. yield FAIL, f"Copyright is incorrect. It should be {expected_copyright}"
  157. @check(
  158. id="com.roboto.fonts/check/name_unique_id",
  159. )
  160. def com_roboto_fonts_check_name_unique_id(ttFont):
  161. """Check font unique id is correct"""
  162. family_name = font_family(ttFont)
  163. style = font_style(ttFont)
  164. expected = f"Google:{family_name} {style}:2016"
  165. font_unique_id = ttFont['name'].getName(3, 3, 1, 1033).toUnicode()
  166. if font_unique_id == expected:
  167. yield PASS, "Unique ID is correct"
  168. else:
  169. yield FAIL, f"Unique ID, '{font_unique_id}' is incorrect. It should be '{expected}'"
  170. @check(
  171. id="com.roboto.fonts/check/digit_widths",
  172. )
  173. def com_roboto_fonts_check_digit_widths(ttFont):
  174. """Check that all digits have the same width"""
  175. widths = set()
  176. for glyph_name in ["zero", "one", "two", "three","four", "five", "six", "seven", "eight", "nine"]:
  177. widths.add(ttFont['hmtx'][glyph_name][0])
  178. if len(widths) != 1:
  179. yield FAIL, "Numerals 0-9 do not have the same width"
  180. else:
  181. yield PASS, "Numerals 0-9 have the same width"
  182. @check(
  183. id="com.roboto.fonts/check/numr_mapped_to_supr",
  184. )
  185. def com_roboto_fonts_check_numr_mapped_to_supr(ttFont):
  186. """Check that 'numr' features maps digits to Unicode superscripts."""
  187. ascii_digits = '0123456789'
  188. superscript_digits = u'⁰¹²³⁴⁵⁶⁷⁸⁹'
  189. numr_glyphs = layout.get_advances(
  190. ascii_digits, ttFont.reader.file.name, '--features=numr')
  191. superscript_glyphs = layout.get_advances(
  192. superscript_digits, ttFont.reader.file.name)
  193. if superscript_glyphs == numr_glyphs:
  194. yield PASS, "'numr' feature mapped to unicode superscript glyphs"
  195. else:
  196. yield FAIL, "'numr' feature is not mapped to unicode superscript glyphs"
  197. @condition
  198. def include_glyphs():
  199. return frozenset([
  200. 0x2117, # SOUND RECORDING COPYRIGHT
  201. 0xEE01, 0xEE02, 0xF6C3, # legacy PUA
  202. # superior and inferior glyphs
  203. 0x2070,
  204. 0x2074,
  205. 0x2075,
  206. 0x2076,
  207. 0x2077,
  208. 0x2078,
  209. 0x2079,
  210. 0x2080,
  211. 0x2081,
  212. 0x2082,
  213. 0x2083,
  214. 0x2084,
  215. 0x2085,
  216. 0x2086,
  217. 0x2087,
  218. 0x2088,
  219. 0x2089,
  220. ])
  221. @condition
  222. def exclude_glyphs():
  223. return frozenset([
  224. 0x2072, 0x2073, 0x208F] + # unassigned characters
  225. list(range(0xE000, 0xF8FF + 1)) + list(range(0xF0000, 0x10FFFF + 1)) # other PUA
  226. ) - include_glyphs() # don't exclude legacy PUA
  227. @check(
  228. id="com.roboto.fonts/check/charset_coverage",
  229. conditions = ["include_glyphs", "exclude_glyphs"]
  230. )
  231. def com_roboto_fonts_check_charset_coverage(ttFont, include_glyphs, exclude_glyphs):
  232. """Check to make sure certain unicode encoded glyphs are included and excluded"""
  233. font_unicodes = set(ttFont.getBestCmap().keys())
  234. to_include = include_glyphs - font_unicodes
  235. if to_include != set():
  236. yield FAIL, f"Font must include the following codepoints {list(map(hex, to_include))}"
  237. else:
  238. yield PASS, "Font includes correct encoded glyphs"
  239. to_exclude = exclude_glyphs - font_unicodes
  240. if to_exclude != exclude_glyphs:
  241. yield FAIL, f"Font must exclude the following codepoints {list(map(hex, to_exclude))}"
  242. else:
  243. yield PASS, "Font excludes correct encoded glyphs"
  244. # TODO TestLigatures
  245. # TODO TestFeatures
  246. @check(
  247. id="com.roboto.fonts/check/vertical_metrics",
  248. )
  249. def com_roboto_fonts_check_vertical_metrics(ttFont):
  250. """Check vertical metrics are correct"""
  251. failed = []
  252. expected = {
  253. # android requires this, and web fonts expect this
  254. ("head", "yMin"): -555,
  255. ("head", "yMax"): 2163,
  256. # test hhea ascent, descent, and lineGap to be equal to Roboto v1 values
  257. ("hhea", "descent"): -500,
  258. ("hhea", "ascent"): 1900,
  259. ("hhea", "lineGap"): 0,
  260. # test OS/2 vertical metrics to be equal to old OS/2 win values
  261. # since fsSelection bit 7 is now enabled
  262. ("OS/2", "sTypoDescender"): -555,
  263. ("OS/2", "sTypoAscender"): 2146,
  264. ("OS/2", "sTypoLineGap"): 0,
  265. ("OS/2", "usWinDescent"): 555,
  266. ("OS/2", "usWinAscent"): 2146,
  267. }
  268. for (table, k), v in expected.items():
  269. font_val = getattr(ttFont[table], k)
  270. if font_val != v:
  271. failed.append((table, k, v, font_val))
  272. if not failed:
  273. yield PASS, "Fonts have correct vertical metrics"
  274. else:
  275. msg = "\n".join(
  276. [
  277. f"- {tbl}.{k} is {font_val} it should be {v}"
  278. for tbl, k, v, font_val in failed
  279. ]
  280. )
  281. yield FAIL, f"Fonts have incorrect vertical metrics:\n{msg}"
  282. @check(
  283. id="com.roboto.fonts/check/cmap4",
  284. )
  285. def com_roboto_fonts_check_cmap4(ttFont):
  286. """Check fonts have cmap format 4"""
  287. cmap_table = ttFont['cmap'].getcmap(3, 1)
  288. if cmap_table and cmap_table.format == 4:
  289. yield PASS, "Font contains a MS Unicode BMP encoded cmap"
  290. else:
  291. yield FAIL, "Font does not contain a MS Unicode BMP encoded cmap"
  292. @check(
  293. id="com.roboto.fonts/check/features",
  294. )
  295. def com_roboto_fonts_check_features(font_features, include_features):
  296. """Check font has correct features.
  297. https://github.com/googlefonts/roboto-classic/issues/97"""
  298. missing = include_features - font_features
  299. if missing:
  300. yield FAIL, f"Font is missing features {missing}"
  301. else:
  302. yield PASS, "Font has correct features"
  303. profile.auto_register(globals(), filter_func=filter_checks)
  304. profile.test_expected_checks(ROBOTO_GENERAL_CHECKS, exclusive=False)