test_general.py 11 KB

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