test_general.py 8.9 KB

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