123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- from fontbakery.callable import check
- from fontbakery.callable import condition
- from fontbakery.checkrunner import Section, PASS, FAIL, WARN
- from fontbakery.fonts_profile import profile_factory
- from fontbakery.profiles.universal import UNIVERSAL_PROFILE_CHECKS
- from fontbakery.profiles.googlefonts_conditions import (
- style,
- expected_style,
- familyname_with_spaces,
- familyname,
- )
- from fontbakery.profiles.googlefonts import (
- com_google_fonts_check_usweightclass,
- com_google_fonts_check_fsselection,
- com_google_fonts_check_name_familyname,
- )
- from nototools.unittests import layout
- # These checks fail in V2. If we try and make these checks pass, we
- # will cause regressions. This isn't acceptable for a family which is
- # requested 40 billion times a week.
- REMOVE_CHECKS = [
- "com.google.fonts/check/required_tables",
- "com.google.fonts/check/family/win_ascent_and_descent",
- "com.google.fonts/check/os2_metrics_match_hhea",
- "com.google.fonts/check/ftxvalidator_is_available",
- "com.google.fonts/check/dsig",
- ]
- def filter_checks(_, check_id, __):
- if check_id in REMOVE_CHECKS:
- return False
- return True
- GOOGLEFONTS_PROFILE_CHECKS = [
- 'com.google.fonts/check/usweightclass',
- 'com.google.fonts/check/fsselection',
- 'com.google.fonts/check/name/familyname',
- ]
- ROBOTO_GENERAL_CHECKS = [c for c in UNIVERSAL_PROFILE_CHECKS + GOOGLEFONTS_PROFILE_CHECKS
- if c not in REMOVE_CHECKS]
- ROBOTO_GENERAL_CHECKS += [
- "com.roboto.fonts/check/italic_angle",
- "com.roboto.fonts/check/fs_type",
- "com.roboto.fonts/check/vendorid",
- "com.roboto.fonts/check/charset_coverage",
- "com.roboto.fonts/check/digit_widths",
- "com.roboto.fonts/check/numr_mapped_to_supr",
- "com.roboto.fonts/check/name_copyright",
- "com.roboto.fonts/check/name_unique_id",
- "com.roboto.fonts/check/vertical_metrics",
- "com.roboto.fonts/check/cmap4",
- ]
- profile_imports = ('fontbakery.profiles.universal',)
- profile = profile_factory(default_section=Section("Roboto v3 general"))
- # Checks ported from https://github.com/googlefonts/roboto/blob/master/scripts/run_general_tests.py
- @condition
- def is_italic(ttFont):
- return True if "Italic" in ttFont.reader.file.name else False
- @condition
- def is_vf(ttFont):
- return True if "fvar" in ttFont else False
- def font_style(ttFont):
- subfamily_name = ttFont['name'].getName(2, 3, 1, 1033)
- typo_subfamily_name = ttFont['name'].getName(17, 3, 1, 1033)
- if typo_subfamily_name:
- return typo_subfamily_name.toUnicode()
- return subfamily_name.toUnicode()
- def font_family(ttFont):
- family_name = ttFont['name'].getName(1, 3, 1, 1033)
- typo_family_name = ttFont['name'].getName(16, 3, 1, 1033)
- if typo_family_name:
- return typo_family_name.toUnicode()
- return family_name.toUnicode()
- @check(
- id="com.roboto.fonts/check/italic_angle",
- conditions = ["is_italic"]
- )
- def com_roboto_fonts_check_italic_angle(ttFont):
- """Check italic fonts have correct italic angle"""
- failed = False
- if ttFont['post'].italicAngle != -12:
- yield FAIL, "post.italicAngle must be set to -12"
- else:
- yield PASS, "post.italicAngle is set correctly"
- @check(
- id="com.roboto.fonts/check/fs_type",
- )
- def com_roboto_fonts_check_fs_type(ttFont):
- """Check metadata is correct"""
- failed = False
- if ttFont['OS/2'].fsType != 0:
- yield FAIL, "OS/2.fsType must be 0"
- else:
- yield PASS, "OS/2.fsType is set correctly"
- @check(
- id="com.roboto.fonts/check/vendorid",
- )
- def com_roboto_fonts_check_vendorid(ttFont):
- """Check vendorID is correct"""
- if ttFont["OS/2"].achVendID != "GOOG":
- yield FAIL, "OS/2.achVendID must be set to 'GOOG'"
- else:
- yield PASS, "OS/2.achVendID is set corrrectly"
- @check(
- id="com.roboto.fonts/check/name_copyright",
- )
- def com_roboto_fonts_check_copyright(ttFont):
- """Check font copyright is correct"""
- expected_copyright = "Copyright 2011 Google Inc. All Rights Reserved."
- copyright_record = ttFont['name'].getName(0, 3, 1, 1033).toUnicode()
- if copyright_record == expected_copyright:
- yield PASS, "Copyright is correct"
- else:
- yield FAIL, f"Copyright is incorrect. It should be {expected_copyright}"
- @check(
- id="com.roboto.fonts/check/name_unique_id",
- )
- def com_roboto_fonts_check_name_unique_id(ttFont):
- """Check font unique id is correct"""
- family_name = font_family(ttFont)
- style = font_style(ttFont)
- expected = f"Google:{family_name} {style}:2016"
- font_unique_id = ttFont['name'].getName(3, 3, 1, 1033).toUnicode()
- if font_unique_id == expected:
- yield PASS, "Unique ID is correct"
- else:
- yield FAIL, f"Unique ID, '{font_unique_id}' is incorrect. It should be '{expected}'"
- @check(
- id="com.roboto.fonts/check/digit_widths",
- )
- def com_roboto_fonts_check_digit_widths(ttFont):
- """Check that all digits have the same width"""
- widths = set()
- for glyph_name in ["zero", "one", "two", "three","four", "five", "six", "seven", "eight", "nine"]:
- widths.add(ttFont['hmtx'][glyph_name][0])
- if len(widths) != 1:
- yield FAIL, "Numerals 0-9 do not have the same width"
- else:
- yield PASS, "Numerals 0-9 have the same width"
- @check(
- id="com.roboto.fonts/check/numr_mapped_to_supr",
- )
- def com_roboto_fonts_check_numr_mapped_to_supr(ttFont):
- """Check that 'numr' features maps digits to Unicode superscripts."""
- ascii_digits = '0123456789'
- superscript_digits = u'⁰¹²³⁴⁵⁶⁷⁸⁹'
- numr_glyphs = layout.get_advances(
- ascii_digits, ttFont.reader.file.name, '--features=numr')
- superscript_glyphs = layout.get_advances(
- superscript_digits, ttFont.reader.file.name)
- if superscript_glyphs == numr_glyphs:
- yield PASS, "'numr' feature mapped to unicode superscript glyphs"
- else:
- yield FAIL, "'numr' feature is not mapped to unicode superscript glyphs"
- @condition
- def include_glyphs():
- return frozenset([
- 0x2117, # SOUND RECORDING COPYRIGHT
- 0xEE01, 0xEE02, 0xF6C3]
- ) # legacy PUA
- @condition
- def exclude_glyphs():
- return frozenset([
- 0x2072, 0x2073, 0x208F] + # unassigned characters
- list(range(0xE000, 0xF8FF + 1)) + list(range(0xF0000, 0x10FFFF + 1)) # other PUA
- ) - include_glyphs() # don't exclude legacy PUA
- @check(
- id="com.roboto.fonts/check/charset_coverage",
- conditions = ["include_glyphs", "exclude_glyphs"]
- )
- def com_roboto_fonts_check_charset_coverage(ttFont, include_glyphs, exclude_glyphs):
- """Check to make sure certain unicode encoded glyphs are included and excluded"""
- font_unicodes = set(ttFont.getBestCmap().keys())
- to_include = include_glyphs - font_unicodes
- if to_include != set():
- yield FAIL, f"Font must include the following codepoints {list(map(hex, to_include))}"
- else:
- yield PASS, "Font includes correct encoded glyphs"
- to_exclude = exclude_glyphs - font_unicodes
- if to_exclude != exclude_glyphs:
- yield FAIL, f"Font must exclude the following codepoints {list(map(hex, to_exclude))}"
- else:
- yield PASS, "Font excludes correct encoded glyphs"
- # TODO TestLigatures
- # TODO TestFeatures
- @check(
- id="com.roboto.fonts/check/vertical_metrics",
- )
- def com_roboto_fonts_check_vertical_metrics(ttFont):
- """Check vertical metrics are correct"""
- failed = []
- expected = {
- # android requires this, and web fonts expect this
- ("head", "yMin"): -555,
- ("head", "yMax"): 2163,
- # test hhea ascent, descent, and lineGap to be equal to Roboto v1 values
- ("hhea", "descent"): -500,
- ("hhea", "ascent"): 1900,
- ("hhea", "lineGap"): 0,
- # test OS/2 vertical metrics to be equal to old OS/2 win values
- # since fsSelection bit 7 is now enabled
- ("OS/2", "sTypoDescender"): -555,
- ("OS/2", "sTypoAscender"): 2146,
- ("OS/2", "sTypoLineGap"): 0,
- ("OS/2", "usWinDescent"): 555,
- ("OS/2", "usWinAscent"): 2146,
- }
- for (table, k), v in expected.items():
- font_val = getattr(ttFont[table], k)
- if font_val != v:
- failed.append((table, k, v, font_val))
- if not failed:
- yield PASS, "Fonts have correct vertical metrics"
- else:
- msg = "\n".join(
- [
- f"- {tbl}.{k} is {font_val} it should be {v}"
- for tbl, k, v, font_val in failed
- ]
- )
- yield FAIL, f"Fonts have incorrect vertical metrics:\n{msg}"
- @check(
- id="com.roboto.fonts/check/cmap4",
- )
- def com_roboto_fonts_check_cmap4(ttFont):
- """Check fonts have cmap format 4"""
- cmap_table = ttFont['cmap'].getcmap(3, 1)
- if cmap_table and cmap_table.format == 4:
- yield PASS, "Font contains a MS Unicode BMP encoded cmap"
- else:
- yield FAIL, "Font does not contain a MS Unicode BMP encoded cmap"
- profile.auto_register(globals(), filter_func=filter_checks)
- profile.test_expected_checks(ROBOTO_GENERAL_CHECKS, exclusive=False)
|