From 9f49523a038434f474dde9257e902a69278d055d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Corr=C3=AAa=20da=20Silva=20Sanches?= Date: Fri, 22 Nov 2024 02:48:55 -0300 Subject: [PATCH] split out typenetwork checks into individual files --- .../checks/vendorspecific/typenetwork.py | 1320 ----------------- .../typenetwork/PUA_encoded_glyphs.py | 50 + .../vendorspecific/typenetwork/__init__.py | 0 .../typenetwork/composite_glyphs.py | 34 + .../typenetwork/family/__init__.py | 0 .../typenetwork/family/duplicated_names.py | 95 ++ .../family/equal_numbers_of_glyphs.py | 81 + .../family/tnum_horizontal_metrics.py | 78 + .../typenetwork/family/valid_strikeout.py | 20 + .../typenetwork/family/valid_underline.py | 20 + .../font_is_centered_vertically.py | 53 + .../typenetwork/glyph_coverage.py | 37 + .../vendorspecific/typenetwork/glyphsets.py | 379 +++++ .../vendorspecific/typenetwork/marks_width.py | 58 + .../typenetwork/name/__init__.py | 0 .../typenetwork/name/mandatory_entries.py | 85 ++ .../typenetwork/usweightclass.py | 146 ++ .../typenetwork/varfont/__init__.py | 0 .../varfont/axes_have_variation.py | 38 + .../typenetwork/varfont/fvar_axes_order.py | 52 + .../typenetwork/vertical_metrics.py | 102 ++ 21 files changed, 1328 insertions(+), 1320 deletions(-) delete mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/PUA_encoded_glyphs.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/__init__.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/composite_glyphs.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/family/__init__.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/family/duplicated_names.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/family/equal_numbers_of_glyphs.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/family/tnum_horizontal_metrics.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/family/valid_strikeout.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/family/valid_underline.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/font_is_centered_vertically.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/glyph_coverage.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/glyphsets.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/marks_width.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/name/__init__.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/name/mandatory_entries.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/usweightclass.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/__init__.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/axes_have_variation.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/fvar_axes_order.py create mode 100644 Lib/fontbakery/checks/vendorspecific/typenetwork/vertical_metrics.py diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork.py b/Lib/fontbakery/checks/vendorspecific/typenetwork.py deleted file mode 100644 index 8c3c3a285d..0000000000 --- a/Lib/fontbakery/checks/vendorspecific/typenetwork.py +++ /dev/null @@ -1,1320 +0,0 @@ -""" -Checks for Type Network -""" -import unicodedata -import string - -from fontbakery.testable import Font, CheckRunContext -from fontbakery.prelude import check, condition, Message, PASS, FAIL, WARN, SKIP, INFO -from fontbakery.utils import ( - bullet_list, - exit_with_install_instructions, - pretty_print_list, -) -from fontbakery.constants import ( - NameID, - PlatformID, - WindowsEncodingID, - WindowsLanguageID, -) - - -@check( - id="typenetwork/glyph_coverage", - rationale=""" - Type Network expects that fonts in its catalog support at least the minimal - set of characters. - """, - conditions=["font_codepoints"], - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def check_glyph_coverage(ttFont, font_codepoints, config): - """Check Type Network minimum glyph coverage.""" - try: - import unicodedata2 - except ImportError: - exit_with_install_instructions("typenetwork") - - TN_latin_set = { - 0x0020: (" ", "SPACE"), - 0x0021: ("!", "EXCLAMATION MARK"), - 0x0022: ('"', "QUOTATION MARK"), - 0x0023: ("#", "NUMBER SIGN"), - 0x0024: ("$", "DOLLAR SIGN"), - 0x0025: ("%", "PERCENT SIGN"), - 0x0026: ("&", "AMPERSAND"), - 0x0027: ("'", "APOSTROPHE"), - 0x0028: ("(", "LEFT PARENTHESIS"), - 0x0029: (")", "RIGHT PARENTHESIS"), - 0x002A: ("*", "ASTERISK"), - 0x002B: ("+", "PLUS SIGN"), - 0x002C: (",", "COMMA"), - 0x002D: ("-", "HYPHEN-MINUS"), - 0x002E: (".", "FULL STOP"), - 0x002F: ("/", "SOLIDUS"), - 0x0030: ("0", "DIGIT ZERO"), - 0x0031: ("1", "DIGIT ONE"), - 0x0032: ("2", "DIGIT TWO"), - 0x0033: ("3", "DIGIT THREE"), - 0x0034: ("4", "DIGIT FOUR"), - 0x0035: ("5", "DIGIT FIVE"), - 0x0036: ("6", "DIGIT SIX"), - 0x0037: ("7", "DIGIT SEVEN"), - 0x0038: ("8", "DIGIT EIGHT"), - 0x0039: ("9", "DIGIT NINE"), - 0x003A: (":", "COLON"), - 0x003B: (";", "SEMICOLON"), - 0x003C: ("<", "LESS-THAN SIGN"), - 0x003D: ("=", "EQUALS SIGN"), - 0x003E: (">", "GREATER-THAN SIGN"), - 0x003F: ("?", "QUESTION MARK"), - 0x0040: ("@", "COMMERCIAL AT"), - 0x0041: ("A", "LATIN CAPITAL LETTER A"), - 0x0042: ("B", "LATIN CAPITAL LETTER B"), - 0x0043: ("C", "LATIN CAPITAL LETTER C"), - 0x0044: ("D", "LATIN CAPITAL LETTER D"), - 0x0045: ("E", "LATIN CAPITAL LETTER E"), - 0x0046: ("F", "LATIN CAPITAL LETTER F"), - 0x0047: ("G", "LATIN CAPITAL LETTER G"), - 0x0048: ("H", "LATIN CAPITAL LETTER H"), - 0x0049: ("I", "LATIN CAPITAL LETTER I"), - 0x004A: ("J", "LATIN CAPITAL LETTER J"), - 0x004B: ("K", "LATIN CAPITAL LETTER K"), - 0x004C: ("L", "LATIN CAPITAL LETTER L"), - 0x004D: ("M", "LATIN CAPITAL LETTER M"), - 0x004E: ("N", "LATIN CAPITAL LETTER N"), - 0x004F: ("O", "LATIN CAPITAL LETTER O"), - 0x0050: ("P", "LATIN CAPITAL LETTER P"), - 0x0051: ("Q", "LATIN CAPITAL LETTER Q"), - 0x0052: ("R", "LATIN CAPITAL LETTER R"), - 0x0053: ("S", "LATIN CAPITAL LETTER S"), - 0x0054: ("T", "LATIN CAPITAL LETTER T"), - 0x0055: ("U", "LATIN CAPITAL LETTER U"), - 0x0056: ("V", "LATIN CAPITAL LETTER V"), - 0x0057: ("W", "LATIN CAPITAL LETTER W"), - 0x0058: ("X", "LATIN CAPITAL LETTER X"), - 0x0059: ("Y", "LATIN CAPITAL LETTER Y"), - 0x005A: ("Z", "LATIN CAPITAL LETTER Z"), - 0x005B: ("[", "LEFT SQUARE BRACKET"), - 0x005C: ("\\", "REVERSE SOLIDUS"), - 0x005D: ("]", "RIGHT SQUARE BRACKET"), - 0x005E: ("^", "ASCII CIRCUMFLEX ACCENT"), - 0x005F: ("_", "LOW LINE"), - 0x0060: ("`", "GRAVE ACCENT"), - 0x0061: ("a", "LATIN SMALL LETTER A"), - 0x0062: ("b", "LATIN SMALL LETTER B"), - 0x0063: ("c", "LATIN SMALL LETTER C"), - 0x0064: ("d", "LATIN SMALL LETTER D"), - 0x0065: ("e", "LATIN SMALL LETTER E"), - 0x0066: ("f", "LATIN SMALL LETTER F"), - 0x0067: ("g", "LATIN SMALL LETTER G"), - 0x0068: ("h", "LATIN SMALL LETTER H"), - 0x0069: ("i", "LATIN SMALL LETTER I"), - 0x006A: ("j", "LATIN SMALL LETTER J"), - 0x006B: ("k", "LATIN SMALL LETTER K"), - 0x006C: ("l", "LATIN SMALL LETTER L"), - 0x006D: ("m", "LATIN SMALL LETTER M"), - 0x006E: ("n", "LATIN SMALL LETTER N"), - 0x006F: ("o", "LATIN SMALL LETTER O"), - 0x0070: ("p", "LATIN SMALL LETTER P"), - 0x0071: ("q", "LATIN SMALL LETTER Q"), - 0x0072: ("r", "LATIN SMALL LETTER R"), - 0x0073: ("s", "LATIN SMALL LETTER S"), - 0x0074: ("t", "LATIN SMALL LETTER T"), - 0x0075: ("u", "LATIN SMALL LETTER U"), - 0x0076: ("v", "LATIN SMALL LETTER V"), - 0x0077: ("w", "LATIN SMALL LETTER W"), - 0x0078: ("x", "LATIN SMALL LETTER X"), - 0x0079: ("y", "LATIN SMALL LETTER Y"), - 0x007A: ("z", "LATIN SMALL LETTER Z"), - 0x007B: ("{", "LEFT CURLY BRACKET"), - 0x007C: ("|", "VERTICAL LINE"), - 0x007D: ("}", "RIGHT CURLY BRACKET"), - 0x007E: ("~", "TILDE"), - 0x00A0: (" ", "NO-BREAK SPACE"), - 0x00A1: ("¡", "INVERTED EXCLAMATION MARK"), - 0x00A2: ("¢", "CENT SIGN"), - 0x00A3: ("£", "POUND SIGN"), - 0x00A4: ("¤", "CURRENCY SIGN"), - 0x00A5: ("¥", "YEN SIGN"), - 0x00A6: ("¦", "BROKEN BAR"), - 0x00A7: ("§", "SECTION SIGN"), - 0x00A8: ("¨", "DIAERESIS"), - 0x00A9: ("©", "COPYRIGHT SIGN"), - 0x00AA: ("ª", "FEMININE ORDINAL INDICATOR"), - 0x00AB: ("«", "LEFT-POINTING DOUBLE ANGLE QUOTATION MARK"), - 0x00AC: ("¬", "NOT SIGN"), - 0x00AE: ("®", "REGISTERED SIGN"), - 0x00AF: ("¯", "MACRON"), - 0x00B0: ("°", "DEGREE SIGN"), - 0x00B1: ("±", "PLUS-MINUS SIGN"), - 0x00B2: ("²", "SUPERSCRIPT TWO"), - 0x00B3: ("³", "SUPERSCRIPT THREE"), - 0x00B4: ("´", "ACUTE ACCENT"), - 0x00B6: ("¶", "PILCROW SIGN"), - 0x00B7: ("·", "MIDDLE DOT"), - 0x00B8: ("¸", "CEDILLA"), - 0x00B9: ("¹", "SUPERSCRIPT ONE"), - 0x00BA: ("º", "MASCULINE ORDINAL INDICATOR"), - 0x00BB: ("»", "RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK"), - 0x00BC: ("¼", "VULGAR FRACTION ONE QUARTER"), - 0x00BD: ("½", "VULGAR FRACTION ONE HALF"), - 0x00BE: ("¾", "VULGAR FRACTION THREE QUARTERS"), - 0x00BF: ("¿", "INVERTED QUESTION MARK"), - 0x00C0: ("À", "LATIN CAPITAL LETTER A WITH GRAVE"), - 0x00C1: ("Á", "LATIN CAPITAL LETTER A WITH ACUTE"), - 0x00C2: ("Â", "LATIN CAPITAL LETTER A WITH CIRCUMFLEX"), - 0x00C3: ("Ã", "LATIN CAPITAL LETTER A WITH TILDE"), - 0x00C4: ("Ä", "LATIN CAPITAL LETTER A WITH DIAERESIS"), - 0x00C5: ("Å", "LATIN CAPITAL LETTER A WITH RING ABOVE"), - 0x00C6: ("Æ", "LATIN CAPITAL LETTER AE"), - 0x00C7: ("Ç", "LATIN CAPITAL LETTER C WITH CEDILLA"), - 0x00C8: ("È", "LATIN CAPITAL LETTER E WITH GRAVE"), - 0x00C9: ("É", "LATIN CAPITAL LETTER E WITH ACUTE"), - 0x00CA: ("Ê", "LATIN CAPITAL LETTER E WITH CIRCUMFLEX"), - 0x00CB: ("Ë", "LATIN CAPITAL LETTER E WITH DIAERESIS"), - 0x00CC: ("Ì", "LATIN CAPITAL LETTER I WITH GRAVE"), - 0x00CD: ("Í", "LATIN CAPITAL LETTER I WITH ACUTE"), - 0x00CE: ("Î", "LATIN CAPITAL LETTER I WITH CIRCUMFLEX"), - 0x00CF: ("Ï", "LATIN CAPITAL LETTER I WITH DIAERESIS"), - 0x00D0: ("Ð", "LATIN CAPITAL LETTER ETH"), - 0x00D1: ("Ñ", "LATIN CAPITAL LETTER N WITH TILDE"), - 0x00D2: ("Ò", "LATIN CAPITAL LETTER O WITH GRAVE"), - 0x00D3: ("Ó", "LATIN CAPITAL LETTER O WITH ACUTE"), - 0x00D4: ("Ô", "LATIN CAPITAL LETTER O WITH CIRCUMFLEX"), - 0x00D5: ("Õ", "LATIN CAPITAL LETTER O WITH TILDE"), - 0x00D6: ("Ö", "LATIN CAPITAL LETTER O WITH DIAERESIS"), - 0x00D7: ("×", "MULTIPLICATION SIGN"), - 0x00D8: ("Ø", "LATIN CAPITAL LETTER O WITH STROKE"), - 0x00D9: ("Ù", "LATIN CAPITAL LETTER U WITH GRAVE"), - 0x00DA: ("Ú", "LATIN CAPITAL LETTER U WITH ACUTE"), - 0x00DB: ("Û", "LATIN CAPITAL LETTER U WITH CIRCUMFLEX"), - 0x00DC: ("Ü", "LATIN CAPITAL LETTER U WITH DIAERESIS"), - 0x00DD: ("Ý", "LATIN CAPITAL LETTER Y WITH ACUTE"), - 0x00DE: ("Þ", "LATIN CAPITAL LETTER THORN"), - 0x00DF: ("ß", "LATIN SMALL LETTER SHARP S"), - 0x00E0: ("à", "LATIN SMALL LETTER A WITH GRAVE"), - 0x00E1: ("á", "LATIN SMALL LETTER A WITH ACUTE"), - 0x00E2: ("â", "LATIN SMALL LETTER A WITH CIRCUMFLEX"), - 0x00E3: ("ã", "LATIN SMALL LETTER A WITH TILDE"), - 0x00E4: ("ä", "LATIN SMALL LETTER A WITH DIAERESIS"), - 0x00E5: ("å", "LATIN SMALL LETTER A WITH RING ABOVE"), - 0x00E6: ("æ", "LATIN SMALL LETTER AE"), - 0x00E7: ("ç", "LATIN SMALL LETTER C WITH CEDILLA"), - 0x00E8: ("è", "LATIN SMALL LETTER E WITH GRAVE"), - 0x00E9: ("é", "LATIN SMALL LETTER E WITH ACUTE"), - 0x00EA: ("ê", "LATIN SMALL LETTER E WITH CIRCUMFLEX"), - 0x00EB: ("ë", "LATIN SMALL LETTER E WITH DIAERESIS"), - 0x00EC: ("ì", "LATIN SMALL LETTER I WITH GRAVE"), - 0x00ED: ("í", "LATIN SMALL LETTER I WITH ACUTE"), - 0x00EE: ("î", "LATIN SMALL LETTER I WITH CIRCUMFLEX"), - 0x00EF: ("ï", "LATIN SMALL LETTER I WITH DIAERESIS"), - 0x00F0: ("ð", "LATIN SMALL LETTER ETH"), - 0x00F1: ("ñ", "LATIN SMALL LETTER N WITH TILDE"), - 0x00F2: ("ò", "LATIN SMALL LETTER O WITH GRAVE"), - 0x00F3: ("ó", "LATIN SMALL LETTER O WITH ACUTE"), - 0x00F4: ("ô", "LATIN SMALL LETTER O WITH CIRCUMFLEX"), - 0x00F5: ("õ", "LATIN SMALL LETTER O WITH TILDE"), - 0x00F6: ("ö", "LATIN SMALL LETTER O WITH DIAERESIS"), - 0x00F7: ("÷", "DIVISION SIGN"), - 0x00F8: ("ø", "LATIN SMALL LETTER O WITH STROKE"), - 0x00F9: ("ù", "LATIN SMALL LETTER U WITH GRAVE"), - 0x00FA: ("ú", "LATIN SMALL LETTER U WITH ACUTE"), - 0x00FB: ("û", "LATIN SMALL LETTER U WITH CIRCUMFLEX"), - 0x00FC: ("ü", "LATIN SMALL LETTER U WITH DIAERESIS"), - 0x00FD: ("ý", "LATIN SMALL LETTER Y WITH ACUTE"), - 0x00FE: ("þ", "LATIN SMALL LETTER THORN"), - 0x00FF: ("ÿ", "LATIN SMALL LETTER Y WITH DIAERESIS"), - 0x0100: ("Ā", "LATIN CAPITAL LETTER A WITH MACRON"), - 0x0101: ("ā", "LATIN SMALL LETTER A WITH MACRON"), - 0x0102: ("Ă", "LATIN CAPITAL LETTER A WITH BREVE"), - 0x0103: ("ă", "LATIN SMALL LETTER A WITH BREVE"), - 0x0104: ("Ą", "LATIN CAPITAL LETTER A WITH OGONEK"), - 0x0105: ("ą", "LATIN SMALL LETTER A WITH OGONEK"), - 0x0106: ("Ć", "LATIN CAPITAL LETTER C WITH ACUTE"), - 0x0107: ("ć", "LATIN SMALL LETTER C WITH ACUTE"), - 0x0108: ("Ĉ", "LATIN CAPITAL LETTER C WITH CIRCUMFLEX"), - 0x0109: ("ĉ", "LATIN SMALL LETTER C WITH CIRCUMFLEX"), - 0x010A: ("Ċ", "LATIN CAPITAL LETTER C WITH DOT ABOVE"), - 0x010B: ("ċ", "LATIN SMALL LETTER C WITH DOT ABOVE"), - 0x010C: ("Č", "LATIN CAPITAL LETTER C WITH CARON"), - 0x010D: ("č", "LATIN SMALL LETTER C WITH CARON"), - 0x010E: ("Ď", "LATIN CAPITAL LETTER D WITH CARON"), - 0x010F: ("ď", "LATIN SMALL LETTER D WITH CARON"), - 0x0110: ("Đ", "LATIN CAPITAL LETTER D WITH STROKE"), - 0x0111: ("đ", "LATIN SMALL LETTER D WITH STROKE"), - 0x0112: ("Ē", "LATIN CAPITAL LETTER E WITH MACRON"), - 0x0113: ("ē", "LATIN SMALL LETTER E WITH MACRON"), - 0x0114: ("Ĕ", "LATIN CAPITAL LETTER E WITH BREVE"), - 0x0115: ("ĕ", "LATIN SMALL LETTER E WITH BREVE"), - 0x0116: ("Ė", "LATIN CAPITAL LETTER E WITH DOT ABOVE"), - 0x0117: ("ė", "LATIN SMALL LETTER E WITH DOT ABOVE"), - 0x0118: ("Ę", "LATIN CAPITAL LETTER E WITH OGONEK"), - 0x0119: ("ę", "LATIN SMALL LETTER E WITH OGONEK"), - 0x011A: ("Ě", "LATIN CAPITAL LETTER E WITH CARON"), - 0x011B: ("ě", "LATIN SMALL LETTER E WITH CARON"), - 0x011C: ("Ĝ", "LATIN CAPITAL LETTER G WITH CIRCUMFLEX"), - 0x011D: ("ĝ", "LATIN SMALL LETTER G WITH CIRCUMFLEX"), - 0x011E: ("Ğ", "LATIN CAPITAL LETTER G WITH BREVE"), - 0x011F: ("ğ", "LATIN SMALL LETTER G WITH BREVE"), - 0x0120: ("Ġ", "LATIN CAPITAL LETTER G WITH DOT ABOVE"), - 0x0121: ("ġ", "LATIN SMALL LETTER G WITH DOT ABOVE"), - 0x0122: ("Ģ", "LATIN CAPITAL LETTER G WITH CEDILLA"), - 0x0123: ("ģ", "LATIN SMALL LETTER G WITH CEDILLA"), - 0x0124: ("Ĥ", "LATIN CAPITAL LETTER H WITH CIRCUMFLEX"), - 0x0125: ("ĥ", "LATIN SMALL LETTER H WITH CIRCUMFLEX"), - 0x0126: ("Ħ", "LATIN CAPITAL LETTER H WITH STROKE"), - 0x0127: ("ħ", "LATIN SMALL LETTER H WITH STROKE"), - 0x0128: ("Ĩ", "LATIN CAPITAL LETTER I WITH TILDE"), - 0x0129: ("ĩ", "LATIN SMALL LETTER I WITH TILDE"), - 0x012A: ("Ī", "LATIN CAPITAL LETTER I WITH MACRON"), - 0x012B: ("ī", "LATIN SMALL LETTER I WITH MACRON"), - 0x012C: ("Ĭ", "LATIN CAPITAL LETTER I WITH BREVE"), - 0x012D: ("ĭ", "LATIN SMALL LETTER I WITH BREVE"), - 0x012E: ("Į", "LATIN CAPITAL LETTER I WITH OGONEK"), - 0x012F: ("į", "LATIN SMALL LETTER I WITH OGONEK"), - 0x0130: ("İ", "LATIN CAPITAL LETTER I WITH DOT ABOVE"), - 0x0131: ("ı", "LATIN SMALL LETTER DOTLESS I"), - 0x0132: ("IJ", "LATIN CAPITAL LIGATURE IJ"), - 0x0133: ("ij", "LATIN SMALL LIGATURE IJ"), - 0x0134: ("Ĵ", "LATIN CAPITAL LETTER J WITH CIRCUMFLEX"), - 0x0135: ("ĵ", "LATIN SMALL LETTER J WITH CIRCUMFLEX"), - 0x0136: ("Ķ", "LATIN CAPITAL LETTER K WITH CEDILLA"), - 0x0137: ("ķ", "LATIN SMALL LETTER K WITH CEDILLA"), - 0x0139: ("Ĺ", "LATIN CAPITAL LETTER L WITH ACUTE"), - 0x013A: ("ĺ", "LATIN SMALL LETTER L WITH ACUTE"), - 0x013B: ("Ļ", "LATIN CAPITAL LETTER L WITH CEDILLA"), - 0x013C: ("ļ", "LATIN SMALL LETTER L WITH CEDILLA"), - 0x013D: ("Ľ", "LATIN CAPITAL LETTER L WITH CARON"), - 0x013E: ("ľ", "LATIN SMALL LETTER L WITH CARON"), - 0x013F: ("Ŀ", "LATIN CAPITAL LETTER L WITH MIDDLE DOT"), - 0x0140: ("ŀ", "LATIN SMALL LETTER L WITH MIDDLE DOT"), - 0x0141: ("Ł", "LATIN CAPITAL LETTER L WITH STROKE"), - 0x0142: ("ł", "LATIN SMALL LETTER L WITH STROKE"), - 0x0143: ("Ń", "LATIN CAPITAL LETTER N WITH ACUTE"), - 0x0144: ("ń", "LATIN SMALL LETTER N WITH ACUTE"), - 0x0145: ("Ņ", "LATIN CAPITAL LETTER N WITH CEDILLA"), - 0x0146: ("ņ", "LATIN SMALL LETTER N WITH CEDILLA"), - 0x0147: ("Ň", "LATIN CAPITAL LETTER N WITH CARON"), - 0x0148: ("ň", "LATIN SMALL LETTER N WITH CARON"), - 0x014A: ("Ŋ", "LATIN CAPITAL LETTER ENG"), - 0x014B: ("ŋ", "LATIN SMALL LETTER ENG"), - 0x014C: ("Ō", "LATIN CAPITAL LETTER O WITH MACRON"), - 0x014D: ("ō", "LATIN SMALL LETTER O WITH MACRON"), - 0x014E: ("Ŏ", "LATIN CAPITAL LETTER O WITH BREVE"), - 0x014F: ("ŏ", "LATIN SMALL LETTER O WITH BREVE"), - 0x0150: ("Ő", "LATIN CAPITAL LETTER O WITH DOUBLE ACUTE"), - 0x0151: ("ő", "LATIN SMALL LETTER O WITH DOUBLE ACUTE"), - 0x0152: ("Œ", "LATIN CAPITAL LIGATURE OE"), - 0x0153: ("œ", "LATIN SMALL LIGATURE OE"), - 0x0154: ("Ŕ", "LATIN CAPITAL LETTER R WITH ACUTE"), - 0x0155: ("ŕ", "LATIN SMALL LETTER R WITH ACUTE"), - 0x0156: ("Ŗ", "LATIN CAPITAL LETTER R WITH CEDILLA"), - 0x0157: ("ŗ", "LATIN SMALL LETTER R WITH CEDILLA"), - 0x0158: ("Ř", "LATIN CAPITAL LETTER R WITH CARON"), - 0x0159: ("ř", "LATIN SMALL LETTER R WITH CARON"), - 0x015A: ("Ś", "LATIN CAPITAL LETTER S WITH ACUTE"), - 0x015B: ("ś", "LATIN SMALL LETTER S WITH ACUTE"), - 0x015C: ("Ŝ", "LATIN CAPITAL LETTER S WITH CIRCUMFLEX"), - 0x015D: ("ŝ", "LATIN SMALL LETTER S WITH CIRCUMFLEX"), - 0x015E: ("Ş", "LATIN CAPITAL LETTER S WITH CEDILLA"), - 0x015F: ("ş", "LATIN SMALL LETTER S WITH CEDILLA"), - 0x0160: ("Š", "LATIN CAPITAL LETTER S WITH CARON"), - 0x0161: ("š", "LATIN SMALL LETTER S WITH CARON"), - 0x0164: ("Ť", "LATIN CAPITAL LETTER T WITH CARON"), - 0x0165: ("ť", "LATIN SMALL LETTER T WITH CARON"), - 0x0166: ("Ŧ", "LATIN CAPITAL LETTER T WITH STROKE"), - 0x0167: ("ŧ", "LATIN SMALL LETTER T WITH STROKE"), - 0x0168: ("Ũ", "LATIN CAPITAL LETTER U WITH TILDE"), - 0x0169: ("ũ", "LATIN SMALL LETTER U WITH TILDE"), - 0x016A: ("Ū", "LATIN CAPITAL LETTER U WITH MACRON"), - 0x016B: ("ū", "LATIN SMALL LETTER U WITH MACRON"), - 0x016C: ("Ŭ", "LATIN CAPITAL LETTER U WITH BREVE"), - 0x016D: ("ŭ", "LATIN SMALL LETTER U WITH BREVE"), - 0x016E: ("Ů", "LATIN CAPITAL LETTER U WITH RING ABOVE"), - 0x016F: ("ů", "LATIN SMALL LETTER U WITH RING ABOVE"), - 0x0170: ("Ű", "LATIN CAPITAL LETTER U WITH DOUBLE ACUTE"), - 0x0171: ("ű", "LATIN SMALL LETTER U WITH DOUBLE ACUTE"), - 0x0172: ("Ų", "LATIN CAPITAL LETTER U WITH OGONEK"), - 0x0173: ("ų", "LATIN SMALL LETTER U WITH OGONEK"), - 0x0174: ("Ŵ", "LATIN CAPITAL LETTER W WITH CIRCUMFLEX"), - 0x0175: ("ŵ", "LATIN SMALL LETTER W WITH CIRCUMFLEX"), - 0x0176: ("Ŷ", "LATIN CAPITAL LETTER Y WITH CIRCUMFLEX"), - 0x0177: ("ŷ", "LATIN SMALL LETTER Y WITH CIRCUMFLEX"), - 0x0178: ("Ÿ", "LATIN CAPITAL LETTER Y WITH DIAERESIS"), - 0x0179: ("Ź", "LATIN CAPITAL LETTER Z WITH ACUTE"), - 0x017A: ("ź", "LATIN SMALL LETTER Z WITH ACUTE"), - 0x017B: ("Ż", "LATIN CAPITAL LETTER Z WITH DOT ABOVE"), - 0x017C: ("ż", "LATIN SMALL LETTER Z WITH DOT ABOVE"), - 0x017D: ("Ž", "LATIN CAPITAL LETTER Z WITH CARON"), - 0x017E: ("ž", "LATIN SMALL LETTER Z WITH CARON"), - 0x01FC: ("Ǽ", "LATIN CAPITAL LETTER AE WITH ACUTE"), - 0x01FD: ("ǽ", "LATIN SMALL LETTER AE WITH ACUTE"), - 0x01FE: ("Ǿ", "LATIN CAPITAL LETTER O WITH STROKE AND ACUTE"), - 0x01FF: ("ǿ", "LATIN SMALL LETTER O WITH STROKE AND ACUTE"), - 0x0218: ("Ș", "LATIN CAPITAL LETTER S WITH COMMA BELOW"), - 0x0219: ("ș", "LATIN SMALL LETTER S WITH COMMA BELOW"), - 0x021A: ("Ț", "LATIN CAPITAL LETTER T WITH COMMA BELOW"), - 0x021B: ("ț", "LATIN SMALL LETTER T WITH COMMA BELOW"), - 0x0237: ("ȷ", "LATIN SMALL LETTER DOTLESS J"), - 0x02C6: ("ˆ", "CIRCUMFLEX ACCENT"), - 0x02C7: ("ˇ", "CARON"), - 0x02D8: ("˘", "BREVE"), - 0x02D9: ("˙", "DOT ABOVE"), - 0x02DA: ("˚", "RING ABOVE"), - 0x02DB: ("˛", "OGONEK"), - 0x02DC: ("˜", "TILDE"), - 0x02DD: ("˝", "DOUBLE ACUTE ACCENT"), - 0x1E80: ("Ẁ", "LATIN CAPITAL LETTER W WITH GRAVE"), - 0x1E81: ("ẁ", "LATIN SMALL LETTER W WITH GRAVE"), - 0x1E82: ("Ẃ", "LATIN CAPITAL LETTER W WITH ACUTE"), - 0x1E83: ("ẃ", "LATIN SMALL LETTER W WITH ACUTE"), - 0x1E84: ("Ẅ", "LATIN CAPITAL LETTER W WITH DIAERESIS"), - 0x1E85: ("ẅ", "LATIN SMALL LETTER W WITH DIAERESIS"), - 0x1E9E: ("ẞ", "LATIN CAPITAL LETTER SHARP S"), - 0x1EF2: ("Ỳ", "LATIN CAPITAL LETTER Y WITH GRAVE"), - 0x1EF3: ("ỳ", "LATIN SMALL LETTER Y WITH GRAVE"), - 0x2013: ("–", "EN DASH"), - 0x2014: ("—", "EM DASH"), - 0x2018: ("‘", "LEFT SINGLE QUOTATION MARK"), - 0x2019: ("’", "RIGHT SINGLE QUOTATION MARK"), - 0x201A: ("‚", "SINGLE LOW-9 QUOTATION MARK"), - 0x201C: ("“", "LEFT DOUBLE QUOTATION MARK"), - 0x201D: ("”", "RIGHT DOUBLE QUOTATION MARK"), - 0x201E: ("„", "DOUBLE LOW-9 QUOTATION MARK"), - 0x2020: ("†", "DAGGER"), - 0x2021: ("‡", "DOUBLE DAGGER"), - 0x2022: ("•", "BULLET"), - 0x2026: ("…", "HORIZONTAL ELLIPSIS"), - 0x2030: ("‰", "PER MILLE SIGN"), - 0x2039: ("‹", "SINGLE LEFT-POINTING ANGLE QUOTATION MARK"), - 0x203A: ("›", "SINGLE RIGHT-POINTING ANGLE QUOTATION MARK"), - 0x2044: ("⁄", "FRACTION SLASH"), - 0x20AC: ("€", "EURO SIGN"), - 0x2122: ("™", "TRADE MARK SIGN"), - 0x2248: ("≈", "ALMOST EQUAL TO"), - 0x2260: ("≠", "NOT EQUAL TO"), - 0x2264: ("≤", "LESS-THAN OR EQUAL TO"), - 0x2265: ("≥", "GREATER-THAN OR EQUAL TO"), - 0x2212: ("−", "MINUS SIGN"), - # 0x00B5: ("µ", "MICRO SIGN"), - # 0x0394: ("Δ", "GREEK CAPITAL LETTER DELTA"), - # 0x03A9: ("Ω", "GREEK CAPITAL LETTER OMEGA"), - # 0x03BC: ("μ", "GREEK SMALL LETTER MU"), - # 0x03C0: ("π", "GREEK SMALL LETTER PI"), - # 0x2126: ("Ω", "OHM SIGN"), - # 0x2202: ("∂", "PARTIAL DIFFERENTIAL"), - # 0x2206: ("∆", "INCREMENT"), - # 0x220F: ("∏", "N-ARY PRODUCT"), - # 0x2211: ("∑", "N-ARY SUMMATION"), - # 0x221A: ("√", "SQUARE ROOT"), - # 0x221E: ("∞", "INFINITY"), - # 0x222B: ("∫", "INTEGRAL"), - # 0x25CA: ("◊", "LOZENGE"), - # 0xFB01: ("fi", "LATIN SMALL LIGATURE FI"), - # 0xFB02: ("fl", "LATIN SMALL LIGATURE FL"), - } - - required_codepoints = set(TN_latin_set) - diff = required_codepoints - font_codepoints - missing = [] - for c in sorted(diff): - try: - missing.append("uni%04X %s (%s)\n" % (c, chr(c), unicodedata2.name(chr(c)))) - except ValueError: - pass - if missing: - yield WARN, Message( - "missing-codepoints", - f"Missing required codepoints:\n\n" f"{bullet_list(config, missing)}", - ) - else: - yield PASS, "OK" - - -@check( - id="typenetwork/vertical_metrics", - rationale=""" - OS/2 and hhea vertical metric values should match. This will produce the - same linespacing on Mac, GNU+Linux and Windows. - - - Mac OS X uses the hhea values.⏎ - - Windows uses OS/2 or Win, depending on the OS or fsSelection bit value. - - When OS/2 and hhea vertical metrics match, the same linespacing results on - macOS, GNU+Linux and Windows. - """, - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def check_vertical_metrics(ttFont): - """Checking vertical metrics.""" - - # Check required tables exist on font - required_tables = {"hhea", "OS/2"} - missing_tables = sorted(required_tables - set(ttFont.keys())) - if missing_tables: - for table_tag in missing_tables: - yield FAIL, Message("lacks-table", f"Font lacks '{table_tag}' table.") - return - - useTypoMetric = ttFont["OS/2"].fsSelection & (1 << 7) - - hheaAscent_equals_typoAscent = ttFont["hhea"].ascent == ttFont["OS/2"].sTypoAscender - hheaDescent_equals_typoDescent = abs(ttFont["hhea"].descent) == abs( - ttFont["OS/2"].sTypoDescender - ) - - hheaAscent_equals_winAscent = ttFont["hhea"].ascent == ttFont["OS/2"].usWinAscent - hheaDescent_equals_winDescent = ( - abs(ttFont["hhea"].descent) == ttFont["OS/2"].usWinDescent - ) - - typoMetricsSum = ( - ttFont["OS/2"].sTypoAscender - + abs(ttFont["OS/2"].sTypoDescender) - + ttFont["OS/2"].sTypoLineGap - ) - hheaMetricsSum = ( - ttFont["hhea"].ascent + abs(ttFont["hhea"].descent) + ttFont["hhea"].lineGap - ) - - if useTypoMetric: - if not hheaAscent_equals_typoAscent: - yield FAIL, Message( - "ascender", - f"OS/2 sTypoAscender ({ttFont['OS/2'].sTypoAscender})" - f" and hhea ascent ({ttFont['hhea'].ascent}) must be equal.", - ) - elif not hheaDescent_equals_typoDescent: - yield FAIL, Message( - "descender", - f"OS/2 sTypoDescender ({ttFont['OS/2'].sTypoDescender})" - f" and hhea descent ({ttFont['hhea'].descent}) must be equal.", - ) - elif ttFont["OS/2"].sTypoLineGap != 0: - yield FAIL, Message("hhea", "typo lineGap is not equal to 0.") - elif ttFont["hhea"].lineGap != 0: - yield FAIL, Message("hhea", "hhea lineGap is not equal to 0.") - else: - yield PASS, "Typo and hhea metrics are equal." - else: - yield WARN, Message( - "metrics-recommendation", - "OS/2 fsSelection USE_TYPO_METRICS is not enabled.\n\n" - "Type Networks recommends to enable it and follow the vertical metrics" - " scheme where basically hhea matches typo metrics. Read in more detail" - " about it in our vertical metrics guide.", - ) - - if hheaAscent_equals_typoAscent and hheaDescent_equals_winDescent: - yield FAIL, Message( - "useTypoMetricsDisabled", - "OS/2.fsSelection bit 7 (USE_TYPO_METRICS) is not enabled", - ) - elif not hheaAscent_equals_winAscent: - yield FAIL, Message( - "ascender", - f"hhea ascent ({ttFont['hhea'].ascent})" - f" and OS/2 win ascent ({ttFont['OS/2'].usWinAscent}) must be equal.", - ) - elif not hheaDescent_equals_winDescent: - yield FAIL, Message( - "descender", - f"hhea descent ({ttFont['hhea'].descent})" - f" and OS/2 win ascent ({ttFont['OS/2'].usWinDescent}) must be equal.", - ) - elif typoMetricsSum != hheaMetricsSum: - yield FAIL, Message( - "typo-and-hhea-sum", - f"OS/2 typo metrics sum ({typoMetricsSum}) must be" - f" equal to win metrics sum ({hheaMetricsSum})", - ) - else: - yield PASS, "hhea and Win metrics are equal and useTypoMetrics is disabled." - - -@check( - id="typenetwork/font_is_centered_vertically", - rationale=""" - FIXME! This check still does not have rationale documentation. - """, - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def check_font_is_centered_vertically(ttFont): - """Checking if font is vertically centered.""" - - # Check required tables exist on font - required_tables = {"hhea", "OS/2"} - missing_tables = sorted(required_tables - set(ttFont.keys())) - if missing_tables: - for table_tag in missing_tables: - yield FAIL, Message("lacks-table", f"Font lacks '{table_tag}' table.") - return - - capHeight = ttFont["OS/2"].sCapHeight - ascent = ttFont["hhea"].ascent - capHeight - descent = abs(ttFont["hhea"].descent) - - ratio = abs(ascent - descent) / max(ascent, descent) - threshold1 = 0.1 - threshold2 = 0.3 - - if threshold1 >= ratio > threshold2: - yield WARN, Message( - "uncentered", - "The font will display slightly vertically uncentered on" - " web environments.", - ) - yield WARN, Message( - "uncentered", - f"The font will display vertically uncentered on" - f" web environments. Top space above cap height is {ascent}" - f" and under baseline is {descent}", - ) - elif ratio >= threshold2: - yield FAIL, Message( - "very-uncentered", - f"The font will display significantly vertically uncentered on" - f" web environments. Top space above cap height is {ascent}" - f" and under baseline is {descent}", - ) - else: - yield PASS, Message( - "centered", - "The font will display vertically centered on web environments.", - ) - - -@condition(Font) -def stylename(font): - ttFont = font.ttFont - if ttFont["name"].getDebugName(16): - styleName = ttFont["name"].getDebugName(17) - else: - styleName = ttFont["name"].getDebugName(2) - return styleName - - -@condition(Font) -def tn_expected_os2_weight(font): - """The weight name and the expected OS/2 usWeightClass value inferred from - the style part of the font name. - Here the common/expected values and weight names: - 100-250, Thin - 200-275, ExtraLight - 300, Light - 400, Regular - 500, Medium - 600, SemiBold - 700, Bold - 800, ExtraBold - 900, Black - Thin is not set to 100 because of legacy Windows GDI issues: - https://www.adobe.com/devnet/opentype/afdko/topic_font_wt_win.html - """ - if not font.stylename: - return None - # Weight name to value mapping: - TN_EXPECTED_WEIGHTS = { - "Thin": [100, 250], - "ExtraLight": [200, 275], - "Light": 300, - "Regular": 400, - "Medium": 500, - "SemiBold": 600, - "Bold": 700, - "ExtraBold": 800, - "Black": 900, - } - stylename = font.stylename - - # Modify style name for weights using space separator - prefixes = ["Semi ", "Ultra ", "Extra "] - for prefix in prefixes: - if prefix in stylename: - stylename = stylename.replace(prefix, prefix.strip()) - - if stylename == "Italic": - weight_name = "Regular" - elif stylename.endswith("Italic"): - weight_name = stylename.replace("Italic", "").rstrip() - elif stylename.endswith("Oblique"): - weight_name = stylename.replace("Oblique", "").rstrip() - else: - weight_name = stylename - - expected = None - for expectedWeightName, expectedWeightValue in TN_EXPECTED_WEIGHTS.items(): - if expectedWeightName.lower() in weight_name.lower().split(" "): - expected = expectedWeightValue - break - - return {"name": weight_name, "weightClass": expected} - - -@check( - id="typenetwork/usweightclass", - conditions=["tn_expected_os2_weight"], - rationale=""" - For Variable Fonts, it should be equal to default wght, for static ttfs, - Thin-Black can be 100-900 or 250-900, - for static otfs, Thin-Black must be 250-900. - - If static otfs are set lower than 250, text may appear blurry in - legacy Windows applications. - Glyphsapp users can change the usWeightClass value of an instance by adding - a 'weightClass' customParameter. - """, - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def check_usweightclass(font, tn_expected_os2_weight): - """Checking OS/2 usWeightClass.""" - failed = False - expected_value = tn_expected_os2_weight["weightClass"] - weight_name = tn_expected_os2_weight["name"].lower() - os2_value = font.ttFont["OS/2"].usWeightClass - - fail_message = "OS/2 usWeightClass is '{}' when it should be '{}'." - warn_message = "OS/2 usWeightClass is '{}' it will be better if it is '{}'." - no_value_message = "OS/2 usWeightClass is '{}' and weight name is '{}'." - - if font.is_variable_font: - fvar = font.ttFont["fvar"] - if font.has_wght_axis: - default_axis_values = {a.axisTag: a.defaultValue for a in fvar.axes} - fvar_value = default_axis_values.get("wght") - - if os2_value != int(fvar_value): - failed = True - yield FAIL, Message( - "bad-value", fail_message.format(os2_value, fvar_value) - ) - else: - if os2_value != 400: - failed = True - yield FAIL, Message("bad-value", fail_message.format(os2_value, 400)) - # overrides for static Thin and ExtaLight fonts - # for static ttfs, we don't mind if Thin is 250 and ExtraLight is 275. - # However, if the values are incorrect we will recommend they set Thin - # to 100 and ExtraLight to 250. - # for static otfs, Thin must be 250 and ExtraLight must be 275 - else: - if not expected_value: - failed = True - yield INFO, Message( - "no-value", no_value_message.format(os2_value, weight_name) - ) - - elif "thin" in weight_name.split(" "): - if os2_value not in expected_value: - failed = True - yield FAIL, Message( - "bad-value", fail_message.format(os2_value, expected_value) - ) - - elif "extralight" in weight_name.split(" "): - if os2_value not in expected_value: - failed = True - yield FAIL, Message( - "bad-value", fail_message.format(os2_value, expected_value) - ) - - elif os2_value != expected_value: - failed = True - yield FAIL, Message( - "bad-value", fail_message.format(os2_value, expected_value) - ) - - if not failed: - yield PASS, "OS/2 usWeightClass is good" - - -@check( - id="typenetwork/family/tnum_horizontal_metrics", - rationale=""" - Tabular figures need to have the same metrics in all styles in order to allow - tables to be set with proper typographic control, but to maintain the placement - of decimals and numeric columns between rows. - """, - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def check_family_tnum_horizontal_metrics(ttFonts, config): - """All tabular figures must have the same width across the family.""" - tnum_widths = {} - half_width_glyphs = {} - for ttFont in list(ttFonts): - glyphs = ttFont.getGlyphSet() - - tabular_suffixes = (".tnum", ".tf", ".tosf", ".tsc", ".tab", ".tabular") - tnum_glyphs = [ - (glyph_id, glyphs[glyph_id]) - for glyph_id in glyphs.keys() - if any(suffix in glyph_id for suffix in tabular_suffixes) - ] - - for glyph_id, glyph in tnum_glyphs: - if glyph.width not in tnum_widths: - tnum_widths[glyph.width] = [glyph_id] - else: - tnum_widths[glyph.width].append(glyph_id) - - max_num = 0 - most_common_width = None - half_width = None - - # Get most common width - for width, glyphs in tnum_widths.items(): - if len(glyphs) > max_num: - max_num = len(glyphs) - most_common_width = width - if most_common_width: - del tnum_widths[most_common_width] - - # Get Half width - for width, glyphs in tnum_widths.items(): - if round(most_common_width / 2) == width: - half_width = width - half_width_glyphs = glyphs - - if half_width: - del tnum_widths[half_width] - - if half_width: - yield INFO, Message( - "half-widths", - f"The are other glyphs with half of the width ({half_width}) of the" - f" most common width such as the following ones:\n\n" - f"{bullet_list(config, half_width_glyphs)}.", - ) - - if len(tnum_widths.keys()): - # prepare string to display - tnumWidthsString = "" - for width, glyphs in tnum_widths.items(): - tnumWidthsString += f"{width}: {pretty_print_list(config, glyphs)}\n\n" - yield WARN, Message( - "inconsistent-widths", - f"The most common tabular glyph width is {most_common_width}." - f" But there are other tabular glyphs with different widths" - f" such as the following ones:\n\n{tnumWidthsString}.", - ) - else: - yield PASS, "OK" - - -@condition(CheckRunContext) -def roman_ttFonts(context): - return [font.ttFont for font in context.fonts if not font.is_italic] - - -@condition(CheckRunContext) -def italic_ttFonts(context): - return [font.ttFont for font in context.fonts if font.is_italic] - - -@check( - id="typenetwork/family/equal_numbers_of_glyphs", - rationale=""" - Check if all fonts in a family have the same number of glyphs. - """, - conditions=["roman_ttFonts", "italic_ttFonts"], - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def equal_numbers_of_glyphs(roman_ttFonts, italic_ttFonts): - """Equal number of glyphs""" - max_roman_count = 0 - max_roman_font = None - roman_failed_fonts = {} - - # Checks roman - for ttFont in list(roman_ttFonts): - fontname = ttFont.reader.file.name - this_count = ttFont["maxp"].numGlyphs - if this_count > max_roman_count: - max_roman_count = this_count - max_roman_font = fontname - - for ttFont in list(roman_ttFonts): - this_count = ttFont["maxp"].numGlyphs - fontname = ttFont.reader.file.name - if this_count != max_roman_count: - roman_failed_fonts[fontname] = this_count - - max_italic_count = 0 - max_italic_font = None - italic_failed_fonts = {} - - # Checks Italics - for ttFont in list(italic_ttFonts): - fontname = ttFont.reader.file.name - this_count = ttFont["maxp"].numGlyphs - if this_count > max_italic_count: - max_italic_count = this_count - max_italic_font = fontname - - for ttFont in list(italic_ttFonts): - this_count = ttFont["maxp"].numGlyphs - fontname = ttFont.reader.file.name - if this_count != max_italic_count: - italic_failed_fonts[fontname] = this_count - - if len(roman_failed_fonts) > 0: - yield WARN, Message( - "roman-different-number-of-glyphs", - f"Romans doesn’t have the same number of glyphs" - f"{max_roman_font} has {max_roman_count} and \n\t{roman_failed_fonts}", - ) - else: - yield PASS, ( - "All roman files in this family have an equal total ammount of glyphs." - ) - - if len(italic_failed_fonts) > 0: - yield WARN, Message( - "italic-different-number-of-glyphs", - f"Italics doesn’t have the same number of glyphs" - f"{max_italic_font} has {max_italic_count} and \n\t{italic_failed_fonts}", - ) - else: - yield PASS, ( - "All italics files in this family have an equal total ammount of glyphs." - ) - - -@check( - id="typenetwork/family/valid_underline", - rationale=""" - If underline thickness is not set nothing gets rendered on Figma. - """, - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], - misc_metadata={"affects": [("Figma", "unspecified")]}, -) -def check_family_valid_underline(ttFont): - """Fonts have underline thickness?""" - underlineThickness = None - failedThickness = False - - underlineThickness = ttFont["post"].underlineThickness - if underlineThickness is None or underlineThickness == 0: - failedThickness = True - - if failedThickness: - msg = f"Thickness of the underline is {underlineThickness} which is not valid." - yield FAIL, Message("invalid-underline-thickness", msg) - else: - yield PASS, "Fonts have a valid underline thickness." - - -@check( - id="typenetwork/family/valid_strikeout", - rationale=""" - If strikeout size is not set, nothing gets rendered on Figma. - """, - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], - misc_metadata={"affects": [("Figma", "unspecified")]}, -) -def check_family_valid_strikeout(ttFont): - """Fonts have strikeout size?""" - strikeoutSize = None - failedThickness = False - - strikeoutSize = ttFont["OS/2"].yStrikeoutSize - if strikeoutSize is None or strikeoutSize == 0: - failedThickness = True - - if failedThickness: - msg = f"Size of the strikeout is {strikeoutSize} which is not valid." - yield FAIL, Message("invalid-strikeout-size", msg) - else: - yield PASS, "Fonts have a valid strikeout size." - - -@check( - id="typenetwork/composite_glyphs", - rationale=""" - For performance reasons, it is recommended that TTF fonts use composite glyphs. - """, - conditions=["is_ttf"], - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def check_composite_glyphs(ttFont): - """Check if TTF font uses composite glyphs.""" - baseGlyphs = [*string.printable] - failed = [] - - numberOfGlyphs = ttFont["maxp"].numGlyphs - for glyph_name in ttFont["glyf"].keys(): - glyph = ttFont["glyf"][glyph_name] - if glyph_name not in baseGlyphs and glyph.isComposite() is False: - failed.append(glyph_name) - - percentageOfNotCompositeGlyphs = round(len(failed) * 100 / numberOfGlyphs) - if percentageOfNotCompositeGlyphs > 50: - yield WARN, Message( - "low-composites", - f"{percentageOfNotCompositeGlyphs}% of the glyphs are not composites.", - ) - else: - yield PASS, ( - f"{100-percentageOfNotCompositeGlyphs}% of the glyphs are composites." - ) - - -@check( - id="typenetwork/PUA_encoded_glyphs", - rationale=""" - Using Private Use Area (PUA) encodings is not recommended. They are - defined by users and are not standardized. That said, PUA are font - specific so they will break if the user tries to copy/paste, - search/replace, or change the font. Using PUA to encode small caps, - for example, is not recommended as small caps can and should be - accessible via Open Type substitution instead. - - If you must encode your characters in the Private Use Area (PUA), - do so with great caution. - """, - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def check_PUA_encoded_glyphs(ttFont, config): - """Check if font has PUA encoded glyphs.""" - - def in_PUA_range(codepoint): - """ - Three private use areas are defined: - one in the Basic Multilingual Plane (U+E000–U+F8FF), - and one each in, and nearly covering, planes 15 and 16 - (U+F0000–U+FFFFD, U+100000–U+10FFFD). - """ - return ( - (codepoint >= 0xE000 and codepoint <= 0xF8FF) - or (codepoint >= 0xF0000 and codepoint <= 0xFFFFD) - or (codepoint >= 0x100000 and codepoint <= 0x10FFFD) - ) - - pua_encoded_glyphs = [] - - for cp, glyphName in ttFont.getBestCmap().items(): - if in_PUA_range(cp) and cp != 0xF8FF: - pua_encoded_glyphs.append(glyphName + f" U+{cp:02x}".upper()) - - if pua_encoded_glyphs: - yield WARN, Message( - "pua-encoded", - f"Glyphs with PUA codepoints:\n\n" - f"{bullet_list(config, pua_encoded_glyphs)}", - ) - else: - yield PASS, "No PUA encoded glyphs." - - -@check( - id="typenetwork/marks_width", - rationale=""" - To avoid incorrect overlappings when typing, glyphs that are spacing marks - must have width, on the other hand, combining marks should be 0 width. - """, - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def check_marks_width(ttFont, config): - """Check if marks glyphs have the correct width.""" - - def _is_non_spacing_mark_char(charcode): - category = unicodedata.category(chr(charcode)) - if category in ("Mn", "Me"): - return True - - def _is_spacing_mark_char(charcode): - category = unicodedata.category(chr(charcode)) - if category in ("Sk", "Lm"): - return True - - cmap = ttFont["cmap"].getBestCmap() - glyphSet = ttFont.getGlyphSet() - - failed_non_spacing_mark_chars = [] - failed_spacing_mark_chars = [] - - for charcode, glypname in cmap.items(): - if _is_non_spacing_mark_char(charcode): - if glyphSet[glypname].width != 0: - failed_non_spacing_mark_chars.append(glypname) - - if _is_spacing_mark_char(charcode): - if glyphSet[glypname].width == 0: - failed_spacing_mark_chars.append(glypname) - - if failed_non_spacing_mark_chars: - yield FAIL, Message( - "non-spacing-not-zero", - f"Combining accents with width advance width:\n\n" - f"{bullet_list(config, failed_non_spacing_mark_chars)}", - ) - - if failed_spacing_mark_chars: - yield FAIL, Message( - "non-spacing-not-zero", - f"Spacing marks without advance width:\n\n" - f"{bullet_list(config, failed_spacing_mark_chars)}", - ) - - if not failed_non_spacing_mark_chars and not failed_spacing_mark_chars: - yield PASS, "Marks have correct widths." - - -@check( - id="typenetwork/name/mandatory_entries", - conditions=["style"], - rationale=""" - For proper functioning, fonts must have some specific records. - Other name records are optional but desireable to be present. - """, - proposal=["https://github.com/fonttools/fontbakery/pull/4260"], -) -def check_name_mandatory_entries(ttFont, style): - """Font has all mandatory 'name' table entries?""" - from fontbakery.utils import get_name_entry_strings - from fontbakery.constants import RIBBI_STYLE_NAMES - - unnecessary_nameIDs = [] - optional_nameIDs = [ - NameID.COPYRIGHT_NOTICE, - NameID.UNIQUE_FONT_IDENTIFIER, - NameID.VERSION_STRING, - NameID.TRADEMARK, - NameID.MANUFACTURER_NAME, - NameID.DESIGNER, - NameID.DESCRIPTION, - NameID.VENDOR_URL, - NameID.DESIGNER_URL, - NameID.LICENSE_DESCRIPTION, - NameID.LICENSE_INFO_URL, - ] - - required_nameIDs = [ - NameID.FONT_FAMILY_NAME, - NameID.FONT_SUBFAMILY_NAME, - NameID.FULL_FONT_NAME, - NameID.POSTSCRIPT_NAME, - ] - - if style not in RIBBI_STYLE_NAMES: - required_nameIDs += [ - NameID.TYPOGRAPHIC_FAMILY_NAME, - NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - ] - else: - unnecessary_nameIDs += [ - NameID.TYPOGRAPHIC_FAMILY_NAME, - NameID.TYPOGRAPHIC_SUBFAMILY_NAME, - ] - - passed = True - # The font must have at least these name IDs: - for nameId in required_nameIDs: - for entry in get_name_entry_strings(ttFont, nameId): - if len(entry) == 0: - passed = False - yield FAIL, Message( - "missing-required-entry", - f"Font lacks entry with nameId={nameId}" - f" ({NameID(nameId).name})", - ) - - # The font should have these name IDs: - for nameId in optional_nameIDs: - if len(get_name_entry_strings(ttFont, nameId)) == 0: - passed = False - yield INFO, Message( - "missing-optional-entry", - f"Font lacks entry with nameId={nameId} ({NameID(nameId).name})", - ) - - # The font should NOT have these name IDs: - for nameId in unnecessary_nameIDs: - if len(get_name_entry_strings(ttFont, nameId)) != 0: - passed = False - yield INFO, Message( - "unnecessary-entry", - f"Font have unnecessary name entry with nameId={nameId}" - f" ({NameID(nameId).name})", - ) - - if passed: - yield PASS, "Font contains values for all mandatory name table entries." - - -@check( - id="typenetwork/varfont/axes_have_variation", - rationale=""" - Axes on a variable font must have variation. In other words min and max values - need to be different. It’s common to find fonts with unnecesary axes - added like `ital`. - """, - conditions=["is_variable_font"], - proposal=[ - "https://github.com/fonttools/fontbakery/pull/4260", - # "https://github.com/TypeNetwork/fontQA/issues/61", # Currently private repo. - ], -) -def check_varfont_axes_have_variation(ttFont): - """Check if font axes have variation""" - failedAxes = [] - for axis in ttFont["fvar"].axes: - if axis.minValue == axis.maxValue: - failedAxes.append( - { - "tag": axis.axisTag, - "minValue": axis.minValue, - "maxValue": axis.maxValue, - } - ) - - if failedAxes: - for failedAxis in failedAxes: - yield FAIL, Message( - "axis-has-no-variation", - f"'{failedAxis['tag']}' axis has no variation its min and max values" - f" are {failedAxis['minValue'], failedAxis['maxValue']}", - ) - else: - yield PASS, "All font axes has variation." - - -@check( - id="typenetwork/varfont/fvar_axes_order", - rationale=""" - If a font doesn’t have a STAT table, instances get sorted better on Adobe Apps - when fvar axes follow a specific order: 'opsz', 'wdth', 'wght','ital', 'slnt'. - - We should deprecate this check since STAT is a required table. - """, - conditions=["is_variable_font"], - proposal=[ - "https://github.com/fonttools/fontbakery/pull/4260", - # "https://github.com/TypeNetwork/fontQA/issues/25", # Currently private repo. - ], -) -def check_varfont_fvar_axes_order(ttFont): - """Check fvar axes order""" - prefferedOrder = ["opsz", "wdth", "wght", "ital", "slnt"] - fontRegisteredAxes = [] - customAxes = [] - - if "STAT" in ttFont.keys(): - yield SKIP, "The font has a STAT table. This will control instances order." - else: - for index, axis in enumerate(ttFont["fvar"].axes): - if axis.axisTag in prefferedOrder: - fontRegisteredAxes.append(axis.axisTag) - else: - customAxes.append((axis.axisTag, index)) - - filtered = [axis for axis in prefferedOrder if axis in fontRegisteredAxes] - - if filtered != fontRegisteredAxes: - yield WARN, Message( - "axes-incorrect-order", - "Font’s registered axes are not in a correct order to get good" - "instances sorting on Adobe apps.\n\n" - f"Current order is {fontRegisteredAxes}, but it should be {filtered}", - ) - else: - yield PASS, "Font’s axes follow the preferred sorting." - - if customAxes: - yield INFO, Message( - "custom-axes", - "The font has custom axes with the indicated order:\n\n" - f"{customAxes}\n\n" - "Its order can depend on the kind of variation and the subfamily" - "groups that may create.", - ) - - -@check( - id="typenetwork/family/duplicated_names", - rationale=""" - Having duplicated name records can produce several issues like not all fonts - being listed on design apps or incorrect automatic creation of CSS classes - and @font-face rules. - """, - proposal=[ - "https://github.com/fonttools/fontbakery/pull/4260", - # "https://github.com/TypeNetwork/fontQA/issues/25", # Currently private repo. - ], -) -def check_family_duplicated_names(ttFonts): - """Check if font doesn’t have duplicated names within a family.""" - duplicate_subfamilyNames = set() - seen_fullNames = set() - duplicate_fullNames = set() - seen_postscriptNames = set() - duplicate_postscriptNames = set() - - PLAT_ID = PlatformID.WINDOWS - ENC_ID = WindowsEncodingID.UNICODE_BMP - LANG_ID = WindowsLanguageID.ENGLISH_USA - - for ttFont in list(ttFonts): - # # Subfamily name - # if ttFont["name"].getName(17, PLAT_ID, ENC_ID, LANG_ID): - # subfamName = ttFont["name"].getName(17, PLAT_ID, ENC_ID, LANG_ID) - # else: - # subfamName = ttFont["name"].getName(2, PLAT_ID, ENC_ID, LANG_ID) - - # if subfamName: - # subfamName = subfamName.toUnicode() - # if subfamName in seen_subfamilyNames: - # duplicate_subfamilyNames.add(subfamName) - # else: - # seen_subfamilyNames.add(subfamName) - - # FullName name - fullName = ttFont["name"].getName(4, PLAT_ID, ENC_ID, LANG_ID) - - if fullName: - fullName = fullName.toUnicode() - if fullName in seen_fullNames: - duplicate_fullNames.add(fullName) - else: - seen_fullNames.add(fullName) - - # Postscript name - postscriptName = ttFont["name"].getName(6, PLAT_ID, ENC_ID, LANG_ID) - if postscriptName: - postscriptName = postscriptName.toUnicode() - if postscriptName in seen_postscriptNames: - duplicate_subfamilyNames.add(postscriptName) - else: - seen_postscriptNames.add(postscriptName) - - # if duplicate_subfamilyNames: - # duplicate_subfamilyNamesString = \ - # "".join(f"* {inst}\n" for inst in sorted(duplicate_subfamilyNames)) - # yield FAIL, Message( - # "duplicate-subfamily-names", - # "Following subfamily names are duplicate:\n\n" - # f"{duplicate_subfamilyNamesString}", - # ) - - if duplicate_fullNames: - duplicate_fullNamesString = "".join( - f"* {inst}\n" for inst in sorted(duplicate_fullNames) - ) - yield FAIL, Message( - "duplicate-full-names", - "Following full names are duplicate:\n\n" f"{duplicate_fullNamesString}", - ) - - if duplicate_postscriptNames: - duplicate_postscriptNamesString = "".join( - f"* {inst}\n" for inst in sorted(duplicate_postscriptNames) - ) - yield FAIL, Message( - "duplicate-postscript-names", - "Following postscript names are duplicate:\n\n" - f"{duplicate_postscriptNamesString}", - ) - - if not duplicate_fullNames and not duplicate_postscriptNames: - yield PASS, "All names are unique" diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/PUA_encoded_glyphs.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/PUA_encoded_glyphs.py new file mode 100644 index 0000000000..0e13319524 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/PUA_encoded_glyphs.py @@ -0,0 +1,50 @@ +from fontbakery.prelude import check, Message, PASS, WARN +from fontbakery.utils import bullet_list + + +def in_PUA_range(codepoint): + """ + Three private use areas are defined: + one in the Basic Multilingual Plane (U+E000–U+F8FF), + and one each in, and nearly covering, planes 15 and 16 + (U+F0000–U+FFFFD, U+100000–U+10FFFD). + """ + return ( + (codepoint >= 0xE000 and codepoint <= 0xF8FF) + or (codepoint >= 0xF0000 and codepoint <= 0xFFFFD) + or (codepoint >= 0x100000 and codepoint <= 0x10FFFD) + ) + + +@check( + id="typenetwork/PUA_encoded_glyphs", + rationale=""" + Using Private Use Area (PUA) encodings is not recommended. They are + defined by users and are not standardized. That said, PUA are font + specific so they will break if the user tries to copy/paste, + search/replace, or change the font. Using PUA to encode small caps, + for example, is not recommended as small caps can and should be + accessible via Open Type substitution instead. + + If you must encode your characters in the Private Use Area (PUA), + do so with great caution. + """, + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def check_PUA_encoded_glyphs(ttFont, config): + """Check if font has PUA encoded glyphs.""" + + pua_encoded_glyphs = [] + + for cp, glyphName in ttFont.getBestCmap().items(): + if in_PUA_range(cp) and cp != 0xF8FF: + pua_encoded_glyphs.append(glyphName + f" U+{cp:02x}".upper()) + + if pua_encoded_glyphs: + yield WARN, Message( + "pua-encoded", + f"Glyphs with PUA codepoints:\n\n" + f"{bullet_list(config, pua_encoded_glyphs)}", + ) + else: + yield PASS, "No PUA encoded glyphs." diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/__init__.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/composite_glyphs.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/composite_glyphs.py new file mode 100644 index 0000000000..2f90196c03 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/composite_glyphs.py @@ -0,0 +1,34 @@ +import string + +from fontbakery.prelude import check, Message, PASS, WARN + + +@check( + id="typenetwork/composite_glyphs", + rationale=""" + For performance reasons, it is recommended that TTF fonts use composite glyphs. + """, + conditions=["is_ttf"], + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def check_composite_glyphs(ttFont): + """Check if TTF font uses composite glyphs.""" + baseGlyphs = [*string.printable] + failed = [] + + numberOfGlyphs = ttFont["maxp"].numGlyphs + for glyph_name in ttFont["glyf"].keys(): + glyph = ttFont["glyf"][glyph_name] + if glyph_name not in baseGlyphs and glyph.isComposite() is False: + failed.append(glyph_name) + + percentageOfNotCompositeGlyphs = round(len(failed) * 100 / numberOfGlyphs) + if percentageOfNotCompositeGlyphs > 50: + yield WARN, Message( + "low-composites", + f"{percentageOfNotCompositeGlyphs}% of the glyphs are not composites.", + ) + else: + yield PASS, ( + f"{100-percentageOfNotCompositeGlyphs}% of the glyphs are composites." + ) diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/family/__init__.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/family/duplicated_names.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/duplicated_names.py new file mode 100644 index 0000000000..fb9dac62c4 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/duplicated_names.py @@ -0,0 +1,95 @@ +from fontbakery.prelude import check, Message, PASS, FAIL +from fontbakery.constants import ( + PlatformID, + WindowsEncodingID, + WindowsLanguageID, +) + + +@check( + id="typenetwork/family/duplicated_names", + rationale=""" + Having duplicated name records can produce several issues like not all fonts + being listed on design apps or incorrect automatic creation of CSS classes + and @font-face rules. + """, + proposal=[ + "https://github.com/fonttools/fontbakery/pull/4260", + # "https://github.com/TypeNetwork/fontQA/issues/25", # Currently private repo. + ], +) +def check_family_duplicated_names(ttFonts): + """Check if font doesn’t have duplicated names within a family.""" + duplicate_subfamilyNames = set() + seen_fullNames = set() + duplicate_fullNames = set() + seen_postscriptNames = set() + duplicate_postscriptNames = set() + + PLAT_ID = PlatformID.WINDOWS + ENC_ID = WindowsEncodingID.UNICODE_BMP + LANG_ID = WindowsLanguageID.ENGLISH_USA + + for ttFont in list(ttFonts): + # # Subfamily name + # if ttFont["name"].getName(17, PLAT_ID, ENC_ID, LANG_ID): + # subfamName = ttFont["name"].getName(17, PLAT_ID, ENC_ID, LANG_ID) + # else: + # subfamName = ttFont["name"].getName(2, PLAT_ID, ENC_ID, LANG_ID) + + # if subfamName: + # subfamName = subfamName.toUnicode() + # if subfamName in seen_subfamilyNames: + # duplicate_subfamilyNames.add(subfamName) + # else: + # seen_subfamilyNames.add(subfamName) + + # FullName name + fullName = ttFont["name"].getName(4, PLAT_ID, ENC_ID, LANG_ID) + + if fullName: + fullName = fullName.toUnicode() + if fullName in seen_fullNames: + duplicate_fullNames.add(fullName) + else: + seen_fullNames.add(fullName) + + # Postscript name + postscriptName = ttFont["name"].getName(6, PLAT_ID, ENC_ID, LANG_ID) + if postscriptName: + postscriptName = postscriptName.toUnicode() + if postscriptName in seen_postscriptNames: + duplicate_subfamilyNames.add(postscriptName) + else: + seen_postscriptNames.add(postscriptName) + + # if duplicate_subfamilyNames: + # duplicate_subfamilyNamesString = \ + # "".join(f"* {inst}\n" for inst in sorted(duplicate_subfamilyNames)) + # yield FAIL, Message( + # "duplicate-subfamily-names", + # "Following subfamily names are duplicate:\n\n" + # f"{duplicate_subfamilyNamesString}", + # ) + + if duplicate_fullNames: + duplicate_fullNamesString = "".join( + f"* {inst}\n" for inst in sorted(duplicate_fullNames) + ) + yield FAIL, Message( + "duplicate-full-names", + "Following full names are duplicate:\n\n" f"{duplicate_fullNamesString}", + ) + + if duplicate_postscriptNames: + duplicate_postscriptNamesString = "".join( + f"* {inst}\n" for inst in sorted(duplicate_postscriptNames) + ) + yield FAIL, Message( + "duplicate-postscript-names", + "Following postscript names are duplicate:\n\n" + f"{duplicate_postscriptNamesString}", + ) + + if not duplicate_fullNames and not duplicate_postscriptNames: + yield PASS, "All names are unique" diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/family/equal_numbers_of_glyphs.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/equal_numbers_of_glyphs.py new file mode 100644 index 0000000000..55ca7c3329 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/equal_numbers_of_glyphs.py @@ -0,0 +1,81 @@ +from fontbakery.testable import CheckRunContext +from fontbakery.prelude import check, condition, Message, PASS, WARN + + +@condition(CheckRunContext) +def roman_ttFonts(context): + return [font.ttFont for font in context.fonts if not font.is_italic] + + +@condition(CheckRunContext) +def italic_ttFonts(context): + return [font.ttFont for font in context.fonts if font.is_italic] + + +@check( + id="typenetwork/family/equal_numbers_of_glyphs", + rationale=""" + Check if all fonts in a family have the same number of glyphs. + """, + conditions=["roman_ttFonts", "italic_ttFonts"], + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def equal_numbers_of_glyphs(roman_ttFonts, italic_ttFonts): + """Equal number of glyphs""" + max_roman_count = 0 + max_roman_font = None + roman_failed_fonts = {} + + # Checks roman + for ttFont in list(roman_ttFonts): + fontname = ttFont.reader.file.name + this_count = ttFont["maxp"].numGlyphs + if this_count > max_roman_count: + max_roman_count = this_count + max_roman_font = fontname + + for ttFont in list(roman_ttFonts): + this_count = ttFont["maxp"].numGlyphs + fontname = ttFont.reader.file.name + if this_count != max_roman_count: + roman_failed_fonts[fontname] = this_count + + max_italic_count = 0 + max_italic_font = None + italic_failed_fonts = {} + + # Checks Italics + for ttFont in list(italic_ttFonts): + fontname = ttFont.reader.file.name + this_count = ttFont["maxp"].numGlyphs + if this_count > max_italic_count: + max_italic_count = this_count + max_italic_font = fontname + + for ttFont in list(italic_ttFonts): + this_count = ttFont["maxp"].numGlyphs + fontname = ttFont.reader.file.name + if this_count != max_italic_count: + italic_failed_fonts[fontname] = this_count + + if len(roman_failed_fonts) > 0: + yield WARN, Message( + "roman-different-number-of-glyphs", + f"Romans doesn’t have the same number of glyphs" + f"{max_roman_font} has {max_roman_count} and \n\t{roman_failed_fonts}", + ) + else: + yield PASS, ( + "All roman files in this family have an equal total ammount of glyphs." + ) + + if len(italic_failed_fonts) > 0: + yield WARN, Message( + "italic-different-number-of-glyphs", + f"Italics doesn’t have the same number of glyphs" + f"{max_italic_font} has {max_italic_count} and \n\t{italic_failed_fonts}", + ) + else: + yield PASS, ( + "All italics files in this family have an equal total ammount of glyphs." + ) diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/family/tnum_horizontal_metrics.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/tnum_horizontal_metrics.py new file mode 100644 index 0000000000..22f3e7132b --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/tnum_horizontal_metrics.py @@ -0,0 +1,78 @@ +from fontbakery.prelude import check, Message, PASS, WARN, INFO +from fontbakery.utils import ( + bullet_list, + pretty_print_list, +) + + +@check( + id="typenetwork/family/tnum_horizontal_metrics", + rationale=""" + Tabular figures need to have the same metrics in all styles in order to allow + tables to be set with proper typographic control, but to maintain the placement + of decimals and numeric columns between rows. + """, + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def check_family_tnum_horizontal_metrics(ttFonts, config): + """All tabular figures must have the same width across the family.""" + tnum_widths = {} + half_width_glyphs = {} + for ttFont in list(ttFonts): + glyphs = ttFont.getGlyphSet() + + tabular_suffixes = (".tnum", ".tf", ".tosf", ".tsc", ".tab", ".tabular") + tnum_glyphs = [ + (glyph_id, glyphs[glyph_id]) + for glyph_id in glyphs.keys() + if any(suffix in glyph_id for suffix in tabular_suffixes) + ] + + for glyph_id, glyph in tnum_glyphs: + if glyph.width not in tnum_widths: + tnum_widths[glyph.width] = [glyph_id] + else: + tnum_widths[glyph.width].append(glyph_id) + + max_num = 0 + most_common_width = None + half_width = None + + # Get most common width + for width, glyphs in tnum_widths.items(): + if len(glyphs) > max_num: + max_num = len(glyphs) + most_common_width = width + if most_common_width: + del tnum_widths[most_common_width] + + # Get Half width + for width, glyphs in tnum_widths.items(): + if round(most_common_width / 2) == width: + half_width = width + half_width_glyphs = glyphs + + if half_width: + del tnum_widths[half_width] + + if half_width: + yield INFO, Message( + "half-widths", + f"The are other glyphs with half of the width ({half_width}) of the" + f" most common width such as the following ones:\n\n" + f"{bullet_list(config, half_width_glyphs)}.", + ) + + if len(tnum_widths.keys()): + # prepare string to display + tnumWidthsString = "" + for width, glyphs in tnum_widths.items(): + tnumWidthsString += f"{width}: {pretty_print_list(config, glyphs)}\n\n" + yield WARN, Message( + "inconsistent-widths", + f"The most common tabular glyph width is {most_common_width}." + f" But there are other tabular glyphs with different widths" + f" such as the following ones:\n\n{tnumWidthsString}.", + ) + else: + yield PASS, "OK" diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/family/valid_strikeout.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/valid_strikeout.py new file mode 100644 index 0000000000..9844028b78 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/valid_strikeout.py @@ -0,0 +1,20 @@ +from fontbakery.prelude import check, Message, FAIL + + +@check( + id="typenetwork/family/valid_strikeout", + rationale=""" + If strikeout size is not set, nothing gets rendered on Figma. + """, + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], + misc_metadata={"affects": [("Figma", "unspecified")]}, +) +def check_family_valid_strikeout(ttFont): + """Font has a value strikeout size?""" + + strikeoutSize = ttFont["OS/2"].yStrikeoutSize + if strikeoutSize is None or strikeoutSize == 0: + yield FAIL, Message( + "invalid-strikeout-size", + f"Size of the strikeout is {strikeoutSize} which is not valid.", + ) diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/family/valid_underline.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/valid_underline.py new file mode 100644 index 0000000000..4de0145336 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/family/valid_underline.py @@ -0,0 +1,20 @@ +from fontbakery.prelude import check, Message, FAIL + + +@check( + id="typenetwork/family/valid_underline", + rationale=""" + If underline thickness is not set nothing gets rendered on Figma. + """, + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], + misc_metadata={"affects": [("Figma", "unspecified")]}, +) +def check_family_valid_underline(ttFont): + """Font has a valid underline thickness?""" + + underlineThickness = ttFont["post"].underlineThickness + if underlineThickness is None or underlineThickness == 0: + yield FAIL, Message( + "invalid-underline-thickness", + f"Thickness of the underline is {underlineThickness} which is not valid.", + ) diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/font_is_centered_vertically.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/font_is_centered_vertically.py new file mode 100644 index 0000000000..6557404aba --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/font_is_centered_vertically.py @@ -0,0 +1,53 @@ +from fontbakery.prelude import check, Message, PASS, FAIL, WARN + + +@check( + id="typenetwork/font_is_centered_vertically", + rationale=""" + FIXME! This check still does not have rationale documentation. + """, + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def check_font_is_centered_vertically(ttFont): + """Checking if font is vertically centered.""" + + # Check required tables exist on font + required_tables = {"hhea", "OS/2"} + missing_tables = sorted(required_tables - set(ttFont.keys())) + if missing_tables: + for table_tag in missing_tables: + yield FAIL, Message("lacks-table", f"Font lacks '{table_tag}' table.") + return + + capHeight = ttFont["OS/2"].sCapHeight + ascent = ttFont["hhea"].ascent - capHeight + descent = abs(ttFont["hhea"].descent) + + ratio = abs(ascent - descent) / max(ascent, descent) + threshold1 = 0.1 + threshold2 = 0.3 + + if threshold1 >= ratio > threshold2: + yield WARN, Message( + "uncentered", + "The font will display slightly vertically uncentered on" + " web environments.", + ) + yield WARN, Message( + "uncentered", + f"The font will display vertically uncentered on" + f" web environments. Top space above cap height is {ascent}" + f" and under baseline is {descent}", + ) + elif ratio >= threshold2: + yield FAIL, Message( + "very-uncentered", + f"The font will display significantly vertically uncentered on" + f" web environments. Top space above cap height is {ascent}" + f" and under baseline is {descent}", + ) + else: + yield PASS, Message( + "centered", + "The font will display vertically centered on web environments.", + ) diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/glyph_coverage.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/glyph_coverage.py new file mode 100644 index 0000000000..efd500bcb7 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/glyph_coverage.py @@ -0,0 +1,37 @@ +from fontbakery.prelude import check, Message, WARN +from fontbakery.utils import ( + bullet_list, + exit_with_install_instructions, +) +from fontbakery.checks.vendorspecific.typenetwork.glyphsets import TN_latin_set + + +@check( + id="typenetwork/glyph_coverage", + rationale=""" + Type Network expects that fonts in its catalog support at least the minimal + set of characters. + """, + conditions=["font_codepoints"], + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def check_glyph_coverage(ttFont, font_codepoints, config): + """Check Type Network minimum glyph coverage.""" + try: + import unicodedata2 + except ImportError: + exit_with_install_instructions("typenetwork") + + required_codepoints = set(TN_latin_set) + diff = required_codepoints - font_codepoints + missing = [] + for c in sorted(diff): + try: + missing.append("uni%04X %s (%s)\n" % (c, chr(c), unicodedata2.name(chr(c)))) + except ValueError: + pass + if missing: + yield WARN, Message( + "missing-codepoints", + f"Missing required codepoints:\n\n{bullet_list(config, missing)}", + ) diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/glyphsets.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/glyphsets.py new file mode 100644 index 0000000000..3699810ad3 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/glyphsets.py @@ -0,0 +1,379 @@ +TN_latin_set = { + 0x0020: (" ", "SPACE"), + 0x0021: ("!", "EXCLAMATION MARK"), + 0x0022: ('"', "QUOTATION MARK"), + 0x0023: ("#", "NUMBER SIGN"), + 0x0024: ("$", "DOLLAR SIGN"), + 0x0025: ("%", "PERCENT SIGN"), + 0x0026: ("&", "AMPERSAND"), + 0x0027: ("'", "APOSTROPHE"), + 0x0028: ("(", "LEFT PARENTHESIS"), + 0x0029: (")", "RIGHT PARENTHESIS"), + 0x002A: ("*", "ASTERISK"), + 0x002B: ("+", "PLUS SIGN"), + 0x002C: (",", "COMMA"), + 0x002D: ("-", "HYPHEN-MINUS"), + 0x002E: (".", "FULL STOP"), + 0x002F: ("/", "SOLIDUS"), + 0x0030: ("0", "DIGIT ZERO"), + 0x0031: ("1", "DIGIT ONE"), + 0x0032: ("2", "DIGIT TWO"), + 0x0033: ("3", "DIGIT THREE"), + 0x0034: ("4", "DIGIT FOUR"), + 0x0035: ("5", "DIGIT FIVE"), + 0x0036: ("6", "DIGIT SIX"), + 0x0037: ("7", "DIGIT SEVEN"), + 0x0038: ("8", "DIGIT EIGHT"), + 0x0039: ("9", "DIGIT NINE"), + 0x003A: (":", "COLON"), + 0x003B: (";", "SEMICOLON"), + 0x003C: ("<", "LESS-THAN SIGN"), + 0x003D: ("=", "EQUALS SIGN"), + 0x003E: (">", "GREATER-THAN SIGN"), + 0x003F: ("?", "QUESTION MARK"), + 0x0040: ("@", "COMMERCIAL AT"), + 0x0041: ("A", "LATIN CAPITAL LETTER A"), + 0x0042: ("B", "LATIN CAPITAL LETTER B"), + 0x0043: ("C", "LATIN CAPITAL LETTER C"), + 0x0044: ("D", "LATIN CAPITAL LETTER D"), + 0x0045: ("E", "LATIN CAPITAL LETTER E"), + 0x0046: ("F", "LATIN CAPITAL LETTER F"), + 0x0047: ("G", "LATIN CAPITAL LETTER G"), + 0x0048: ("H", "LATIN CAPITAL LETTER H"), + 0x0049: ("I", "LATIN CAPITAL LETTER I"), + 0x004A: ("J", "LATIN CAPITAL LETTER J"), + 0x004B: ("K", "LATIN CAPITAL LETTER K"), + 0x004C: ("L", "LATIN CAPITAL LETTER L"), + 0x004D: ("M", "LATIN CAPITAL LETTER M"), + 0x004E: ("N", "LATIN CAPITAL LETTER N"), + 0x004F: ("O", "LATIN CAPITAL LETTER O"), + 0x0050: ("P", "LATIN CAPITAL LETTER P"), + 0x0051: ("Q", "LATIN CAPITAL LETTER Q"), + 0x0052: ("R", "LATIN CAPITAL LETTER R"), + 0x0053: ("S", "LATIN CAPITAL LETTER S"), + 0x0054: ("T", "LATIN CAPITAL LETTER T"), + 0x0055: ("U", "LATIN CAPITAL LETTER U"), + 0x0056: ("V", "LATIN CAPITAL LETTER V"), + 0x0057: ("W", "LATIN CAPITAL LETTER W"), + 0x0058: ("X", "LATIN CAPITAL LETTER X"), + 0x0059: ("Y", "LATIN CAPITAL LETTER Y"), + 0x005A: ("Z", "LATIN CAPITAL LETTER Z"), + 0x005B: ("[", "LEFT SQUARE BRACKET"), + 0x005C: ("\\", "REVERSE SOLIDUS"), + 0x005D: ("]", "RIGHT SQUARE BRACKET"), + 0x005E: ("^", "ASCII CIRCUMFLEX ACCENT"), + 0x005F: ("_", "LOW LINE"), + 0x0060: ("`", "GRAVE ACCENT"), + 0x0061: ("a", "LATIN SMALL LETTER A"), + 0x0062: ("b", "LATIN SMALL LETTER B"), + 0x0063: ("c", "LATIN SMALL LETTER C"), + 0x0064: ("d", "LATIN SMALL LETTER D"), + 0x0065: ("e", "LATIN SMALL LETTER E"), + 0x0066: ("f", "LATIN SMALL LETTER F"), + 0x0067: ("g", "LATIN SMALL LETTER G"), + 0x0068: ("h", "LATIN SMALL LETTER H"), + 0x0069: ("i", "LATIN SMALL LETTER I"), + 0x006A: ("j", "LATIN SMALL LETTER J"), + 0x006B: ("k", "LATIN SMALL LETTER K"), + 0x006C: ("l", "LATIN SMALL LETTER L"), + 0x006D: ("m", "LATIN SMALL LETTER M"), + 0x006E: ("n", "LATIN SMALL LETTER N"), + 0x006F: ("o", "LATIN SMALL LETTER O"), + 0x0070: ("p", "LATIN SMALL LETTER P"), + 0x0071: ("q", "LATIN SMALL LETTER Q"), + 0x0072: ("r", "LATIN SMALL LETTER R"), + 0x0073: ("s", "LATIN SMALL LETTER S"), + 0x0074: ("t", "LATIN SMALL LETTER T"), + 0x0075: ("u", "LATIN SMALL LETTER U"), + 0x0076: ("v", "LATIN SMALL LETTER V"), + 0x0077: ("w", "LATIN SMALL LETTER W"), + 0x0078: ("x", "LATIN SMALL LETTER X"), + 0x0079: ("y", "LATIN SMALL LETTER Y"), + 0x007A: ("z", "LATIN SMALL LETTER Z"), + 0x007B: ("{", "LEFT CURLY BRACKET"), + 0x007C: ("|", "VERTICAL LINE"), + 0x007D: ("}", "RIGHT CURLY BRACKET"), + 0x007E: ("~", "TILDE"), + 0x00A0: (" ", "NO-BREAK SPACE"), + 0x00A1: ("¡", "INVERTED EXCLAMATION MARK"), + 0x00A2: ("¢", "CENT SIGN"), + 0x00A3: ("£", "POUND SIGN"), + 0x00A4: ("¤", "CURRENCY SIGN"), + 0x00A5: ("¥", "YEN SIGN"), + 0x00A6: ("¦", "BROKEN BAR"), + 0x00A7: ("§", "SECTION SIGN"), + 0x00A8: ("¨", "DIAERESIS"), + 0x00A9: ("©", "COPYRIGHT SIGN"), + 0x00AA: ("ª", "FEMININE ORDINAL INDICATOR"), + 0x00AB: ("«", "LEFT-POINTING DOUBLE ANGLE QUOTATION MARK"), + 0x00AC: ("¬", "NOT SIGN"), + 0x00AE: ("®", "REGISTERED SIGN"), + 0x00AF: ("¯", "MACRON"), + 0x00B0: ("°", "DEGREE SIGN"), + 0x00B1: ("±", "PLUS-MINUS SIGN"), + 0x00B2: ("²", "SUPERSCRIPT TWO"), + 0x00B3: ("³", "SUPERSCRIPT THREE"), + 0x00B4: ("´", "ACUTE ACCENT"), + 0x00B6: ("¶", "PILCROW SIGN"), + 0x00B7: ("·", "MIDDLE DOT"), + 0x00B8: ("¸", "CEDILLA"), + 0x00B9: ("¹", "SUPERSCRIPT ONE"), + 0x00BA: ("º", "MASCULINE ORDINAL INDICATOR"), + 0x00BB: ("»", "RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK"), + 0x00BC: ("¼", "VULGAR FRACTION ONE QUARTER"), + 0x00BD: ("½", "VULGAR FRACTION ONE HALF"), + 0x00BE: ("¾", "VULGAR FRACTION THREE QUARTERS"), + 0x00BF: ("¿", "INVERTED QUESTION MARK"), + 0x00C0: ("À", "LATIN CAPITAL LETTER A WITH GRAVE"), + 0x00C1: ("Á", "LATIN CAPITAL LETTER A WITH ACUTE"), + 0x00C2: ("Â", "LATIN CAPITAL LETTER A WITH CIRCUMFLEX"), + 0x00C3: ("Ã", "LATIN CAPITAL LETTER A WITH TILDE"), + 0x00C4: ("Ä", "LATIN CAPITAL LETTER A WITH DIAERESIS"), + 0x00C5: ("Å", "LATIN CAPITAL LETTER A WITH RING ABOVE"), + 0x00C6: ("Æ", "LATIN CAPITAL LETTER AE"), + 0x00C7: ("Ç", "LATIN CAPITAL LETTER C WITH CEDILLA"), + 0x00C8: ("È", "LATIN CAPITAL LETTER E WITH GRAVE"), + 0x00C9: ("É", "LATIN CAPITAL LETTER E WITH ACUTE"), + 0x00CA: ("Ê", "LATIN CAPITAL LETTER E WITH CIRCUMFLEX"), + 0x00CB: ("Ë", "LATIN CAPITAL LETTER E WITH DIAERESIS"), + 0x00CC: ("Ì", "LATIN CAPITAL LETTER I WITH GRAVE"), + 0x00CD: ("Í", "LATIN CAPITAL LETTER I WITH ACUTE"), + 0x00CE: ("Î", "LATIN CAPITAL LETTER I WITH CIRCUMFLEX"), + 0x00CF: ("Ï", "LATIN CAPITAL LETTER I WITH DIAERESIS"), + 0x00D0: ("Ð", "LATIN CAPITAL LETTER ETH"), + 0x00D1: ("Ñ", "LATIN CAPITAL LETTER N WITH TILDE"), + 0x00D2: ("Ò", "LATIN CAPITAL LETTER O WITH GRAVE"), + 0x00D3: ("Ó", "LATIN CAPITAL LETTER O WITH ACUTE"), + 0x00D4: ("Ô", "LATIN CAPITAL LETTER O WITH CIRCUMFLEX"), + 0x00D5: ("Õ", "LATIN CAPITAL LETTER O WITH TILDE"), + 0x00D6: ("Ö", "LATIN CAPITAL LETTER O WITH DIAERESIS"), + 0x00D7: ("×", "MULTIPLICATION SIGN"), + 0x00D8: ("Ø", "LATIN CAPITAL LETTER O WITH STROKE"), + 0x00D9: ("Ù", "LATIN CAPITAL LETTER U WITH GRAVE"), + 0x00DA: ("Ú", "LATIN CAPITAL LETTER U WITH ACUTE"), + 0x00DB: ("Û", "LATIN CAPITAL LETTER U WITH CIRCUMFLEX"), + 0x00DC: ("Ü", "LATIN CAPITAL LETTER U WITH DIAERESIS"), + 0x00DD: ("Ý", "LATIN CAPITAL LETTER Y WITH ACUTE"), + 0x00DE: ("Þ", "LATIN CAPITAL LETTER THORN"), + 0x00DF: ("ß", "LATIN SMALL LETTER SHARP S"), + 0x00E0: ("à", "LATIN SMALL LETTER A WITH GRAVE"), + 0x00E1: ("á", "LATIN SMALL LETTER A WITH ACUTE"), + 0x00E2: ("â", "LATIN SMALL LETTER A WITH CIRCUMFLEX"), + 0x00E3: ("ã", "LATIN SMALL LETTER A WITH TILDE"), + 0x00E4: ("ä", "LATIN SMALL LETTER A WITH DIAERESIS"), + 0x00E5: ("å", "LATIN SMALL LETTER A WITH RING ABOVE"), + 0x00E6: ("æ", "LATIN SMALL LETTER AE"), + 0x00E7: ("ç", "LATIN SMALL LETTER C WITH CEDILLA"), + 0x00E8: ("è", "LATIN SMALL LETTER E WITH GRAVE"), + 0x00E9: ("é", "LATIN SMALL LETTER E WITH ACUTE"), + 0x00EA: ("ê", "LATIN SMALL LETTER E WITH CIRCUMFLEX"), + 0x00EB: ("ë", "LATIN SMALL LETTER E WITH DIAERESIS"), + 0x00EC: ("ì", "LATIN SMALL LETTER I WITH GRAVE"), + 0x00ED: ("í", "LATIN SMALL LETTER I WITH ACUTE"), + 0x00EE: ("î", "LATIN SMALL LETTER I WITH CIRCUMFLEX"), + 0x00EF: ("ï", "LATIN SMALL LETTER I WITH DIAERESIS"), + 0x00F0: ("ð", "LATIN SMALL LETTER ETH"), + 0x00F1: ("ñ", "LATIN SMALL LETTER N WITH TILDE"), + 0x00F2: ("ò", "LATIN SMALL LETTER O WITH GRAVE"), + 0x00F3: ("ó", "LATIN SMALL LETTER O WITH ACUTE"), + 0x00F4: ("ô", "LATIN SMALL LETTER O WITH CIRCUMFLEX"), + 0x00F5: ("õ", "LATIN SMALL LETTER O WITH TILDE"), + 0x00F6: ("ö", "LATIN SMALL LETTER O WITH DIAERESIS"), + 0x00F7: ("÷", "DIVISION SIGN"), + 0x00F8: ("ø", "LATIN SMALL LETTER O WITH STROKE"), + 0x00F9: ("ù", "LATIN SMALL LETTER U WITH GRAVE"), + 0x00FA: ("ú", "LATIN SMALL LETTER U WITH ACUTE"), + 0x00FB: ("û", "LATIN SMALL LETTER U WITH CIRCUMFLEX"), + 0x00FC: ("ü", "LATIN SMALL LETTER U WITH DIAERESIS"), + 0x00FD: ("ý", "LATIN SMALL LETTER Y WITH ACUTE"), + 0x00FE: ("þ", "LATIN SMALL LETTER THORN"), + 0x00FF: ("ÿ", "LATIN SMALL LETTER Y WITH DIAERESIS"), + 0x0100: ("Ā", "LATIN CAPITAL LETTER A WITH MACRON"), + 0x0101: ("ā", "LATIN SMALL LETTER A WITH MACRON"), + 0x0102: ("Ă", "LATIN CAPITAL LETTER A WITH BREVE"), + 0x0103: ("ă", "LATIN SMALL LETTER A WITH BREVE"), + 0x0104: ("Ą", "LATIN CAPITAL LETTER A WITH OGONEK"), + 0x0105: ("ą", "LATIN SMALL LETTER A WITH OGONEK"), + 0x0106: ("Ć", "LATIN CAPITAL LETTER C WITH ACUTE"), + 0x0107: ("ć", "LATIN SMALL LETTER C WITH ACUTE"), + 0x0108: ("Ĉ", "LATIN CAPITAL LETTER C WITH CIRCUMFLEX"), + 0x0109: ("ĉ", "LATIN SMALL LETTER C WITH CIRCUMFLEX"), + 0x010A: ("Ċ", "LATIN CAPITAL LETTER C WITH DOT ABOVE"), + 0x010B: ("ċ", "LATIN SMALL LETTER C WITH DOT ABOVE"), + 0x010C: ("Č", "LATIN CAPITAL LETTER C WITH CARON"), + 0x010D: ("č", "LATIN SMALL LETTER C WITH CARON"), + 0x010E: ("Ď", "LATIN CAPITAL LETTER D WITH CARON"), + 0x010F: ("ď", "LATIN SMALL LETTER D WITH CARON"), + 0x0110: ("Đ", "LATIN CAPITAL LETTER D WITH STROKE"), + 0x0111: ("đ", "LATIN SMALL LETTER D WITH STROKE"), + 0x0112: ("Ē", "LATIN CAPITAL LETTER E WITH MACRON"), + 0x0113: ("ē", "LATIN SMALL LETTER E WITH MACRON"), + 0x0114: ("Ĕ", "LATIN CAPITAL LETTER E WITH BREVE"), + 0x0115: ("ĕ", "LATIN SMALL LETTER E WITH BREVE"), + 0x0116: ("Ė", "LATIN CAPITAL LETTER E WITH DOT ABOVE"), + 0x0117: ("ė", "LATIN SMALL LETTER E WITH DOT ABOVE"), + 0x0118: ("Ę", "LATIN CAPITAL LETTER E WITH OGONEK"), + 0x0119: ("ę", "LATIN SMALL LETTER E WITH OGONEK"), + 0x011A: ("Ě", "LATIN CAPITAL LETTER E WITH CARON"), + 0x011B: ("ě", "LATIN SMALL LETTER E WITH CARON"), + 0x011C: ("Ĝ", "LATIN CAPITAL LETTER G WITH CIRCUMFLEX"), + 0x011D: ("ĝ", "LATIN SMALL LETTER G WITH CIRCUMFLEX"), + 0x011E: ("Ğ", "LATIN CAPITAL LETTER G WITH BREVE"), + 0x011F: ("ğ", "LATIN SMALL LETTER G WITH BREVE"), + 0x0120: ("Ġ", "LATIN CAPITAL LETTER G WITH DOT ABOVE"), + 0x0121: ("ġ", "LATIN SMALL LETTER G WITH DOT ABOVE"), + 0x0122: ("Ģ", "LATIN CAPITAL LETTER G WITH CEDILLA"), + 0x0123: ("ģ", "LATIN SMALL LETTER G WITH CEDILLA"), + 0x0124: ("Ĥ", "LATIN CAPITAL LETTER H WITH CIRCUMFLEX"), + 0x0125: ("ĥ", "LATIN SMALL LETTER H WITH CIRCUMFLEX"), + 0x0126: ("Ħ", "LATIN CAPITAL LETTER H WITH STROKE"), + 0x0127: ("ħ", "LATIN SMALL LETTER H WITH STROKE"), + 0x0128: ("Ĩ", "LATIN CAPITAL LETTER I WITH TILDE"), + 0x0129: ("ĩ", "LATIN SMALL LETTER I WITH TILDE"), + 0x012A: ("Ī", "LATIN CAPITAL LETTER I WITH MACRON"), + 0x012B: ("ī", "LATIN SMALL LETTER I WITH MACRON"), + 0x012C: ("Ĭ", "LATIN CAPITAL LETTER I WITH BREVE"), + 0x012D: ("ĭ", "LATIN SMALL LETTER I WITH BREVE"), + 0x012E: ("Į", "LATIN CAPITAL LETTER I WITH OGONEK"), + 0x012F: ("į", "LATIN SMALL LETTER I WITH OGONEK"), + 0x0130: ("İ", "LATIN CAPITAL LETTER I WITH DOT ABOVE"), + 0x0131: ("ı", "LATIN SMALL LETTER DOTLESS I"), + 0x0132: ("IJ", "LATIN CAPITAL LIGATURE IJ"), + 0x0133: ("ij", "LATIN SMALL LIGATURE IJ"), + 0x0134: ("Ĵ", "LATIN CAPITAL LETTER J WITH CIRCUMFLEX"), + 0x0135: ("ĵ", "LATIN SMALL LETTER J WITH CIRCUMFLEX"), + 0x0136: ("Ķ", "LATIN CAPITAL LETTER K WITH CEDILLA"), + 0x0137: ("ķ", "LATIN SMALL LETTER K WITH CEDILLA"), + 0x0139: ("Ĺ", "LATIN CAPITAL LETTER L WITH ACUTE"), + 0x013A: ("ĺ", "LATIN SMALL LETTER L WITH ACUTE"), + 0x013B: ("Ļ", "LATIN CAPITAL LETTER L WITH CEDILLA"), + 0x013C: ("ļ", "LATIN SMALL LETTER L WITH CEDILLA"), + 0x013D: ("Ľ", "LATIN CAPITAL LETTER L WITH CARON"), + 0x013E: ("ľ", "LATIN SMALL LETTER L WITH CARON"), + 0x013F: ("Ŀ", "LATIN CAPITAL LETTER L WITH MIDDLE DOT"), + 0x0140: ("ŀ", "LATIN SMALL LETTER L WITH MIDDLE DOT"), + 0x0141: ("Ł", "LATIN CAPITAL LETTER L WITH STROKE"), + 0x0142: ("ł", "LATIN SMALL LETTER L WITH STROKE"), + 0x0143: ("Ń", "LATIN CAPITAL LETTER N WITH ACUTE"), + 0x0144: ("ń", "LATIN SMALL LETTER N WITH ACUTE"), + 0x0145: ("Ņ", "LATIN CAPITAL LETTER N WITH CEDILLA"), + 0x0146: ("ņ", "LATIN SMALL LETTER N WITH CEDILLA"), + 0x0147: ("Ň", "LATIN CAPITAL LETTER N WITH CARON"), + 0x0148: ("ň", "LATIN SMALL LETTER N WITH CARON"), + 0x014A: ("Ŋ", "LATIN CAPITAL LETTER ENG"), + 0x014B: ("ŋ", "LATIN SMALL LETTER ENG"), + 0x014C: ("Ō", "LATIN CAPITAL LETTER O WITH MACRON"), + 0x014D: ("ō", "LATIN SMALL LETTER O WITH MACRON"), + 0x014E: ("Ŏ", "LATIN CAPITAL LETTER O WITH BREVE"), + 0x014F: ("ŏ", "LATIN SMALL LETTER O WITH BREVE"), + 0x0150: ("Ő", "LATIN CAPITAL LETTER O WITH DOUBLE ACUTE"), + 0x0151: ("ő", "LATIN SMALL LETTER O WITH DOUBLE ACUTE"), + 0x0152: ("Œ", "LATIN CAPITAL LIGATURE OE"), + 0x0153: ("œ", "LATIN SMALL LIGATURE OE"), + 0x0154: ("Ŕ", "LATIN CAPITAL LETTER R WITH ACUTE"), + 0x0155: ("ŕ", "LATIN SMALL LETTER R WITH ACUTE"), + 0x0156: ("Ŗ", "LATIN CAPITAL LETTER R WITH CEDILLA"), + 0x0157: ("ŗ", "LATIN SMALL LETTER R WITH CEDILLA"), + 0x0158: ("Ř", "LATIN CAPITAL LETTER R WITH CARON"), + 0x0159: ("ř", "LATIN SMALL LETTER R WITH CARON"), + 0x015A: ("Ś", "LATIN CAPITAL LETTER S WITH ACUTE"), + 0x015B: ("ś", "LATIN SMALL LETTER S WITH ACUTE"), + 0x015C: ("Ŝ", "LATIN CAPITAL LETTER S WITH CIRCUMFLEX"), + 0x015D: ("ŝ", "LATIN SMALL LETTER S WITH CIRCUMFLEX"), + 0x015E: ("Ş", "LATIN CAPITAL LETTER S WITH CEDILLA"), + 0x015F: ("ş", "LATIN SMALL LETTER S WITH CEDILLA"), + 0x0160: ("Š", "LATIN CAPITAL LETTER S WITH CARON"), + 0x0161: ("š", "LATIN SMALL LETTER S WITH CARON"), + 0x0164: ("Ť", "LATIN CAPITAL LETTER T WITH CARON"), + 0x0165: ("ť", "LATIN SMALL LETTER T WITH CARON"), + 0x0166: ("Ŧ", "LATIN CAPITAL LETTER T WITH STROKE"), + 0x0167: ("ŧ", "LATIN SMALL LETTER T WITH STROKE"), + 0x0168: ("Ũ", "LATIN CAPITAL LETTER U WITH TILDE"), + 0x0169: ("ũ", "LATIN SMALL LETTER U WITH TILDE"), + 0x016A: ("Ū", "LATIN CAPITAL LETTER U WITH MACRON"), + 0x016B: ("ū", "LATIN SMALL LETTER U WITH MACRON"), + 0x016C: ("Ŭ", "LATIN CAPITAL LETTER U WITH BREVE"), + 0x016D: ("ŭ", "LATIN SMALL LETTER U WITH BREVE"), + 0x016E: ("Ů", "LATIN CAPITAL LETTER U WITH RING ABOVE"), + 0x016F: ("ů", "LATIN SMALL LETTER U WITH RING ABOVE"), + 0x0170: ("Ű", "LATIN CAPITAL LETTER U WITH DOUBLE ACUTE"), + 0x0171: ("ű", "LATIN SMALL LETTER U WITH DOUBLE ACUTE"), + 0x0172: ("Ų", "LATIN CAPITAL LETTER U WITH OGONEK"), + 0x0173: ("ų", "LATIN SMALL LETTER U WITH OGONEK"), + 0x0174: ("Ŵ", "LATIN CAPITAL LETTER W WITH CIRCUMFLEX"), + 0x0175: ("ŵ", "LATIN SMALL LETTER W WITH CIRCUMFLEX"), + 0x0176: ("Ŷ", "LATIN CAPITAL LETTER Y WITH CIRCUMFLEX"), + 0x0177: ("ŷ", "LATIN SMALL LETTER Y WITH CIRCUMFLEX"), + 0x0178: ("Ÿ", "LATIN CAPITAL LETTER Y WITH DIAERESIS"), + 0x0179: ("Ź", "LATIN CAPITAL LETTER Z WITH ACUTE"), + 0x017A: ("ź", "LATIN SMALL LETTER Z WITH ACUTE"), + 0x017B: ("Ż", "LATIN CAPITAL LETTER Z WITH DOT ABOVE"), + 0x017C: ("ż", "LATIN SMALL LETTER Z WITH DOT ABOVE"), + 0x017D: ("Ž", "LATIN CAPITAL LETTER Z WITH CARON"), + 0x017E: ("ž", "LATIN SMALL LETTER Z WITH CARON"), + 0x01FC: ("Ǽ", "LATIN CAPITAL LETTER AE WITH ACUTE"), + 0x01FD: ("ǽ", "LATIN SMALL LETTER AE WITH ACUTE"), + 0x01FE: ("Ǿ", "LATIN CAPITAL LETTER O WITH STROKE AND ACUTE"), + 0x01FF: ("ǿ", "LATIN SMALL LETTER O WITH STROKE AND ACUTE"), + 0x0218: ("Ș", "LATIN CAPITAL LETTER S WITH COMMA BELOW"), + 0x0219: ("ș", "LATIN SMALL LETTER S WITH COMMA BELOW"), + 0x021A: ("Ț", "LATIN CAPITAL LETTER T WITH COMMA BELOW"), + 0x021B: ("ț", "LATIN SMALL LETTER T WITH COMMA BELOW"), + 0x0237: ("ȷ", "LATIN SMALL LETTER DOTLESS J"), + 0x02C6: ("ˆ", "CIRCUMFLEX ACCENT"), + 0x02C7: ("ˇ", "CARON"), + 0x02D8: ("˘", "BREVE"), + 0x02D9: ("˙", "DOT ABOVE"), + 0x02DA: ("˚", "RING ABOVE"), + 0x02DB: ("˛", "OGONEK"), + 0x02DC: ("˜", "TILDE"), + 0x02DD: ("˝", "DOUBLE ACUTE ACCENT"), + 0x1E80: ("Ẁ", "LATIN CAPITAL LETTER W WITH GRAVE"), + 0x1E81: ("ẁ", "LATIN SMALL LETTER W WITH GRAVE"), + 0x1E82: ("Ẃ", "LATIN CAPITAL LETTER W WITH ACUTE"), + 0x1E83: ("ẃ", "LATIN SMALL LETTER W WITH ACUTE"), + 0x1E84: ("Ẅ", "LATIN CAPITAL LETTER W WITH DIAERESIS"), + 0x1E85: ("ẅ", "LATIN SMALL LETTER W WITH DIAERESIS"), + 0x1E9E: ("ẞ", "LATIN CAPITAL LETTER SHARP S"), + 0x1EF2: ("Ỳ", "LATIN CAPITAL LETTER Y WITH GRAVE"), + 0x1EF3: ("ỳ", "LATIN SMALL LETTER Y WITH GRAVE"), + 0x2013: ("–", "EN DASH"), + 0x2014: ("—", "EM DASH"), + 0x2018: ("‘", "LEFT SINGLE QUOTATION MARK"), + 0x2019: ("’", "RIGHT SINGLE QUOTATION MARK"), + 0x201A: ("‚", "SINGLE LOW-9 QUOTATION MARK"), + 0x201C: ("“", "LEFT DOUBLE QUOTATION MARK"), + 0x201D: ("”", "RIGHT DOUBLE QUOTATION MARK"), + 0x201E: ("„", "DOUBLE LOW-9 QUOTATION MARK"), + 0x2020: ("†", "DAGGER"), + 0x2021: ("‡", "DOUBLE DAGGER"), + 0x2022: ("•", "BULLET"), + 0x2026: ("…", "HORIZONTAL ELLIPSIS"), + 0x2030: ("‰", "PER MILLE SIGN"), + 0x2039: ("‹", "SINGLE LEFT-POINTING ANGLE QUOTATION MARK"), + 0x203A: ("›", "SINGLE RIGHT-POINTING ANGLE QUOTATION MARK"), + 0x2044: ("⁄", "FRACTION SLASH"), + 0x20AC: ("€", "EURO SIGN"), + 0x2122: ("™", "TRADE MARK SIGN"), + 0x2248: ("≈", "ALMOST EQUAL TO"), + 0x2260: ("≠", "NOT EQUAL TO"), + 0x2264: ("≤", "LESS-THAN OR EQUAL TO"), + 0x2265: ("≥", "GREATER-THAN OR EQUAL TO"), + 0x2212: ("−", "MINUS SIGN"), + # 0x00B5: ("µ", "MICRO SIGN"), + # 0x0394: ("Δ", "GREEK CAPITAL LETTER DELTA"), + # 0x03A9: ("Ω", "GREEK CAPITAL LETTER OMEGA"), + # 0x03BC: ("μ", "GREEK SMALL LETTER MU"), + # 0x03C0: ("π", "GREEK SMALL LETTER PI"), + # 0x2126: ("Ω", "OHM SIGN"), + # 0x2202: ("∂", "PARTIAL DIFFERENTIAL"), + # 0x2206: ("∆", "INCREMENT"), + # 0x220F: ("∏", "N-ARY PRODUCT"), + # 0x2211: ("∑", "N-ARY SUMMATION"), + # 0x221A: ("√", "SQUARE ROOT"), + # 0x221E: ("∞", "INFINITY"), + # 0x222B: ("∫", "INTEGRAL"), + # 0x25CA: ("◊", "LOZENGE"), + # 0xFB01: ("fi", "LATIN SMALL LIGATURE FI"), + # 0xFB02: ("fl", "LATIN SMALL LIGATURE FL"), +} diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/marks_width.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/marks_width.py new file mode 100644 index 0000000000..61521b1802 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/marks_width.py @@ -0,0 +1,58 @@ +import unicodedata + +from fontbakery.prelude import check, Message, PASS, FAIL +from fontbakery.utils import bullet_list + + +@check( + id="typenetwork/marks_width", + rationale=""" + To avoid incorrect overlappings when typing, glyphs that are spacing marks + must have width, on the other hand, combining marks should be 0 width. + """, + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def check_marks_width(ttFont, config): + """Check if marks glyphs have the correct width.""" + + def _is_non_spacing_mark_char(charcode): + category = unicodedata.category(chr(charcode)) + if category in ("Mn", "Me"): + return True + + def _is_spacing_mark_char(charcode): + category = unicodedata.category(chr(charcode)) + if category in ("Sk", "Lm"): + return True + + cmap = ttFont["cmap"].getBestCmap() + glyphSet = ttFont.getGlyphSet() + + failed_non_spacing_mark_chars = [] + failed_spacing_mark_chars = [] + + for charcode, glypname in cmap.items(): + if _is_non_spacing_mark_char(charcode): + if glyphSet[glypname].width != 0: + failed_non_spacing_mark_chars.append(glypname) + + if _is_spacing_mark_char(charcode): + if glyphSet[glypname].width == 0: + failed_spacing_mark_chars.append(glypname) + + if failed_non_spacing_mark_chars: + yield FAIL, Message( + "non-spacing-not-zero", + f"Combining accents with width advance width:\n\n" + f"{bullet_list(config, failed_non_spacing_mark_chars)}", + ) + + if failed_spacing_mark_chars: + yield FAIL, Message( + "non-spacing-not-zero", + f"Spacing marks without advance width:\n\n" + f"{bullet_list(config, failed_spacing_mark_chars)}", + ) + + if not failed_non_spacing_mark_chars and not failed_spacing_mark_chars: + yield PASS, "Marks have correct widths." diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/name/__init__.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/name/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/name/mandatory_entries.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/name/mandatory_entries.py new file mode 100644 index 0000000000..c3beb1ca2e --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/name/mandatory_entries.py @@ -0,0 +1,85 @@ +from fontbakery.prelude import check, Message, PASS, FAIL, INFO +from fontbakery.constants import NameID + + +@check( + id="typenetwork/name/mandatory_entries", + conditions=["style"], + rationale=""" + For proper functioning, fonts must have some specific records. + Other name records are optional but desireable to be present. + """, + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def check_name_mandatory_entries(ttFont, style): + """Font has all mandatory 'name' table entries?""" + from fontbakery.utils import get_name_entry_strings + from fontbakery.constants import RIBBI_STYLE_NAMES + + optional_nameIDs = [ + NameID.COPYRIGHT_NOTICE, + NameID.UNIQUE_FONT_IDENTIFIER, + NameID.VERSION_STRING, + NameID.TRADEMARK, + NameID.MANUFACTURER_NAME, + NameID.DESIGNER, + NameID.DESCRIPTION, + NameID.VENDOR_URL, + NameID.DESIGNER_URL, + NameID.LICENSE_DESCRIPTION, + NameID.LICENSE_INFO_URL, + ] + + required_nameIDs = [ + NameID.FONT_FAMILY_NAME, + NameID.FONT_SUBFAMILY_NAME, + NameID.FULL_FONT_NAME, + NameID.POSTSCRIPT_NAME, + ] + + unnecessary_nameIDs = [] + + if style not in RIBBI_STYLE_NAMES: + required_nameIDs += [ + NameID.TYPOGRAPHIC_FAMILY_NAME, + NameID.TYPOGRAPHIC_SUBFAMILY_NAME, + ] + else: + unnecessary_nameIDs += [ + NameID.TYPOGRAPHIC_FAMILY_NAME, + NameID.TYPOGRAPHIC_SUBFAMILY_NAME, + ] + + passed = True + # The font must have at least these name IDs: + for nameId in required_nameIDs: + for entry in get_name_entry_strings(ttFont, nameId): + if len(entry) == 0: + passed = False + yield FAIL, Message( + "missing-required-entry", + f"Font lacks entry with nameId={nameId}" + f" ({NameID(nameId).name})", + ) + + # The font should have these name IDs: + for nameId in optional_nameIDs: + if len(get_name_entry_strings(ttFont, nameId)) == 0: + passed = False + yield INFO, Message( + "missing-optional-entry", + f"Font lacks entry with nameId={nameId} ({NameID(nameId).name})", + ) + + # The font should NOT have these name IDs: + for nameId in unnecessary_nameIDs: + if len(get_name_entry_strings(ttFont, nameId)) != 0: + passed = False + yield INFO, Message( + "unnecessary-entry", + f"Font have unnecessary name entry with nameId={nameId}" + f" ({NameID(nameId).name})", + ) + + if passed: + yield PASS, "Font contains values for all mandatory name table entries." diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/usweightclass.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/usweightclass.py new file mode 100644 index 0000000000..7bc7875498 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/usweightclass.py @@ -0,0 +1,146 @@ +from fontbakery.testable import Font +from fontbakery.prelude import check, condition, Message, PASS, FAIL, INFO + + +@condition(Font) +def stylename(font): + ttFont = font.ttFont + if ttFont["name"].getDebugName(16): + styleName = ttFont["name"].getDebugName(17) + else: + styleName = ttFont["name"].getDebugName(2) + return styleName + + +@condition(Font) +def tn_expected_os2_weight(font): + """The weight name and the expected OS/2 usWeightClass value inferred from + the style part of the font name. + Here the common/expected values and weight names: + 100-250, Thin + 200-275, ExtraLight + 300, Light + 400, Regular + 500, Medium + 600, SemiBold + 700, Bold + 800, ExtraBold + 900, Black + Thin is not set to 100 because of legacy Windows GDI issues: + https://www.adobe.com/devnet/opentype/afdko/topic_font_wt_win.html + """ + if not font.stylename: + return None + # Weight name to value mapping: + TN_EXPECTED_WEIGHTS = { + "Thin": [100, 250], + "ExtraLight": [200, 275], + "Light": 300, + "Regular": 400, + "Medium": 500, + "SemiBold": 600, + "Bold": 700, + "ExtraBold": 800, + "Black": 900, + } + stylename = font.stylename + + # Modify style name for weights using space separator + prefixes = ["Semi ", "Ultra ", "Extra "] + for prefix in prefixes: + if prefix in stylename: + stylename = stylename.replace(prefix, prefix.strip()) + + if stylename == "Italic": + weight_name = "Regular" + elif stylename.endswith("Italic"): + weight_name = stylename.replace("Italic", "").rstrip() + elif stylename.endswith("Oblique"): + weight_name = stylename.replace("Oblique", "").rstrip() + else: + weight_name = stylename + + expected = None + for expectedWeightName, expectedWeightValue in TN_EXPECTED_WEIGHTS.items(): + if expectedWeightName.lower() in weight_name.lower().split(" "): + expected = expectedWeightValue + break + + return {"name": weight_name, "weightClass": expected} + + +@check( + id="typenetwork/usweightclass", + conditions=["tn_expected_os2_weight"], + rationale=""" + For Variable Fonts, it should be equal to default wght, for static ttfs, + Thin-Black can be 100-900 or 250-900, + for static otfs, Thin-Black must be 250-900. + + If static otfs are set lower than 250, text may appear blurry in + legacy Windows applications. + Glyphsapp users can change the usWeightClass value of an instance by adding + a 'weightClass' customParameter. + """, + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def check_usweightclass(font, tn_expected_os2_weight): + """Checking OS/2 usWeightClass.""" + failed = False + expected_value = tn_expected_os2_weight["weightClass"] + weight_name = tn_expected_os2_weight["name"].lower() + os2_value = font.ttFont["OS/2"].usWeightClass + + fail_message = "OS/2 usWeightClass is '{}' when it should be '{}'." + warn_message = "OS/2 usWeightClass is '{}' it will be better if it is '{}'." + no_value_message = "OS/2 usWeightClass is '{}' and weight name is '{}'." + + if font.is_variable_font: + fvar = font.ttFont["fvar"] + if font.has_wght_axis: + default_axis_values = {a.axisTag: a.defaultValue for a in fvar.axes} + fvar_value = default_axis_values.get("wght") + + if os2_value != int(fvar_value): + failed = True + yield FAIL, Message( + "bad-value", fail_message.format(os2_value, fvar_value) + ) + else: + if os2_value != 400: + failed = True + yield FAIL, Message("bad-value", fail_message.format(os2_value, 400)) + # overrides for static Thin and ExtaLight fonts + # for static ttfs, we don't mind if Thin is 250 and ExtraLight is 275. + # However, if the values are incorrect we will recommend they set Thin + # to 100 and ExtraLight to 250. + # for static otfs, Thin must be 250 and ExtraLight must be 275 + else: + if not expected_value: + failed = True + yield INFO, Message( + "no-value", no_value_message.format(os2_value, weight_name) + ) + + elif "thin" in weight_name.split(" "): + if os2_value not in expected_value: + failed = True + yield FAIL, Message( + "bad-value", fail_message.format(os2_value, expected_value) + ) + + elif "extralight" in weight_name.split(" "): + if os2_value not in expected_value: + failed = True + yield FAIL, Message( + "bad-value", fail_message.format(os2_value, expected_value) + ) + + elif os2_value != expected_value: + failed = True + yield FAIL, Message( + "bad-value", fail_message.format(os2_value, expected_value) + ) + + if not failed: + yield PASS, "OS/2 usWeightClass is good" diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/__init__.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/axes_have_variation.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/axes_have_variation.py new file mode 100644 index 0000000000..69bc7e2104 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/axes_have_variation.py @@ -0,0 +1,38 @@ +from fontbakery.prelude import check, Message, PASS, FAIL + + +@check( + id="typenetwork/varfont/axes_have_variation", + rationale=""" + Axes on a variable font must have variation. In other words min and max values + need to be different. It’s common to find fonts with unnecesary axes + added like `ital`. + """, + conditions=["is_variable_font"], + proposal=[ + "https://github.com/fonttools/fontbakery/pull/4260", + # "https://github.com/TypeNetwork/fontQA/issues/61", # Currently private repo. + ], +) +def check_varfont_axes_have_variation(ttFont): + """Check if font axes have variation""" + failedAxes = [] + for axis in ttFont["fvar"].axes: + if axis.minValue == axis.maxValue: + failedAxes.append( + { + "tag": axis.axisTag, + "minValue": axis.minValue, + "maxValue": axis.maxValue, + } + ) + + if failedAxes: + for failedAxis in failedAxes: + yield FAIL, Message( + "axis-has-no-variation", + f"'{failedAxis['tag']}' axis has no variation its min and max values" + f" are {failedAxis['minValue'], failedAxis['maxValue']}", + ) + else: + yield PASS, "All font axes has variation." diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/fvar_axes_order.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/fvar_axes_order.py new file mode 100644 index 0000000000..72b8d39594 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/varfont/fvar_axes_order.py @@ -0,0 +1,52 @@ +from fontbakery.prelude import check, Message, PASS, WARN, SKIP, INFO + + +@check( + id="typenetwork/varfont/fvar_axes_order", + rationale=""" + If a font doesn’t have a STAT table, instances get sorted better on Adobe Apps + when fvar axes follow a specific order: 'opsz', 'wdth', 'wght','ital', 'slnt'. + + We should deprecate this check since STAT is a required table. + """, + conditions=["is_variable_font"], + proposal=[ + "https://github.com/fonttools/fontbakery/pull/4260", + # "https://github.com/TypeNetwork/fontQA/issues/25", # Currently private repo. + ], +) +def check_varfont_fvar_axes_order(ttFont): + """Check fvar axes order""" + prefferedOrder = ["opsz", "wdth", "wght", "ital", "slnt"] + fontRegisteredAxes = [] + customAxes = [] + + if "STAT" in ttFont.keys(): + yield SKIP, "The font has a STAT table. This will control instances order." + else: + for index, axis in enumerate(ttFont["fvar"].axes): + if axis.axisTag in prefferedOrder: + fontRegisteredAxes.append(axis.axisTag) + else: + customAxes.append((axis.axisTag, index)) + + filtered = [axis for axis in prefferedOrder if axis in fontRegisteredAxes] + + if filtered != fontRegisteredAxes: + yield WARN, Message( + "axes-incorrect-order", + "Font’s registered axes are not in a correct order to get good" + "instances sorting on Adobe apps.\n\n" + f"Current order is {fontRegisteredAxes}, but it should be {filtered}", + ) + else: + yield PASS, "Font’s axes follow the preferred sorting." + + if customAxes: + yield INFO, Message( + "custom-axes", + "The font has custom axes with the indicated order:\n\n" + f"{customAxes}\n\n" + "Its order can depend on the kind of variation and the subfamily" + "groups that may create.", + ) diff --git a/Lib/fontbakery/checks/vendorspecific/typenetwork/vertical_metrics.py b/Lib/fontbakery/checks/vendorspecific/typenetwork/vertical_metrics.py new file mode 100644 index 0000000000..f6b4811931 --- /dev/null +++ b/Lib/fontbakery/checks/vendorspecific/typenetwork/vertical_metrics.py @@ -0,0 +1,102 @@ +from fontbakery.prelude import check, Message, PASS, FAIL, WARN + + +@check( + id="typenetwork/vertical_metrics", + rationale=""" + OS/2 and hhea vertical metric values should match. This will produce the + same linespacing on Mac, GNU+Linux and Windows. + + - Mac OS X uses the hhea values.⏎ + - Windows uses OS/2 or Win, depending on the OS or fsSelection bit value. + + When OS/2 and hhea vertical metrics match, the same linespacing results on + macOS, GNU+Linux and Windows. + """, + proposal=["https://github.com/fonttools/fontbakery/pull/4260"], +) +def check_vertical_metrics(ttFont): + """Checking vertical metrics.""" + + # Check required tables exist on font + required_tables = {"hhea", "OS/2"} + missing_tables = sorted(required_tables - set(ttFont.keys())) + if missing_tables: + for table_tag in missing_tables: + yield FAIL, Message("lacks-table", f"Font lacks '{table_tag}' table.") + return + + useTypoMetric = ttFont["OS/2"].fsSelection & (1 << 7) + + hheaAscent_equals_typoAscent = ttFont["hhea"].ascent == ttFont["OS/2"].sTypoAscender + hheaDescent_equals_typoDescent = abs(ttFont["hhea"].descent) == abs( + ttFont["OS/2"].sTypoDescender + ) + + hheaAscent_equals_winAscent = ttFont["hhea"].ascent == ttFont["OS/2"].usWinAscent + hheaDescent_equals_winDescent = ( + abs(ttFont["hhea"].descent) == ttFont["OS/2"].usWinDescent + ) + + typoMetricsSum = ( + ttFont["OS/2"].sTypoAscender + + abs(ttFont["OS/2"].sTypoDescender) + + ttFont["OS/2"].sTypoLineGap + ) + hheaMetricsSum = ( + ttFont["hhea"].ascent + abs(ttFont["hhea"].descent) + ttFont["hhea"].lineGap + ) + + if useTypoMetric: + if not hheaAscent_equals_typoAscent: + yield FAIL, Message( + "ascender", + f"OS/2 sTypoAscender ({ttFont['OS/2'].sTypoAscender})" + f" and hhea ascent ({ttFont['hhea'].ascent}) must be equal.", + ) + elif not hheaDescent_equals_typoDescent: + yield FAIL, Message( + "descender", + f"OS/2 sTypoDescender ({ttFont['OS/2'].sTypoDescender})" + f" and hhea descent ({ttFont['hhea'].descent}) must be equal.", + ) + elif ttFont["OS/2"].sTypoLineGap != 0: + yield FAIL, Message("hhea", "typo lineGap is not equal to 0.") + elif ttFont["hhea"].lineGap != 0: + yield FAIL, Message("hhea", "hhea lineGap is not equal to 0.") + else: + yield PASS, "Typo and hhea metrics are equal." + else: + yield WARN, Message( + "metrics-recommendation", + "OS/2 fsSelection USE_TYPO_METRICS is not enabled.\n\n" + "Type Networks recommends to enable it and follow the vertical metrics" + " scheme where basically hhea matches typo metrics. Read in more detail" + " about it in our vertical metrics guide.", + ) + + if hheaAscent_equals_typoAscent and hheaDescent_equals_winDescent: + yield FAIL, Message( + "useTypoMetricsDisabled", + "OS/2.fsSelection bit 7 (USE_TYPO_METRICS) is not enabled", + ) + elif not hheaAscent_equals_winAscent: + yield FAIL, Message( + "ascender", + f"hhea ascent ({ttFont['hhea'].ascent})" + f" and OS/2 win ascent ({ttFont['OS/2'].usWinAscent}) must be equal.", + ) + elif not hheaDescent_equals_winDescent: + yield FAIL, Message( + "descender", + f"hhea descent ({ttFont['hhea'].descent})" + f" and OS/2 win ascent ({ttFont['OS/2'].usWinDescent}) must be equal.", + ) + elif typoMetricsSum != hheaMetricsSum: + yield FAIL, Message( + "typo-and-hhea-sum", + f"OS/2 typo metrics sum ({typoMetricsSum}) must be" + f" equal to win metrics sum ({hheaMetricsSum})", + ) + else: + yield PASS, "hhea and Win metrics are equal and useTypoMetrics is disabled."