test_general.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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]
  202. ) # legacy PUA
  203. @condition
  204. def exclude_glyphs():
  205. return frozenset([
  206. 0x2072, 0x2073, 0x208F] + # unassigned characters
  207. list(range(0xE000, 0xF8FF + 1)) + list(range(0xF0000, 0x10FFFF + 1)) # other PUA
  208. ) - include_glyphs() # don't exclude legacy PUA
  209. @check(
  210. id="com.roboto.fonts/check/charset_coverage",
  211. conditions = ["include_glyphs", "exclude_glyphs"]
  212. )
  213. def com_roboto_fonts_check_charset_coverage(ttFont, include_glyphs, exclude_glyphs):
  214. """Check to make sure certain unicode encoded glyphs are included and excluded"""
  215. font_unicodes = set(ttFont.getBestCmap().keys())
  216. to_include = include_glyphs - font_unicodes
  217. if to_include != set():
  218. yield FAIL, f"Font must include the following codepoints {list(map(hex, to_include))}"
  219. else:
  220. yield PASS, "Font includes correct encoded glyphs"
  221. to_exclude = exclude_glyphs - font_unicodes
  222. if to_exclude != exclude_glyphs:
  223. yield FAIL, f"Font must exclude the following codepoints {list(map(hex, to_exclude))}"
  224. else:
  225. yield PASS, "Font excludes correct encoded glyphs"
  226. # TODO TestLigatures
  227. # TODO TestFeatures
  228. @check(
  229. id="com.roboto.fonts/check/vertical_metrics",
  230. )
  231. def com_roboto_fonts_check_vertical_metrics(ttFont):
  232. """Check vertical metrics are correct"""
  233. failed = []
  234. expected = {
  235. # android requires this, and web fonts expect this
  236. ("head", "yMin"): -555,
  237. ("head", "yMax"): 2163,
  238. # test hhea ascent, descent, and lineGap to be equal to Roboto v1 values
  239. ("hhea", "descent"): -500,
  240. ("hhea", "ascent"): 1900,
  241. ("hhea", "lineGap"): 0,
  242. # test OS/2 vertical metrics to be equal to old OS/2 win values
  243. # since fsSelection bit 7 is now enabled
  244. ("OS/2", "sTypoDescender"): -555,
  245. ("OS/2", "sTypoAscender"): 2146,
  246. ("OS/2", "sTypoLineGap"): 0,
  247. ("OS/2", "usWinDescent"): 555,
  248. ("OS/2", "usWinAscent"): 2146,
  249. }
  250. for (table, k), v in expected.items():
  251. font_val = getattr(ttFont[table], k)
  252. if font_val != v:
  253. failed.append((table, k, v, font_val))
  254. if not failed:
  255. yield PASS, "Fonts have correct vertical metrics"
  256. else:
  257. msg = "\n".join(
  258. [
  259. f"- {tbl}.{k} is {font_val} it should be {v}"
  260. for tbl, k, v, font_val in failed
  261. ]
  262. )
  263. yield FAIL, f"Fonts have incorrect vertical metrics:\n{msg}"
  264. @check(
  265. id="com.roboto.fonts/check/cmap4",
  266. )
  267. def com_roboto_fonts_check_cmap4(ttFont):
  268. """Check fonts have cmap format 4"""
  269. cmap_table = ttFont['cmap'].getcmap(3, 1)
  270. if cmap_table and cmap_table.format == 4:
  271. yield PASS, "Font contains a MS Unicode BMP encoded cmap"
  272. else:
  273. yield FAIL, "Font does not contain a MS Unicode BMP encoded cmap"
  274. @check(
  275. id="com.roboto.fonts/check/features",
  276. )
  277. def com_roboto_fonts_check_features(font_features, include_features):
  278. """Check font has correct features.
  279. https://github.com/googlefonts/roboto-classic/issues/97"""
  280. missing = include_features - font_features
  281. if missing:
  282. yield FAIL, f"Font is missing features {missing}"
  283. else:
  284. yield PASS, "Font has correct features"
  285. profile.auto_register(globals(), filter_func=filter_checks)
  286. profile.test_expected_checks(ROBOTO_GENERAL_CHECKS, exclusive=False)