From 43b872a6ce346415c8c4eea7247ab75c5c7a560d Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Tue, 2 May 2023 13:04:31 +0700 Subject: [PATCH] Keep things up-to-date with NerdFonts 3.0.0 release --- .gitignore | 2 + Makefile | 14 - bin/scripts/name_parser/FontnameParser.py | 334 +++++++++++++++++++ bin/scripts/name_parser/FontnameTools.py | 382 ++++++++++++++++++++++ bin/scripts/name_parser/query_monospace | 94 ++++++ bin/scripts/name_parser/query_names | 60 ++++ bin/scripts/name_parser/query_panose | 16 + bin/scripts/name_parser/query_sftn | 35 ++ bin/scripts/name_parser/query_version | 26 ++ build-hdmx-for-sarasa.py | 49 --- correct-ttf-font-family-name.py | 61 ---- font-patcher | 380 +++++++++++++-------- original/.gitignore | 1 - patch_Iosevka.sh | 25 +- patched/.gitignore | 1 - src/glyphs/octicons.ttf | Bin 43920 -> 0 bytes src/glyphs/octicons/octicons.ttf | Bin 0 -> 76520 bytes update.sh | 28 +- 18 files changed, 1217 insertions(+), 291 deletions(-) create mode 100644 .gitignore delete mode 100644 Makefile create mode 100644 bin/scripts/name_parser/FontnameParser.py create mode 100644 bin/scripts/name_parser/FontnameTools.py create mode 100644 bin/scripts/name_parser/query_monospace create mode 100644 bin/scripts/name_parser/query_names create mode 100644 bin/scripts/name_parser/query_panose create mode 100644 bin/scripts/name_parser/query_sftn create mode 100644 bin/scripts/name_parser/query_version delete mode 100644 build-hdmx-for-sarasa.py delete mode 100644 correct-ttf-font-family-name.py delete mode 100644 src/glyphs/octicons.ttf create mode 100644 src/glyphs/octicons/octicons.ttf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ac1609 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/font-patcher-log.txt +__pycache__/ diff --git a/Makefile b/Makefile deleted file mode 100644 index 3624c94..0000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -all: update iosevka - -update: - ./update.sh - -# Keep the patched font files -clean: - rm -rf original/*.ttf - rm -rf original/*.ttx - rm -rf patched/*.ttx - rm -rf patched/*.original.ttf - -iosevka: clean - ./patch_Iosevka.sh $(IOSEVKA_VERSION) diff --git a/bin/scripts/name_parser/FontnameParser.py b/bin/scripts/name_parser/FontnameParser.py new file mode 100644 index 0000000..2fb2060 --- /dev/null +++ b/bin/scripts/name_parser/FontnameParser.py @@ -0,0 +1,334 @@ +#!/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, logger): + """Parse a font filename and store the results""" + self.parse_ok = False + self.use_short_families = (False, False, False) # ( camelcase name, short styles, aggressive ) + self.keep_regular_in_family = None # None = auto, True, False + self.suppress_preferred_if_identical = True + self.family_suff = '' + self.ps_fontname_suff = '' + self.short_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.rest = self._rest + self.add_name_substitution_table(FontnameTools.SIL_TABLE) + self.rename_oblique = True + self.logger = logger + + def _make_ps_name(self, n, is_family): + """Helper to limit font name length in PS names""" + fam = 'family ' if is_family else '' + limit = 31 if is_family else 63 + if len(n) <= limit: + return n + r = re.search('(.*)(-.*)', n) + if not r: + new_n = n[:limit] + else: + q = limit - len(r.groups()[1]) + if q < 1: + q = 1 + self.logger.error('====-< Shortening too long PS {}name: Garbage warning'. format(fam)) + new_n = r.groups()[0][:q] + r.groups()[1] + if new_n != n: + self.logger.error('====-< Shortening too long PS {}name: {} -> {}'.format(fam, n, new_n)) + return new_n + + def _shortened_name(self): + """Return a blank free basename-rest combination""" + if not self.use_short_families[0]: + return (self.basename, self.rest) + else: + return (FontnameTools.concat(self.basename, self.rest).replace(' ', ''), '') + + 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_expect_no_italic(self, noitalic): + """Prevents rewriting Oblique as family name part""" + # To prevent naming clashes usually Oblique is moved out in the family name + # because some fonts have Italic and Oblique, and we want to generate pure + # RIBBI families in ID1/2. + # But some fonts have Oblique instead of Italic, here the prevential movement + # is not needed, or rather contraproductive. This can not be detected on a + # font file level but needs to be specified per family from the outside. + # Returns true if setting was successful. + if 'Italic' in self.style_token: + self.rename_oblique = True + return not noitalic + self.rename_oblique = not noitalic + return True + + 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, family, ps_fontname, short_family): + """Add a custom additonal string that shows up in the resulting names""" + self.family_suff = family.strip() + self.ps_fontname_suff = ps_fontname.replace(' ', '') + self.short_family_suff = short_family.strip() + return self + + def enable_short_families(self, camelcase_name, prefix, aggressive): + """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/True + if isinstance(prefix, str): + prefix = self._basename.startswith(prefix) + self.use_short_families = ( camelcase_name, prefix, aggressive ) + 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 + self.rest = self._rest + for regex, replacement in self.name_subst: + base_and_rest = self.basename + (' ' + self.rest if len(self.rest) else '') + m = re.match(regex, base_and_rest, re.IGNORECASE) + if not m: + continue + i = len(self.basename) - len(m.group(0)) + if i < 0: + self.basename = m.expand(replacement).rstrip() + self.rest = self.rest[-(i+1):].lstrip() + else: + self.basename = m.expand(replacement) + self.basename[len(m.group(0)):] + 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.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)""" + styles = self.style_token + weights = self.weight_token + if self.keep_regular_in_family == None: + keep_regular = FontnameTools.is_keep_regular(self._basename + ' ' + self._rest) + else: + keep_regular = self.keep_regular_in_family + if ('Regular' in styles + and (not keep_regular + 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) + (name, rest) = self._shortened_name() + if self.use_short_families[1]: + [ weights, styles ] = FontnameTools.short_styles([ weights, styles ], self.use_short_families[2]) + return FontnameTools.concat(name, rest, self.other_token, self.short_family_suff, weights, styles) + + def psname(self): + """Get the SFNT PostScriptName (ID 6)""" + # This is almost self.family() + '-' + self.subfamily() + (name, rest) = self._shortened_name() + styles = self.style_token + weights = self.weight_token + if self.use_short_families[1]: + styles = FontnameTools.short_styles(styles, self.use_short_families[2]) + weights = FontnameTools.short_styles(weights, self.use_short_families[2]) + fam = FontnameTools.camel_casify(FontnameTools.concat(name, rest, self.other_token, self.ps_fontname_suff)) + sub = FontnameTools.camel_casify(FontnameTools.concat(weights, styles)) + if len(sub) > 0: + sub = '-' + sub + fam = FontnameTools.postscript_char_filter(fam) + sub = FontnameTools.postscript_char_filter(sub) + return self._make_ps_name(fam + sub, False) + + def preferred_family(self): + """Get the SFNT Preferred Familyname (ID 16)""" + (name, rest) = self._shortened_name() + pfn = FontnameTools.concat(name, rest, self.other_token, self.family_suff) + if self.suppress_preferred_if_identical and pfn == self.family(): + # Do not set if identical to ID 1 + return '' + return pfn + + def preferred_styles(self): + """Get the SFNT Preferred Styles (ID 17)""" + styles = self.style_token + weights = self.weight_token + # For naming purposes we want Oblique to be part of the styles + (weights, styles) = FontnameTools.make_oblique_style(weights, styles) + pfs = FontnameTools.concat(weights, styles) + if self.suppress_preferred_if_identical and pfs == self.subfamily(): + # Do not set if identical to ID 2 + return '' + return pfs + + def family(self): + """Get the SFNT Familyname (ID 1)""" + # We use the short form of the styles to save on number of chars + (name, rest) = self._shortened_name() + other = self.other_token + weights = self.weight_token + aggressive = self.use_short_families[2] + if not self.rename_oblique: + (weights, styles) = FontnameTools.make_oblique_style(weights, []) + if self.use_short_families[1]: + [ other, weights ] = FontnameTools.short_styles([ other, weights ], aggressive) + weights = [ w if w != 'Oblique' else 'Obl' for w in weights ] + return FontnameTools.concat(name, rest, other, self.short_family_suff, weights) + + def subfamily(self): + """Get the SFNT SubFamily (ID 2)""" + styles = self.style_token + weights = self.weight_token + if not self.rename_oblique: + (weights, styles) = FontnameTools.make_oblique_style(weights, styles) + if len(styles) == 0: + if 'Oblique' in weights: + return FontnameTools.concat(styles, 'Italic') + return 'Regular' + if 'Oblique' in weights and not 'Italic' in styles: + return FontnameTools.concat(styles, 'Italic') + return FontnameTools.concat(styles) + + def ps_familyname(self): + """Get the PS Familyname""" + fam = self.preferred_family() + if len(fam) < 1: + fam = self.family() + return self._make_ps_name(fam, True) + + 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 checklen(self, max_len, entry_id, name): + """Check the length of a name string and report violations""" + if len(name) <= max_len: + self.logger.debug('=====> {:18} ok ({:2} <={:2}): {}'.format(entry_id, len(name), max_len, name)) + else: + self.logger.error('====-< {:18} too long ({:2} > {:2}): {}'.format(entry_id, len(name), max_len, name)) + return name + + def rename_font(self, font): + """Rename the font to include all information we found (font is fontforge font object)""" + font.fondname = None + font.fontname = self.psname() + 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* + + version_tag = '' + sfnt_list = [] + TO_DEL = ['Family', 'SubFamily', 'Fullname', 'PostScriptName', 'Preferred Family', + 'Preferred Styles', 'Compatible Full', 'WWS Family', 'WWS Subfamily', + 'UniqueID', 'CID findfont Name'] + # Remove these entries in all languages and add (at least the vital ones) some + # back, but only as 'English (US)'. This makes sure we do not leave contradicting + # names over different languages. + for l, k, v in list(font.sfnt_names): + if not k in TO_DEL: + sfnt_list += [( l, k, v )] + if k == 'Version' and l == 'English (US)': + version_tag = ' ' + v.split()[-1] + + sfnt_list += [( 'English (US)', 'Family', self.checklen(31, 'Family (ID 1)', self.family()) )] # 1 + sfnt_list += [( 'English (US)', 'SubFamily', self.checklen(31, 'SubFamily (ID 2)', self.subfamily()) )] # 2 + sfnt_list += [( 'English (US)', 'UniqueID', self.fullname() + version_tag )] # 3 + sfnt_list += [( 'English (US)', 'Fullname', self.checklen(63, 'Fullname (ID 4)', self.fullname()) )] # 4 + sfnt_list += [( 'English (US)', 'PostScriptName', self.checklen(63, 'PSN (ID 6)', self.psname()) )] # 6 + + p_fam = self.preferred_family() + if len(p_fam): + sfnt_list += [( 'English (US)', 'Preferred Family', self.checklen(31, 'PrefFamily (ID 16)', p_fam) )] # 16 + p_sty = self.preferred_styles() + if len(p_sty): + sfnt_list += [( 'English (US)', 'Preferred Styles', self.checklen(31, 'PrefStyles (ID 17)', p_sty) )] # 17 + + font.sfnt_names = tuple(sfnt_list) + + font.macstyle = self.macstyle(0) + font.os2_stylemap = self.fs_selection(0) diff --git a/bin/scripts/name_parser/FontnameTools.py b/bin/scripts/name_parser/FontnameTools.py new file mode 100644 index 0000000..f4a9c13 --- /dev/null +++ b/bin/scripts/name_parser/FontnameTools.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python +# coding=utf8 + +import re +import sys + +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-z0-9])(?=[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) is not list: + 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 + # Left side needs to be lower case + '-': '', + 'book': '', + 'text': '', + 'ce': 'CE', + #'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.lower()] + return style_name + + @staticmethod + def find_in_dicts(key, dicts): + """Find an entry in a list of dicts, return entry and in which list it was""" + for i, d in enumerate(dicts): + if key in d: + return ( d[key], i ) + return (None, 0) + + @staticmethod + def get_shorten_form_idx(aggressive, prefix, form_if_prefixed): + """Get the tuple index of known_* data tables""" + if aggressive: + return 0 + if len(prefix): + return form_if_prefixed + return 1 + + @staticmethod + def shorten_style_name(name, aggressive): + """Substitude some known styles to short form""" + # If aggressive is False create the mild short form + # aggressive == True: Always use first form of everything + # aggressive == False: + # - has no modifier: use the second form + # - has modifier: use second form of mod plus first form of weights2 + # - has modifier: use second form of mod plus second form of widths + name_rest = name + name_pre = '' + form = FontnameTools.get_shorten_form_idx(aggressive, '', 0) + for mod in FontnameTools.known_modifiers: + if name.startswith(mod) and len(name) > len(mod): # Second condition specifically for 'Demi' + name_pre = FontnameTools.known_modifiers[mod][form] + name_rest = name[len(mod):] + break + subst, i = FontnameTools.find_in_dicts(name_rest, [ FontnameTools.known_weights2, FontnameTools.known_widths ]) + form = FontnameTools.get_shorten_form_idx(aggressive, name_pre, i) + if isinstance(subst, tuple): + return name_pre + subst[form] + if not len(name_pre): + # The following sets do not allow modifiers + subst, _ = FontnameTools.find_in_dicts(name_rest, [ FontnameTools.known_weights1, FontnameTools.known_slopes ]) + if isinstance(subst, tuple): + return subst[form] + return name + + @staticmethod + def short_styles(lists, aggressive): + """Shorten all style names in a list or a list of lists""" + if not len(lists) or not isinstance(lists[0], list): + return list(map(lambda x: FontnameTools.shorten_style_name(x, aggressive), lists)) + return [ list(map(lambda x: FontnameTools.shorten_style_name(x, aggressive), styles)) for styles in lists ] + + @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, allow_regex_token = False): + """Try to find any case insensitive token from tokens in the name, return tuple with found token-list and rest""" + # The default mode (allow_regex_token = False) will try to find any verbatim string in the + # tokens list (case insensitive matching) and give that tokens list item back with + # unchanged case (i.e. [ 'Bold' ] will match "bold" and return it as [ 'Bold', ] + # In the regex mode (allow_regex_token = True) it will use the tokens elements as + # regexes and return the original (i.e. from name) case. + # + # Token are always used in a regex and may not capture, use non capturing + # grouping if needed (?: ... ) + lower_tokens = [ t.lower() for t in tokens ] + not_matched = "" + all_tokens = [] + j = 1 + regex = re.compile('(.*?)(' + '|'.join(tokens) + ')(.*)', re.IGNORECASE) + while j: + j = regex.match(name) + if not j: + break + if len(j.groups()) != 3: + sys.exit('Malformed regex in FontnameTools.get_name_token()') + not_matched += ' ' + j.groups()[0] # Blanc prevents unwanted concatenation of unmatched substrings + tok = j.groups()[1].lower() + if tok in lower_tokens: + 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 postscript_char_filter(name): + """Filter out characters that are not allowed in Postscript names""" + # The name string must be restricted to the printable ASCII subset, codes 33 to 126, + # except for the 10 characters '[', ']', '(', ')', '{', '}', '<', '>', '/', '%' + out = "" + for c in name: + if c in '[](){}<>/%' or ord(c) < 33 or ord(c) > 126: + continue + out += c + return out + + SIL_TABLE = [ + ( '(a)nonymous', r'\1nonymice' ), + ( '(b)itstream( ?)(v)era( ?sans ?mono)?', r'\1itstrom\2Wera' ), + ( '(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 :-> + ( 'Gohufont', r'GohuFont'), # Correct to CamelCase + # Noone cares that font names starting with a digit are forbidden: + ( 'IBM 3270', r'3270'), # for historical reasons and 'IBM' is a TM or something + # Some name parts that are too long for us + ( '(.*sans ?m)ono', r'\1'), # Various SomenameSansMono fonts + ( '(.*code ?lat)in Expanded', r'\1X'), # for 'M PLUS Code Latin Expanded' + ( '(.*code ?lat)in', r'\1'), # for 'M PLUS Code Latin' + ( '(b)ig( ?)(b)lue( ?)(t)erminal', r'\1ig\3lue\5erm'), # Shorten BigBlueTerminal + ( '(.*)437TT', r'\g<1>437'), # Shorten BigBlueTerminal 437 TT even further + ( '(.*dyslexic ?alt)a', r'\1'), # Open Dyslexic Alta -> Open Dyslexic Alt + ( '(.*dyslexic ?m)ono', r'\1'), # Open Dyslexic Mono -> Open Dyslexic M + ( '(overpass ?m)ono', r'\1'), # Overpass Mono -> Overpass M + ( '(proggyclean) ?tt', r'\1'), # Remove TT from ProggyClean + ( '(terminess) ?\(ttf\)', r'\1'), # Remove TTF from Terminus (after renamed to Terminess) + ( '(im ?writing ?q)uattro', r'\1uat'), # Rename iM Writing Quattro to Quat + ( '(im ?writing ?(mono|duo|quat)) ?s', r'\1'), # Remove S from all iM Writing styles + ] + + # From https://adobe-type-tools.github.io/font-tech-notes/pdfs/5088.FontNames.pdf + # The first short variant is from the linked table. + # The second (longer) short variant is from diverse fonts like Noto. + # We can + # - use the long form + # - use the very short form (first) + # - use mild short form: + # - has no modifier: use the second form + # - has modifier: use second form of mod plus first form of weights2 + # - has modifier: use second form of mod plus second form of widths + # This is encoded in get_shorten_form_idx() + known_weights1 = { # can not take modifiers + 'Medium': ('Md', 'Med'), + 'Nord': ('Nd', 'Nord'), + 'Book': ('Bk', 'Book'), + 'Poster': ('Po', 'Poster'), + 'Demi': ('Dm', 'Demi'), # Demi is sometimes used as a weight, sometimes as a modifier + 'Regular': ('Rg', 'Reg'), + 'Display': ('DS', 'Disp'), + 'Super': ('Su', 'Sup'), + 'Retina': ('Rt', 'Ret'), + } + known_weights2 = { # can take modifiers + 'Black': ('Blk', 'Black'), + 'Bold': ('Bd', 'Bold'), + 'Heavy': ('Hv', 'Heavy'), + 'Thin': ('Th', 'Thin'), + 'Light': ('Lt', 'Light'), + ' ': (), # Just for CodeClimate :-/ + } + known_widths = { # can take modifiers + 'Compressed': ('Cm', 'Comp'), + 'Extended': ('Ex', 'Extd'), + 'Condensed': ('Cn', 'Cond'), + 'Narrow': ('Nr', 'Narrow'), + 'Compact': ('Ct', 'Compact'), + } + known_slopes = { # can not take modifiers + 'Inclined': ('Ic', 'Incl'), + 'Oblique': ('Obl', 'Obl'), + 'Italic': ('It', 'Italic'), + 'Upright': ('Up', 'Uprght'), + 'Kursiv': ('Ks', 'Kurs'), + 'Sloped': ('Sl', 'Slop'), + } + known_modifiers = { + 'Demi': ('Dm', 'Dem'), + 'Ultra': ('Ult', 'Ult'), + 'Semi': ('Sm', 'Sem'), + 'Extra': ('X', 'Ext'), + } + + @staticmethod + def is_keep_regular(basename): + """This has been decided by the font designers, we need to mimic that (for comparison purposes)""" + KEEP_REGULAR = [ + 'Agave', + 'Arimo', + 'Aurulent', + 'Cascadia', + 'Cousine', + 'Fantasque', + 'Fira', + + 'Overpass', + 'Lilex', + 'Inconsolata$', # not InconsolataGo + 'IAWriter', + 'Meslo', + 'Monoid', + 'Mononoki', + 'Hack', + 'JetBrains Mono', + 'Noto Sans', + 'Noto Serif', + 'Victor', + ] + for kr in KEEP_REGULAR: + if (basename.rstrip() + '$').startswith(kr): return True + return False + + @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(r'\bsemi-condensed\b', 'SemiCondensed', name, 1, re.IGNORECASE) # Just for "3270 Semi-Condensed" :-/ + 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 = [ m + s + for s in list(FontnameTools.known_weights2) + list(FontnameTools.known_widths) + for m in list(FontnameTools.known_modifiers) + [''] if m != s + ] + list(FontnameTools.known_weights1) + list(FontnameTools.known_slopes) + styles = [ 'Bold', 'Italic', 'Regular', 'Normal', ] + weights = [ w for w in weights if w not in styles ] + # Some font specialities: + other = [ + '-', 'Book', 'For', 'Powerline', + 'Text', # Plex + 'IIx', # Profont IIx + 'LGC', # Inconsolata LGC + r'\bCE\b', # ProggycleanTT CE + r'[12][cmp]n?', # MPlus + r'(?:uni-)?1[14]', # GohuFont uni + ] + + # 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, True) + if (len(style) < 4 + and style.lower() != 'pro'): # Prevent 'r' of Pro to be detected as style_abbrev + ( 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 + matches2 = re.match(r'(\w+)-(.*)', style) + if matches2: + return FontnameTools.parse_font_name(familyname + matches2.group(1) + '-' + matches2.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) diff --git a/bin/scripts/name_parser/query_monospace b/bin/scripts/name_parser/query_monospace new file mode 100644 index 0000000..3132ffa --- /dev/null +++ b/bin/scripts/name_parser/query_monospace @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# coding=utf8 + +import sys +import os.path +import fontforge + +###### Some helpers (code from font-patcher) + +def check_panose_monospaced(font): + """ Check if the font's Panose flags say it is monospaced """ + # https://forum.high-logic.com/postedfiles/Panose.pdf + panose = list(font.os2_panose) + if panose[0] < 2 or panose[0] > 5: + return -1 # invalid Panose info + panose_mono = ((panose[0] == 2 and panose[3] == 9) or + (panose[0] == 3 and panose[3] == 3)) + return 1 if panose_mono else 0 + +def is_monospaced(font): + """ Check if a font is probably monospaced """ + # Some fonts lie (or have not any Panose flag set), spot check monospaced: + width = -1 + width_mono = True + for glyph in [ 0x49, 0x4D, 0x57, 0x61, 0x69, 0x2E ]: # wide and slim glyphs 'I', 'M', 'W', 'a', 'i', '.' + if not glyph in font: + # A 'strange' font, believe Panose + return check_panose_monospaced(font) == 1 + # print(" -> {} {}".format(glyph, font[glyph].width)) + if width < 0: + width = font[glyph].width + continue + if font[glyph].width != width: + # Exception for fonts like Code New Roman Regular or Hermit Light/Bold: + # Allow small 'i' and dot to be smaller than normal + # I believe the source fonts are buggy + if glyph in [ 0x69, 0x2E ]: + if width > font[glyph].width: + continue + (xmin, _, xmax, _) = font[glyph].boundingBox() + if width > xmax - xmin: + continue + width_mono = False + break + # We believe our own check more then Panose ;-D + return width_mono + +def get_advance_width(font, extended, minimum): + """ Get the maximum/minimum advance width in the extended(?) range """ + width = 0 + if extended: + end = 0x17f + else: + end = 0x07e + for glyph in range(0x21, end): + if not glyph in font: + continue + if glyph in range(0x7F, 0xBF): + continue # ignore special characters like '1/4' etc + if width == 0: + width = font[glyph].width + continue + if not minimum and width < font[glyph].width: + width = font[glyph].width + elif minimum and width > font[glyph].width: + width = font[glyph].width + return width + + +###### 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)) + + +for filename in sys.argv[1:]: + fullfile = os.path.basename(filename) + fname = os.path.splitext(fullfile)[0] + + font = fontforge.open(filename, 1) + width_mono = is_monospaced(font) + panose_mono = check_panose_monospaced(font) + if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1): + print('[{:50.50}] Warning: Monospaced check: Panose assumed to be wrong; Glyph widths {} / {} - {} and Panose says "monospace {}" ({})'.format(fullfile, get_advance_width(font, False, True), + get_advance_width(font, False, False), get_advance_width(font, True, False), panose_mono, list(font.os2_panose))) + if not width_mono: + print('[{:50.50}] Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless; Glyph widths {} / {} - {}'.format(fullfile, get_advance_width(font, False, True), + get_advance_width(font, False, False), get_advance_width(font, True, False), panose_mono, list(font.os2_panose))) + else: + print('[{:50.50}] OK'.format(fullfile)) + font.close() diff --git a/bin/scripts/name_parser/query_names b/bin/scripts/name_parser/query_names new file mode 100644 index 0000000..e05c351 --- /dev/null +++ b/bin/scripts/name_parser/query_names @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# coding=utf8 +# +# Usually called via +# $ fontforge query_names fontfile.tff 2>/dev/null + +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 = '| {:50.50}|{:>2.2}| {:64.64}|{:>2.2}| {:64.64}|{:>2.2}| {:55.55}|{:>2.2}| {:30.30}|{:>2.2}| {:40.40}|{:>2.2}| {:40.40}|{:>2.2}|' + if header: + d = '' + return f.format(*stuff) + '\n' + f.format(d, d, d, d, d, d, d, d, d, d, d, d, d, d).replace(' ', '-') + 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', '', 'PS Name', '', '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) + psname = font.fontname + 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, str(len(fullfile)), + psname, str(len(psname)), + sfnt_full, str(len(sfnt_full)), + sfnt_fam, str(len(sfnt_fam)), + sfnt_subfam, str(len(sfnt_subfam)), + # show length zero if a zero length string is stored, show nothing if nothing is stored: + sfnt_pfam, str(len(sfnt_pfam)) if 'Preferred Family' in sfnt else '', + sfnt_psubfam, str(len(sfnt_psubfam)) if 'Preferred Family' in sfnt else '') + + print(o2) diff --git a/bin/scripts/name_parser/query_panose b/bin/scripts/name_parser/query_panose new file mode 100644 index 0000000..2492444 --- /dev/null +++ b/bin/scripts/name_parser/query_panose @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# 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 0000000..48c3f43 --- /dev/null +++ b/bin/scripts/name_parser/query_sftn @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# 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 0000000..3a9a195 --- /dev/null +++ b/bin/scripts/name_parser/query_version @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# 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/build-hdmx-for-sarasa.py b/build-hdmx-for-sarasa.py deleted file mode 100644 index 9ad316d..0000000 --- a/build-hdmx-for-sarasa.py +++ /dev/null @@ -1,49 +0,0 @@ -#! /usr/bin/python - -# credit: https://github.com/be5invis/Sarasa-Gothic/issues/108#issuecomment-517240248 - -# usage: -# python build-hdmx-for-sarasa.py your-sarasa-font.ttf - -import sys -import math - -from fontTools.ttLib import TTFont, newTable - - -def main(): - headFlagInstructionsMayAlterAdvanceWidth = 0x0010 - sarasaHintPpemMin = 11 - sarasaHintPpemMax = 48 - - filename = sys.argv[1] - - font = TTFont(filename, recalcBBoxes=False) - - originalFontHead = font["head"] - originalFontHmtx = font["hmtx"] - - originalFontHead.flags |= headFlagInstructionsMayAlterAdvanceWidth - - hdmxTable = newTable("hdmx") - hdmxTable.hdmx = {} - - # build hdmx table for odd and hinted ppems only. - for ppem in range( - math.floor(sarasaHintPpemMin / 2) * 2 + 1, sarasaHintPpemMax + 1, 2 - ): - halfUpm = originalFontHead.unitsPerEm / 2 - halfPpem = math.ceil(ppem / 2) - hdmxTable.hdmx[ppem] = { - name: math.ceil(width / halfUpm) * halfPpem - for name, (width, _) in originalFontHmtx.metrics.items() - } - - font["hdmx"] = hdmxTable - - font.save(filename) - font.close() - - -if __name__ == "__main__": - main() diff --git a/correct-ttf-font-family-name.py b/correct-ttf-font-family-name.py deleted file mode 100644 index 2a5360f..0000000 --- a/correct-ttf-font-family-name.py +++ /dev/null @@ -1,61 +0,0 @@ -#! /usr/bin/python - -# usage: -# python correct-ttf-font-family-name.py filename.ttf - -import sys - -from fontTools.ttLib import TTFont - - -def main(): - filename = sys.argv[1] - - font = TTFont(filename, recalcBBoxes=False) - fontName = font["name"] - - originalFontUniqueID = fontName.getName(3, 1, 0, 0).toUnicode() - originalFontFullname = fontName.getName(4, 1, 0, 0).toUnicode() - originalFontPreferredStyle = fontName.getName(17, 1, 0, 0).toUnicode() - - for entry in fontName.names: - nameID = entry.nameID - platformID = entry.platformID - platEncID = entry.platEncID - langID = entry.langID - - if langID in [1028, 1041, 2052, 3076]: - string = ( - entry.toUnicode() - .replace(" CL", " CL Nerd Font") - .replace(" TC", " TC Nerd Font") - .replace(" J", " J Nerd Font") - .replace(" SC", " SC Nerd Font") - .replace(" HC", " HC Nerd Font") - ) - fontName.setName(string, nameID, platformID, platEncID, langID) - - elif nameID in [1, 16]: - string = originalFontUniqueID.replace( - f" {originalFontPreferredStyle}", " Nerd Font" - ) - fontName.setName(string, nameID, platformID, platEncID, langID) - - elif nameID == 3: - string = originalFontUniqueID.replace( - f" {originalFontPreferredStyle}", - f" Nerd Font {originalFontPreferredStyle}", - ) - fontName.setName(string, nameID, platformID, platEncID, langID) - - elif nameID == 6: - fontName.setName( - originalFontFullname, nameID, platformID, platEncID, langID - ) - - font.save(filename) - font.close() - - -if __name__ == "__main__": - main() diff --git a/font-patcher b/font-patcher index 581eaee..68a9dcc 100755 --- a/font-patcher +++ b/font-patcher @@ -1,14 +1,14 @@ #!/usr/bin/env python # coding=utf8 -# Nerd Fonts Version: 2.3.3 +# Nerd Fonts Version: 3.0.0 # Script version is further down from __future__ import absolute_import, print_function, unicode_literals # Change the script version when you edit this script: -script_version = "3.6.1" +script_version = "4.1.1" -version = "2.3.3" +version = "3.0.0" projectName = "Nerd Fonts" projectNameAbbreviation = "NF" projectNameSingular = projectName[:-1] @@ -22,6 +22,7 @@ import errno import subprocess import json from enum import Enum +import logging try: import configparser except ImportError: @@ -135,6 +136,7 @@ class TableHEADWriter: positions = {'checksumAdjustment': 2+2+4, 'flags': 2+2+4+4+4, 'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2, + 'avgWidth': 2, } where = self.tab_offset + positions[where] self.f.seek(where) @@ -238,10 +240,10 @@ def force_panose_monospaced(font): panose = list(font.os2_panose) if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default) panose[0] = 2 # make kind latin text and display - print(" Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')") + logger.info("Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')") font.os2_panose = tuple(panose) if panose[0] == 2 and panose[3] != 9: - print(" Setting Panose 'Proportion' to 'Monospaced' (was '{}')".format(panose_proportion_to_text(panose[3]))) + logger.info("Setting Panose 'Proportion' to 'Monospaced' (was '%s')", panose_proportion_to_text(panose[3])) panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced font.os2_panose = tuple(panose) @@ -282,6 +284,35 @@ def get_btb_metrics(font): win_btb = win_height + win_gap return (hhea_btb, typo_btb, win_btb, win_gap) +def get_old_average_x_width(font): + """ Determine xAvgCharWidth of the OS/2 table """ + # Fontforge can not create fonts with old (i.e. prior to OS/2 version 3) + # table values, but some very old applications do need them sometimes + # https://learn.microsoft.com/en-us/typography/opentype/spec/os2#xavgcharwidth + s = 0 + weights = { + 'a': 64, 'b': 14, 'c': 27, 'd': 35, 'e': 100, 'f': 20, 'g': 14, 'h': 42, 'i': 63, + 'j': 3, 'k': 6, 'l': 35, 'm': 20, 'n': 56, 'o': 56, 'p': 17, 'q': 4, 'r': 49, + 's': 56, 't': 71, 'u': 31, 'v': 10, 'w': 18, 'x': 3, 'y': 18, 'z': 2, 32: 166, + } + for g in weights: + if g not in font: + logger.critical("Can not determine ancient style xAvgCharWidth") + sys.exit(1) + s += font[g].width * weights[g] + return int(s / 1000) + +def create_filename(fonts): + """ Determine filename from font object(s) """ + sfnt = { k: v for l, k, v in fonts[0].sfnt_names } + sfnt_pfam = sfnt.get('Preferred Family', sfnt['Family']) + sfnt_psubfam = sfnt.get('Preferred Styles', sfnt['SubFamily']) + if len(fonts) > 1: + return sfnt_pfam + if len(sfnt_psubfam) > 0: + sfnt_psubfam = '-' + sfnt_psubfam + return (sfnt_pfam + sfnt_psubfam).replace(' ', '') + class font_patcher: def __init__(self, args): @@ -293,9 +324,11 @@ class font_patcher: self.font_dim = None # class 'dict' self.font_extrawide = False self.source_monospaced = None # Later True or False + self.symbolsonly = False self.onlybitmaps = 0 self.essential = set() self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) + self.xavgwidth = [] # list of ints def patch(self, font): self.sourceFont = font @@ -317,7 +350,7 @@ class font_patcher: # For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2: - print("Very wide and short font, disabling 2 cell Powerline glyphs") + logger.warning("Very wide and short font, disabling 2 cell Powerline glyphs") self.font_extrawide = True # Prevent opening and closing the fontforge font. Makes things faster when patching @@ -326,8 +359,12 @@ class font_patcher: symfont = None if not os.path.isdir(self.args.glyphdir): - sys.exit("{}: Can not find symbol glyph directory {} " - "(probably you need to download the src/glyphs/ directory?)".format(projectName, self.args.glyphdir)) + logger.critical("Can not find symbol glyph directory %s " + "(probably you need to download the src/glyphs/ directory?)", self.args.glyphdir) + sys.exit(1) + + if self.args.dry_run: + return for patch in self.patch_set: if patch['Enabled']: @@ -337,11 +374,13 @@ class font_patcher: symfont.close() symfont = None if not os.path.isfile(self.args.glyphdir + patch['Filename']): - sys.exit("{}: Can not find symbol source for '{}'\n{:>{}} (i.e. {})".format( - projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename'])) + logger.critical("Can not find symbol source for '%s' (i.e. %s)", + patch['Name'], self.args.glyphdir + patch['Filename']) + sys.exit(1) if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK): - sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format( - projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename'])) + logger.critical("Can not open symbol source for '%s' (i.e. %s)", + patch['Name'], self.args.glyphdir + patch['Filename']) + sys.exit(1) symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename'])) symfont.encoding = 'UnicodeFull' @@ -383,11 +422,11 @@ class font_patcher: break outfile = os.path.normpath(os.path.join( sanitize_filename(self.args.outputdir, True), - sanitize_filename(sourceFont.familyname) + ".ttc")) + sanitize_filename(create_filename(sourceFonts)) + ".ttc")) sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer) message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile) else: - fontname = sourceFont.fullname + fontname = create_filename(sourceFonts) if not fontname: fontname = sourceFont.cidfontname outfile = os.path.normpath(os.path.join( @@ -395,9 +434,11 @@ class font_patcher: sanitize_filename(fontname) + self.args.extension)) bitmaps = str() if len(self.sourceFont.bitmapSizes): - if not self.args.quiet: - print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes)) + logger.debug("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes)) bitmaps = str('otf') # otf/ttf, both is bf_ttf + if self.args.dry_run: + logger.debug("=====> Filename '{}'".format(outfile)) + return sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags) message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile) @@ -407,23 +448,34 @@ class font_patcher: source_font = TableHEADWriter(self.args.font) dest_font = TableHEADWriter(outfile) for idx in range(source_font.num_fonts): - if not self.args.quiet: - print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts)) + logger.debug("Tweaking %d/%d", idx + 1, source_font.num_fonts) + xwidth_s = '' + xwidth = self.xavgwidth[idx] + if isinstance(xwidth, int): + if isinstance(xwidth, bool) and xwidth: + source_font.find_table([b'OS/2'], idx) + xwidth = source_font.getshort('avgWidth') + xwidth_s = ' (copied from source)' + dest_font.find_table([b'OS/2'], idx) + d_xwidth = dest_font.getshort('avgWidth') + if d_xwidth != xwidth: + logger.debug("Changing xAvgCharWidth from %d to %d%s", d_xwidth, xwidth, xwidth_s) + dest_font.putshort(xwidth, 'avgWidth') + dest_font.reset_table_checksum() source_font.find_head_table(idx) dest_font.find_head_table(idx) if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0: - if not self.args.quiet: - print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08)) + logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08) dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int' if source_font.lowppem != dest_font.lowppem: - if not self.args.quiet: - print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem)) + logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem) dest_font.putshort(source_font.lowppem, 'lowestRecPPEM') if dest_font.modified: dest_font.reset_table_checksum() - dest_font.reset_full_checksum() + if dest_font.modified: + dest_font.reset_full_checksum() except Exception as error: - print("Can not handle font flags ({})".format(repr(error))) + logger.error("Can not handle font flags (%s)", repr(error)) finally: try: source_font.close() @@ -431,12 +483,13 @@ class font_patcher: except: pass if self.args.is_variable: - print("Warning: Source font is a variable open type font (VF) and the patch results will most likely not be what you want") + logger.critical("Source font is a variable open type font (VF) and the patch results will most likely not be what you want") print(message) if self.args.postprocess: subprocess.call([self.args.postprocess, outfile]) - print("\nPost Processed: {}".format(outfile)) + print("\n") + logger.info("Post Processed: %s", outfile) def setup_name_backup(self, font): @@ -454,11 +507,8 @@ class font_patcher: font.fullname = font.persistent["fullname"] if isinstance(font.persistent["familyname"], str): font.familyname = font.persistent["familyname"] - verboseAdditionalFontNameSuffix = " " + projectNameSingular - if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later - additionalFontNameSuffix = " " + projectNameAbbreviation - else: - additionalFontNameSuffix = verboseAdditionalFontNameSuffix + verboseAdditionalFontNameSuffix = "" + additionalFontNameSuffix = "" if not self.args.complete: # NOTE not all symbol fonts have appended their suffix here if self.args.fontawesome: @@ -489,17 +539,24 @@ class font_patcher: additionalFontNameSuffix += " WEA" verboseAdditionalFontNameSuffix += " Plus Weather Icons" - # if all source glyphs included simplify the name - else: - additionalFontNameSuffix = " " + projectNameSingular + " Complete" - verboseAdditionalFontNameSuffix = " " + projectNameSingular + " Complete" - - # add mono signifier to end of name + # add mono signifier to beginning of name suffix if self.args.single: - additionalFontNameSuffix += " M" - verboseAdditionalFontNameSuffix += " Mono" + variant_abbrev = "M" + variant_full = " Mono" + elif self.args.nonmono and not self.symbolsonly: + variant_abbrev = "P" + variant_full = " Propo" + else: + variant_abbrev = "" + variant_full = "" - if FontnameParserOK and self.args.makegroups: + ps_suffix = projectNameAbbreviation + variant_abbrev + additionalFontNameSuffix + + # add 'Nerd Font' to beginning of name suffix + verboseAdditionalFontNameSuffix = " " + projectNameSingular + variant_full + verboseAdditionalFontNameSuffix + additionalFontNameSuffix = " " + projectNameSingular + variant_full + additionalFontNameSuffix + + if FontnameParserOK and self.args.makegroups > 0: use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse # Use fullname if it is 'equal' to the fontname if font.fullname: @@ -511,12 +568,14 @@ class font_patcher: # Gohu fontnames hide the weight, but the file names are ok... if parser_name.startswith('Gohu'): parser_name = os.path.splitext(os.path.basename(self.args.font))[0] - n = FontnameParser(parser_name) + n = FontnameParser(parser_name, logger) if not n.parse_ok: - print("Have only minimal naming information, check resulting name. Maybe omit --makegroups option") + logger.warning("Have only minimal naming information, check resulting name. Maybe specify --makegroups 0") n.drop_for_powerline() - n.enable_short_families(True, "Noto") - n.set_for_windows(self.args.windows) + n.enable_short_families(True, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ]) + if not n.set_expect_no_italic(self.args.noitalic): + logger.critical("Detected 'Italic' slant but --has-no-italic specified") + sys.exit(1) # All the following stuff is ignored in makegroups-mode @@ -564,23 +623,7 @@ class font_patcher: if len(subFamily) == 0: subFamily = "Regular" - if self.args.windows: - maxFamilyLength = 31 - maxFontLength = maxFamilyLength - len('-' + subFamily) - familyname += " " + projectNameAbbreviation - if self.args.single: - familyname += "M" - fullname += " Windows Compatible" - - # now make sure less than 32 characters name length - if len(fontname) > maxFontLength: - fontname = fontname[:maxFontLength] - if len(familyname) > maxFamilyLength: - familyname = familyname[:maxFamilyLength] - else: - familyname += " " + projectNameSingular - if self.args.single: - familyname += " Mono" + familyname += " " + projectNameSingular + variant_full # Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with # the same name as the same font, even if subFamily is different. Make sure to @@ -593,6 +636,10 @@ class font_patcher: reservedFontNameReplacements = { 'source' : 'sauce', 'Source' : 'Sauce', + 'Bitstream Vera Sans Mono' : 'Bitstrom Wera', + 'BitstreamVeraSansMono' : 'BitstromWera', + 'bitstream vera sans mono' : 'bitstrom wera', + 'bitstreamverasansmono' : 'bitstromwera', 'hermit' : 'hurmit', 'Hermit' : 'Hurmit', 'hasklig' : 'hasklug', @@ -659,7 +706,7 @@ class font_patcher: fullname = replace_font_name(fullname, additionalFontNameReplacements2) fontname = replace_font_name(fontname, additionalFontNameReplacements2) - if not (FontnameParserOK and self.args.makegroups): + if not (FontnameParserOK and self.args.makegroups > 0): # replace any extra whitespace characters: font.familyname = " ".join(familyname.split()) font.fullname = " ".join(fullname.split()) @@ -670,13 +717,9 @@ class font_patcher: font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname) font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) else: - fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation - if self.args.single: - if self.args.windows: - fam_suffix += 'M' - else: - fam_suffix += ' Mono' - n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix) + short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full + # inject_suffix(family, ps_fontname, short_family) + n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family) n.rename_font(font) font.comment = projectInfo @@ -692,6 +735,7 @@ class font_patcher: self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version) + # The Version SFNT name is later reused by the NameParser for UniqueID # print("Version now is {}".format(sourceFont.version)) @@ -700,17 +744,17 @@ class font_patcher: # the tables have been removed from the repo with >this< commit if self.args.configfile and self.config.read(self.args.configfile): if self.args.removeligatures: - print("Removing ligatures from configfile `Subtables` section") + logger.info("Removing ligatures from configfile `Subtables` section") ligature_subtables = json.loads(self.config.get("Subtables", "ligatures")) for subtable in ligature_subtables: - print("Removing subtable:", subtable) + logger.debug("Removing subtable: %s", subtable) try: self.sourceFont.removeLookupSubtable(subtable) - print("Successfully removed subtable:", subtable) + logger.debug("Successfully removed subtable: %s", subtable) except Exception: - print("Failed to remove subtable:", subtable) + logger.error("Failed to remove subtable: %s", subtable) elif self.args.removeligatures: - print("Unable to read configfile, unable to remove ligatures") + logger.error("Unable to read configfile, unable to remove ligatures") def assert_monospace(self): @@ -722,16 +766,17 @@ class font_patcher: panose_mono = check_panose_monospaced(self.sourceFont) # The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown' if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1): - print(" Warning: Monospaced check: Panose assumed to be wrong") - print(" {} and {}".format( + logger.warning("Monospaced check: Panose assumed to be wrong") + logger.warning(" %s and %s", report_advance_widths(self.sourceFont), - panose_check_to_text(panose_mono, self.sourceFont.os2_panose))) + panose_check_to_text(panose_mono, self.sourceFont.os2_panose)) if self.args.single and not width_mono: - print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless") + logger.warning("Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless") if offending_char is not None: - print(" Offending char: 0x{:X}".format(offending_char)) + logger.warning(" Offending char: %X", offending_char) if self.args.single <= 1: - sys.exit(projectName + ": Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching") + logger.critical("Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching") + sys.exit(1) if width_mono: force_panose_monospaced(self.sourceFont) @@ -740,19 +785,20 @@ class font_patcher: """ Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """ box_enabled = self.source_monospaced # Box glyph only for monospaced + box_keep = False if box_enabled: self.sourceFont.selection.select(("ranges",), 0x2500, 0x259f) box_glyphs_target = len(list(self.sourceFont.selection)) box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs)) if box_glyphs_target > box_glyphs_current: - # Sourcefont does not have all of these glyphs, do not mix sets - if not self.args.quiet and box_glyphs_current > 0: - print("INFO: {}/{} box drawing glyphs will be replaced".format( - box_glyphs_current, box_glyphs_target)) - box_keep = False + # Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing) + if box_glyphs_current > 0: + logger.debug("%d/%d box drawing glyphs will be replaced", + box_glyphs_current, box_glyphs_target) box_enabled = True else: - box_keep = True # just scale do not copy + # Sourcefont does have all of these glyphs + # box_keep = True # just scale do not copy (need to scale to fit new cell size) box_enabled = False # Cowardly not scaling existing glyphs, although the code would allow this # Stretch 'xz' or 'pa' (preserve aspect ratio) @@ -985,14 +1031,14 @@ class font_patcher: {'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x23FB, 'SymEnd': 0x23FE, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Power, Power On/Off, Power On, Sleep {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off) - {'Enabled': self.args.material, 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': False , 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesign/MaterialDesignIconsDesktop.ttf", 'Exact': True, 'SymStart': 0xF0001,'SymEnd': 0xF1AF0,'SrcStart': None, 'ScaleRules': MDI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.weather, 'Name': "Weather Icons", 'Filename': "weather-icons/weathericons-regular-webfont.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF0EB, 'SrcStart': 0xE300, 'ScaleRules': WEATH_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, - {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass - {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart - {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap - {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF27C, 'SrcStart': 0xF4A9, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Desktop + {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass + {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart + {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap + {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF305, 'SrcStart': 0xF4A9, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleRules': CODI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR} ] @@ -1067,11 +1113,22 @@ class font_patcher: our_btb = typo_btb if use_typo else win_btb if our_btb == hhea_btb: metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font + elif abs(our_btb - hhea_btb) / our_btb < 0.03: + logger.info("Font vertical metrics slightly off (%.1f%)", (our_btb - hhea_btb) / our_btb * 100.0) + metrics = Metric.TYPO if use_typo else Metric.WIN else: - # We trust the WIN metric more, see experiments in #1056 - print("{}: WARNING Font vertical metrics inconsistent (HHEA {} / TYPO {} / WIN {}), using WIN".format(projectName, hhea_btb, typo_btb, win_btb)) - our_btb = win_btb - metrics = Metric.WIN + # Try the other metric + our_btb = typo_btb if not use_typo else win_btb + if our_btb == hhea_btb: + logger.warning("Font vertical metrics probably wrong USE TYPO METRICS, assume opposite (i.e. %s)", 'True' if not use_typo else 'False') + use_typo = not use_typo + self.sourceFont.os2_use_typo_metrics = 1 if use_typo else 0 + metrics = Metric.TYPO if use_typo else Metric.WIN + else: + # We trust the WIN metric more, see experiments in #1056 + logger.warning("Font vertical metrics inconsistent (HHEA %d / TYPO %d / WIN %d), using WIN", hhea_btb, typo_btb, win_btb) + our_btb = win_btb + metrics = Metric.WIN # print("FINI hhea {} typo {} win {} use {} {} {}".format(hhea_btb, typo_btb, win_btb, use_typo, our_btb != hhea_btb, self.sourceFont.fontname)) @@ -1094,6 +1151,7 @@ class font_patcher: if self.font_dim['height'] == 0: # This can only happen if the input font is empty # Assume we are using our prepared templates + self.symbolsonly = True self.font_dim = { 'xmin' : 0, 'ymin' : -self.sourceFont.descent, @@ -1104,7 +1162,8 @@ class font_patcher: } our_btb = self.sourceFont.descent + self.sourceFont.ascent elif self.font_dim['height'] < 0: - sys.exit("{}: Can not detect sane font height".format(projectName)) + logger.critical("Can not detect sane font height") + sys.exit(1) # Make all metrics equal self.sourceFont.os2_typolinegap = 0 @@ -1118,12 +1177,13 @@ class font_patcher: self.sourceFont.os2_use_typo_metrics = 1 (check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont) if check_hhea_btb != check_typo_btb or check_typo_btb != check_win_btb or check_win_btb != our_btb: - sys.exit("{}: Error in baseline to baseline code detected".format(projectName)) + logger.critical("Error in baseline to baseline code detected") + sys.exit(1) # Step 2 # Find the biggest char width and advance width # 0x00-0x17f is the Latin Extended-A range - warned1 = self.args.quiet or self.args.nonmono # Do not warn if quiet or proportional target + warned1 = self.args.nonmono # Do not warn if proportional target warned2 = warned1 for glyph in range(0x21, 0x17f): if glyph in range(0x7F, 0xBF) or glyph in [ @@ -1141,22 +1201,25 @@ class font_patcher: if self.font_dim['width'] < self.sourceFont[glyph].width: self.font_dim['width'] = self.sourceFont[glyph].width if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z - print("Warning: Extended glyphs wider than basic glyphs, results might be useless\n {}".format( - report_advance_widths(self.sourceFont))) + logger.debug("Extended glyphs wider than basic glyphs, results might be useless\n %s", + report_advance_widths(self.sourceFont)) warned1 = True # print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) if xmax > self.font_dim['xmax']: self.font_dim['xmax'] = xmax if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z - print("Info: Extended glyphs wider bounding box than basic glyphs") + logger.debug("Extended glyphs wider bounding box than basic glyphs") warned2 = True # print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) if self.font_dim['width'] < self.font_dim['xmax']: - if not self.args.quiet: - print("Warning: Font has negative right side bearing in extended glyphs") + logger.debug("Font has negative right side bearing in extended glyphs") self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used # print("FINAL", self.font_dim) + self.xavgwidth.append(self.args.xavgwidth) + if isinstance(self.xavgwidth[-1], int) and self.xavgwidth[-1] == 0: + self.xavgwidth[-1] = get_old_average_x_width(self.sourceFont) + def get_target_width(self, stretch): """ Get the target width (1 or 2 'cell') for a given stretch parameter """ @@ -1249,7 +1312,7 @@ class font_patcher: if sym_glyph.altuni: possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ] if len(possible_codes) == 0: - print(" Can not determine codepoint of {:X}. Skipping...".format(sym_glyph.unicode)) + logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode) continue currentSourceFontGlyph = min(possible_codes) else: @@ -1272,9 +1335,8 @@ class font_patcher: # check if a glyph already exists in this location if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential: if currentSourceFontGlyph in self.sourceFont: - if not self.args.quiet: - careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing' - print(" Found {} Glyph at {:X}. Skipping...".format(careful_type, currentSourceFontGlyph)) + careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing' + logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph) # We don't want to touch anything so move to next Glyph continue else: @@ -1422,8 +1484,8 @@ class font_patcher: if self.args.single: (xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox() if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)): - print("\n Warning: Scaled glyph U+{:X} wider than one monospace width ({} / {} (overlap {}))".format( - currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)) + logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %f))", + currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap) # end for @@ -1564,7 +1626,7 @@ def half_gap(gap, top): gap_top = int(gap / 2) gap_bottom = gap - gap_top if top: - print("Redistributing line gap of {} ({} top and {} bottom)".format(gap, gap_top, gap_bottom)) + logger.info("Redistributing line gap of %d (%d top and %d bottom)", gap, gap_top, gap_bottom) return gap_top return gap_bottom @@ -1689,8 +1751,8 @@ def check_fontforge_min_version(): # versions tested: 20150612, 20150824 if actualVersion < minimumVersion: - sys.stderr.write("{}: You seem to be using an unsupported (old) version of fontforge: {}\n".format(projectName, actualVersion)) - sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion)) + logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion) + logger.critical("Please use at least version: %d", minimumVersion) sys.exit(1) def check_version_with_git(version): @@ -1738,7 +1800,6 @@ def setup_arguments(): parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='count', help='Whether to generate the glyphs as single-width not double-width (default is double-width)') parser.add_argument('-l', '--adjust-line-height', dest='adjustLineHeight', default=False, action='store_true', help='Whether to adjust line heights (attempt to center powerline separators more evenly)') parser.add_argument('-q', '--quiet', '--shutup', dest='quiet', default=False, action='store_true', help='Do not generate verbose output') - parser.add_argument('-w', '--windows', dest='windows', default=False, action='store_true', help='Limit the internal font name to 31 characters (for Windows compatibility)') parser.add_argument('-c', '--complete', dest='complete', default=False, action='store_true', help='Add all available Glyphs') parser.add_argument('--careful', dest='careful', default=False, action='store_true', help='Do not overwrite existing glyphs if detected') parser.add_argument('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specificed in JSON configuration file') @@ -1748,15 +1809,34 @@ def setup_arguments(): 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('--makegroups', dest='makegroups', default=False, action='store_true', help='Use alternative method to name patched fonts (experimental)') + parser.add_argument('--makegroups', dest='makegroups', default=1, type=int, nargs='?', help='Use alternative method to name patched fonts (recommended)', const=1, choices=range(0, 6 + 1)) + # --makegroup has an additional undocumented numeric specifier. '--makegroup' is in fact '--makegroup 1'. + # Original font name: Hugo Sans Mono ExtraCondensed Light Italic + # NF Fam agg. + # 0 turned off, use old naming scheme [-] [-] [-] + # 1 HugoSansMono Nerd Font ExtraCondensed Light Italic [ ] [ ] [ ] + # 2 HugoSansMono Nerd Font ExtCn Light Italic [ ] [X] [ ] + # 3 HugoSansMono Nerd Font XCn Lt It [ ] [X] [X] + # 4 HugoSansMono NF ExtraCondensed Light Italic [X] [ ] [ ] + # 5 HugoSansMono NF ExtCn Light Italic [X] [X] [ ] + # 6 HugoSansMono NF XCn Lt It [X] [X] [X] + parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")') + parser.add_argument('--has-no-italic', dest='noitalic', default=False, action='store_true', help='Font family does not have Italic (but Oblique)') # progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse progressbars_group_parser = parser.add_mutually_exclusive_group(required=False) - progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set') + progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set (default)') progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set') parser.set_defaults(progressbars=True) - parser.add_argument('--also-windows', dest='alsowindows', default=False, action='store_true', help='Create two fonts, the normal and the --windows version') + parser.add_argument('--debug', dest='debugmode', default=False, action='store_true', help='Verbose mode') + parser.add_argument('--dry', dest='dry_run', default=False, action='store_true', help='Do neither patch nor store the font, to check naming') + parser.add_argument('--xavgcharwidth', dest='xavgwidth', default=None, type=int, nargs='?', help='Adjust xAvgCharWidth (optional: concrete value)', const=True) + # --xavgcharwidth for compatibility with old applications like notepad and non-latin fonts + # Possible values with examples: + # - copy from sourcefont (default) + # 0 - calculate from font according to OS/2-version-2 + # 500 - set to 500 # symbol fonts to include arguments sym_font_group = parser.add_argument_group('Symbol Fonts') @@ -1774,8 +1854,9 @@ def setup_arguments(): args = parser.parse_args() - if args.makegroups and not FontnameParserOK: - sys.exit("{}: FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName)) + if args.makegroups > 0 and not FontnameParserOK: + logger.critical("FontnameParser module missing (bin/scripts/name_parser/Fontname*), specify --makegroups 0") + sys.exit(1) # if you add a new font, set it to True here inside the if condition if args.complete: @@ -1808,24 +1889,23 @@ def setup_arguments(): font_complete = False args.complete = font_complete - if args.alsowindows: - args.windows = False - if args.nonmono and args.single: - print("Warning: Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.") + logging.warning("Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.") args.nonmono = False make_sure_path_exists(args.outputdir) if not os.path.isfile(args.font): - sys.exit("{}: Font file does not exist: {}".format(projectName, args.font)) + logging.critical("Font file does not exist: %s", args.font) + sys.exit(1) if not os.access(args.font, os.R_OK): - sys.exit("{}: Can not open font file for reading: {}".format(projectName, args.font)) + logging.critical("Can not open font file for reading: %s", args.font) + sys.exit(1) is_ttc = len(fontforge.fontsInFile(args.font)) > 1 try: source_font_test = TableHEADWriter(args.font) args.is_variable = source_font_test.find_table([b'avar', b'cvar', b'fvar', b'gvarb', b'HVAR', b'MVAR', b'VVAR'], 0) if args.is_variable: - print(" Warning: Source font is a variable open type font (VF), opening might fail...") + logging.warning("Source font is a variable open type font (VF), opening might fail...") except: args.is_variable = False finally: @@ -1840,10 +1920,20 @@ def setup_arguments(): args.extension = '.' + args.extension if re.match("\.ttc$", args.extension, re.IGNORECASE): if not is_ttc: - sys.exit(projectName + ": Can not create True Type Collections from single font files") + logging.critical("Can not create True Type Collections from single font files") + sys.exit(1) else: if is_ttc: - sys.exit(projectName + ": Can not create single font files from True Type Collections") + logging.critical("Can not create single font files from True Type Collections") + sys.exit(1) + + if isinstance(args.xavgwidth, int) and not isinstance(args.xavgwidth, bool): + if args.xavgwidth < 0: + logging.critical("--xavgcharwidth takes no negative numbers") + sys.exit(2) + if args.xavgwidth > 16384: + logging.critical("--xavgcharwidth takes only numbers up to 16384") + sys.exit(2) return args @@ -1851,24 +1941,43 @@ def setup_arguments(): def main(): global version git_version = check_version_with_git(version) - print("{} Patcher v{} ({}) (ff {}) executing".format( - projectName, git_version if git_version else version, script_version, fontforge.version())) + allversions = "Patcher v{} ({}) (ff {})".format( + git_version if git_version else version, script_version, fontforge.version()) + print("{} {}".format(projectName, allversions)) if git_version: version = git_version check_fontforge_min_version() args = setup_arguments() + + global logger + logger = logging.getLogger(os.path.basename(args.font)) + logger.setLevel(logging.DEBUG) + f_handler = logging.FileHandler('font-patcher-log.txt') + f_handler.setFormatter(logging.Formatter('%(levelname)s: %(name)s %(message)s')) + logger.addHandler(f_handler) + logger.debug(allversions) + logger.debug("Options %s", repr(sys.argv[1:])) + c_handler = logging.StreamHandler(stream=sys.stdout) + c_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + if not args.debugmode: + c_handler.setLevel(logging.INFO) + logger.addHandler(c_handler) + logger.debug("Naming mode %d", args.makegroups) + patcher = font_patcher(args) sourceFonts = [] all_fonts = fontforge.fontsInFile(args.font) for i, subfont in enumerate(all_fonts): if len(all_fonts) > 1: - print("\n{}: Processing {} ({}/{})".format(projectName, subfont, i + 1, len(all_fonts))) + print("\n") + logger.info("Processing %s (%d/%d)", subfont, i + 1, len(all_fonts)) try: sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",)) except Exception: - sys.exit("{}: Can not open font '{}', try to open with fontforge interactively to get more information".format( - projectName, subfont)) + logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information", + subfont) + sys.exit(1) patcher.patch(sourceFonts[-1]) @@ -1877,13 +1986,6 @@ def main(): patcher.setup_font_names(f) patcher.generate(sourceFonts) - # This mainly helps to improve CI runtime - if patcher.args.alsowindows: - patcher.args.windows = True - for f in sourceFonts: - patcher.setup_font_names(f) - patcher.generate(sourceFonts) - for f in sourceFonts: f.close() diff --git a/original/.gitignore b/original/.gitignore index f7ad452..4d7a873 100644 --- a/original/.gitignore +++ b/original/.gitignore @@ -1,3 +1,2 @@ *.ttf -*.ttx *.zip diff --git a/patch_Iosevka.sh b/patch_Iosevka.sh index 1ce4d55..e173b64 100755 --- a/patch_Iosevka.sh +++ b/patch_Iosevka.sh @@ -9,11 +9,6 @@ if ! command -v fontforge >/dev/null; then exit 1 fi -# if ! command -v ttx >/dev/null; then -# printf "\033[1;31mfonttools\033[0m is not installed.\n" -# exit 1 -# fi - if [[ $# -eq 0 ]]; then versions=$(curl -H "Accept: application/vnd.github.v3+json" -s https://api.github.com/repos/be5invis/Iosevka/releases | jq -r '.[] | .tag_name' | sed -e 's/^v//g') version=$(echo "${versions}" | fzf --no-multi --prompt "Release: ") @@ -32,28 +27,16 @@ variants=( zipfile="original/ttf-iosevka-term-${version}.zip" if [ ! -f "${zipfile}" ]; then printf "\033[1;34mDownloading Iosevka Term version \033[1;31m%s\033[1;34m zip file ...\033[0m\n" "${version}" - curl -fSL https://github.com/be5invis/Iosevka/releases/download/v${version}/ttf-iosevka-term-${version}.zip -o ${zipfile} + curl -fSL "https://github.com/be5invis/Iosevka/releases/download/v${version}/ttf-iosevka-term-${version}.zip" -o "${zipfile}" fi printf "\033[1;34mUnzipping the downloaded archive ...\033[0m\n" -unzip ${zipfile} -d ./original +unzip "${zipfile}" -d ./original for variant in "${variants[@]}"; do printf "\033[1;34mPatching Iosevka term \033[1;31m%s\033[1;34m ...\033[0m\n" "${variant}" # Run the font-patcher script - fontforge -script ./font-patcher --quiet --no-progressbars --careful --complete ./original/iosevka-term-${variant}.ttf - mv -f ./*Complete.ttf ./patched/iosevka-term-${variant}-nerd-font.ttf - - # Correct xAvgCharWidth - # ttx -t "OS/2" ./original/iosevka-term-${variant}.ttf - # ttx -t "OS/2" ./patched/iosevka-term-${variant}-nerd-font.ttf - # original_x_avg_char_width=$(grep xAvgCharWidth ./original/iosevka-term-${variant}.ttx | cut -d '"' -f 2) - # sed -i "s/xAvgCharWidth value=\"[0-9]\+\"/xAvgCharWidth value=\"${original_x_avg_char_width}\"/g" ./patched/iosevka-term-${variant}-nerd-font.ttx - # mv -f ./patched/iosevka-term-${variant}-nerd-font.ttf ./patched/iosevka-term-${variant}-nerd-font.original.ttf - # ttx -o ./patched/iosevka-term-${variant}-nerd-font.ttf -m ./patched/iosevka-term-${variant}-nerd-font.original.ttf ./patched/iosevka-term-${variant}-nerd-font.ttx - - # Build hdmx table and correct TTF font family name - #python3 ./build-hdmx-for-sarasa.py ./patched/iosevka-term-${variant}-nerd-font.ttf - #python3 ./correct-ttf-font-family-name.py ./patched/iosevka-term-${variant}-nerd-font.ttf + fontforge -script ./font-patcher --careful --complete ./original/"iosevka-term-${variant}.ttf" + mv -fv ./IosevkaTermNerdFont-*.ttf ./patched/"iosevka-term-${variant}-nerd-font.ttf" done diff --git a/patched/.gitignore b/patched/.gitignore index ba6e845..d259f05 100644 --- a/patched/.gitignore +++ b/patched/.gitignore @@ -1,2 +1 @@ *.ttf -*.ttx diff --git a/src/glyphs/octicons.ttf b/src/glyphs/octicons.ttf deleted file mode 100644 index ff0dda18470bed62d7090c8dd6762dba05e098c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43920 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 literal 0 HcmV?d00001 diff --git a/update.sh b/update.sh index e5ce8f9..ce080d7 100755 --- a/update.sh +++ b/update.sh @@ -4,16 +4,17 @@ echo "Downloading 'font-patcher' script..." curl -fsSL https://github.com/ryanoasis/nerd-fonts/raw/master/font-patcher -o ./font-patcher chmod 755 ./font-patcher -for path in codicons font-awesome powerline-symbols weather-icons; do +for path in codicons font-awesome materialdesign octicons powerline-symbols weather-icons; do mkdir -p ./src/glyphs/"$path" done +mkdir -p ./bin/scripts/name_parser -echo "Downloading glyph fonts..." glyphs=( "codicons/codicon.ttf" "font-awesome/FontAwesome.otf" "materialdesign/MaterialDesignIconsDesktop.ttf" "materialdesign/MaterialDesignIconsDesktop_orig.ttf" + "octicons/octicons.ttf" "powerline-symbols/PowerlineSymbols.otf" "weather-icons/weathericons-regular-webfont.ttf" "Pomicons.otf" @@ -24,16 +25,33 @@ glyphs=( "font-awesome-extension.ttf" "font-logos.ttf" "materialdesignicons-webfont.ttf" - "octicons.ttf" "original-source.otf" ) -upstream_src_glyphs_url="https://github.com/ryanoasis/nerd-fonts/raw/master/src/glyphs" +name_parser=( + "FontnameParser.py" + "FontnameTools.py" + "query_monospace" + "query_names" + "query_panose" + "query_sftn" + "query_version" +) +upstream_src_glyphs_url="https://github.com/ryanoasis/nerd-fonts/raw/master/src/glyphs" +upstream_name_parser_url="https://github.com/ryanoasis/nerd-fonts/raw/master/bin/scripts/name_parser" + +echo "Downloading glyph fonts..." for glyph in "${glyphs[@]}"; do # replace all `whitespace` characters with `%20` percent_encoded_uri="${upstream_src_glyphs_url}/${glyph//\ /%20}" - curl -fSL ${percent_encoded_uri} -o "src/glyphs/${glyph}" + curl -fSL "${percent_encoded_uri}" -o "src/glyphs/${glyph}" done find ./src/glyphs/ -type f -exec chmod 644 '{}' \; + +echo "Downloading helper scripts for font-patcher ..." +for file in "${name_parser[@]}"; do + curl -fSL "${upstream_name_parser_url}/${file}" -o "bin/scripts/name_parser/${file}" +done +find ./bin/scripts/name_parser/ -type f -exec chmod 644 '{}' \;