diff --git a/bin/scripts/name_parser/FontnameParser.py b/bin/scripts/name_parser/FontnameParser.py new file mode 100644 index 0000000000..cd62535eda --- /dev/null +++ b/bin/scripts/name_parser/FontnameParser.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python +# coding=utf8 + +import re +from FontnameTools import FontnameTools + +class FontnameParser: + """Parse a font name and generate all kinds of names""" + + def __init__(self, filename): + """Parse a font filename and store the results""" + self.parse_ok = False + self.for_windows = False + self.use_short_families = (False, False) # ( camelcase name, short styles ) + self.keep_regular_in_family = False + self.suppress_preferred_if_identical = True + self.fullname_suff = '' + self.fontname_suff = '' + self.family_suff = '' + self.name_subst = [] + [ self.parse_ok, self._basename, self.weight_token, self.style_token, self.other_token, self.rest ] = FontnameTools.parse_font_name(filename) + self.basename = self._basename + self.add_name_substitution_table(FontnameTools.SIL_TABLE) + + def _make_ps_mame(self, n): + """Helper to limit font name length in PS names""" + if self.for_windows and len(n) > 31: + print('Shortening too long PS family name') + return n[:31] + return n + + def set_for_windows(self, for_windows): + """Create slightly different names, suitable for Windows use""" + self.for_windows = for_windows + return self + + def set_keep_regular_in_family(self, keep): + """Familyname may contain 'Regular' where it should normally be suppressed""" + self.keep_regular_in_family = keep + + def set_suppress_preferred(self, suppress): + """Suppress ID16/17 if it is identical to ID1/2 (True is default)""" + self.suppress_preferred_if_identical = suppress + + def inject_suffix(self, fullname, fontname, family): + """Add a custom additonal string that shows up in the resulting names""" + self.fullname_suff = fullname.strip() + self.fontname_suff = fontname.replace(' ', '') + self.family_suff = family.strip() + return self + # font-patcher behavior: + # verboseSuff = "Nerd Font" + # shortSuff = win ? "NF" : "Nerd Font" + # verboseSuff += "Plus Font Awesome" + # shortSuff += "A" + # OR when complete: + # shortSuff = "Nerd Font Complete" + # verboseSuff = "Nerd Font Complete" + # AND + # shortSuff += "M" + # verboseSuff += "Mono" + # + # fullname += verboseSuff + # fontname += shortSuff + # if win familyname += "NF" + # else familyname += "Nerd Font" + # if win fullname += "Windows Compatible" + # if !win familyname += "Mono" + # + # THUS: + # fontname => shortSuff + # fullname => verboseSuff {{ we do the following already: }} + win ? "Windows Compatible" : "" + # family => win ? "NF" : "Nerd Font" + mono ? "Mono" : "" + + def enable_short_families(self, camelcase_name, prefix): + """Enable short styles in Family when (original) font name starts with prefix; enable CamelCase basename in (Typog.) Family""" + # camelcase_name is boolean + # prefix is either a string or False + self.use_short_families = ( camelcase_name, prefix and self._basename.startswith(prefix) ) + return self + + def add_name_substitution_table(self, table): + """Have some fonts renamed, takes list of tuples (regex, replacement)""" + # The regex will be anchored to name begin and used case insensitive + # Replacement can have regex matches, mind to catch the correct source case + self.name_subst = table + self.basename = self._basename + for regex, replacement in self.name_subst: + newname = re.sub('^' + regex, replacement, self._basename, 1, re.IGNORECASE) + if newname != self._basename: + self.basename = newname + break + return self + + def drop_for_powerline(self): + """Remove 'for Powerline' from all names (can not be undone)""" + if 'Powerline' in self.other_token: + idx = self.other_token.index('Powerline') + self.other_token.pop(idx) + if idx > 0 and self.other_token[idx - 1] == 'For': + self.other_token.pop(idx - 1) + self._basename = re.sub(r'\b(for\s?)?powerline\b', '', self._basename, 1, re.IGNORECASE).strip() + self.basename = self._basename + self.add_name_substitution_table(self.name_subst) # re-evaluate + return self + + ### Following the creation of the name parts: + # + # Relevant websites + # https://www.fonttutorials.com/how-to-name-font-family/ + # https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids + # https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fss + # https://docs.microsoft.com/en-us/typography/opentype/spec/head#macstyle + + # Example (mind that they group 'semibold' as classic-group-of-4 Bold, while we will always only take bold as Bold): + # Adobe Caslon Pro Regular ID1: Adobe Caslon Pro ID2: Regular + # Adobe Caslon Pro Italic ID1: Adobe Caslon Pro ID2: Italic + # Adobe Caslon Pro Semibold ID1: Adobe Caslon Pro ID2: Bold ID16: Adobe Caslon Pro ID17: Semibold + # Adobe Caslon Pro Semibold Italic ID1: Adobe Caslon Pro ID2: Bold Italic ID16: Adobe Caslon Pro ID17: Semibold Italic + # Adobe Caslon Pro Bold ID1: Adobe Caslon Pro Bold ID2: Regular ID16: Adobe Caslon Pro ID17: Bold + # Adobe Caslon Pro Bold Italic ID1: Adobe Caslon Pro Bold ID2: Italic ID16: Adobe Caslon Pro ID17: Bold Italic + + # fontname === preferred_family + preferred_styles + # fontname === family + subfamily + # + # familybase = basename + rest + other (+ suffix) + # ID 1/2 just have self.style in the subfamily, all the rest ends up in the family + # ID 16/17 have self.style and self.weight in the subfamily, the rest ends up in the family + + def fullname(self): + """Get the SFNT Fullname (ID 4)""" + if self.for_windows: + win = 'Windows Compatible' + else: + win = '' + styles = self.style_token + weights = self.weight_token + if ('Regular' in styles + and (not self.keep_regular_in_family # User says: Regular is the normal font, so it is not mentioned + or len(self.weight_token) > 0)): # This is actually a malformed font name + styles = list(self.style_token) + styles.remove('Regular') + # For naming purposes we want Oblique to be part of the styles + (weights, styles) = FontnameTools.make_oblique_style(weights, styles) + return FontnameTools.concat(self.basename, self.rest, self.other_token, self.fullname_suff, win, weights, styles) + + def psname(self): + """Get the SFNT PostScriptName (ID 6)""" + # The name string must be no longer than 63 characters and restricted to the printable ASCII subset, codes 33 to 126, + # except for the 10 characters '[', ']', '(', ')', '{', '}', '<', '>', '/', '%' + out = "" + # This is almost self.family() + '-' + self.subfamily() but without short styles: + for c in FontnameTools.concat(self.basename, self.rest, self.other_token, self.fontname_suff) + '-' + FontnameTools.concat(self.weight_token, self.style_token): + if c in '[](){}<>/%': + continue + if ord(c) < 33 or ord(c) > 126: + continue + out += c + return out[:63] + + def preferred_family(self): + """Get the SFNT Preferred Familyname (ID 16)""" + if self.suppress_preferred_if_identical and len(self.weight_token) == 0: + # Do not set if identical to ID 1 + return '' + (short_name, _) = self.use_short_families + name = self.basename if not short_name else self.basename.replace(' ', '') + return FontnameTools.concat(name, self.rest, self.other_token, self.family_suff) + + def preferred_styles(self): + """Get the SFNT Preferred Styles (ID 17)""" + styles = self.style_token + weights = self.weight_token + if self.suppress_preferred_if_identical and len(weights) == 0: + # Do not set if identical to ID 2 + return '' + # For naming purposes we want Oblique to be part of the styles + (weights, styles) = FontnameTools.make_oblique_style(weights, styles) + return FontnameTools.concat(weights, styles) + + def family(self): + """Get the SFNT Familyname (ID 1)""" + # We use the short form of the styles to save on number of chars + name = self.basename + other = self.other_token + weight = self.weight_token + (short_name, short_styles) = self.use_short_families + if short_styles: + other = FontnameTools.short_styles(other) + weight = FontnameTools.short_styles(weight) + if short_name: + name = self.basename.replace(' ', '') + return FontnameTools.concat(name, self.rest, other, self.family_suff, weight) + + def subfamily(self): + """Get the SFNT SubFamily (ID 2)""" + if len(self.style_token) == 0: + if 'Oblique' in self.weight_token: + return FontnameTools.concat(self.style_token, 'Italic') + return 'Regular' + if 'Oblique' in self.weight_token and not 'Italic' in self.style_token: + return FontnameTools.concat(self.style_token, 'Italic') + return FontnameTools.concat(self.style_token) + + def ps_familyname(self): + """Get the PS Familyname""" + return self._make_ps_mame(self.family()) + + def ps_fontname(self): + """Get the PS fontname""" + # This Adobe restriction is classically ignored + # if len(n) > 29: + # print('Shortening too long PS fontname') + # return n[:29] + return self._make_ps_mame(self.psname()) + + def macstyle(self, style): + """Modify a given macStyle value for current name, just bits 0 and 1 touched""" + b = style & (~3) + b |= 1 if 'Bold' in self.style_token else 0 + b |= 2 if 'Italic' in self.style_token else 0 + return b + + def fs_selection(self, fs): + """Modify a given fsSelection value for current name, bits 0, 5, 6, 8, 9 touched""" + ITALIC = 1 << 0; BOLD = 1 << 5; REGULAR = 1 << 6; WWS = 1 << 8; OBLIQUE = 1 << 9 + b = fs & (~(ITALIC | BOLD | REGULAR | WWS | OBLIQUE)) + if 'Bold' in self.style_token: + b |= BOLD + # Ignore Italic if we have Oblique + if 'Oblique' in self.weight_token: + b |= OBLIQUE + elif 'Italic' in self.style_token: + b |= ITALIC + # Regular is just the basic weight + if len(self.weight_token) == 0: + b |= REGULAR + b |= WWS # We assert this by our naming process + return b + + def rename_font(self, font): + """Rename the font to include all information we found (font is fontforge font object)""" + font.fontname = self.ps_fontname() + font.fullname = self.fullname() + font.familyname = self.ps_familyname() + + # We have to work around several issues in fontforge: + # + # a. Remove some entries from SFNT table; fontforge has no API function for that + # + # b. Fontforge does not allow to set SubFamily (and other) to any value: + # + # Fontforge lets you set any value, unless it is the default value. If it + # is the default value it does not set anything. It also does not remove + # a previously existing non-default value. Why it is done this way is + # unclear: + # fontforge/python.c SetSFNTName() line 11431 + # return( 1 ); /* If they set it to the default, there's nothing to do */ + # + # Then is the question: What is the default? It is taken from the + # currently set fontname (??!). The fontname is parsed and everything + # behind the dash is the default SubFamily: + # fontforge/tottf.c DefaultTTFEnglishNames() + # fontforge/splinefont.c _GetModifiers() + # + # To fix this without touching Fontforge we need to set the SubFamily + # directly in the SFNT table: + # + # c. Fontforge has the bug that it allows to write empty-string to a SFNT field + # and it is actually embedded as empty string, but empty strings are not + # shown if you query the sfnt_names *rolleyes* + + sfnt_list = [] + TO_DEL = ['Family', 'SubFamily', 'Fullname', 'Postscriptname', 'Preferred Family', 'Preferred Styles', 'Compatible Full'] + for l, k, v in list(font.sfnt_names): + if not k in TO_DEL: + sfnt_list += [( l, k, v )] + + sfnt_list += [( 'English (US)', 'Family', self.family() )] + sfnt_list += [( 'English (US)', 'SubFamily', self.subfamily() )] + sfnt_list += [( 'English (US)', 'Fullname', self.fullname() )] + sfnt_list += [( 'English (US)', 'PostScriptName', self.psname() )] + + p_fam = self.preferred_family() + if len(p_fam): + sfnt_list += [( 'English (US)', 'Preferred Family', p_fam )] + p_sty = self.preferred_styles() + if len(p_sty): + sfnt_list += [( 'English (US)', 'Preferred Styles', p_sty )] + + font.sfnt_names = tuple(sfnt_list) + + font.macstyle = self.macstyle(font.macstyle) + + # TODO: fsSelection, unfortunately fontforge does not support that directly + # but has some automaton to deduce it from macstyle, which means loosing information + # https://github.com/fontforge/fontforge/issues/2131 + # https://github.com/jsomedon/Fix-fsSelection-bits-for-SF-fonts/blob/main/fix_fsSelection.sh + # Well, lets ignore it for now, as we always did ;) diff --git a/bin/scripts/name_parser/FontnameTools.py b/bin/scripts/name_parser/FontnameTools.py new file mode 100644 index 0000000000..7cb5abdfba --- /dev/null +++ b/bin/scripts/name_parser/FontnameTools.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python +# coding=utf8 + +import re + +class FontnameTools: + """Deconstruct a font filename to get standardized name parts""" + + @staticmethod + def front_upper(word): + """Capitalize a string (but keep case of subsequent chars)""" + return word[:1].upper() + word[1:] + + @staticmethod + def camel_casify(word): + """Remove blanks and use CamelCase for the new word""" + return ''.join(map(FontnameTools.front_upper, word.split(' '))) + + @staticmethod + def camel_explode(word): + """Explode CamelCase -> Camel Case""" + # But do not explode "JetBrains" etc at string start... + excludes = [ + 'JetBrains', + 'DejaVu', + 'OpenDyslexicAlta', + 'OpenDyslexicMono', + 'OpenDyslexic', + 'DaddyTimeMono', + 'InconsolataGo', + 'ProFontWindows', + 'ProFont', + 'ProggyClean', + ] + m = re.match('(' + '|'.join(excludes) + ')(.*)', word) + (prefix, word) = m.group(1,2) if m != None else ('', word) + if len(word) == 0: + return prefix + parts = re.split('(?<=[a-z])(?=[A-Z])', word) + if len(prefix): + parts.insert(0, prefix) + return ' '.join(parts) + + @staticmethod + def drop_empty(l): + """Remove empty strings from list of strings""" + return [x for x in l if len(x) > 0] + + @staticmethod + def concat(*all_things): + """Flatten list of (strings or lists of strings) to a blank-separated string""" + all = [] + for thing in all_things: + if type(thing) == str: + all.append(thing) + else: + all += thing + return ' '.join(FontnameTools.drop_empty(all)) + + @staticmethod + def unify_style_names(style_name): + """Substitude some known token with standard wording""" + known_names = { + # Source of the table is the current sourcefonts + '-': '', + 'Book': '', + 'Roman': '', + #'Text': '', + #'SemiBold': 'Demi', + 'ob': 'Oblique', + 'it': 'Italic', + 'i': 'Italic', + 'b': 'Bold', + 'Normal': 'Regular', + 'c': 'Condensed', + 'r': 'Regular', + 'm': 'Medium', + 'l': 'Light', + } + if style_name in known_names: + return known_names[style_name] + return style_name + + @staticmethod + def shorten_style_name(name): + """Substitude some known styles to short form""" + known_names = { + # Chiefly from Noto + 'Condensed': 'Cond', + 'SemiCondensed': 'SemCond', + 'ExtraCondensed': 'ExtCond', + 'ExtraBold': 'ExtBd', + 'SemiBold': 'SemBd', + 'Medium': 'Med', + 'ExtraLight': 'ExtLt', + 'Black': 'Blk', + } + if name in known_names: + return known_names[name] + return name + + @staticmethod + def short_styles(styles): + """Shorten all style names in a list""" + return list(map(FontnameTools.shorten_style_name, styles)) + @staticmethod + def make_oblique_style(weights, styles): + """Move "Oblique" from weights to styles for font naming purposes""" + if 'Oblique' in weights: + weights = list(weights) + weights.remove('Oblique') + styles = list(styles) + styles.append('Oblique') + return (weights, styles) + + @staticmethod + def get_name_token(name, tokens): + """Try to find any case insensitive token from tokens in the name, return tuple with found token-list and rest""" + lower_tokens = [ t.lower() for t in tokens ] + not_matched = "" + all_tokens = [] + j = 1 + while j: + j = re.match('(.*?)(' + '|'.join(tokens) + ')(.*)', name, re.IGNORECASE) + if not j: + break + not_matched += ' ' + j.groups()[0] # Blanc prevents unwanted concatenation of unmatched substrings + tok = j.groups()[1].lower() + tok = tokens[lower_tokens.index(tok)] + tok = FontnameTools.unify_style_names(tok) + if len(tok): + all_tokens.append(tok) + name = j.groups()[2] # Recurse rest + not_matched += ' ' + name + return ( not_matched.strip(), all_tokens ) + + @staticmethod + def _parse_simple_font_name(name): + """Parse a filename that does not follow the 'FontFamilyName-FontStyle' pattern""" + # No dash in name, maybe we have blanc separated filename? + if ' ' in name: + return FontnameTools.parse_font_name(name.replace(' ', '-')) + # Do we have a number-name boundary? + p = re.split('(?<=[0-9])(?=[a-zA-Z])', name) + if len(p) > 1: + return FontnameTools.parse_font_name('-'.join(p)) + # Or do we have CamelCase? + n = FontnameTools.camel_explode(name) + if n != name: + return FontnameTools.parse_font_name(n.replace(' ', '-')) + return (False, FontnameTools.camel_casify(name), [], [], [], '') + + @staticmethod + def parse_font_name(name): + """Expects a filename following the 'FontFamilyName-FontStyle' pattern and returns ... parts""" + name = re.sub('[_\s]+', ' ', name) + matches = re.match(r'([^-]+)(?:-(.*))?', name) + familyname = FontnameTools.camel_casify(matches.group(1)) + style = matches.group(2) + + if not style: + return FontnameTools._parse_simple_font_name(name) + + # These are the FontStyle keywords we know, in three categories + # Weights end up as Typographic Family parts ('after the dash') + # Styles end up as Family parts (for classic grouping of four) + # Others also end up in Typographic Family ('before the dash') + weights = [ 'Thin', 'Light', 'ExtraLight', 'SemiBold', 'Demi', + 'SemiLight', 'Medium', 'Black', 'ExtraBold', 'Heavy', + 'Oblique', 'Condensed', 'SemiCondensed', 'ExtraCondensed', + 'Narrow', 'SemiNarrow', 'Retina', ] + styles = [ 'Bold', 'Italic', 'Regular', 'Normal', ] + other = [ 'Book', '-', 'Roman', 'Text', 'For', 'Powerline', ] + other += [ 'LGC', '11', 'uni-11', '14', 'uni-14', 'IIx', 'CE'] # Some font specialities + other += [ '1mn', '1c', '1m', '1p', '2c', '2m', '2p', ] # MPlus names need to be preserved (sort array elements long to short, i.e. '1mn' first) + + # Sometimes used abbreviations + weight_abbrevs = [ 'ob', 'c', 'm', 'l', ] + style_abbrevs = [ 'it', 'r', 'b', 'i', ] + + ( style, weight_token ) = FontnameTools.get_name_token(style, weights) + ( style, style_token ) = FontnameTools.get_name_token(style, styles) + ( style, other_token ) = FontnameTools.get_name_token(style, other) + if len(style) < 4: + ( style, weight_token_abbrevs ) = FontnameTools.get_name_token(style, weight_abbrevs) + ( style, style_token_abbrevs ) = FontnameTools.get_name_token(style, style_abbrevs) + weight_token += weight_token_abbrevs + style_token += style_token_abbrevs + while 'Regular' in style_token and len(style_token) > 1: + # Correct situation where "Regular" and something else is given + style_token.remove('Regular') + + # Recurse to see if unmatched stuff between dashes can belong to familyname + if '-' in style: + matches = re.match(r'(\w+)-(.*)', style) + return FontnameTools.parse_font_name(familyname + matches.group(1) + '-' + matches.group(2)) + + style = re.sub(r'(^|\s)\d+(\.\d+)+(\s|$)', r'\1\3', style) # Remove (free standing) version numbers + style_parts = FontnameTools.drop_empty(style.split(' ')) + style = ' '.join(map(FontnameTools.front_upper, style_parts)) + familyname = FontnameTools.camel_explode(familyname) + return (True, familyname, weight_token, style_token, other_token, style) + + SIL_TABLE = [ + ( '(s)ource', r'\1auce' ), + ( '(h)ermit', r'\1urmit' ), + ( '(h)asklig', r'\1asklug' ), + ( '(s)hare', r'\1hure' ), + ( 'IBM[- ]?plex', r'Blex' ), # We do not keep the case here + ( '(t)erminus', r'\1erminess' ), + ( '(l)iberation', r'\1iteration' ), + ( 'iA([- ]?)writer', r'iM\1Writing' ), + ( '(a)nka/(c)oder', r'\1na\2onder' ), + ( '(c)ascadia( ?)(c)ode', r'\1askaydia\2\3ove' ), + ( '(c)ascadia( ?)(m)ono', r'\1askaydia\2\3ono' ), + ( '(m)plus', r'\1+'), # Added this, because they use a plus symbol :-> + # Noone cares that font names starting with a digit are forbidden: + # ( '(3270)', r'Ibeam\1'), + ] diff --git a/bin/scripts/name_parser/name_parser_test1 b/bin/scripts/name_parser/name_parser_test1 new file mode 100644 index 0000000000..1b03602e6d --- /dev/null +++ b/bin/scripts/name_parser/name_parser_test1 @@ -0,0 +1,189 @@ +#!/usr/bin/env python +# coding=utf8 + +import sys +import re +import os.path +import fontforge +from FontnameParser import FontnameParser + +###### Some helpers + +def get_sfnt_dict(font): + """Extract SFNT table as nice dict""" + d = [] + for i, el in enumerate(font.sfnt_names): + d += [(el[1], el[2])] + return dict(d) + +def format_names(header, *stuff): + """Unify outputs (with header)""" + f = '{:1.1}|{:50.50} |{:1.1}| {:50.50} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:.30}' + if header: + d = '------------------------------------------------------------' + return f.format(*stuff) + '\n' + f.format('', d, d, d, d, d, d, d, d, d, d, d) + return f.format(*stuff).rstrip() + +def lenient_cmp(s1, s2): + """Compare two font name (parts) but be a bit lenient ;->""" + # We do not care about: + # - Case + # - "Display" vs "Disp" (in Noto) + # Allow for "IBM 3278" name + s = [ s1, s2 ] + for i in range(2): + # Usually given transform from 'their' to 'our' style + s[i] = s[i].lower() + s[i] = re.sub(r'\bdisp\b', 'display', s[i]) # Noto + s[i] = s[i].replace('ibm 3270', '3270') # 3270 + s[i] = s[i].replace('3270-', '3270 ') # 3270 + s[i] = s[i].replace('lekton-', 'lekton ') # Lekton + s[i] = s[i].replace('semi-narrow', 'seminarrow') # 3270 + s[i] = s[i].replace('bolditalic', 'bold italic') + s[i] = re.sub(r'\bfor\b', '', s[i]) # Meslo, Monofur + s[i] = re.sub(r'\bpowerline\b', '', s[i]) # Meslo, Monofur + s[i] = s[i].replace('fira mono', 'fura mono') # Obviously someone forgot to rename the fonts in Fira/ + s[i] = s[i].replace('aurulentsansmono-', 'aurulent sans mono ') # Aurulent fullname oddity + s[i] = s[i].replace('mononoki-', 'mononoki ') # Mononoki has somtimes a dash + s[i] = re.sub(r'\br\b', 'regular', s[i]) # Nonstandard style in Agave + s[i] = re.sub(r'(bitstream vera sans mono.*) oblique', r'\1 italic', s[i]) # They call it Oblique but the filename says Italic + s[i] = re.sub(r'gohufont (uni-)?(11|14)', 'gohufont', s[i]) # They put the 'name' into the subfamily/weight + s[i] = s[i].replace('xltobl', 'extralight oblique') # Iosevka goes inventing names + s[i] = re.sub(r'proggyclean(?!TT)( ?)', 'proggycleantt\1', s[i]) # ProggyClean has no TT in filename + + s[i] = re.sub(r' +', ' ', s[i]).strip() + return s[0] == s[1] + +TEST_TABLE = [ + ( '(m)plus', '\\1+'), + ( 'IAWriter', 'iA Writer'), + ( 'IBMPlex', 'IBM Plex'), + ( 'Vera', 'Bitstream Vera Sans'), +] + +KEEP_REGULAR = [ + 'Overpass', + 'Lilex', + 'Inconsolata$', # not InconsolataGo + 'iA Writer', + 'Fira', + 'Cascadia', + 'Aurulent', + 'Agave', + 'Meslo', + 'Monoid', + 'Mononoki', + 'Arimo', + 'Cousine', + 'Fantasque', + 'Hack', + 'JetBrains Mono', + 'Noto Sans', + 'Noto Serif', + 'Victor', +] + +def is_keep_regular(parsed): + """This has been decided by the font designers, we need to mimic that (for comparison purposes)""" + for kr in KEEP_REGULAR: + if (parsed.basename + '$').startswith(kr): return True + return False + +###### Let's go! + +if len(sys.argv) < 2: + print('Usage: {} font_name [font_name ...]\n'.format(sys.argv[0])) + sys.exit(1) + +try: + with open(sys.argv[0] + '.known_issues', 'r') as f: + known_issues = f.read().splitlines() + # known_issues = [line.rstrip() for line in known_issues] + print('Found {:.0f} known issues'.format(len(known_issues) / 3)) # approx ;) +except OSError: + print('Can not open known_issues file') + known_issues = [] +new_issues = open(sys.argv[0] + '.known_issues.new', 'w') + +print('Examining {} font files'.format(len(sys.argv) - 1)) +all_files = 0 +issue_files = 0 +known_files = 0 + +print(format_names(True, '', 'Filename', '', 'Fullname', '', 'Family', '', 'Subfamily', '', 'Typogr. Family', '', 'Typogr. Subfamily')) + +for filename in sys.argv[1:]: + fullfile = os.path.basename(filename) + fname = os.path.splitext(fullfile)[0] + if fname == 'NotoColorEmoji': + continue # font is not patchable + n = FontnameParser(fname).enable_short_families(False, 'Noto').add_name_substitution_table(TEST_TABLE) + n.set_keep_regular_in_family(is_keep_regular(n)) # Stardard is FALSE + # Example for name injection: + # n.inject_suffix("Nerd Font Complete Mono", "Nerd Font Complete M", "Nerd Font Mono") + + font = fontforge.open(filename, 1) + sfnt = get_sfnt_dict(font) + font.close() + all_files += 1 + + sfnt_full = sfnt['Fullname'] + sfnt_fam = sfnt['Family'] + sfnt_subfam = sfnt['SubFamily'] + sfnt_pfam = sfnt['Preferred Family'] if 'Preferred Family' in sfnt else '' + sfnt_psubfam = sfnt['Preferred Styles'] if 'Preferred Styles' in sfnt else '' + + t1 = not lenient_cmp(sfnt_full, n.fullname()) + t2 = not lenient_cmp(sfnt_fam, n.family()) + t3 = not lenient_cmp(sfnt_subfam, n.subfamily()) + t4 = not lenient_cmp(sfnt_pfam, n.preferred_family()) + t5 = not lenient_cmp(sfnt_psubfam, n.preferred_styles()) + + # Lenience: Allow for dropping unneeded prefered stuff: + # New (sub)family is same as old preferred sub(family) + if t4 and n.preferred_family() == '' and sfnt_pfam.lower() == n.family().lower(): + t4 = False + if t5 and n.preferred_styles() == '' and sfnt_psubfam.lower() == n.subfamily().lower(): + t5 = False + + if t1 or t2 or t3 or t4 or t5: + m1 = '+'; m2 = '-' + else: + m1 = ''; m2 = '' + if not n.parse_ok: + m1 = '!' + t1_ = 'X' if t1 else '' + t2_ = 'X' if t2 else '' + t3_ = 'X' if t3 else '' + t4_ = 'X' if t4 else '' + t5_ = 'X' if t5 else '' + + o1 = format_names(False, m1, fullfile, t1_, n.fullname(), t2_, n.family(), t3_, n.subfamily(), t4_, n.preferred_family(), t5_, n.preferred_styles()) + o2 = format_names(False, m2, fullfile, '', sfnt_full, '', sfnt_fam, '', sfnt_subfam, '', sfnt_pfam, '', sfnt_psubfam) + + if len(m1): + issue_files += 1 + if not o1 in known_issues or not o2 in known_issues: + new_issues.writelines(['#### AUTOGENERATED\n', o1 + '\n', o2 + '\n']) + else: + known_files += 1 + idx = known_issues.index(o1) - 1 # should be the index of the explanation line + if known_issues[idx][0] != '#': + sys.exit('Problem with known issues file, line', known_issues.index(o1)) + new_issues.writelines([known_issues[idx] + '\n', o1 + '\n', o2 + '\n']) + # remove known_issue triplet + known_issues.pop(idx) + known_issues.pop(idx) + known_issues.pop(idx) + + print(o1, o2, sep='\n') + +print('Fonts with different name rendering: {}/{} ({}/{} are in known_issues)'.format(issue_files, all_files, known_files, issue_files)) + +if len(known_issues) > 0: + print('There are {} lines not needed in known_issues, appending commented out in new known_issues'.format(len(known_issues))) + new_issues.write('\n#### The following lines are not needed anymore\n\n') + for l in known_issues: + new_issues.writelines([' ', l, '\n']) + +new_issues.close() diff --git a/bin/scripts/name_parser/name_parser_test1.known_issues b/bin/scripts/name_parser/name_parser_test1.known_issues new file mode 100644 index 0000000000..41b50d7cda --- /dev/null +++ b/bin/scripts/name_parser/name_parser_test1.known_issues @@ -0,0 +1,210 @@ +#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead ++|3270Medium.otf | | 3270 Medium |X| 3270 Medium |X| Regular |X| 3270 |X| Medium +-|3270Medium.otf | | 3270-Medium | | IBM 3270 | | Medium | | | | +#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead ++|3270Medium.ttf | | 3270 Medium |X| 3270 Medium |X| Regular |X| 3270 |X| Medium +-|3270Medium.ttf | | 3270-Medium | | IBM 3270 | | Medium | | | | +#### Limit Subfamiliy to 4 standard styles, obviously for them Medium is Regular ++|3270Narrow.otf | | 3270 Narrow | | 3270 Narrow |X| Regular |X| 3270 |X| Narrow +-|3270Narrow.otf | | 3270 Narrow | | IBM 3270 Narrow | | Medium | | | | +#### Limit Subfamiliy to 4 standard styles, obviously for them Medium is Regular ++|3270Narrow.ttf | | 3270 Narrow | | 3270 Narrow |X| Regular |X| 3270 |X| Narrow +-|3270Narrow.ttf | | 3270 Narrow | | IBM 3270 Narrow | | Medium | | | | +#### Limit Subfamiliy to 4 standard styles, obviously for them Medium is Regular ++|3270SemiNarrow.otf | | 3270 SemiNarrow | | 3270 SemiNarrow |X| Regular |X| 3270 |X| SemiNarrow +-|3270SemiNarrow.otf | | 3270 Semi-Narrow | | IBM 3270 Semi-Narrow | | Medium | | | | +#### Limit Subfamiliy to 4 standard styles, obviously for them Medium is Regular ++|3270SemiNarrow.ttf | | 3270 SemiNarrow | | 3270 SemiNarrow |X| Regular |X| 3270 |X| SemiNarrow +-|3270SemiNarrow.ttf | | 3270 Semi-Narrow | | IBM 3270 Semi-Narrow | | Medium | | | | +#### Drop special/unexpected name in Typographic Family ++|Anonymice Powerline.ttf | | Anonymice Powerline | | Anonymice Powerline | | Regular |X| | | +-|Anonymice Powerline.ttf | | Anonymice Powerline | | Anonymice Powerline | | Regular | | Anonymous Pro for Powerline | | Regular +#### Font file says it is italic and not oblique ++|VeraMono-Bold-Italic.ttf | | Bitstream Vera Sans Mono Bold Italic | | Bitstream Vera Sans Mono |X| Bold Italic | | | | +-|VeraMono-Bold-Italic.ttf | | Bitstream Vera Sans Mono Bold Oblique | | Bitstream Vera Sans Mono | | Bold Oblique | | | | +#### Font file says it is italic and not oblique ++|VeraMono-Italic.ttf | | Bitstream Vera Sans Mono Italic | | Bitstream Vera Sans Mono |X| Italic | | | | +-|VeraMono-Italic.ttf | | Bitstream Vera Sans Mono Oblique | | Bitstream Vera Sans Mono | | Oblique | | | | +#### Limit Subfamiliy to 4 standard styles, Roman is usually equivalent to Regular ++|VeraMono.ttf | | Bitstream Vera Sans Mono | | Bitstream Vera Sans Mono |X| Regular | | | | +-|VeraMono.ttf | | Bitstream Vera Sans Mono | | Bitstream Vera Sans Mono | | Roman | | | | +#### Limit Subfamiliy to 4 standard styles, Book is usually equivalent to Regular +!|DaddyTimeMono.ttf | | DaddyTimeMono | | DaddyTimeMono |X| Regular | | | | +-|DaddyTimeMono.ttf | | DaddyTimeMono | | DaddyTimeMono | | Book | | DaddyTimeMono | | +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|DejaVuSansMono-BoldOblique.ttf | | DejaVu Sans Mono Bold Oblique |X| DejaVu Sans Mono Oblique |X| Bold Italic |X| DejaVu Sans Mono |X| Bold Oblique +-|DejaVuSansMono-BoldOblique.ttf | | DejaVu Sans Mono Bold Oblique | | DejaVu Sans Mono | | Bold Oblique | | | | +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|DejaVuSansMono-Oblique.ttf | | DejaVu Sans Mono Oblique |X| DejaVu Sans Mono Oblique |X| Italic |X| DejaVu Sans Mono |X| Oblique +-|DejaVuSansMono-Oblique.ttf | | DejaVu Sans Mono Oblique | | DejaVu Sans Mono | | Oblique | | | | +#### Limit Subfamiliy to 4 standard styles, Book is usually equivalent to Regular ++|DejaVuSansMono.ttf | | DejaVu Sans Mono | | DejaVu Sans Mono |X| Regular | | | | +-|DejaVuSansMono.ttf | | DejaVu Sans Mono | | DejaVu Sans Mono | | Book | | | | +#### No need to have Typographic Family/Subfamily if it is identical to normal Family/Subfamily ++|FuraMono-Bold Powerline.otf | | Fura Mono Powerline Bold | | Fura Mono Powerline | | Bold |X| | | +-|FuraMono-Bold Powerline.otf | | Fira Mono Bold for Powerline | | Fira Mono for Powerline | | Bold | | Fira Mono for Powerline | | +#### False positive, move Powerline from end to middle, Powerline will be dropped when patching anyhow ++|FuraMono-Medium Powerline.otf | | Fura Mono Powerline Medium | | Fura Mono Powerline Medium | | Regular |X| Fura Mono Powerline | | Medium +-|FuraMono-Medium Powerline.otf | | Fira Mono Medium for Powerline | | Fira Mono Medium for Powerline | | Regular | | Fira Mono Medium for Powerline | | Medium +#### No need to have Typographic Family/Subfamily if it is identical to normal Family/Subfamily ++|FuraMono-Regular Powerline.otf | | Fura Mono Powerline | | Fura Mono Powerline | | Regular |X| | | +-|FuraMono-Regular Powerline.otf | | Fira Mono | | Fira Mono for Powerline | | Regular | | Fira Mono for Powerline | | +#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead ++|gohufont-11.ttf | | Gohufont 11 | | Gohufont 11 |X| Regular | | | | +-|gohufont-11.ttf | | GohuFont | | GohuFont | | Medium | | | | +#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead ++|gohufont-14.ttf | | Gohufont 14 | | Gohufont 14 |X| Regular | | | | +-|gohufont-14.ttf | | GohuFont | | GohuFont | | 14 | | | | +#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead ++|gohufont-uni-11.ttf | | Gohufont uni-11 | | Gohufont uni-11 |X| Regular | | | | +-|gohufont-uni-11.ttf | | GohuFont | | GohuFont | | Medium | | | | +#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead ++|gohufont-uni-14.ttf | | Gohufont uni-14 | | Gohufont uni-14 |X| Regular | | | | +-|gohufont-uni-14.ttf | | GohuFont | | GohuFont | | uni-14 | | | | +#### Limit Subfamiliy to 4 standard styles, Normal is usually equivalent to Regular ++|heavy_data.ttf | | Heavy Data | | Heavy Data |X| Regular | | | | +-|heavy_data.ttf | | Heavy Data | | Heavy Data | | Normal | | | | +#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead ++|Hermit-light.otf | | Hermit Light |X| Hermit Light |X| Regular |X| Hermit |X| Light +-|Hermit-light.otf | | Hermit Light | | Hermit | | light | | | | +#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead ++|Hermit-medium.otf | | Hermit Medium |X| Hermit Medium |X| Regular |X| Hermit |X| Medium +-|Hermit-medium.otf | | Hermit Medium | | Hermit | | medium | | | | +#### Limit Subfamiliy to 4 standard styles, put Bold into Subfamily name ++|iAWriterDuospace-Bold.otf | | iA Writer Duospace Bold |X| iA Writer Duospace |X| Bold | | | | +-|iAWriterDuospace-Bold.otf | | iA Writer Duospace Bold | | iA Writer Duospace Bold | | Regular | | iA Writer Duospace | | Bold +#### Limit Subfamiliy to 4 standard styles, put Bold into Subfamily name ++|iAWriterDuospace-Bold.ttf | | iA Writer Duospace Bold |X| iA Writer Duospace |X| Bold | | | | +-|iAWriterDuospace-Bold.ttf | | iA Writer Duospace Bold | | iA Writer Duospace Bold | | Regular | | iA Writer Duospace | | Bold +#### Limit Subfamiliy to 4 standard styles, put Bold into Subfamily name ++|iAWriterDuospace-BoldItalic.otf | | iA Writer Duospace Bold Italic |X| iA Writer Duospace |X| Bold Italic | | |X| +-|iAWriterDuospace-BoldItalic.otf | | iA Writer Duospace BoldItalic | | iA Writer Duospace Bold | | Italic | | iA Writer Duospace | | BoldItalic +#### Limit Subfamiliy to 4 standard styles, put Bold into Subfamily name ++|iAWriterDuospace-BoldItalic.ttf | | iA Writer Duospace Bold Italic |X| iA Writer Duospace |X| Bold Italic | | |X| +-|iAWriterDuospace-BoldItalic.ttf | | iA Writer Duospace BoldItalic | | iA Writer Duospace Bold | | Italic | | iA Writer Duospace | | BoldItalic +#### Ignore naming part Text ++|IBMPlexMono-TextItalic.ttf | | IBM Plex Mono Text Italic | | IBM Plex Mono Text | | Italic |X| |X| +-|IBMPlexMono-TextItalic.ttf | | IBM Plex Mono Text Italic | | IBM Plex Mono Text | | Italic | | IBM Plex Mono | | Text Italic +#### Ignore naming part Text ++|IBMPlexMono-Text.ttf | | IBM Plex Mono Text | | IBM Plex Mono Text | | Regular |X| |X| +-|IBMPlexMono-Text.ttf | | IBM Plex Mono Text | | IBM Plex Mono Text | | Regular | | IBM Plex Mono | | Text +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-boldoblique.ttf | | Iosevka Bold Oblique | | Iosevka Oblique |X| Bold Italic | | Iosevka | | Bold Oblique +-|iosevka-boldoblique.ttf | | Iosevka Bold Oblique | | Iosevka Oblique | | Bold | | Iosevka | | Bold Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-term-boldoblique.ttf | | Iosevka Term Bold Oblique | | Iosevka Term Oblique |X| Bold Italic | | Iosevka Term | | Bold Oblique +-|iosevka-term-boldoblique.ttf | | Iosevka Term Bold Oblique | | Iosevka Term Oblique | | Bold | | Iosevka Term | | Bold Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-extraboldoblique.ttf | | Iosevka ExtraBold Oblique | | Iosevka ExtraBold Oblique |X| Italic | | Iosevka | | ExtraBold Oblique +-|iosevka-extraboldoblique.ttf | | Iosevka Extrabold Oblique | | Iosevka Extrabold Oblique | | Regular | | Iosevka | | Extrabold Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-term-extraboldoblique.ttf | | Iosevka Term ExtraBold Oblique | | Iosevka Term ExtraBold Oblique |X| Italic | | Iosevka Term | | ExtraBold Oblique +-|iosevka-term-extraboldoblique.ttf | | Iosevka Term Extrabold Oblique | | Iosevka Term Extrabold Oblique | | Regular | | Iosevka Term | | Extrabold Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-extralightoblique.ttf | | Iosevka ExtraLight Oblique | | Iosevka ExtraLight Oblique |X| Italic | | Iosevka | | ExtraLight Oblique +-|iosevka-extralightoblique.ttf | | Iosevka Extralight Oblique | | Iosevka Extralight Oblique | | Regular | | Iosevka | | Extralight Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-term-extralightoblique.ttf | | Iosevka Term ExtraLight Oblique | | Iosevka Term ExtraLight Obliqu |X| Italic | | Iosevka Term | | ExtraLight Oblique +-|iosevka-term-extralightoblique.ttf | | Iosevka Term Extralight Oblique | | Iosevka Term XLtObl | | Regular | | Iosevka Term | | Extralight Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-heavyoblique.ttf | | Iosevka Heavy Oblique | | Iosevka Heavy Oblique |X| Italic | | Iosevka | | Heavy Oblique +-|iosevka-heavyoblique.ttf | | Iosevka Heavy Oblique | | Iosevka Heavy Oblique | | Regular | | Iosevka | | Heavy Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-term-heavyoblique.ttf | | Iosevka Term Heavy Oblique | | Iosevka Term Heavy Oblique |X| Italic | | Iosevka Term | | Heavy Oblique +-|iosevka-term-heavyoblique.ttf | | Iosevka Term Heavy Oblique | | Iosevka Term Heavy Oblique | | Regular | | Iosevka Term | | Heavy Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-lightoblique.ttf | | Iosevka Light Oblique | | Iosevka Light Oblique |X| Italic | | Iosevka | | Light Oblique +-|iosevka-lightoblique.ttf | | Iosevka Light Oblique | | Iosevka Light Oblique | | Regular | | Iosevka | | Light Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-term-lightoblique.ttf | | Iosevka Term Light Oblique | | Iosevka Term Light Oblique |X| Italic | | Iosevka Term | | Light Oblique +-|iosevka-term-lightoblique.ttf | | Iosevka Term Light Oblique | | Iosevka Term Light Oblique | | Regular | | Iosevka Term | | Light Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-mediumoblique.ttf | | Iosevka Medium Oblique | | Iosevka Medium Oblique |X| Italic | | Iosevka | | Medium Oblique +-|iosevka-mediumoblique.ttf | | Iosevka Medium Oblique | | Iosevka Medium Oblique | | Regular | | Iosevka | | Medium Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-term-mediumoblique.ttf | | Iosevka Term Medium Oblique | | Iosevka Term Medium Oblique |X| Italic | | Iosevka Term | | Medium Oblique +-|iosevka-term-mediumoblique.ttf | | Iosevka Term Medium Oblique | | Iosevka Term Medium Oblique | | Regular | | Iosevka Term | | Medium Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-oblique.ttf | | Iosevka Oblique | | Iosevka Oblique |X| Italic | | Iosevka | | Oblique +-|iosevka-oblique.ttf | | Iosevka Oblique | | Iosevka Oblique | | Regular | | Iosevka | | Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-term-oblique.ttf | | Iosevka Term Oblique | | Iosevka Term Oblique |X| Italic | | Iosevka Term | | Oblique +-|iosevka-term-oblique.ttf | | Iosevka Term Oblique | | Iosevka Term Oblique | | Regular | | Iosevka Term | | Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-semiboldoblique.ttf | | Iosevka SemiBold Oblique | | Iosevka SemiBold Oblique |X| Italic | | Iosevka | | SemiBold Oblique +-|iosevka-semiboldoblique.ttf | | Iosevka Semibold Oblique | | Iosevka Semibold Oblique | | Regular | | Iosevka | | Semibold Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-term-semiboldoblique.ttf | | Iosevka Term SemiBold Oblique | | Iosevka Term SemiBold Oblique |X| Italic | | Iosevka Term | | SemiBold Oblique +-|iosevka-term-semiboldoblique.ttf | | Iosevka Term Semibold Oblique | | Iosevka Term Semibold Oblique | | Regular | | Iosevka Term | | Semibold Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-term-thinoblique.ttf | | Iosevka Term Thin Oblique | | Iosevka Term Thin Oblique |X| Italic | | Iosevka Term | | Thin Oblique +-|iosevka-term-thinoblique.ttf | | Iosevka Term Thin Oblique | | Iosevka Term Thin Oblique | | Regular | | Iosevka Term | | Thin Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family ++|iosevka-thinoblique.ttf | | Iosevka Thin Oblique | | Iosevka Thin Oblique |X| Italic | | Iosevka | | Thin Oblique +-|iosevka-thinoblique.ttf | | Iosevka Thin Oblique | | Iosevka Thin Oblique | | Regular | | Iosevka | | Thin Oblique +#### Do we really base on the VF (variable font) We can not create VF. This makes no sense. ++|Lilex-VF.ttf |X| Lilex VF |X| Lilex VF | | Regular | | | | +-|Lilex-VF.ttf | | Lilex Regular | | Lilex | | Regular | | | | +#### Limit Subfamiliy to 4 standard styles, put Retina into Family instead ++|Monoid-Retina.ttf | | Monoid Retina |X| Monoid Retina |X| Regular |X| Monoid |X| Retina +-|Monoid-Retina.ttf | | Monoid Retina | | Monoid | | Retina | | | | +#### Remove nonstandard BoldItalic typographic style ++|mononoki-BoldItalic.ttf | | Mononoki Bold Italic | | Mononoki | | Bold Italic | | |X| +-|mononoki-BoldItalic.ttf | | mononoki Bold Italic | | mononoki | | Bold Italic | | | | BoldItalic +#### They say SemiBold is the same as Light Bold, we can not generalize this and make SemiBold self standing ++|overpass-mono-semibold.otf | | Overpass Mono SemiBold |X| Overpass Mono SemiBold |X| Regular | | Overpass Mono | | SemiBold +-|overpass-mono-semibold.otf | | Overpass Mono SemiBold | | Overpass Mono Light | | Bold | | Overpass Mono | | SemiBold +#### They say SemiBold is the same as Light Bold, we can not generalize this and make SemiBold self standing ++|overpass-semibold.otf | | Overpass SemiBold |X| Overpass SemiBold |X| Regular | | Overpass | | SemiBold +-|overpass-semibold.otf | | Overpass SemiBold | | Overpass Light | | Bold | | Overpass | | SemiBold +#### Nonstandard font naming: fullname shall be same as familyname plus more ++|ProFontIIx.ttf | | ProFont IIx |X| ProFont IIx | | Regular | | | | +-|ProFontIIx.ttf | | ProFont IIx | | ProFontIIx | | Regular | | | | +#### We are fine here (just list with exclamation mark because it is a potentially problematic case) +!|ProFontWindows.ttf | | ProFontWindows | | ProFontWindows | | Regular | | | | + |ProFontWindows.ttf | | ProFontWindows | | ProFontWindows | | Regular | | | | +#### No mention of TT in file name ++|ProggyCleanCE.ttf |X| ProggyClean CE |X| ProggyClean CE | | Regular | | | | +-|ProggyCleanCE.ttf | | ProggyCleanTT CE | | ProggyCleanTT CE | | Regular | | | | +#### No mention of TT in file name +!|ProggyClean.ttf |X| ProggyClean |X| ProggyClean | | Regular | | | | +-|ProggyClean.ttf | | ProggyCleanTT | | ProggyCleanTT | | Regular | | | | +#### No mention of TT in file name ++|ProggyCleanSZ.ttf |X| ProggyClean SZ |X| ProggyClean SZ | | Regular | | | | +-|ProggyCleanSZ.ttf | | ProggyCleanTTSZ | | ProggyCleanTTSZ | | Regular | | | | +#### They put one name part in parens ++|TerminusTTF-Bold Italic-4.40.1.ttf |X| Terminus TTF Bold Italic |X| Terminus TTF | | Bold Italic | | | | +-|TerminusTTF-Bold Italic-4.40.1.ttf | | Terminus (TTF) Bold Italic | | Terminus (TTF) | | Bold Italic | | | | +#### They put one name part in parens ++|TerminusTTF-Bold-4.40.1.ttf |X| Terminus TTF Bold |X| Terminus TTF | | Bold | | | | +-|TerminusTTF-Bold-4.40.1.ttf | | Terminus (TTF) Bold | | Terminus (TTF) | | Bold | | | | +#### They put one name part in parens ++|TerminusTTF-Italic-4.40.1.ttf |X| Terminus TTF Italic |X| Terminus TTF | | Italic | | | | +-|TerminusTTF-Italic-4.40.1.ttf | | Terminus (TTF) Italic | | Terminus (TTF) | | Italic | | | | +#### They put one name part in parens ++|TerminusTTF-4.40.1.ttf |X| Terminus TTF |X| Terminus TTF |X| Regular | | | | +-|TerminusTTF-4.40.1.ttf | | Terminus (TTF) | | Terminus (TTF) | | Medium | | | | +#### Ubuntu Condensed should be grouped with Ubuntu, that they didn't is an error? ++|Ubuntu-C.ttf | | Ubuntu Condensed | | Ubuntu Condensed | | Regular |X| Ubuntu |X| Condensed +-|Ubuntu-C.ttf | | Ubuntu Condensed | | Ubuntu Condensed | | Regular | | Ubuntu Condensed | | Regular +#### They say Medium is the same as Light Bold, we can not generalize this and make Medium self standing ++|Ubuntu-MI.ttf | | Ubuntu Medium Italic |X| Ubuntu Medium |X| Italic | | Ubuntu | | Medium Italic +-|Ubuntu-MI.ttf | | Ubuntu Medium Italic | | Ubuntu Light | | Bold Italic | | Ubuntu | | Medium Italic +#### They say Medium is the same as Light Bold, we can not generalize this and make Medium self standing ++|Ubuntu-M.ttf | | Ubuntu Medium |X| Ubuntu Medium |X| Regular | | Ubuntu | | Medium +-|Ubuntu-M.ttf | | Ubuntu Medium | | Ubuntu Light | | Bold | | Ubuntu | | Medium +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead ++|VictorMono-ExtraLightOblique.ttf | | Victor Mono ExtraLight Oblique |X| Victor Mono ExtraLight Oblique | | Italic | | Victor Mono | | ExtraLight Oblique +-|VictorMono-ExtraLightOblique.ttf | | Victor Mono ExtraLight Oblique | | Victor Mono ExtraLight | | Italic | | Victor Mono | | ExtraLight Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead ++|VictorMono-LightOblique.ttf | | Victor Mono Light Oblique |X| Victor Mono Light Oblique | | Italic | | Victor Mono | | Light Oblique +-|VictorMono-LightOblique.ttf | | Victor Mono Light Oblique | | Victor Mono Light | | Italic | | Victor Mono | | Light Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead ++|VictorMono-MediumOblique.ttf | | Victor Mono Medium Oblique |X| Victor Mono Medium Oblique | | Italic | | Victor Mono | | Medium Oblique +-|VictorMono-MediumOblique.ttf | | Victor Mono Medium Oblique | | Victor Mono Medium | | Italic | | Victor Mono | | Medium Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead ++|VictorMono-SemiBoldOblique.ttf | | Victor Mono SemiBold Oblique |X| Victor Mono SemiBold Oblique | | Italic | | Victor Mono | | SemiBold Oblique +-|VictorMono-SemiBoldOblique.ttf | | Victor Mono SemiBold Oblique | | Victor Mono SemiBold | | Italic | | Victor Mono | | SemiBold Oblique +#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead ++|VictorMono-ThinOblique.ttf | | Victor Mono Thin Oblique |X| Victor Mono Thin Oblique | | Italic | | Victor Mono | | Thin Oblique +-|VictorMono-ThinOblique.ttf | | Victor Mono Thin Oblique | | Victor Mono Thin | | Italic | | Victor Mono | | Thin Oblique diff --git a/bin/scripts/name_parser/name_parser_test2 b/bin/scripts/name_parser/name_parser_test2 new file mode 100644 index 0000000000..4da47db6a4 --- /dev/null +++ b/bin/scripts/name_parser/name_parser_test2 @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# coding=utf8 + +import sys +import re +import os.path +import glob +import subprocess +import fontforge + +###### Some helpers + +def get_sfnt_dict(font): + """Extract SFNT table as nice dict""" + d = [] + for i, el in enumerate(font.sfnt_names): + d += [(el[1], el[2])] + return dict(d) + +def format_names(header, *stuff): + """Unify outputs (with header)""" + f = '{:1.1}|{:50.50} |{:1.1}| {:50.50} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:.30}' + if header: + d = '------------------------------------------------------------' + return f.format(*stuff) + '\n' + f.format('', d, d, d, d, d, d, d, d, d, d, d) + return f.format(*stuff).rstrip() + +def lenient_cmp(s1, s2, allow_shuffle_all): + """Compare two font name (parts) but be a bit lenient ;->""" + # We do not care about: + # - Case + # - "Display" vs "Disp" (in Noto) + # Allow for "IBM 3278" name + s = [ s1, s2 ] + for i in range(2): + # Usually given transform from 'their' to 'our' style + s[i] = s[i].lower() + s[i] = re.sub(r'\bdisp\b', 'display', s[i]) # Noto + s[i] = s[i].replace('ibm 3270', '3270') # 3270 + s[i] = s[i].replace('3270-', '3270 ') # 3270 + s[i] = s[i].replace('lekton-', 'lekton ') # Lekton + s[i] = s[i].replace('semi-narrow', 'seminarrow') # 3270 + s[i] = s[i].replace('bolditalic', 'bold italic') + s[i] = re.sub(r'\bfor\b', '', s[i]) # Meslo, Monofur + s[i] = re.sub(r'\bpowerline\b', '', s[i]) # Meslo, Monofur + s[i] = s[i].replace('fira mono', 'fura mono') # Obviously someone forgot to rename the fonts in Fira/ + s[i] = s[i].replace('aurulentsansmono-', 'aurulent sans mono ') # Aurulent fullname oddity + s[i] = s[i].replace('mononoki-', 'mononoki ') # Mononoki has somtimes a dash + s[i] = re.sub(r'\br\b', 'regular', s[i]) # Nonstandard style in Agave + s[i] = re.sub(r'(bitstream vera sans mono.*) oblique', r'\1 italic', s[i]) # They call it Oblique but the filename says Italic + s[i] = re.sub(r'gohufont (uni-)?(11|14)', 'gohufont', s[i]) # They put the 'name' into the subfamily/weight + s[i] = s[i].replace('xltobl', 'extralight oblique') # Iosevka goes inventing names + s[i] = re.sub(r'proggyclean(?!TT)( ?)', 'proggycleantt\1', s[i]) # ProggyClean has no TT in filename + + s[i] = re.sub(r' +', ' ', s[i]).strip() + + p = [] + for e in s: + parts = e.split(' ') + if not allow_shuffle_all and len(parts) > 2: + tail = parts[1:] + tail.sort() + parts = [parts[0]] + tail + elif len(parts) > 1: + parts.sort() + p.append(' '.join(parts)) + return p[0] == p[1] + +###### Let's go! + +if len(sys.argv) < 2: + print('Usage: {} font_name [font_name ...]\n'.format(sys.argv[0])) + sys.exit(1) + +existing_font = glob.glob('*.[ot]tf') +if len(existing_font): + sys.exit('Would overwrite any existing *.ttf and *.otf, bailing out (remove them first)') + + +print(format_names(True, '', 'Filename', '', 'Fullname', '', 'Family', '', 'Subfamily', '', 'Typogr. Family', '', 'Typogr. Subfamily')) +font_patcher = os.path.realpath(os.path.dirname(os.path.realpath(sys.argv[0]))+'/../../../font-patcher') + +for filename in sys.argv[1:]: + data = [] + fullfile = os.path.basename(filename) + fname = os.path.splitext(fullfile)[0] + if fname == 'NotoColorEmoji': + continue # font is not patchable + + for option in ['--parser', '--powerline']: + subprocess.call(['fontforge', '--script', font_patcher, '--powerline', option, filename ], + stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) + new_font = glob.glob('*.[ot]tf') + font = fontforge.open(new_font[0], 1) + sfnt = get_sfnt_dict(font) + font.close() + os.system('rm -f *.[ot]tf') + + sfnt_full = sfnt['Fullname'] + sfnt_fam = sfnt['Family'] + sfnt_subfam = sfnt['SubFamily'] + sfnt_pfam = sfnt['Preferred Family'] if 'Preferred Family' in sfnt else '' + sfnt_psubfam = sfnt['Preferred Styles'] if 'Preferred Styles' in sfnt else '' + + data.append(( os.path.basename(new_font[0]), sfnt_full, sfnt_fam, sfnt_subfam, sfnt_pfam, sfnt_psubfam )) + + t1 = not lenient_cmp(data[0][1], data[1][1], False) + t2 = not lenient_cmp(data[0][2], data[1][2], False) + t3 = not lenient_cmp(data[0][3], data[1][3], True) + t4 = not lenient_cmp(data[0][4], data[1][4], False) + t5 = not lenient_cmp(data[0][5], data[1][5], True) + + # Lenience: Allow for dropping unneeded prefered stuff: + # New (sub)family is same as old preferred sub(family) + if t4 and data[0][4] == '' and data[1][4].lower() == data[0][2].lower(): + t4 = False + if t5 and data[0][5] == '' and data[1][5].lower() == data[0][3].lower(): + t5 = False + + if t1 or t2 or t3 or t4 or t5: + m1 = '+'; m2 = '-' + else: + m1 = ''; m2 = '' + t1_ = 'X' if t1 else '' + t2_ = 'X' if t2 else '' + t3_ = 'X' if t3 else '' + t4_ = 'X' if t4 else '' + t5_ = 'X' if t5 else '' + + o1 = format_names(False, m1, data[0][0], t1_, data[0][1], t2_, data[0][2], t3_, data[0][3], t4_, data[0][4], t5_, data[0][5]) + o2 = format_names(False, m2, data[1][0], '', data[1][1], '', data[1][2], '', data[1][3], '', data[1][4], '', data[1][5]) + + print(o1, o2, sep='\n') diff --git a/bin/scripts/name_parser/query_names b/bin/scripts/name_parser/query_names new file mode 100644 index 0000000000..6a2987034c --- /dev/null +++ b/bin/scripts/name_parser/query_names @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# coding=utf8 + +import sys +import os.path +import fontforge + +###### Some helpers + +def get_sfnt_dict(font): + """Extract SFNT table as nice dict""" + return { k: v for l, k, v in font.sfnt_names } + +def format_names(header, *stuff): + """Unify outputs (with header)""" + f = '{:1.1}|{:50.50} |{:1.1}| {:50.50} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:.30}' + if header: + d = '------------------------------------------------------------' + return f.format(*stuff) + '\n' + f.format('', d, d, d, d, d, d, d, d, d, d, d) + return f.format(*stuff).rstrip() + +###### Let's go! + +if len(sys.argv) < 2: + print('Usage: {} font_name [font_name ...]\n'.format(sys.argv[0])) + sys.exit(1) + +print('Examining {} font files'.format(len(sys.argv) - 1)) + +print(format_names(True, '', 'Filename', '', 'Fullname', '', 'Family', '', 'Subfamily', '', 'Typogr. Family', '', 'Typogr. Subfamily')) + +for filename in sys.argv[1:]: + fullfile = os.path.basename(filename) + fname = os.path.splitext(fullfile)[0] + + font = fontforge.open(filename, 1) + sfnt = get_sfnt_dict(font) + font.close() + + sfnt_full = sfnt['Fullname'] + sfnt_fam = sfnt['Family'] + sfnt_subfam = sfnt['SubFamily'] + sfnt_pfam = sfnt['Preferred Family'] if 'Preferred Family' in sfnt else '' + sfnt_psubfam = sfnt['Preferred Styles'] if 'Preferred Styles' in sfnt else '' + + o2 = format_names(False, '', fullfile, '', sfnt_full, '', sfnt_fam, '', sfnt_subfam, '', sfnt_pfam, '', sfnt_psubfam) + + print(o2) diff --git a/bin/scripts/name_parser/query_panose b/bin/scripts/name_parser/query_panose new file mode 100644 index 0000000000..1db0617a16 --- /dev/null +++ b/bin/scripts/name_parser/query_panose @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# coding=utf8 + +import fontforge +import sys + +if len(sys.argv) != 2: + print("Usage: {} font_name\n".format(sys.argv[0])) + sys.exit(1) + +font = fontforge.open(sys.argv[1]) + +panose = list(font.os2_panose) +print("Panose 4 = {} in {}".format(panose[3], font.fullname)) + +font.close() diff --git a/bin/scripts/name_parser/query_sftn b/bin/scripts/name_parser/query_sftn new file mode 100644 index 0000000000..e15356414e --- /dev/null +++ b/bin/scripts/name_parser/query_sftn @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# coding=utf8 + +import fontforge +import sys + +def get_sfnt_dict(font): + """Extract SFNT table as nice dict""" + return { k: v for l, k, v in font.sfnt_names } + +if len(sys.argv) < 2 or len(sys.argv) > 3: + print("Usage: {} [] font_name\n".format(sys.argv[0])) + sys.exit(1) + +if len(sys.argv) == 2: + fname = sys.argv[1] + sname = None +else: + fname = sys.argv[2] + sname = sys.argv[1] + +font = fontforge.open(fname) +sfnt = get_sfnt_dict(font) +font.close() + +if sname: + for key in sname.split(','): + if key in sfnt: + print("SFNT {:20.20} is {:80.80}".format(key, '\'' + sfnt[key] + '\'')); + else: + print("SFNT {:20.20} is not set".format(key)); +else: + for k in sfnt: + print("{:20.20} {:80.80}".format(k, sfnt[k])) + diff --git a/bin/scripts/name_parser/query_version b/bin/scripts/name_parser/query_version new file mode 100644 index 0000000000..54f26a5b6d --- /dev/null +++ b/bin/scripts/name_parser/query_version @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# coding=utf8 + +import fontforge +import sys + +def get_sfnt_dict(font): + """Extract SFNT table as nice dict""" + return { k: v for l, k, v in font.sfnt_names } + +if len(sys.argv) != 2: + print("Usage: {} font_name\n".format(sys.argv[0])) + sys.exit(1) + +font = fontforge.open(sys.argv[1]) +sfnt = get_sfnt_dict(font) + +print("Version is '{}'".format(font.version)); +print("CID Version is '{}'".format(font.cidversion)); +print("SFNT Revision is '{}'".format(font.sfntRevision)); +if "Version" in sfnt: + print("SFNT ['Version'] is '{}'".format(sfnt["Version"])); +else: + print("SFNT ['Version'] is not set".format(sys.argv[1])); + +font.close() diff --git a/font-patcher b/font-patcher index ad87cd4a45..511947da96 100755 --- a/font-patcher +++ b/font-patcher @@ -36,6 +36,9 @@ except ImportError: ) ) +# This is (for experimenting) far far away... +sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/bin/scripts/name_parser/') +from FontnameParser import FontnameParser class font_patcher: def __init__(self): @@ -154,6 +157,7 @@ class font_patcher: parser.add_argument('-ext', '--extension', dest='extension', default="", type=str, nargs='?', help='Change font file type to create (e.g., ttf, otf)') parser.add_argument('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to') parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching') + parser.add_argument('--parser', dest='parser', default=False, action='store_true', help='Use alternative method to name patched fonts (experimental)') # progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse progressbars_group_parser = parser.add_mutually_exclusive_group(required=False) @@ -268,6 +272,16 @@ class font_patcher: additionalFontNameSuffix += " M" verboseAdditionalFontNameSuffix += " Mono" + if self.args.parser: + n = FontnameParser(self.sourceFont.fontname) + if not n.parse_ok: + sys.exit("Something with name parsing went wrong") + n.drop_for_powerline() + n.enable_short_families(True, False) + n.set_for_windows(self.args.windows) + + # All the following stuff is ignored in parser-mode + # basically split the font name around the dash "-" to get the fontname and the style (e.g. Bold) # this does not seem very reliable so only use the style here as a fallback if the font does not # have an internal style defined (in sfnt_names) @@ -393,15 +407,22 @@ class font_patcher: fullname = replace_font_name(fullname, additionalFontNameReplacements2) fontname = replace_font_name(fontname, additionalFontNameReplacements2) - # replace any extra whitespace characters: - self.sourceFont.familyname = " ".join(familyname.split()) - self.sourceFont.fullname = " ".join(fullname.split()) - self.sourceFont.fontname = " ".join(fontname.split()) + if not self.args.parser: + # replace any extra whitespace characters: + self.sourceFont.familyname = " ".join(familyname.split()) + self.sourceFont.fullname = " ".join(fullname.split()) + self.sourceFont.fontname = " ".join(fontname.split()) + + self.sourceFont.appendSFNTName(str('English (US)'), str('Preferred Family'), self.sourceFont.familyname) + self.sourceFont.appendSFNTName(str('English (US)'), str('Family'), self.sourceFont.familyname) + self.sourceFont.appendSFNTName(str('English (US)'), str('Compatible Full'), self.sourceFont.fullname) + self.sourceFont.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) + else: + fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation + fam_suffix += ' Mono' if self.args.single else '' + n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix) + n.rename_font(self.sourceFont) - self.sourceFont.appendSFNTName(str('English (US)'), str('Preferred Family'), self.sourceFont.familyname) - self.sourceFont.appendSFNTName(str('English (US)'), str('Family'), self.sourceFont.familyname) - self.sourceFont.appendSFNTName(str('English (US)'), str('Compatible Full'), self.sourceFont.fullname) - self.sourceFont.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) self.sourceFont.comment = projectInfo self.sourceFont.fontlog = projectInfo