diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c74e8c352..a092021fd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ A more detailed list of changes is available in the corresponding milestones for ## Upcoming release: 0.8.10 (2022-Jul-??) +### New Checks +#### On the Google Fonts Profile + - **[com.google.fonts/check/font_names]:** Ensure font names match our specification (PR #3800) + - **[com.google.fonts/check/fvar_instances]:** Ensure fvar instances match our specification (PR #3800) + - **[com.google.fonts/check/STAT]:** Ensure fonts have compulsory STAT table axis values (PR #3800) + +### Changes to existing checks +#### On the Google Fonts Profile + - **[com.google.fonts/check/name/familyname]:** Removed due to new font names check (PR #3800) + - **[com.google.fonts/check/name/subfamilyname]:** Removed due to new font names check (PR #3800) + - **[com.google.fonts/check/name/fullfontname]:** Removed due to new font names check (PR #3800) + - **[com.google.fonts/check/name/postscriptname]:** Removed due to new font names check (PR #3800) + - **[com.google.fonts/check/name/typographicfamilyname]:** Removed due to new font names check (PR #3800) + - **[com.google.fonts/check/name/typographicsubfamilyname]:** Removed due to new font names check (PR #3800) + - **[com.google.fonts/check/varfont_has_instances]:** Removed due to new fvar instances check (PR #3800) + - **[com.google.fonts/check/varfont_weight_instances]:** Removed due to new fvar instances check (PR #3800) + - **[com.google.fonts/check/varfont_instance_coordinates]:** Removed due to new fvar instances check (PR #3800) + - **[com.google.fonts/check/varfont_instance_names]:** Removed due to new fvar instances check (PR #3800) + ### BugFixes - fixed bug on fontbakery_version check so that it now understands that v0.x.9 is older than v0.x.10 (issue #3813) - Fix fontbakery.profiles.shared_conditions.*_*_coord functions so they work on Italic fonts (issue #3828, PR #3834) diff --git a/Lib/fontbakery/profiles/googlefonts.py b/Lib/fontbakery/profiles/googlefonts.py index fd219aec58..bf29bfcc75 100644 --- a/Lib/fontbakery/profiles/googlefonts.py +++ b/Lib/fontbakery/profiles/googlefonts.py @@ -4,7 +4,7 @@ from fontbakery.status import INFO, WARN, ERROR, SKIP, PASS, FAIL from fontbakery.section import Section from fontbakery.callable import check, disable -from fontbakery.utils import filesize_formatting +from fontbakery.utils import bullet_list, filesize_formatting, markdown_table from fontbakery.message import Message from fontbakery.fonts_profile import profile_factory from fontbakery.constants import (NameID, @@ -156,22 +156,16 @@ 'com.google.fonts/check/hinting_impact', 'com.google.fonts/check/file_size', 'com.google.fonts/check/varfont/has_HVAR', - 'com.google.fonts/check/name/typographicfamilyname', - 'com.google.fonts/check/name/subfamilyname', - 'com.google.fonts/check/name/typographicsubfamilyname', + 'com.google.fonts/check/font_names', 'com.google.fonts/check/gasp', - 'com.google.fonts/check/name/familyname', 'com.google.fonts/check/name/mandatory_entries', 'com.google.fonts/check/name/copyright_length', 'com.google.fonts/check/fontdata_namecheck', 'com.google.fonts/check/name/ascii_only_entries', - 'com.google.fonts/check/varfont_has_instances', - 'com.google.fonts/check/varfont_weight_instances', + 'com.google.fonts/check/fvar_instances', 'com.google.fonts/check/old_ttfautohint', 'com.google.fonts/check/vttclean', - 'com.google.fonts/check/name/postscriptname', 'com.google.fonts/check/aat', - 'com.google.fonts/check/name/fullfontname', 'com.google.fonts/check/mac_style', 'com.google.fonts/check/fsselection', 'com.google.fonts/check/smart_dropout', @@ -182,8 +176,6 @@ 'com.google.fonts/check/cjk_vertical_metrics', 'com.google.fonts/check/cjk_vertical_metrics_regressions', 'com.google.fonts/check/cjk_not_enough_glyphs', - 'com.google.fonts/check/varfont_instance_coordinates', - 'com.google.fonts/check/varfont_instance_names', 'com.google.fonts/check/varfont_duplicate_instance_names', 'com.google.fonts/check/varfont/consistent_axes', 'com.google.fonts/check/varfont/unsupported_axes', @@ -197,7 +189,8 @@ 'com.google.fonts/check/os2/use_typo_metrics', 'com.google.fonts/check/meta/script_lang_tags', 'com.google.fonts/check/no_debugging_tables', - 'com.google.fonts/check/render_own_name' + 'com.google.fonts/check/render_own_name', + 'com.google.fonts/check/stat', ] GOOGLEFONTS_PROFILE_CHECKS = \ @@ -230,68 +223,16 @@ """, proposal = 'legacy:check/001' ) -def com_google_fonts_check_canonical_filename(font): +def com_google_fonts_check_canonical_filename(ttFont): """Checking file is named canonically.""" - from fontTools.ttLib import TTFont - from .shared_conditions import (is_variable_font, - variable_font_filename) - from .googlefonts_conditions import canonical_stylename - from fontbakery.utils import suffix - from fontbakery.constants import STATIC_STYLE_NAMES - - failed = False - if "_" in os.path.basename(font): - failed = True - yield FAIL,\ - Message("invalid-char", - f'font filename "{font}" is invalid.' - f' It must not contain underscore characters!') - return - - ttFont = TTFont(font) - if is_variable_font(ttFont): - if suffix(font) in STATIC_STYLE_NAMES: - failed = True - yield FAIL,\ - Message("varfont-with-static-filename", - "This is a variable font, but it is using" - " a naming scheme typical of a static font.") - - expected = variable_font_filename(ttFont) - if expected is None: - failed = True - yield FAIL,\ - Message("unknown-name", - "FontBakery was unable to figure out which" - " filename to expect for this variable font.\n" - "This most likely means that the name table entries" - " used as reference such as FONT_FAMILY_NAME may" - " not be properly set.\n" - "Please review the name table entries.") - return - - font_filename = os.path.basename(font) - if font_filename != expected: - failed = True - yield FAIL,\ - Message("bad-varfont-filename", - f"The file '{font_filename}' must be renamed" - f" to '{expected}' according to the" - f" Google Fonts naming policy for variable fonts.") - + from axisregistry import build_filename + current_filename = os.path.basename(ttFont.reader.file.name) + expected_filename = build_filename(ttFont) + if current_filename != expected_filename: + yield FAIL, Message('bad-filename', + f'Expected "{expected_filename}. Got {current_filename}.') else: - if not canonical_stylename(font): - failed = True - style_names = '", "'.join(STATIC_STYLE_NAMES) - yield FAIL,\ - Message("bad-static-filename", - f'Style name used in "{font}" is not canonical.' - f' You should rebuild the font using' - f' any of the following' - f' style names: "{style_names}".') - - if not failed: - yield PASS, f"{font} is named canonically." + yield PASS, f'Font filename is correct, "{current_filename}".' @check( @@ -1021,7 +962,6 @@ def font_codepoints(ttFont): ) def com_google_fonts_check_glyph_coverage(ttFont, font_codepoints, config): """Check Google Fonts glyph coverage.""" - from fontbakery.utils import bullet_list from glyphsets import GFGlyphData as glyph_data import unicodedata2 @@ -3243,6 +3183,87 @@ def com_google_fonts_check_mac_style(ttFont, style): bitname="BOLD") +@check( + id = 'com.google.fonts/check/font_names', + conditions = ['expected_font_names'], + rationale = """ + Google Fonts has several rules which need to be adhered to when + setting a font's name table. Please read: + https://googlefonts.github.io/gf-guide/statics.html#supported-styles + https://googlefonts.github.io/gf-guide/statics.html#style-linking + https://googlefonts.github.io/gf-guide/statics.html#unsupported-styles + https://googlefonts.github.io/gf-guide/statics.html#single-weight-families + """ +) +def com_google_fonts_check_font_names(ttFont, expected_font_names): + """Check font names are correct""" + def style_names(nametable): + res = {} + for nameID in ( + NameID.FONT_FAMILY_NAME, + NameID.FONT_SUBFAMILY_NAME, + NameID.FULL_FONT_NAME, + NameID.POSTSCRIPT_NAME, + NameID.TYPOGRAPHIC_FAMILY_NAME, + NameID.TYPOGRAPHIC_SUBFAMILY_NAME + ): + rec = nametable.getName(nameID, 3, 1, 0x409) + if rec: + res[nameID] = rec.toUnicode() + return res + + font_names = style_names(ttFont['name']) + expected_names = style_names(expected_font_names['name']) + + name_ids = { + NameID.FONT_FAMILY_NAME: "Family Name", + NameID.FONT_SUBFAMILY_NAME: "Subfamily Name", + NameID.FULL_FONT_NAME: "Full Name", + NameID.POSTSCRIPT_NAME: "Poscript Name", + NameID.TYPOGRAPHIC_FAMILY_NAME: "Typographic Family Name", + NameID.TYPOGRAPHIC_SUBFAMILY_NAME: "Typographic Subfamily Name", + } + table = [] + for nameID in set(font_names.keys()) | set(expected_names.keys()): + id_name = name_ids[nameID] + row = {"nameID": id_name} + if nameID in font_names: + row["current"] = font_names[nameID] + else: + row["current"] = "N/A" + if nameID in expected_names: + row["expected"] = expected_names[nameID] + else: + row["expected"] = "N/A" + table.append(row) + + new_names = set(font_names) - set(expected_names) + missing_names = set(expected_names) - set(font_names) + same_names = set(font_names) & set(expected_names) + + md_table = markdown_table(table) + + passed = True + if new_names or missing_names: + passed = False + + for nameID in same_names: + if nameID == NameID.FULL_FONT_NAME and \ + all([ + " Regular" in expected_names[nameID], + font_names[nameID] == expected_names[nameID].replace(" Regular", "") + ]): + yield WARN, Message('lacks-regular', "Regular missing from full name") + elif font_names[nameID] != expected_names[nameID]: + passed = False + + if not passed: + yield FAIL, Message('bad-names', + f'Font names are incorrect:\n\n{md_table}') + else: + yield PASS, f"Font names are good:\n\n{md_table}" + + # FIXME! # Temporarily disabled since GFonts hosted Cabin files seem to have changed in ways # that break some of the assumptions in the check implementation below. @@ -3327,350 +3348,6 @@ def com_google_fonts_check_name_mandatory_entries(ttFont, style): yield PASS, "Font contains values for all mandatory name table entries." -@check( - id = 'com.google.fonts/check/name/familyname', - conditions = ['style', - 'familyname_with_spaces'], - rationale = """ - Checks that the family name infered from the font filename matches the string - at nameID 1 (NAMEID_FONT_FAMILY_NAME) if it conforms to RIBBI and otherwise - checks that nameID 1 is the family name + the style name. - """, - proposal = 'legacy:check/157' -) -def com_google_fonts_check_name_familyname(ttFont, style, familyname_with_spaces): - """Check name table: FONT_FAMILY_NAME entries.""" - from fontbakery.utils import name_entry_id - - def get_only_weight(value): - onlyWeight = {"BlackItalic": "Black", - "BoldItalic": "", - "ExtraBold": "ExtraBold", - "ExtraBoldItalic": "ExtraBold", - "ExtraLightItalic": "ExtraLight", - "LightItalic": "Light", - "MediumItalic": "Medium", - "SemiBoldItalic": "SemiBold", - "ThinItalic": "Thin"} - return onlyWeight.get(value, value) - - def has_english_lang_id(name): - if name.platformID == PlatformID.MACINTOSH: - return name.langID == 0 - - if name.platformID == PlatformID.WINDOWS: - return name.langID in [ - 0x0C09, # Australia - 0x2809, # Belize - 0x1009, # Canada - 0x2409, # Caribbean - 0x4009, # India - 0x1809, # Ireland - 0x2009, # Jamaica - 0x4409, # Malaysia - 0x1409, # New Zealand - 0x3409, # Republic of the Philippines - 0x4809, # Singapore - 0x1C09, # South Africa - 0x2C09, # Trinidad and Tobago - 0x0809, # United Kingdom - 0x0409, # United States - 0x3009, # Zimbabwe - ] - - failed = False - only_weight = get_only_weight(style) - for name in ttFont['name'].names: - if not has_english_lang_id(name): - # The value of familyname_with_spaces is derived from - # the filename which is expected to match the English name IDs. - # See: https://github.com/googlefonts/fontbakery/issues/3089 - continue - - if name.nameID == NameID.FONT_FAMILY_NAME: - - if name.platformID == PlatformID.MACINTOSH: - expected_value = familyname_with_spaces - - elif name.platformID == PlatformID.WINDOWS: - if style in ['Regular', - 'Italic', - 'Bold', - 'Bold Italic']: - expected_value = familyname_with_spaces - else: - expected_value = " ".join([familyname_with_spaces, - only_weight]).strip() - else: - failed = True - yield FAIL,\ - Message("lacks-name", - f"Font should not have a " - f"{name_entry_id(name)} entry!") - continue - - string = name.string.decode(name.getEncoding()).strip() - - if (camelcased_familyname_exception(string) and - string.replace(" ", "") == expected_value.replace(" ", "")): - continue - - if string != expected_value: - failed = True - yield FAIL,\ - Message("mismatch", - f'Entry {name_entry_id(name)} on the "name" table:' - f' Expected "{expected_value}"' - f' but got "{string}".') - if not failed: - yield PASS,\ - Message("ok", - "FONT_FAMILY_NAME entries are all good.") - - -@check( - id = 'com.google.fonts/check/name/subfamilyname', - conditions = ['expected_style'], - proposal = 'legacy:check/158' -) -def com_google_fonts_check_name_subfamilyname(ttFont, expected_style): - """Check name table: FONT_SUBFAMILY_NAME entries.""" - failed = False - nametable = ttFont['name'] - win_name = nametable.getName(NameID.FONT_SUBFAMILY_NAME, - PlatformID.WINDOWS, - WindowsEncodingID.UNICODE_BMP, - WindowsLanguageID.ENGLISH_USA) - mac_name = nametable.getName(NameID.FONT_SUBFAMILY_NAME, - PlatformID.MACINTOSH, - MacintoshEncodingID.ROMAN, - MacintoshLanguageID.ENGLISH) - - if mac_name and mac_name.toUnicode() != expected_style.mac_style_name: - failed = True - yield FAIL,\ - Message("bad-familyname", - f'SUBFAMILY_NAME for Mac "{mac_name.toUnicode()}"' - f' must be "{expected_style.mac_style_name}"') - if win_name.toUnicode() != expected_style.win_style_name: - failed = True - yield FAIL,\ - Message("bad-familyname", - f'SUBFAMILY_NAME for Win "{win_name.toUnicode()}"' - f' must be "{expected_style.win_style_name}"') - if not failed: - yield PASS, "FONT_SUBFAMILY_NAME entries are all good." - - -@check( - id = 'com.google.fonts/check/name/fullfontname', - rationale = """ - Requirements for the FULL_FONT_NAME entries in the 'name' table. - """, - conditions = ['style_with_spaces', - 'familyname_with_spaces'], - proposal = 'legacy:check/159' -) -def com_google_fonts_check_name_fullfontname(ttFont, - style_with_spaces, - familyname_with_spaces): - """Check name table: FULL_FONT_NAME entries.""" - from fontbakery.utils import name_entry_id - failed = False - for name in ttFont['name'].names: - if name.nameID == NameID.FULL_FONT_NAME: - camelcased_name = familyname_with_spaces.replace(" ", "") - if camelcased_familyname_exception(camelcased_name): - familyname = camelcased_name - else: - familyname = familyname_with_spaces - - expected_value = "{} {}".format(familyname, - style_with_spaces) - string = name.string.decode(name.getEncoding()).strip() - - if string != expected_value: - failed = True - # special case - # see https://github.com/googlefonts/fontbakery/issues/1436 - if style_with_spaces == "Regular" \ - and string == familyname_with_spaces: - yield WARN,\ - Message("lacks-regular", - f'{name_entry_id(name)}\n' - f'Got "{string}" which lacks "Regular",' - f' but it is probably OK in this case.') - else: - yield FAIL,\ - Message("bad-entry", - f'{name_entry_id(name)}\n' - f'Expected: "{expected_value}"\n' - f'But got: "{string}"') - if not failed: - yield PASS, "FULL_FONT_NAME entries are all good." - - -@check( - id = 'com.google.fonts/check/name/postscriptname', - rationale = """ - Requirements for the POSTSCRIPT_NAME entries in the 'name' table. - """, - conditions = ['style', - 'familyname'], - proposal = 'legacy:check/160' -) -def com_google_fonts_check_name_postscriptname(ttFont, style, familyname): - """Check name table: POSTSCRIPT_NAME entries.""" - from fontbakery.utils import name_entry_id - - failed = False - for name in ttFont['name'].names: - if name.nameID == NameID.POSTSCRIPT_NAME: - expected_value = f"{familyname}-{style}" - - string = name.string.decode(name.getEncoding()).strip() - if string != expected_value: - failed = True - yield FAIL,\ - Message("bad-entry", - f'{name_entry_id(name)}\n' - f'Expected: "{expected_value}"\n' - f'But got: "{string}"') - if not failed: - yield PASS, "POSTCRIPT_NAME entries are all good." - - -@check( - id = 'com.google.fonts/check/name/typographicfamilyname', - rationale = """ - Requirements for the TYPOGRAPHIC_FAMILY_NAME entries in the 'name' table. - """, - conditions = ['style', - 'familyname_with_spaces'], - proposal = 'legacy:check/161' -) -def com_google_fonts_check_name_typographicfamilyname(ttFont, style, familyname_with_spaces): - """Check name table: TYPOGRAPHIC_FAMILY_NAME entries.""" - from fontbakery.utils import name_entry_id - - failed = False - if style in ['Regular', - 'Italic', - 'Bold', - 'BoldItalic']: - for name in ttFont['name'].names: - if name.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME: - failed = True - yield FAIL,\ - Message("ribbi", - (f'Font style is "{style}" and, for that reason,' - f' it is not expected to have a ' - f'{name_entry_id(name)} entry!')) - else: - expected_value = familyname_with_spaces - has_entry = False - for name in ttFont['name'].names: - if name.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME: - string = name.string.decode(name.getEncoding()).strip() - if string == expected_value: - has_entry = True - else: - failed = True - yield FAIL,\ - Message("non-ribbi-bad-value", - (f'{name_entry_id(name)}\n' - f'Expected: "{expected_value}"\n' - f'But got: "{string}".')) - if not failed and not has_entry: - failed = True - yield FAIL,\ - Message("non-ribbi-lacks-entry", - ("Non-RIBBI fonts must have a TYPOGRAPHIC_FAMILY_NAME" - " entry on the name table.")) - if not failed: - yield PASS, "TYPOGRAPHIC_FAMILY_NAME entries are all good." - - -@check( - id = 'com.google.fonts/check/name/typographicsubfamilyname', - rationale = """ - Requirements for the TYPOGRAPHIC_SUBFAMILY_NAME entries in the 'name' table. - """, - conditions = ['expected_style'], - proposal = 'legacy:check/162' -) -def com_google_fonts_check_name_typographicsubfamilyname(ttFont, expected_style): - """Check name table: TYPOGRAPHIC_SUBFAMILY_NAME entries.""" - failed = False - nametable = ttFont['name'] - win_name = nametable.getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - PlatformID.WINDOWS, - WindowsEncodingID.UNICODE_BMP, - WindowsLanguageID.ENGLISH_USA) - mac_name = nametable.getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - PlatformID.MACINTOSH, - MacintoshEncodingID.ROMAN, - MacintoshLanguageID.ENGLISH) - - if all([win_name, mac_name]): - if win_name.toUnicode() != mac_name.toUnicode(): - failed = True - yield FAIL,\ - Message("mismatch", - f'TYPOGRAPHIC_SUBFAMILY_NAME entry' - f' for Win "{win_name.toUnicode()}"' - f' and Mac "{mac_name.toUnicode()}" do not match.') - - if expected_style.is_ribbi: - if win_name and win_name.toUnicode() != expected_style.win_style_name: - failed = True - yield FAIL,\ - Message("bad-win-name", - f'TYPOGRAPHIC_SUBFAMILY_NAME entry' - f' for Win "{win_name.toUnicode()}"' - f' must be "{expected_style.win_style_name}".' - f' Please note, since the font style is RIBBI,' - f' this record can be safely deleted.') - - if mac_name and mac_name.toUnicode() != expected_style.mac_style_name: - failed = True - yield FAIL,\ - Message("bad-mac-name", - f'TYPOGRAPHIC_SUBFAMILY_NAME entry' - f' for Mac "{mac_name.toUnicode()}"' - f' must be "{expected_style.mac_style_name}".' - f' Please note, since the font style is RIBBI,' - f' this record can be safely deleted.') - - if expected_style.typo_style_name: - if not win_name: - failed = True - yield FAIL,\ - Message("missing-typo-win", - f'TYPOGRAPHIC_SUBFAMILY_NAME for Win is missing.' - f' It must be "{expected_style.typo_style_name}".') - - elif win_name.toUnicode() != expected_style.typo_style_name: - failed = True - yield FAIL,\ - Message("bad-typo-win", - f'TYPOGRAPHIC_SUBFAMILY_NAME for Win' - f' "{win_name.toUnicode()}" is incorrect.' - f' It must be "{expected_style.typo_style_name}".') - - if mac_name and mac_name.toUnicode() != expected_style.typo_style_name: - failed = True - yield FAIL,\ - Message("bad-typo-mac", - f'TYPOGRAPHIC_SUBFAMILY_NAME for Mac' - f' "{mac_name.toUnicode()}" is incorrect.' - f' It must be "{expected_style.typo_style_name}".' - f' Please note, this record can be safely deleted.') - - if not failed: - yield PASS, "TYPOGRAPHIC_SUBFAMILY_NAME entries are all good." - - @check( id = 'com.google.fonts/check/name/copyright_length', rationale = """ @@ -4101,76 +3778,182 @@ def com_google_fonts_check_aat(ttFont): @check( - id = 'com.google.fonts/check/fvar_name_entries', - conditions = ['is_variable_font'], + id = 'com.google.fonts/check/stat', + conditions = ['is_variable_font', 'expected_font_names'], rationale = """ - The purpose of this check is to make sure that all name entries referenced - by variable font instances do exist in the name table. - """, - proposal = 'https://github.com/googlefonts/fontbakery/issues/2069' -) -def com_google_fonts_check_fvar_name_entries(ttFont): - """All name entries referenced by fvar instances exist on the name table?""" + Check a font's STAT table contains compulsory Axis Values which exist + in the Google Fonts Axis Registry. - failed = False - for instance in ttFont["fvar"].instances: + We cannot determine what Axis Values the user will set for axes such as + opsz, GRAD since these axes are unique for each font so we'll skip them. + """ +) +def com_google_fonts_check_stat(ttFont, expected_font_names): + """Check a font's STAT table contains compulsory Axis Values.""" + axes_to_check = { + "CASL", "CRSV", "FILL", "FLAR", "MONO", "SOFT", "VOLM", "wdth", "wght", "WONK" + } + def stat_axis_values(ttFont, include_axes=axes_to_check): + name = ttFont["name"] + stat = ttFont["STAT"].table + axes = [a.AxisTag for a in stat.DesignAxisRecord.Axis] + res = {} + if ttFont["STAT"].table.AxisValueCount == 0: + return res + axis_values = stat.AxisValueArray.AxisValue + for ax in axis_values: + axis_tag = axes[ax.AxisIndex] + if axis_tag not in include_axes: + continue + ax_name = name.getName(ax.ValueNameID, 3, 1, 0x409).toUnicode() + res[(axis_tag, ax_name)] = { + "Axis": axis_tag, + "Name": ax_name, + "Flags": ax.Flags, + "Value": ax.Value, + "LinkedValue": None if not hasattr(ax, "LinkedValue") else ax.LinkedValue + } + return res + + font_axis_values = stat_axis_values(ttFont) + expected_axis_values = stat_axis_values(expected_font_names) + + table = [] + for axis, name in set(font_axis_values.keys()) | set(expected_axis_values.keys()): + row = {} + key = (axis, name) + if key in font_axis_values: + row["Name"] = name + row["Axis"] = axis + row["Current Value"] = font_axis_values[key]["Value"] + row["Current Flags"] = font_axis_values[key]["Flags"] + row["Current LinkedValue"] = font_axis_values[key]["LinkedValue"] + else: + row["Name"] = name + row["Axis"] = axis + row["Current Value"] = "N/A" + row["Current Flags"] = "N/A" + row["Current LinkedValue"] = "N/A" + if key in expected_axis_values: + row["Name"] = name + row["Axis"] = axis + row["Expected Value"] = expected_axis_values[key]["Value"] + row["Expected Flags"] = expected_axis_values[key]["Flags"] + row["Expected LinkedValue"] = expected_axis_values[key]["LinkedValue"] + else: + row["Name"] = name + row["Axis"] = axis + row["Expected Value"] = "N/A" + row["Expected Flags"] = "N/A" + row["Expected LinkedValue"] = "N/A" + table.append(row) + table.sort(key=lambda k: (k["Axis"], str(k["Expected Value"]))) + md_table = markdown_table(table) - entries = [entry for entry in ttFont["name"].names - if entry.nameID == instance.subfamilyNameID] - if len(entries) == 0: - failed = True - yield FAIL,\ - Message("missing-name", - f"Named instance with coordinates {instance.coordinates}" - f" lacks an entry on the name table" - f" (nameID={instance.subfamilyNameID}).") + passed = True + is_italic = any(a.axisTag in ["ital", "slnt"] for a in ttFont["fvar"].axes) + missing_ital_av = any("Italic" in r["Name"] for r in table) + if is_italic and missing_ital_av: + passed = False + yield FAIL, Message("missing-ital-axis-values", + "Italic Axis Value missing.") - if not failed: - yield PASS, "OK" + if font_axis_values != expected_axis_values: + passed = False + yield FAIL, Message('bad-axis-values', + f"Compulsory STAT Axis Values are incorrect:\n\n {md_table}\n") + if passed: + yield PASS, "Compulsory STAT Axis Values are correct." @check( - id = 'com.google.fonts/check/varfont_has_instances', - conditions = ['is_variable_font'], + id = 'com.google.fonts/check/fvar_instances', + conditions = ['is_variable_font', 'expected_font_names'], rationale = """ - Named instances must be present in all variable fonts in order not to frustrate - the users' typical expectations of a traditional static font workflow. - """, - proposal = 'https://github.com/googlefonts/fontbakery/issues/2127' + Check a font's fvar instance coordinates comply with our guidelines: + https://googlefonts.github.io/gf-guide/variable.html#fvar-instances + """ ) -def com_google_fonts_check_varfont_has_instances(ttFont): - """A variable font must have named instances.""" +def com_google_fonts_check_fvar_instances(ttFont, expected_font_names): + """Check variable font instances""" + def get_instances(ttFont): + name = ttFont['name'] + fvar = ttFont['fvar'] + res = {} + for inst in fvar.instances: + inst_name = name.getName(inst.subfamilyNameID, 3, 1, 0x409) + if not inst_name: + continue + res[inst_name.toUnicode()] = inst.coordinates + return res + font_instances = get_instances(ttFont) + expected_instances = get_instances(expected_font_names) + table = [] + for name in set(font_instances.keys()) | set(expected_instances.keys()): + row = {"Name": name} + if name in font_instances: + row["current"] = ", ".join([f"{k}={v}" for k,v in font_instances[name].items()]) + else: + row["current"] = "N/A" + if name in expected_instances: + row["expected"] = ", ".join([f"{k}={v}" for k,v in expected_instances[name].items()]) + else: + row["expected"] = "N/A" + table.append(row) + table = sorted(table, key=lambda k: str(k["expected"])) + + missing = set(expected_instances.keys()) - set(font_instances.keys()) + new = set(font_instances.keys()) - set(expected_instances.keys()) + same = set(font_instances.keys()) & set(expected_instances.keys()) + # check if instances have correct weight. + if all("wght" in expected_instances[i] for i in expected_instances): + wght_wrong = any(font_instances[i]["wght"] != expected_instances[i]["wght"] for i in same) + else: + wght_wrong = False - if len(ttFont["fvar"].instances): - yield PASS, "OK" + md_table = markdown_table(table) + if any([wght_wrong, missing, new]): + hints = "" + if missing: + hints += "- Add missing instances\n" + if new: + hints += "- Delete additional instances\n" + if wght_wrong: + hints += "- wght coordinates are wrong for some instances" + yield FAIL, Message('bad-fvar-instances', + f"fvar instances are incorrect:\n{hints}\n{md_table}") + elif any(font_instances[i] != expected_instances[i] for i in same): + yield WARN, Message("suspicious-fvar-coords", + (f"fvar instance coordinates for non-wght axes are not the same as the fvar defaults. " + f"This may be intentional so please check with the font author:\n\n{md_table}")) else: - yield FAIL,\ - Message("lacks-named-instances", - "This variable font lacks" - " named instances on the fvar table.") + yield PASS, f"fvar instances are good:\n\n{md_table}" @check( - id = 'com.google.fonts/check/varfont_weight_instances', + id = 'com.google.fonts/check/fvar_name_entries', conditions = ['is_variable_font'], rationale = """ - The named instances on the weight axis of a variable font must have coordinates - that are multiples of 100 on the design space. + The purpose of this check is to make sure that all name entries referenced + by variable font instances do exist in the name table. """, - proposal = 'https://github.com/googlefonts/fontbakery/issues/2258' + proposal = 'https://github.com/googlefonts/fontbakery/issues/2069' ) -def com_google_fonts_check_varfont_weight_instances(ttFont): - """Variable font weight coordinates must be multiples of 100.""" +def com_google_fonts_check_fvar_name_entries(ttFont): + """All name entries referenced by fvar instances exist on the name table?""" failed = False for instance in ttFont["fvar"].instances: - if 'wght' in instance.coordinates and instance.coordinates['wght'] % 100 != 0: + + entries = [entry for entry in ttFont["name"].names + if entry.nameID == instance.subfamilyNameID] + if len(entries) == 0: failed = True yield FAIL,\ - Message("bad-coordinate", - f"Found a variable font instance with" - f" 'wght'={instance.coordinates['wght']}." - f" This should instead be a multiple of 100.") + Message("missing-name", + f"Named instance with coordinates {instance.coordinates}" + f" lacks an entry on the name table" + f" (nameID={instance.subfamilyNameID}).") if not failed: yield PASS, "OK" @@ -4332,8 +4115,6 @@ def com_google_fonts_check_ligature_carets(ttFont, ligature_glyphs): ) def com_google_fonts_check_kerning_for_non_ligated_sequences(ttFont, config, ligatures, has_kerning_info): """Is there kerning info for non-ligated sequences?""" - from fontbakery.utils import bullet_list - def look_for_nonligated_kern_info(table): for pairpos in table.SubTable: for i, glyph in enumerate(pairpos.Coverage.glyphs): @@ -5228,91 +5009,14 @@ def com_google_fonts_check_cjk_not_enough_glyphs(ttFont): yield PASS, "Font has the correct quantity of CJK glyphs" -@check( - id = 'com.google.fonts/check/varfont_instance_coordinates', - conditions = ['is_variable_font'], - proposal = 'https://github.com/googlefonts/fontbakery/pull/2520' -) -def com_google_fonts_check_varfont_instance_coordinates(ttFont): - """Check variable font instances have correct coordinate values""" - from fontbakery.parse import instance_parse - from fontbakery.constants import SHOW_GF_DOCS_MSG - - failed = False - for instance in ttFont['fvar'].instances: - name = ttFont['name'].getName( - instance.subfamilyNameID, - PlatformID.WINDOWS, - WindowsEncodingID.UNICODE_BMP, - WindowsLanguageID.ENGLISH_USA - ).toUnicode() - expected_instance = instance_parse(name) - for axis in instance.coordinates: - if axis in expected_instance.coordinates and \ - instance.coordinates[axis] != expected_instance.coordinates[axis]: - yield FAIL,\ - Message("bad-coordinate", - f'Instance "{name}" {axis} value ' - f'is "{instance.coordinates[axis]}". ' - f'It should be "{expected_instance.coordinates[axis]}"') - failed = True - - if failed: - yield FAIL, f"{SHOW_GF_DOCS_MSG}#axes" - else: - yield PASS, "Instance coordinates are correct" - - -@check( - id = 'com.google.fonts/check/varfont_instance_names', - conditions = ['is_variable_font'], - proposal = 'https://github.com/googlefonts/fontbakery/pull/2520' -) -def com_google_fonts_check_varfont_instance_names(ttFont): - """Check variable font instances have correct names""" - # This check and the fontbakery.parse module used to be more complicated. - # On 2020-06-26, we decided to only allow Thin-Black + Italic instances. - # If we decide to add more particles to instance names, It's worthwhile - # revisiting our previous implementation which can be found in commits - # earlier than or equal to ca71d787eb2b8b5a9b111884080dde5d45f5579f - from fontbakery.parse import instance_parse - from fontbakery.constants import SHOW_GF_DOCS_MSG - - failed = [] - for instance in ttFont['fvar'].instances: - name = ttFont['name'].getName( - instance.subfamilyNameID, - PlatformID.WINDOWS, - WindowsEncodingID.UNICODE_BMP, - WindowsLanguageID.ENGLISH_USA - ).toUnicode() - expected_instance = instance_parse(name) - - # Check if name matches predicted name - if expected_instance.name != name: - failed.append(name) - - if failed: - failed_instances = "\n\t- ".join([""] + failed) - yield FAIL,\ - Message('bad-instance-names', - f'Following instances are not supported: {failed_instances}\n' - f'\n' - f'{SHOW_GF_DOCS_MSG}#fvar-instances') - else: - yield PASS, "Instance names are correct" - - @check( id = 'com.google.fonts/check/varfont_duplicate_instance_names', rationale = """ This check's purpose is to detect duplicate named instances names in a given variable font. - Repeating instance names may be the result of instances for several VF axes defined in `fvar`, but since currently only weight+italic tokens are allowed in instance names as per GF specs, they ended up repeating. - Instead, only a base set of fonts for the most default representation of the family can be defined through instances in the `fvar` table, all other instances will have to be left to access through the `STAT` table. @@ -5912,7 +5616,7 @@ def normalize(name): If the progression rates of axes is linear, this check can be ignored. Fontmake will also skip adding an avar table if the progression rates are linear. However, we still recommend designers visually proof each - instance is at the desired weight, width etc. + instance is at the expected weight, width etc. """, conditions = ["is_variable_font"], proposal = 'https://github.com/googlefonts/fontbakery/issues/3100' @@ -6241,8 +5945,6 @@ def com_google_fonts_check_render_own_name(ttFont): def com_google_fonts_check_repo_sample_image(readme_contents, readme_directory, config): """Check README.md has a sample image.""" import re - from fontbakery.utils import bullet_list - image_path = False line_number = 0 for line in readme_contents.split('\n'): diff --git a/Lib/fontbakery/profiles/googlefonts_conditions.py b/Lib/fontbakery/profiles/googlefonts_conditions.py index 337764e583..e07ea67f0f 100644 --- a/Lib/fontbakery/profiles/googlefonts_conditions.py +++ b/Lib/fontbakery/profiles/googlefonts_conditions.py @@ -706,3 +706,15 @@ def upstream_yaml(family_directory): @condition def is_noto(font_familyname): return font_familyname.startswith("Noto ") + + +def expected_font_names(ttFont, ttFonts): + from axisregistry import build_name_table, build_fvar_instances, build_stat + from copy import deepcopy + siblings = [f for f in ttFonts if f != ttFont] + font_cp = deepcopy(ttFont) + build_name_table(font_cp, siblings=siblings) + if "fvar" in font_cp: + build_fvar_instances(font_cp) + build_stat(font_cp, siblings) + return font_cp diff --git a/Lib/fontbakery/utils.py b/Lib/fontbakery/utils.py index 09dc0e4048..92af6a4b3f 100644 --- a/Lib/fontbakery/utils.py +++ b/Lib/fontbakery/utils.py @@ -218,6 +218,30 @@ def bullet_list(config, items, bullet="-", indentation="\t"): glue=f"\n\n{indentation}{bullet} And") +def markdown_table(items): + """Format a list of dicts into a markdown table. + + >>> markdown_table( + >>> [{"name": "Sam", "age": 30}, {"name": "Ash", "age": 25}] + >>> ) + ... + | name | age | + | :--- | :--- | + | Sam | 30 | + | Ash | 25 | + """ + res = [] + header = "| " + " | ".join(items[0].keys()) + " |" + res.append(header) + lb = "|" + " :--- |" * len(items[0]) + res.append(lb) + for row in items: + vals = list(row.values()) + r = "| " + " | ".join(map(str, vals)) + " |" + res.append(r) + return "\n".join(res) + + def get_regular(fonts): # TODO: Maybe also support getting a regular instance from a variable font? for font in fonts: diff --git a/data/test/varfont/Georama[wdth,wght].ttf b/data/test/varfont/Georama[wdth,wght].ttf new file mode 100644 index 0000000000..c83df204ad Binary files /dev/null and b/data/test/varfont/Georama[wdth,wght].ttf differ diff --git a/data/test/varfont/RobotoSerif[GRAD,opsz,wdth,wght].ttf b/data/test/varfont/RobotoSerif[GRAD,opsz,wdth,wght].ttf new file mode 100644 index 0000000000..402b8a7edc Binary files /dev/null and b/data/test/varfont/RobotoSerif[GRAD,opsz,wdth,wght].ttf differ diff --git a/tests/profiles/googlefonts_test.py b/tests/profiles/googlefonts_test.py index 0f2e6f0737..3df6db7109 100644 --- a/tests/profiles/googlefonts_test.py +++ b/tests/profiles/googlefonts_test.py @@ -1,3 +1,4 @@ +from fontbakery.profiles.googlefonts_conditions import expected_font_names import pytest import os from fontTools.ttLib import TTFont @@ -161,66 +162,51 @@ def test_example_checkrunner_based(cabin_regular_path): break -def test_check_canonical_filename(): +@pytest.mark.parametrize( + """fp,result""", + [ + (TEST_FILE("montserrat/Montserrat-Thin.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-Light.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-Regular.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-Medium.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-SemiBold.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-Bold.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-ExtraBold.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-Black.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-ThinItalic.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-ExtraLightItalic.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-LightItalic.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-Italic.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-MediumItalic.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-SemiBoldItalic.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-ExtraBoldItalic.ttf"), PASS), + (TEST_FILE("montserrat/Montserrat-BlackItalic.ttf"), PASS), + (TEST_FILE("cabinvfbeta/CabinVFBeta-Italic[wght].ttf"), PASS), + (TEST_FILE("cabinvfbeta/CabinVFBeta[wdth,wght].ttf"), PASS), # axis tags are sorted + (TEST_FILE("cabinvfbeta/CabinVFBeta.ttf"), FAIL), + (TEST_FILE("cabinvfbeta/Cabin-Italic.ttf"), FAIL), + (TEST_FILE("cabinvfbeta/Cabin-Roman.ttf"), FAIL), + (TEST_FILE("cabinvfbeta/Cabin-Italic-VF.ttf"), FAIL), + (TEST_FILE("cabinvfbeta/Cabin-Roman-VF.ttf"), FAIL), + (TEST_FILE("cabinvfbeta/Cabin-VF.ttf"), FAIL), + (TEST_FILE("cabinvfbeta/CabinVFBeta[wght,wdth].ttf"), FAIL), # axis tags are NOT sorted here + ] +) +def test_check_canonical_filename(fp, result): """ Files are named canonically. """ check = CheckTester(googlefonts_profile, "com.google.fonts/check/canonical_filename") + ttFont = TTFont(fp) - static_canonical_names = [ - TEST_FILE("montserrat/Montserrat-Thin.ttf"), - TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), - TEST_FILE("montserrat/Montserrat-Light.ttf"), - TEST_FILE("montserrat/Montserrat-Regular.ttf"), - TEST_FILE("montserrat/Montserrat-Medium.ttf"), - TEST_FILE("montserrat/Montserrat-SemiBold.ttf"), - TEST_FILE("montserrat/Montserrat-Bold.ttf"), - TEST_FILE("montserrat/Montserrat-ExtraBold.ttf"), - TEST_FILE("montserrat/Montserrat-Black.ttf"), - TEST_FILE("montserrat/Montserrat-ThinItalic.ttf"), - TEST_FILE("montserrat/Montserrat-ExtraLightItalic.ttf"), - TEST_FILE("montserrat/Montserrat-LightItalic.ttf"), - TEST_FILE("montserrat/Montserrat-Italic.ttf"), - TEST_FILE("montserrat/Montserrat-MediumItalic.ttf"), - TEST_FILE("montserrat/Montserrat-SemiBoldItalic.ttf"), - TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"), - TEST_FILE("montserrat/Montserrat-ExtraBoldItalic.ttf"), - TEST_FILE("montserrat/Montserrat-BlackItalic.ttf"), - ] - - varfont_canonical_names = [ - TEST_FILE("cabinvfbeta/CabinVFBeta-Italic[wght].ttf"), - TEST_FILE("cabinvfbeta/CabinVFBeta[wdth,wght].ttf"), # axis tags are sorted - ] - - non_canonical_names = [ - TEST_FILE("cabinvfbeta/CabinVFBeta.ttf"), - TEST_FILE("cabinvfbeta/Cabin-Italic.ttf"), - TEST_FILE("cabinvfbeta/Cabin-Roman.ttf"), - TEST_FILE("cabinvfbeta/Cabin-Italic-VF.ttf"), - TEST_FILE("cabinvfbeta/Cabin-Roman-VF.ttf"), - TEST_FILE("cabinvfbeta/Cabin-VF.ttf"), - TEST_FILE("cabinvfbeta/CabinVFBeta[wght,wdth].ttf"), # axis tags are NOT sorted here - ] - - for canonical in static_canonical_names + varfont_canonical_names: - assert_PASS(check(canonical), - f'with "{canonical}" ...') - - for non_canonical in non_canonical_names: - assert_results_contain(check(non_canonical), - FAIL, 'bad-varfont-filename', - f'with "{non_canonical}" ...') - - assert_results_contain(check(TEST_FILE("Bad_Name.ttf")), - FAIL, 'invalid-char', - 'with filename containing an underscore...') - - assert_results_contain(check(TEST_FILE("mutatorsans-vf/MutatorSans-VF.ttf")), - FAIL, 'unknown-name', - 'with a variable font that lacks some important name table entries...') - - # TODO: FAIL, 'bad-static-filename' - # TODO: FAIL, 'varfont-with-static-filename' + if result == PASS: + assert_PASS(check(ttFont), + f'with "{ttFont.reader.file.name}" ...') + else: + assert_results_contain(check(ttFont), + FAIL, 'bad-filename', + f'with "{ttFont.reader.file.name}" ...') def test_check_description_broken_links(): @@ -2424,6 +2410,132 @@ def test_check_metadata_category(): f'with "{good_value}"...') +@pytest.mark.parametrize( + """fp,mod,result""", + [ + # tests from test_check_name_familyname: + (TEST_FILE("cabin/Cabin-Regular.ttf"), {}, PASS), + (TEST_FILE("cabin/Cabin-Regular.ttf"), {NameID.FONT_FAMILY_NAME: "Wrong"}, FAIL), + (TEST_FILE("overpassmono/OverpassMono-Regular.ttf"), {}, PASS), + (TEST_FILE("overpassmono/OverpassMono-Bold.ttf"), {}, PASS), + (TEST_FILE("overpassmono/OverpassMono-Regular.ttf"), {1: "Foo"}, FAIL), + (TEST_FILE("merriweather/Merriweather-Black.ttf"), {}, PASS), + (TEST_FILE("merriweather/Merriweather-LightItalic.ttf"), {}, PASS), + (TEST_FILE("merriweather/Merriweather-LightItalic.ttf"), {NameID.FONT_FAMILY_NAME: "Merriweather Light Italic"}, FAIL), + (TEST_FILE("abeezee/ABeeZee-Regular.ttf"), {}, PASS), + # tests from test_check_name_subfamilyname + (TEST_FILE("overpassmono/OverpassMono-Regular.ttf"), {}, PASS), + (TEST_FILE("overpassmono/OverpassMono-Bold.ttf"), {}, PASS), + (TEST_FILE("merriweather/Merriweather-Black.ttf"), {}, PASS), + (TEST_FILE("merriweather/Merriweather-LightItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-BlackItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-Black.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-Bold.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-ExtraBoldItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-ExtraBold.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-ExtraLightItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-Italic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-LightItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-Light.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-MediumItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-Medium.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-Regular.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-SemiBoldItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-SemiBold.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-ThinItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-Thin.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-ThinItalic.ttf"), {NameID.FONT_SUBFAMILY_NAME: "Not a proper style"}, FAIL), + # tests from test_check_name_fullfontname + (TEST_FILE("cabin/Cabin-Regular.ttf"), {}, PASS), + # warn should be raised since full name is missing Regular + (TEST_FILE("cabin/Cabin-Regular.ttf"), {4: "Cabin"}, WARN), + (TEST_FILE("cabin/Cabin-BoldItalic.ttf"), {}, PASS), + (TEST_FILE("cabin/Cabin-BoldItalic.ttf"), {NameID.FULL_FONT_NAME: "Make it fail"}, FAIL), + (TEST_FILE("abeezee/ABeeZee-Regular.ttf"), {}, PASS), + # tests from test_check_name_typographicfamilyname + (TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"), {NameID.TYPOGRAPHIC_FAMILY_NAME: "Arbitrary name"}, FAIL), + (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), {NameID.TYPOGRAPHIC_FAMILY_NAME: "Foo"}, FAIL), + (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), {NameID.TYPOGRAPHIC_FAMILY_NAME: None}, FAIL), + # tests from test_check_name_typographicsubfamilyname + (TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"), {NameID.TYPOGRAPHIC_SUBFAMILY_NAME: "Foo"}, FAIL), + (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), {}, PASS), + (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), {NameID.TYPOGRAPHIC_SUBFAMILY_NAME: None}, FAIL), + (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), {NameID.TYPOGRAPHIC_SUBFAMILY_NAME: "Generic Name"}, FAIL), + # variable font checks + (TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"), {}, PASS), + # Open Sans' origin is Light so this should pass + (TEST_FILE("varfont/OpenSans[wdth,wght].ttf"), {2: "Regular", 17: "Light"}, PASS), + (TEST_FILE("varfont/OpenSans[wdth,wght].ttf"), {2: "Regular", 17: "Condensed Light"}, FAIL), + (TEST_FILE("varfont/RobotoSerif[GRAD,opsz,wdth,wght].ttf"), {}, PASS), + # Roboto Serif has an opsz axes so this should pass + ( + TEST_FILE("varfont/RobotoSerif[GRAD,opsz,wdth,wght].ttf"), + { + NameID.FONT_FAMILY_NAME: "Roboto Serif 20pt", + NameID.FONT_SUBFAMILY_NAME: "Regular", + NameID.TYPOGRAPHIC_FAMILY_NAME: "Roboto Serif", + NameID.TYPOGRAPHIC_SUBFAMILY_NAME: "20pt Regular", + }, + PASS), + (TEST_FILE("varfont/Georama[wdth,wght].ttf"), {}, PASS), + # Georama's default fvar vals are wdth=62.5, wght=100 + # which means ExtraCondensed Thin should appear in the family name + (TEST_FILE("varfont/Georama[wdth,wght].ttf"), + { + NameID.FONT_FAMILY_NAME: "Georama ExtraCondensed Thin", + NameID.FONT_SUBFAMILY_NAME: "Regular", + NameID.TYPOGRAPHIC_FAMILY_NAME: "Georama", + NameID.TYPOGRAPHIC_SUBFAMILY_NAME: "ExtraCondensed Thin", + }, + PASS), + ] +) +def test_check_font_names(fp, mod, result): + """Check font names are correct""" + # Please note: This check was introduced in + # https://github.com/googlefonts/fontbakery/pull/3800 which has replaced + # the following checks: + # com.google.fonts/check/name/familyname + # com.google.fonts/check/name/subfamilyname + # com.google.fonts/check/name/typographicfamilyname + # com.google.fonts/check/name/typographicsubfamilyname + # It works by simply using the nametable builder which is found in the + # axis registry, + # https://github.com/googlefonts/axisregistry/blob/main/Lib/axisregistry/__init__.py#L232 + # this repository already has good unit tests but this check will also include the previous + # test cases found in fontbakery. + # https://github.com/googlefonts/axisregistry/blob/main/tests/test_names.py + from fontbakery.profiles.googlefonts_conditions import expected_font_names + check = CheckTester(googlefonts_profile, + "com.google.fonts/check/font_names") + ttFont = TTFont(fp) + # get the expecteed font names now before we modify them + expected = expected_font_names(ttFont, []) + if mod: + for k,v in mod.items(): + if v is None: + ttFont['name'].removeNames(k) + else: + ttFont['name'].setName(v, k, 3, 1, 0x409) + + if result == PASS: + assert_PASS(check(ttFont, {"expected_font_names": expected}), + "with a good font...") + elif result == WARN: + assert_results_contain(check(ttFont, {"expected_font_names": expected}), + WARN, 'lacks-regular', + f'with bad names') + else: + assert_results_contain(check(ttFont, {"expected_font_names": expected}), + FAIL, 'bad-names', + f'with bad names') + + def test_check_name_mandatory_entries(): """ Font has all mandatory 'name' table entries ? """ check = CheckTester(googlefonts_profile, @@ -2487,294 +2599,6 @@ def test_condition_familyname_with_spaces(): assert familyname_with_spaces("BodoniModa11") == "Bodoni Moda 11" -def test_check_name_familyname(): - """ Check name table: FONT_FAMILY_NAME entries. """ - check = CheckTester(googlefonts_profile, - "com.google.fonts/check/name/familyname") - - # TODO: FAIL, "lacks-name" - - test_cases = [ - #expect filename mac_value win_value - (PASS, "ok", TEST_FILE("cabin/Cabin-Regular.ttf"), "Cabin", "Cabin"), - (FAIL, "mismatch", TEST_FILE("cabin/Cabin-Regular.ttf"), "Wrong", "Cabin"), - (PASS, "ok", TEST_FILE("overpassmono/OverpassMono-Regular.ttf"), "Overpass Mono", "Overpass Mono"), - (PASS, "ok", TEST_FILE("overpassmono/OverpassMono-Bold.ttf"), "Overpass Mono", "Overpass Mono"), - (FAIL, "mismatch", TEST_FILE("overpassmono/OverpassMono-Regular.ttf"), "Overpass Mono", "Foo"), - (PASS, "ok", TEST_FILE("merriweather/Merriweather-Black.ttf"), "Merriweather", "Merriweather Black"), - (PASS, "ok", TEST_FILE("merriweather/Merriweather-LightItalic.ttf"), "Merriweather", "Merriweather Light"), - (FAIL, "mismatch", TEST_FILE("merriweather/Merriweather-LightItalic.ttf"), "Merriweather", "Merriweather Light Italic"), - (PASS, "ok", TEST_FILE("abeezee/ABeeZee-Regular.ttf"), "ABeeZee", "ABeeZee"), - # Note: ABeeZee is a good camel-cased name exception. - ] - - for expected, keyword, filename, mac_value, win_value in test_cases: - ttFont = TTFont(filename) - for i, name in enumerate(ttFont['name'].names): - if name.platformID == PlatformID.MACINTOSH: - value = mac_value - if name.platformID == PlatformID.WINDOWS: - value = win_value - assert value - - if name.nameID == NameID.FONT_FAMILY_NAME: - ttFont['name'].names[i].string = value.encode(name.getEncoding()) - assert_results_contain(check(ttFont), - expected, keyword, - f'with filename="{filename}",' - f' value="{value}", style="{check["style"]}"...') - - -def test_check_name_subfamilyname(): - """ Check name table: FONT_SUBFAMILY_NAME entries. """ - check = CheckTester(googlefonts_profile, - "com.google.fonts/check/name/subfamilyname") - - PASS_test_cases = [ - # filename mac_value win_value - (TEST_FILE("overpassmono/OverpassMono-Regular.ttf"), "Regular", "Regular"), - (TEST_FILE("overpassmono/OverpassMono-Bold.ttf"), "Bold", "Bold"), - (TEST_FILE("merriweather/Merriweather-Black.ttf"), "Black", "Regular"), - (TEST_FILE("merriweather/Merriweather-LightItalic.ttf"), "Light Italic", "Italic"), - (TEST_FILE("montserrat/Montserrat-BlackItalic.ttf"), "Black Italic", "Italic"), - (TEST_FILE("montserrat/Montserrat-Black.ttf"), "Black", "Regular"), - (TEST_FILE("montserrat/Montserrat-BoldItalic.ttf"), "Bold Italic", "Bold Italic"), - (TEST_FILE("montserrat/Montserrat-Bold.ttf"), "Bold", "Bold"), - (TEST_FILE("montserrat/Montserrat-ExtraBoldItalic.ttf"), "ExtraBold Italic", "Italic"), - (TEST_FILE("montserrat/Montserrat-ExtraBold.ttf"), "ExtraBold", "Regular"), - (TEST_FILE("montserrat/Montserrat-ExtraLightItalic.ttf"), "ExtraLight Italic", "Italic"), - (TEST_FILE("montserrat/Montserrat-ExtraLight.ttf"), "ExtraLight", "Regular"), - (TEST_FILE("montserrat/Montserrat-Italic.ttf"), "Italic", "Italic"), - (TEST_FILE("montserrat/Montserrat-LightItalic.ttf"), "Light Italic", "Italic"), - (TEST_FILE("montserrat/Montserrat-Light.ttf"), "Light", "Regular"), - (TEST_FILE("montserrat/Montserrat-MediumItalic.ttf"), "Medium Italic", "Italic"), - (TEST_FILE("montserrat/Montserrat-Medium.ttf"), "Medium", "Regular"), - (TEST_FILE("montserrat/Montserrat-Regular.ttf"), "Regular", "Regular"), - (TEST_FILE("montserrat/Montserrat-SemiBoldItalic.ttf"), "SemiBold Italic", "Italic"), - (TEST_FILE("montserrat/Montserrat-SemiBold.ttf"), "SemiBold", "Regular"), - (TEST_FILE("montserrat/Montserrat-ThinItalic.ttf"), "Thin Italic", "Italic"), - (TEST_FILE("montserrat/Montserrat-Thin.ttf"), "Thin", "Regular") - ] - - for filename, mac_value, win_value in PASS_test_cases: - ttFont = TTFont(filename) - for i, name in enumerate(ttFont['name'].names): - if name.platformID == PlatformID.MACINTOSH: - value = mac_value - if name.platformID == PlatformID.WINDOWS: - value = win_value - assert value - - if name.nameID == NameID.FONT_SUBFAMILY_NAME: - ttFont['name'].names[i].string = value.encode(name.getEncoding()) - - results = check(ttFont) - style = check["expected_style"] - assert_PASS(results, - f"with filename='{filename}', value='{value}', " - f"style_win='{style.win_style_name}', " - f"style_mac='{style.mac_style_name}'...") - - # - FAIL, "bad-familyname" - "Bad familyname value on a FONT_SUBFAMILY_NAME entry." - filename = TEST_FILE("montserrat/Montserrat-ThinItalic.ttf") - ttFont = TTFont(filename) - # We setup a bad entry: - ttFont["name"].setName("Not a proper style", - NameID.FONT_SUBFAMILY_NAME, - PlatformID.MACINTOSH, - MacintoshEncodingID.ROMAN, - MacintoshLanguageID.ENGLISH) - - # And this should now FAIL: - assert_results_contain(check(ttFont), - FAIL, 'bad-familyname') - - # Repeat this for a Win subfamily name - ttFont = TTFont(filename) - ttFont["name"].setName("Not a proper style", - NameID.FONT_SUBFAMILY_NAME, - PlatformID.WINDOWS, - WindowsEncodingID.UNICODE_BMP, - WindowsLanguageID.ENGLISH_USA) - assert_results_contain(check(ttFont), - FAIL, 'bad-familyname') - - -def test_check_name_fullfontname(): - """ Check name table: FULL_FONT_NAME entries. """ - check = CheckTester(googlefonts_profile, - "com.google.fonts/check/name/fullfontname") - - # Our reference Cabin Regular is known to be good - ttFont = TTFont(TEST_FILE("cabin/Cabin-Regular.ttf")) - assert_PASS(check(ttFont), - "with a good Regular font...") - - # Let's now test the Regular exception - # ('Regular' can be optionally ommited on the FULL_FONT_NAME entry): - for index, name in enumerate(ttFont["name"].names): - if name.nameID == NameID.FULL_FONT_NAME: - backup = name.string - ttFont["name"].names[index].string = "Cabin".encode(name.getEncoding()) - assert_results_contain(check(ttFont), - WARN, 'lacks-regular', - 'with a good Regular font that omits "Regular" on FULL_FONT_NAME...') - # restore it: - ttFont["name"].names[index].string = backup - - # Let's also make sure our good reference Cabin BoldItalic PASSes the check. - # This also tests the splitting of filename infered style with a space char - ttFont = TTFont(TEST_FILE("cabin/Cabin-BoldItalic.ttf")) - assert_PASS(check(ttFont), - "with a good Bold Italic font...") - - # And here we test the FAIL codepath: - for index, name in enumerate(ttFont["name"].names): - if name.nameID == NameID.FULL_FONT_NAME: - backup = name.string - ttFont["name"].names[index].string = "MAKE IT FAIL".encode(name.getEncoding()) - assert_results_contain(check(ttFont), - FAIL, 'bad-entry', - 'with a bad FULL_FONT_NAME entry...') - # restore it: - ttFont["name"].names[index].string = backup - - # And we should also accept a few camel-cased familyname exceptions, - # so this one should also be fine: - ttFont = TTFont(TEST_FILE("abeezee/ABeeZee-Regular.ttf")) - assert_PASS(check(ttFont), - "with a good camel-cased fontname...") - - -def NOT_IMPLEMENTED_test_check_name_postscriptname(): - """ Check name table: POSTSCRIPT_NAME entries. """ - # check = CheckTester(googlefonts_profile, - # "com.google.fonts/check/name/postscriptname") - # TODO: Implement-me! - # - # code-paths: - # - FAIL, "bad-entry" - # - PASS - - -def test_check_name_typographicfamilyname(): - """ Check name table: TYPOGRAPHIC_FAMILY_NAME entries. """ - check = CheckTester(googlefonts_profile, - "com.google.fonts/check/name/typographicfamilyname") - - # RIBBI fonts must not have a TYPOGRAPHIC_FAMILY_NAME entry - ttFont = TTFont(TEST_FILE("montserrat/Montserrat-BoldItalic.ttf")) - assert_PASS(check(ttFont), - f"with a RIBBI without nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entry...") - - # so we add one and make sure the check reports the problem: - ttFont['name'].names[5].nameID = NameID.TYPOGRAPHIC_FAMILY_NAME # 5 is arbitrary here - assert_results_contain(check(ttFont), - FAIL, 'ribbi', - f'with a RIBBI that has got a nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entry...') - - # non-RIBBI fonts must have a TYPOGRAPHIC_FAMILY_NAME entry - ttFont = TTFont(TEST_FILE("montserrat/Montserrat-ExtraLight.ttf")) - assert_PASS(check(ttFont), - f"with a non-RIBBI containing a nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entry...") - - # set bad values on all TYPOGRAPHIC_FAMILY_NAME entries: - for i, name in enumerate(ttFont['name'].names): - if name.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME: - ttFont['name'].names[i].string = "foo".encode(name.getEncoding()) - - assert_results_contain(check(ttFont), - FAIL, 'non-ribbi-bad-value', - 'with a non-RIBBI with bad nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entries...') - - # remove all TYPOGRAPHIC_FAMILY_NAME entries - # by changing their nameid to something else: - for i, name in enumerate(ttFont['name'].names): - if name.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME: - ttFont['name'].names[i].nameID = 255 # blah! :-) - - assert_results_contain(check(ttFont), - FAIL, 'non-ribbi-lacks-entry', - f'with a non-RIBBI lacking a nameid={NameID.TYPOGRAPHIC_FAMILY_NAME} entry...') - - -def test_check_name_typographicsubfamilyname(): - """ Check name table: TYPOGRAPHIC_SUBFAMILY_NAME entries. """ - check = CheckTester(googlefonts_profile, - "com.google.fonts/check/name/typographicsubfamilyname") - - RIBBI = "montserrat/Montserrat-BoldItalic.ttf" - NON_RIBBI = "montserrat/Montserrat-ExtraLight.ttf" - - # Add incorrect TYPOGRAPHIC_SUBFAMILY_NAME entries to a RIBBI font - ttFont = TTFont(TEST_FILE(RIBBI)) - ttFont['name'].setName("FOO", - NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - PlatformID.WINDOWS, - WindowsEncodingID.UNICODE_BMP, - WindowsLanguageID.ENGLISH_USA) - ttFont['name'].setName("BAR", - NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - PlatformID.MACINTOSH, - MacintoshEncodingID.ROMAN, - MacintoshLanguageID.ENGLISH) - assert_results_contain(check(ttFont), - FAIL, 'mismatch', - f'with a RIBBI that has got incorrect' - f' nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entries...') - assert_results_contain(check(ttFont), - FAIL, 'bad-win-name') - assert_results_contain(check(ttFont), - FAIL, 'bad-mac-name') - - # non-RIBBI fonts must have a TYPOGRAPHIC_SUBFAMILY_NAME entry - ttFont = TTFont(TEST_FILE(NON_RIBBI)) - assert_PASS(check(ttFont), - f'with a non-RIBBI containing a nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entry...') - - # set bad values on the win TYPOGRAPHIC_SUBFAMILY_NAME entry: - ttFont = TTFont(TEST_FILE(NON_RIBBI)) - ttFont['name'].setName("Generic subfamily name", - NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - PlatformID.WINDOWS, - WindowsEncodingID.UNICODE_BMP, - WindowsLanguageID.ENGLISH_USA) - assert_results_contain(check(ttFont), - FAIL, 'bad-typo-win', - f'with a non-RIBBI with bad nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entries...') - - # set bad values on the mac TYPOGRAPHIC_SUBFAMILY_NAME entry: - ttFont = TTFont(TEST_FILE(NON_RIBBI)) - ttFont['name'].setName("Generic subfamily name", - NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - PlatformID.MACINTOSH, - MacintoshEncodingID.ROMAN, - MacintoshLanguageID.ENGLISH) - assert_results_contain(check(ttFont), - FAIL, 'bad-typo-mac', - f'with a non-RIBBI with bad nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entries...') - - - # remove all TYPOGRAPHIC_SUBFAMILY_NAME entries - ttFont = TTFont(TEST_FILE(NON_RIBBI)) - win_name = ttFont['name'].getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - PlatformID.WINDOWS, - WindowsEncodingID.UNICODE_BMP, - WindowsLanguageID.ENGLISH_USA) - mac_name = ttFont['name'].getName(NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - PlatformID.MACINTOSH, - MacintoshEncodingID.ROMAN, - MacintoshLanguageID.ENGLISH) - win_name.nameID = 254 - if mac_name: - mac_name.nameID = 255 - assert_results_contain(check(ttFont), - FAIL, 'missing-typo-win', - f'with a non-RIBBI lacking a nameid={NameID.TYPOGRAPHIC_SUBFAMILY_NAME} entry...') - # note: the check must not complain - # about the lack of a mac entry! - - def test_check_name_copyright_length(): """ Length of copyright notice must not exceed 500 characters. """ check = CheckTester(googlefonts_profile, @@ -3012,21 +2836,22 @@ def test_check_aat(): def test_check_fvar_name_entries(): """ All name entries referenced by fvar instances exist on the name table? """ + # TODO fix check = CheckTester(googlefonts_profile, - "com.google.fonts/check/fvar_name_entries") + "com.google.fonts/check/fvar_instances") + + ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf")) - # This broken version of the Expletus variable font, was where this kind of problem was first observed: - ttFont = TTFont(TEST_FILE("broken_expletus_vf/ExpletusSansBeta-VF.ttf")) + # rename the first fvar instance so the font is broken + ttFont["name"].setName("foo", 258, 3, 1, 0x409) # So it must FAIL the check: assert_results_contain(check(ttFont), - FAIL, 'missing-name', + FAIL, 'bad-fvar-instances', 'with a bad font...') - # If we add the name entry with id=265 (which was the one missing) - # then the check must now PASS: - from fontTools.ttLib.tables._n_a_m_e import makeName - ttFont["name"].names.append(makeName("Foo", 265, 1, 0, 0)) + # rename the first fvar instance so it is correct + ttFont["name"].setName("Regular", 258, 3, 1, 0x409) assert_PASS(check(ttFont), 'with a good font...') @@ -3034,13 +2859,14 @@ def test_check_fvar_name_entries(): def test_check_varfont_has_instances(): """ A variable font must have named instances. """ + # TODO Fix! check = CheckTester(googlefonts_profile, - "com.google.fonts/check/varfont_has_instances") + "com.google.fonts/check/fvar_instances") # ExpletusVF does have instances. # Note: The "broken" in the path name refers to something else. # (See test_check_fvar_name_entries) - ttFont = TTFont(TEST_FILE("broken_expletus_vf/ExpletusSansBeta-VF.ttf")) + ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf")) # So it must PASS the check: assert_PASS(check(ttFont), @@ -3051,14 +2877,14 @@ def test_check_varfont_has_instances(): del ttFont["fvar"].instances[0] assert_results_contain(check(ttFont), - FAIL, 'lacks-named-instances', + FAIL, 'bad-fvar-instances', 'with a bad font...') def test_check_varfont_weight_instances(): """ Variable font weight coordinates must be multiples of 100. """ check = CheckTester(googlefonts_profile, - "com.google.fonts/check/varfont_weight_instances") + "com.google.fonts/check/fvar_instances") # This copy of Markazi Text has an instance with # a 491 'wght' coordinate instead of 500. @@ -3066,12 +2892,15 @@ def test_check_varfont_weight_instances(): # So it must FAIL the check: assert_results_contain(check(ttFont), - FAIL, 'bad-coordinate', + FAIL, 'bad-fvar-instances', 'with a bad font...') # Let's then change the weight coordinates to make it PASS the check: + # instances are from 400-700 (Regular-Bold) so set start to 400 + wght_val = 400 for i, instance in enumerate(ttFont["fvar"].instances): - ttFont["fvar"].instances[i].coordinates['wght'] -= instance.coordinates['wght'] % 100 + ttFont["fvar"].instances[i].coordinates['wght'] = wght_val + wght_val += 100 assert_PASS(check(ttFont), 'with a good font...') @@ -3686,7 +3515,7 @@ def test_check_cjk_not_enough_glyphs(): def test_check_varfont_instance_coordinates(vf_ttFont): check = CheckTester(googlefonts_profile, - "com.google.fonts/check/varfont_instance_coordinates") + "com.google.fonts/check/fvar_instances") # OpenSans-Roman-VF is correct assert_PASS(check(vf_ttFont), @@ -3698,14 +3527,14 @@ def test_check_varfont_instance_coordinates(vf_ttFont): for axis in instance.coordinates.keys(): instance.coordinates[axis] = 0 assert_results_contain(check(vf_ttFont2), - FAIL, "bad-coordinate", + FAIL, "bad-fvar-instances", 'with a variable font which does not have' ' correct instance coordinates.') def test_check_varfont_instance_names(vf_ttFont): check = CheckTester(googlefonts_profile, - "com.google.fonts/check/varfont_instance_names") + "com.google.fonts/check/fvar_instances") assert_PASS(check(vf_ttFont), 'with a variable font which has correct instance names.') @@ -3714,7 +3543,7 @@ def test_check_varfont_instance_names(vf_ttFont): vf_ttFont2 = copy(vf_ttFont) for instance in vf_ttFont2['fvar'].instances: instance.subfamilyNameID = 300 - broken_name ="ExtraBlack Condensed 300pt" + broken_name = "ExtraBlack Condensed 300pt" vf_ttFont2['name'].setName(broken_name, 300, PlatformID.MACINTOSH, @@ -3726,13 +3555,13 @@ def test_check_varfont_instance_names(vf_ttFont): WindowsEncodingID.UNICODE_BMP, WindowsLanguageID.ENGLISH_USA) assert_results_contain(check(vf_ttFont2), - FAIL, 'bad-instance-names', + FAIL, 'bad-fvar-instances', 'with a variable font which does not have correct instance names.') def test_check_varfont_duplicate_instance_names(vf_ttFont): check = CheckTester(googlefonts_profile, - "com.google.fonts/check/varfont_duplicate_instance_names") + "com.google.fonts/check/fvar_instances") assert_PASS(check(vf_ttFont), 'with a variable font which has unique instance names.') @@ -3751,7 +3580,7 @@ def test_check_varfont_duplicate_instance_names(vf_ttFont): platEncID=WindowsEncodingID.UNICODE_BMP, langID=WindowsLanguageID.ENGLISH_USA) assert_results_contain(check(vf_ttFont2), - FAIL, 'duplicate-instance-names') + FAIL, 'bad-fvar-instances') def test_check_varfont_unsupported_axes(): @@ -4252,3 +4081,112 @@ def test_check_metadata_category_hints(): md.category[:] = ["DISPLAY"] assert_PASS(check(font, {"family_metadata": md}), f'with a good category "{md.category}" for familyname "{md.name}"...') + + +@pytest.mark.parametrize( + """fp,mod,result""", + [ + # font includes condensed fvar instances so it should fail + (TEST_FILE("cabinvfbeta/CabinVFBeta.ttf"), [], FAIL), + # official fonts have been fixed so this should pass + (TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"), [], PASS), + (TEST_FILE("cabinvf/Cabin-Italic[wdth,wght].ttf"), [], PASS), + # lets inject an instance which is not a multiple of 100 + (TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"), [("Book", 450)], FAIL), + ] +) +def test_check_fvar_instances(fp, mod, result): + """Check font fvar instances are correct""" + from fontTools.ttLib.tables._f_v_a_r import NamedInstance + + check = CheckTester(googlefonts_profile, + "com.google.fonts/check/fvar_instances") + ttFont = TTFont(fp) + expected = expected_font_names(ttFont, []) + if mod: + for name, wght_val in mod: + inst = NamedInstance() + inst.subfamilyNameID = ttFont['name'].addName(name) + inst.coordinates = {"wght": wght_val} + ttFont['fvar'].instances.append(inst) + + if result == PASS: + assert_PASS(check(ttFont, {"expected_font_names": expected}), + f'with a good font') + elif result == FAIL: + assert_results_contain(check(ttFont, {"expected_font_names": expected}), + FAIL, 'bad-fvar-instances', + 'with a bad font') + +@pytest.mark.parametrize( + """fps,new_stat,result""", + [ + # Fail (we didn't really know what we were doing at this stage) + ( + [ + TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"), + TEST_FILE("cabinvf/Cabin-Italic[wdth,wght].ttf"), + ], + [], + FAIL + ), + # Fix previous test for Cabin[wdth,wght].ttf + ( + [ + TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"), + TEST_FILE("cabinvf/Cabin-Italic[wdth,wght].ttf"), + ], + # STAT for Cabin[wdth,wght].ttf + [ + dict( + name="Weight", + tag="wght", + values=[ + dict(value=400, name="Regular", linkedValue=700.0, flags=0x2), + dict(value=500, name="Medium"), + dict(value=600, name="SemiBold"), + dict(value=700, name="Bold"), + ] + ), + dict( + name="Width", + tag="wdth", + values=[ + dict(value=75, name="Condensed"), + dict(value=87.5, name="SemiCondensed"), + dict(value=100, name="Normal", flags=0x2), + ] + ), + dict( + name="Italic", + tag="ital", + values=[ + dict(value=0.0, name="Normal", linkedValue=1.0, flags=0x2) + ] + ) + ], + PASS + ) + ] +) +def test_check_stat(fps, new_stat, result): + """Check STAT table Axis Values are correct""" + # more comprehensive checks are available in the axisregistry: + #https://github.com/googlefonts/axisregistry/blob/main/tests/test_names.py#L442 + # this check merely exists to check that everything is hooked up correctly + from fontTools.otlLib.builder import buildStatTable + check = CheckTester(googlefonts_profile, + "com.google.fonts/check/stat") + ttFonts = [TTFont(f) for f in fps] + ttFont = ttFonts[0] + expected = expected_font_names(ttFont, ttFonts) + if new_stat: + buildStatTable(ttFont, new_stat) + + if result == PASS: + assert_PASS(check(ttFont, {"expected_font_names": expected}), + f'with a good font') + elif result == FAIL: + assert_results_contain(check(ttFont, {"expected_font_names": expected}), + FAIL, 'bad-axis-values', + 'with a bad font')