From fd0287215234f4f7ab9fd634fdf915c19f639976 Mon Sep 17 00:00:00 2001 From: ObserverOfTime Date: Sat, 6 May 2023 18:16:21 +0300 Subject: [PATCH] Update Iosevka & glyphs & font patcher It's been a while --- .gitattributes | 1 + Iosevka | 2 +- .../bin/scripts/name_parser/FontnameParser.py | 334 ++++ .../bin/scripts/name_parser/FontnameTools.py | 382 +++++ vendor/bin/scripts/name_parser/README.md | 71 + .../bin/scripts/name_parser/query_monospace | 94 ++ vendor/bin/scripts/name_parser/query_names | 60 + vendor/bin/scripts/name_parser/query_panose | 16 + vendor/bin/scripts/name_parser/query_sftn | 35 + vendor/bin/scripts/name_parser/query_version | 26 + vendor/font-patcher | 1372 ++++++++++++----- ...mbols 1000 EM Nerd Font Complete Blank.sfd | 3 - ...mbols 2048 EM Nerd Font Complete Blank.sfd | 3 - vendor/glyphs/PowerlineExtraSymbols.otf | 4 +- vendor/glyphs/README.md | 3 + .../Symbols-1000-em Nerd Font Complete.ttf | 3 - .../Symbols-2048-em Nerd Font Complete.ttf | 3 - vendor/glyphs/extraglyphs.sfd | 3 + vendor/glyphs/font-logos.ttf | 4 +- vendor/glyphs/materialdesign/LICENSE | 3 + .../MaterialDesignIconsDesktop.ttf | 3 + .../MaterialDesignIconsDesktop_orig.ttf | 3 + vendor/glyphs/materialdesign/README.md | 3 + vendor/glyphs/octicons.ttf | 3 - vendor/glyphs/octicons/LICENSE | 3 + vendor/glyphs/octicons/analyze_octicons | 3 + vendor/glyphs/octicons/generate | 3 + vendor/glyphs/octicons/mapping | 3 + vendor/glyphs/octicons/octicons.ttf | 3 + vendor/glyphs/original-source.otf | 4 +- 30 files changed, 2034 insertions(+), 419 deletions(-) create mode 100644 vendor/bin/scripts/name_parser/FontnameParser.py create mode 100644 vendor/bin/scripts/name_parser/FontnameTools.py create mode 100644 vendor/bin/scripts/name_parser/README.md create mode 100644 vendor/bin/scripts/name_parser/query_monospace create mode 100644 vendor/bin/scripts/name_parser/query_names create mode 100644 vendor/bin/scripts/name_parser/query_panose create mode 100644 vendor/bin/scripts/name_parser/query_sftn create mode 100644 vendor/bin/scripts/name_parser/query_version delete mode 100644 vendor/glyphs/NerdFontsSymbols 1000 EM Nerd Font Complete Blank.sfd delete mode 100644 vendor/glyphs/NerdFontsSymbols 2048 EM Nerd Font Complete Blank.sfd create mode 100644 vendor/glyphs/README.md delete mode 100644 vendor/glyphs/Symbols-1000-em Nerd Font Complete.ttf delete mode 100644 vendor/glyphs/Symbols-2048-em Nerd Font Complete.ttf create mode 100644 vendor/glyphs/extraglyphs.sfd create mode 100644 vendor/glyphs/materialdesign/LICENSE create mode 100644 vendor/glyphs/materialdesign/MaterialDesignIconsDesktop.ttf create mode 100644 vendor/glyphs/materialdesign/MaterialDesignIconsDesktop_orig.ttf create mode 100644 vendor/glyphs/materialdesign/README.md delete mode 100644 vendor/glyphs/octicons.ttf create mode 100644 vendor/glyphs/octicons/LICENSE create mode 100644 vendor/glyphs/octicons/analyze_octicons create mode 100644 vendor/glyphs/octicons/generate create mode 100644 vendor/glyphs/octicons/mapping create mode 100644 vendor/glyphs/octicons/octicons.ttf diff --git a/.gitattributes b/.gitattributes index 3a439ad..44b4296 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ # https://github.com/ryanoasis/nerd-fonts vendor/font-patcher linguist-vendored +vendor/bin/** linguist-vendored vendor/glyphs/** filter=lfs diff=lfs merge=lfs -text private-build-plans.toml linguist-detectable diff --git a/Iosevka b/Iosevka index b474ea7..319931c 160000 --- a/Iosevka +++ b/Iosevka @@ -1 +1 @@ -Subproject commit b474ea733417fa68b9436560ae86e8540f4236c7 +Subproject commit 319931ccecace2f08b538aaf9ddc2f55fd9f0736 diff --git a/vendor/bin/scripts/name_parser/FontnameParser.py b/vendor/bin/scripts/name_parser/FontnameParser.py new file mode 100644 index 0000000..2fb2060 --- /dev/null +++ b/vendor/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/vendor/bin/scripts/name_parser/FontnameTools.py b/vendor/bin/scripts/name_parser/FontnameTools.py new file mode 100644 index 0000000..f4a9c13 --- /dev/null +++ b/vendor/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/vendor/bin/scripts/name_parser/README.md b/vendor/bin/scripts/name_parser/README.md new file mode 100644 index 0000000..ece39f7 --- /dev/null +++ b/vendor/bin/scripts/name_parser/README.md @@ -0,0 +1,71 @@ +## Creating Consistently Grouped Patched Fonts + +This is a small sub-project to font-patcher that uses a little bit more knowledge +to come up with font names and name parts. In applications multiple fonts are grouped +under a 'Family'. Each member of the Family has a different 'SubFamily' or 'Style'. + +Consider a font named 'Times' that has two variants: normal and bold. For this font the +Family would be 'Times' and the 'Style' would be 'Regular' (i.e normal) in one file and +'Bold' in the other file. + +With this information applications are able to group all 'Times' together and additionally choose the +'Bold' font if the user pushes the 'B' button on the font style dialog in that application. + +### Motivation + +Quite a number of patched fonts have inconsistent or simply wrong font grouping. The naming in +general is sometimes surprising and not following naming conventions. This is in part due to +the font-patcher, but in part the source fonts are already strange. +This results in invisible (but installed) fonts in some applications, inconsistent naming +(Familyname differs from Fullname) and not correctly working bold/italic selectors in some applications. + +And we would like to have the information within the names sorted in a consistent way. +usually a font name consists of these parts (in this order): + +1. Name base (e.g. `Noto`) +2. Variant (e.g. `Sans`) +3. Subvariant (e.g. `Display`) +4. Weight (e.g. `Black`) +5. Style (e.g. `Italic`) + +This is important because we want to add subvariant information, namely the `Nerd Font` part. + +Example: + +* (old) `Iosevka Term Light Italic Nerd Font` +* (new) `Iosevka Term Nerd Font Light Italic` + +### The Plan + +To solve these issues the font name parts have to be analyzed more thoroughly and then categorized. +These categories are then used to assemble the names in correct order. The simple (not +typographically aware) applications shall always get groups of at most four styles, and these +are Regular, Bold, Italic, and Bold-Italic. Other styles turn up as Families, because this is the +only way they would work in these more simple applications. + +Typographically aware applications, on the other hand, get all styles grouped under one Family name. + +First experiments showed that the full information can usually be restored already from the file +names that our source fonts have. + +This new naming is complete optional (but recommended). Give the option `--makegroups` to `font-patcher` +and it will try to come up with reasonable grouping and naming. Leave the option out and it will +work as it always did. + +### The Tests + +In this directory were two tests. If interested you need to go back in the git history. +They are not needed anymore. + +### Helper scripts + +There are some helper scripts that help examining the font files. Of course there are other, +more professional tools to dump font information, but here we get all we need in a concise +way: +* `query_mono` `font_name [font_name ...]` +* `query_names` `font_name [font_name ...]` +* `query_panose` `font_name` +* `query_sftn` `[] font_name` +* `query_version` `font_name` + +They can be invoked like this `$ fontforge query_sfnt foo.ttf`. diff --git a/vendor/bin/scripts/name_parser/query_monospace b/vendor/bin/scripts/name_parser/query_monospace new file mode 100644 index 0000000..3132ffa --- /dev/null +++ b/vendor/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/vendor/bin/scripts/name_parser/query_names b/vendor/bin/scripts/name_parser/query_names new file mode 100644 index 0000000..e05c351 --- /dev/null +++ b/vendor/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/vendor/bin/scripts/name_parser/query_panose b/vendor/bin/scripts/name_parser/query_panose new file mode 100644 index 0000000..2492444 --- /dev/null +++ b/vendor/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/vendor/bin/scripts/name_parser/query_sftn b/vendor/bin/scripts/name_parser/query_sftn new file mode 100644 index 0000000..48c3f43 --- /dev/null +++ b/vendor/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/vendor/bin/scripts/name_parser/query_version b/vendor/bin/scripts/name_parser/query_version new file mode 100644 index 0000000..3a9a195 --- /dev/null +++ b/vendor/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/vendor/font-patcher b/vendor/font-patcher index 4fe8b11..eddc093 100755 --- a/vendor/font-patcher +++ b/vendor/font-patcher @@ -1,14 +1,14 @@ #!/usr/bin/env python # coding=utf8 -# Nerd Fonts Version: 2.3.0-RC +# 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.2.0" +script_version = "4.1.5" -version = "2.3.0-RC" +version = "3.0.0" projectName = "Nerd Fonts" projectNameAbbreviation = "NF" projectNameSingular = projectName[:-1] @@ -21,6 +21,8 @@ from argparse import RawTextHelpFormatter import errno import subprocess import json +from enum import Enum +import logging try: import configparser except ImportError: @@ -36,7 +38,6 @@ except ImportError: ) ) -# This is for experimenting sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/bin/scripts/name_parser/') try: from FontnameParser import FontnameParser @@ -88,8 +89,8 @@ class TableHEADWriter: checksum = (checksum + extra) & 0xFFFFFFFF return checksum - def find_head_table(self, idx): - """ Search all tables for the HEAD table and store its metadata """ + def find_table(self, tablenames, idx): + """ Search all tables for one of the tables in tablenames and store its metadata """ # Use font with index idx if this is a font collection file self.f.seek(0, 0) tag = self.f.read(4) @@ -117,16 +118,25 @@ class TableHEADWriter: self.tab_check = self.getlong() self.tab_offset = self.getlong() self.tab_length = self.getlong() - if tab_name == b'head': - return - raise Exception('No HEAD table found in font idx {}'.format(idx)) + if tab_name in tablenames: + return True + return False + + def find_head_table(self, idx): + """ Search all tables for the HEAD table and store its metadata """ + # Use font with index idx if this is a font collection file + found = self.find_table([ b'head' ], idx) + if not found: + raise Exception('No HEAD table found in font idx {}'.format(idx)) + def goto(self, where): """ Go to a named location in the file or to the specified index """ - if type(where) is str: + if isinstance(where, str): 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) @@ -179,15 +189,31 @@ def check_panose_monospaced(font): (panose[0] == 3 and panose[3] == 3)) return 1 if panose_mono else 0 +def panose_check_to_text(value, panose = False): + """ Convert value from check_panose_monospaced() to human readable string """ + if value == 0: + return "Panose says \"not monospaced\"" + if value == 1: + return "Panose says \"monospaced\"" + return "Panose is invalid" + (" ({})".format(list(panose)) if panose else "") + +def panose_proportion_to_text(value): + """ Interpret a Panose proportion value (4th value) for family 2 (latin text) """ + proportion = { + 0: "Any", 1: "No Fit", 2: "Old Style", 3: "Modern", 4: "Even Width", + 5: "Extended", 6: "Condensed", 7: "Very Extended", 8: "Very Condensed", + 9: "Monospaced" } + return proportion.get(value, "??? {}".format(value)) + 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', '.' + for glyph in [ 0x49, 0x4D, 0x57, 0x61, 0x69, 0x6d, 0x2E ]: # wide and slim glyphs 'I', 'M', 'W', 'a', 'i', 'm', '.' if not glyph in font: # A 'strange' font, believe Panose - return check_panose_monospaced(font) == 1 + return (check_panose_monospaced(font) == 1, None) # print(" -> {} {}".format(glyph, font[glyph].width)) if width < 0: width = font[glyph].width @@ -205,7 +231,21 @@ def is_monospaced(font): width_mono = False break # We believe our own check more then Panose ;-D - return width_mono + return (width_mono, None if width_mono else glyph) + +def force_panose_monospaced(font): + """ Forces the Panose flag to monospaced if they are unset or halfway ok already """ + # For some Windows applications (e.g. 'cmd'), they seem to honour the Panose table + # https://forum.high-logic.com/postedfiles/Panose.pdf + 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 + 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: + 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) def get_advance_width(font, extended, minimum): """ Get the maximum/minimum advance width in the extended(?) range """ @@ -228,6 +268,51 @@ def get_advance_width(font, extended, minimum): width = font[glyph].width return width +def report_advance_widths(font): + return "Advance widths (base/extended): {} - {} / {} - {}".format( + get_advance_width(font, True, True), get_advance_width(font, False, True), + get_advance_width(font, False, False), get_advance_width(font, True, False)) + +def get_btb_metrics(font): + """ Get the baseline to baseline distance for all three metrics """ + hhea_height = font.hhea_ascent - font.hhea_descent + typo_height = font.os2_typoascent - font.os2_typodescent + win_height = font.os2_winascent + font.os2_windescent + win_gap = max(0, font.hhea_linegap - win_height + hhea_height) + hhea_btb = hhea_height + font.hhea_linegap + typo_btb = typo_height + font.os2_typolinegap + 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): @@ -237,21 +322,24 @@ class font_patcher: self.sourceFont = None # class 'fontforge.font' self.patch_set = None # class 'list' 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 self.setup_version() self.get_essential_references() self.setup_name_backup(font) - if self.args.single: - self.assert_monospace() + self.assert_monospace() self.remove_ligatures() self.setup_patch_set() - self.setup_line_dimensions() self.get_sourcefont_dimensions() + self.improve_line_dimensions() self.sourceFont.encoding = 'UnicodeFull' # Update the font encoding to ensure that the Unicode glyphs are available self.onlybitmaps = self.sourceFont.onlybitmaps # Fetch this property before adding outlines. NOTE self.onlybitmaps initialized and never used @@ -259,13 +347,11 @@ class font_patcher: # Force width to be equal on all glyphs to ensure the font is considered monospaced on Windows. # This needs to be done on all characters, as some information seems to be lost from the original font file. self.set_sourcefont_glyph_widths() - # For some Windows applications (e.g. 'cmd') that is not enough. But they seem to honour the Panose table - # https://forum.high-logic.com/postedfiles/Panose.pdf - panose = list(self.sourceFont.os2_panose) - if panose[0] == 0 or panose[0] == 2: # 0 (1st value) = family kind; 0 = any (default); 2 = latin text and display - panose[0] = 2 # Assert kind - panose[3] = 9 # 3 (4th value) = propotion; 9 = monospaced - self.sourceFont.os2_panose = tuple(panose) + + # 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: + 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 # multiple ranges using the same symbol font. @@ -273,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']: @@ -284,12 +374,15 @@ 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' # Match the symbol font size to the source font size symfont.em = self.sourceFont.em @@ -299,7 +392,7 @@ class font_patcher: SrcStart = patch['SrcStart'] if not SrcStart: SrcStart = patch['SymStart'] - self.copy_glyphs(SrcStart, symfont, patch['SymStart'], patch['SymEnd'], patch['Exact'], patch['ScaleGlyph'], patch['Name'], patch['Attributes']) + self.copy_glyphs(SrcStart, symfont, patch['SymStart'], patch['SymEnd'], patch['Exact'], patch['ScaleRules'], patch['Name'], patch['Attributes']) if symfont: symfont.close() @@ -315,6 +408,11 @@ class font_patcher: def generate(self, sourceFonts): sourceFont = sourceFonts[0] + # the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'. + if int(fontforge.version()) >= 20201107: + gen_flags = (str('opentype'), str('PfEd-comments'), str('no-FFTM-table')) + else: + gen_flags = (str('opentype'), str('PfEd-comments')) if len(sourceFonts) > 1: layer = None # use first non-background layer @@ -322,53 +420,76 @@ class font_patcher: if not sourceFont.layers[l].is_background: layer = l break - outfile = os.path.normpath(os.path.join(self.args.outputdir, sourceFont.familyname + ".ttc")) - # the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'. - sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=(str('opentype'), str('PfEd-comments')), layer=layer) - message = "\nGenerated: {} fonts in '{}'".format(len(sourceFonts), outfile) + outfile = os.path.normpath(os.path.join( + sanitize_filename(self.args.outputdir, True), + 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(self.args.outputdir, fontname + self.args.extension)) - # the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'. + outfile = os.path.normpath(os.path.join( + sanitize_filename(self.args.outputdir, True), + sanitize_filename(fontname) + self.args.extension)) bitmaps = str() if len(self.sourceFont.bitmapSizes): - print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes)) + logger.debug("Preserving bitmaps %s", repr(self.sourceFont.bitmapSizes)) bitmaps = str('otf') # otf/ttf, both is bf_ttf - sourceFont.generate(outfile, bitmap_type=bitmaps, flags=(str('opentype'), str('PfEd-comments'))) - message = "\nGenerated: {} in '{}'".format(self.sourceFont.fullname, outfile) + if self.args.dry_run: + logger.debug("=====> Filename '%s'", outfile) + return + sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags) + message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile) # Adjust flags that can not be changed via fontforge - try: - source_font = TableHEADWriter(self.args.font) - dest_font = TableHEADWriter(outfile) - for idx in range(source_font.num_fonts): - print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts)) - source_font.find_head_table(idx) - dest_font.find_head_table(idx) - if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0: - print("Changing flags from 0x{:X} to 0x{:X}".format(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: - print("Changing lowestRecPPEM from {} to {}".format(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() - except Exception as error: - print("Can not handle font flags ({})".format(repr(error))) - finally: + if re.search('\\.[ot]tf$', self.args.font, re.IGNORECASE) and re.search('\\.[ot]tf$', outfile, re.IGNORECASE): try: - source_font.close() - dest_font.close() - except: - pass + source_font = TableHEADWriter(self.args.font) + dest_font = TableHEADWriter(outfile) + for idx in range(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: + 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: + 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() + if dest_font.modified: + dest_font.reset_full_checksum() + except Exception as error: + logger.error("Can not handle font flags (%s)", repr(error)) + finally: + try: + source_font.close() + dest_font.close() + except: + pass + if self.args.is_variable: + 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): @@ -382,13 +503,12 @@ class font_patcher: def setup_font_names(self, font): font.fontname = font.persistent["fontname"] - font.fullname = font.persistent["fullname"] - 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 + if isinstance(font.persistent["fullname"], str): + font.fullname = font.persistent["fullname"] + if isinstance(font.persistent["familyname"], str): + font.familyname = font.persistent["familyname"] + verboseAdditionalFontNameSuffix = "" + additionalFontNameSuffix = "" if not self.args.complete: # NOTE not all symbol fonts have appended their suffix here if self.args.fontawesome: @@ -419,18 +539,25 @@ 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: - use_fullname = type(font.fullname) == str # Usually the fullname is better to parse + 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: use_fullname |= font.fontname.lower() == FontnameTools.postscript_char_filter(font.fullname).lower() @@ -441,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 @@ -484,30 +613,17 @@ class font_patcher: subFamily = fallbackStyle # some fonts have inaccurate 'SubFamily', if it is Regular let us trust the filename more: - if subFamily == "Regular": + if subFamily == "Regular" and len(fallbackStyle): subFamily = fallbackStyle # This is meant to cover the case where the SubFamily is "Italic" and the filename is *-BoldItalic. if len(subFamily) < len(fallbackStyle): subFamily = fallbackStyle - if self.args.windows: - maxFamilyLength = 31 - maxFontLength = maxFamilyLength - len('-' + subFamily) - familyname += " " + projectNameAbbreviation - if self.args.single: - familyname += "M" - fullname += " Windows Compatible" + if len(subFamily) == 0: + subFamily = "Regular" - # 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 @@ -520,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', @@ -586,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()) @@ -597,9 +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 - fam_suffix += ' Mono' if self.args.single else '' - 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 @@ -615,111 +735,147 @@ 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)) def remove_ligatures(self): # let's deal with ligatures (mostly for monospaced fonts) - # the tables have been removed from the repo with >this< commit + # Usually removes 'fi' ligs that end up being only one cell wide, and 'ldot' 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") - else: - print("No configfile given, skipping configfile related actions") + logger.error("Unable to read configfile, unable to remove ligatures") def assert_monospace(self): # Check if the sourcefont is monospaced - width_mono = is_monospaced(self.sourceFont) + width_mono, offending_char = is_monospaced(self.sourceFont) + self.source_monospaced = width_mono + if self.args.nonmono: + return 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(" Glyph widths {} / {} - {} and Panose says \"monospace {}\" ({})".format(get_advance_width(self.sourceFont, False, True), - get_advance_width(self.sourceFont, False, False), get_advance_width(self.sourceFont, True, False), panose_mono, list(self.sourceFont.os2_panose))) - if not width_mono: - print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless") + 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)) + if self.args.single and not width_mono: + logger.warning("Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless") + if offending_char is not None: + 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) def setup_patch_set(self): """ Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """ - # Supported params: overlap | careful - # Powerline dividers + + 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 (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: + # 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) + # Supported params: overlap | careful | xy-ratio | dont_copy + # Overlap value is used horizontally but vertically limited to 0.01 + # Careful does not overwrite/modify existing glyphs + # The xy-ratio limits the x-scale for a given y-scale to make the ratio <= this value (to prevent over-wide glyphs) + # '1' means occupu 1 cell (default for 'xy') + # '2' means occupy 2 cells (default for 'pa') + # '!' means do the 'pa' scaling even with non mono fonts (else it just scales down, never up) + # Dont_copy does not overwrite existing glyphs but rescales the preexisting ones + + SYM_ATTR_DEFAULT = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}} + } SYM_ATTR_POWERLINE = { 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}, # Arrow tips - 0xe0b0: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, - 0xe0b1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, - 0xe0b2: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, - 0xe0b3: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, + 0xe0b0: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}}, + 0xe0b1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.7}}, + 0xe0b2: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}}, + 0xe0b3: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.7}}, # Rounded arcs - 0xe0b4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}}, - 0xe0b5: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}}, - 0xe0b6: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}}, - 0xe0b7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}}, + 0xe0b4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01, 'xy-ratio': 0.59}}, + 0xe0b5: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.5}}, + 0xe0b6: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01, 'xy-ratio': 0.59}}, + 0xe0b7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.5}}, # Bottom Triangles 0xe0b8: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, - 0xe0b9: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, + 0xe0b9: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, 0xe0ba: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, - 0xe0bb: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, + 0xe0bb: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}}, # Top Triangles 0xe0bc: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, - 0xe0bd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, + 0xe0bd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, 0xe0be: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, - 0xe0bf: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, + 0xe0bf: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}}, # Flames - 0xe0c0: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}}, - 0xe0c1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}}, - 0xe0c2: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}}, - 0xe0c3: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}}, + 0xe0c0: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}}, + 0xe0c1: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {}}, + 0xe0c2: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}}, + 0xe0c3: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {}}, # Small squares - 0xe0c4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, - 0xe0c5: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}}, + 0xe0c4: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.86}}, + 0xe0c5: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.86}}, # Bigger squares - 0xe0c6: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, - 0xe0c7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}}, + 0xe0c6: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.78}}, + 0xe0c7: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.78}}, # Waveform - 0xe0c8: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}}, + 0xe0c8: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}}, + 0xe0ca: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}}, # Hexagons - 0xe0cc: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, - 0xe0cd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, + 0xe0cc: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02, 'xy-ratio': 0.85}}, + 0xe0cd: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'xy-ratio': 0.865}}, # Legos - 0xe0ce: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}}, - 0xe0cf: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {}}, - 0xe0d1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, + 0xe0ce: {'align': 'l', 'valign': 'c', 'stretch': 'pa', 'params': {}}, + 0xe0cf: {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}, + 0xe0d0: {'align': 'l', 'valign': 'c', 'stretch': 'pa', 'params': {}}, + 0xe0d1: {'align': 'l', 'valign': 'c', 'stretch': 'pa', 'params': {}}, # Top and bottom trapezoid - 0xe0d2: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}, - 0xe0d4: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}} + 0xe0d2: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}}, + 0xe0d4: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}} } - - SYM_ATTR_DEFAULT = { - # 'pa' == preserve aspect ratio - 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}} + SYM_ATTR_TRIGRAPH = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa1!', 'params': {'overlap': -0.10, 'careful': True}} } - SYM_ATTR_FONTA = { # 'pa' == preserve aspect ratio 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}, @@ -729,20 +885,83 @@ class font_patcher: 0xf0dd: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}}, 0xf0de: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}} } - + SYM_ATTR_HEAVYBRACKETS = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {'careful': True}} + } + SYM_ATTR_BOX = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'dont_copy': box_keep}}, + # No overlap with checkered greys (commented out because that raises problems on rescaling clients) + # 0x2591: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}}, + # 0x2592: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}}, + # 0x2593: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}}, + } CUSTOM_ATTR = { # 'pa' == preserve aspect ratio 'default': {'align': 'c', 'valign': '', 'stretch': '', 'params': {}} } - # Most glyphs we want to maximize during the scale. However, there are some - # that need to be small or stay relative in size to each other. - # The following list are those glyphs. A tuple represents a range. - DEVI_SCALE_LIST = {'ScaleGlyph': 0xE60E, # Android logo + # Most glyphs we want to maximize (individually) during the scale + # However, there are some that need to be small or stay relative in + # size to each other. + # The glyph-specific behavior can be given as ScaleRules in the patch-set. + # + # ScaleRules can contain two different kind of rules (possibly in parallel): + # - ScaleGlyph: + # Here one specific glyph is used as 'scale blueprint'. Other glyphs are + # scaled by the same factor as this glyph. This is useful if you have one + # 'biggest' glyph and all others should stay relatively in size. + # Shifting in addition to scaling can be selected too (see below). + # - ScaleGroups: + # Here you specify a group of glyphs that should be handled together + # with the same scaling and shifting. The basis for it is a 'combined + # bounding box' of all glyphs in that group. All glyphs are handled as + # if they fill that combined bounding box. + # + # The ScaleGlyph method: You set 'ScaleGlyph' to the unicode of the reference glyph. + # Note that there can be only one per patch-set. + # Additionally you set 'GlyphsToScale' that contains all the glyphs that shall be + # handled (scaled) like the reference glyph. + # It is a List of: ((glyph code) or (tuple of two glyph codes that form a closed range)) + # 'GlyphsToScale': [ + # 0x0100, 0x0300, 0x0400, # The single glyphs 0x0100, 0x0300, and 0x0400 + # (0x0200, 0x0210), # All glyphs 0x0200 to 0x0210 including both 0x0200 and 0x0210 + # ]} + # If you want to not only scale but also shift as the refenerce glyph you give the + # data as 'GlyphsToScale+'. Note that only one set is used and the plus version is preferred. + # + # For the ScaleGroup method you define any number groups of glyphs and each group is + # handled separately. The combined bounding box of all glyphs in the group is determined + # and based on that the scale and shift for all the glyphs in the group. + # You define the groups as value of 'ScaleGroups'. + # It is a List of: ((lists of glyph codes) or (ranges of glyph codes)) + # 'ScaleGroups': [ + # [0x0100, 0x0300, 0x0400], # One group consists of glyphs 0x0100, 0x0300, and 0x0400 + # range(0x0200, 0x0210 + 1), # Another group contains glyphs 0x0200 to 0x0210 incl. + # + # Note the subtle differences: tuple vs. range; closed vs open range; etc + # See prepareScaleRules() for some more details. + # For historic reasons ScaleGroups is sometimes called 'new method' and ScaleGlyph 'old'. + # The codepoints mentioned here are symbol-font-codepoints. + + BOX_SCALE_LIST = {'ScaleGroups': [ + [*range(0x2500, 0x2570 + 1), *range(0x2574, 0x257f + 1)], # box drawing + range(0x2571, 0x2573 + 1), # diagonals + [*range(0x2580, 0x2590 + 1), 0x2594, 0x2595], # blocks + range(0x2591, 0x2593 + 1), # greys + range(0x2594, 0x259f + 1), # quards (Note: quard 2597 in Hack is wrong, scales like block!) + ]} + CODI_SCALE_LIST = {'ScaleGroups': [ + range(0xea99, 0xeaa1 + 1), # arrows + range(0xeb6e, 0xeb71 + 1), # triangles + range(0xeab4, 0xeab7 + 1), # chevrons + [0xea71, *range(0xeaa6, 0xeaab + 1), 0xeabc, 0xeb18, 0xeb87, 0xeb88, 0xeb8a, 0xeb8c, 0xebb4], # cicles + [0xeacc, 0xeaba], # dash + ]} + DEVI_SCALE_LIST = {'ScaleGlyph': 0xE60E, # Android logo 'GlyphsToScale': [ (0xe6bd, 0xe6c3) # very small things - ]} - FONTA_SCALE_LIST = {'GlyphsToScale': [ + ]} + FONTA_SCALE_LIST = {'ScaleGroups': [ [0xf005, 0xf006, 0xf089], # star, star empty, half star range(0xf026, 0xf028 + 1), # volume off, down, up range(0xf02b, 0xf02c + 1), # tag, tags @@ -752,8 +971,10 @@ class font_patcher: range(0xf060, 0xf063 + 1), # arrows [0xf053, 0xf054, 0xf077, 0xf078], # chevron all directions range(0xf07d, 0xf07e + 1), # resize - [0xf0d7, 0xf0da, 0xf0dc, 0xf0fe], # caret all directions and same looking sort + range(0xf0a4, 0xf0a7 + 1), # pointing hands + [0xf0d7, 0xf0d8, 0xf0d9, 0xf0da, 0xf0dc, 0xf0dd, 0xf0de], # caret all directions and same looking sort range(0xf100, 0xf107 + 1), # angle + range(0xf130, 0xf131 + 1), # mic range(0xf141, 0xf142 + 1), # ellipsis range(0xf153, 0xf15a + 1), # currencies range(0xf175, 0xf178 + 1), # long arrows @@ -761,59 +982,102 @@ class font_patcher: range(0xf221, 0xf22d + 1), # gender or so range(0xf255, 0xf25b + 1), # hand symbols ]} - OCTI_SCALE_LIST = {'ScaleGlyph': 0xF02E, # looking glass (probably biggest glyph?) - 'GlyphsToScale': [ - (0xf03d, 0xf040), # arrows - 0xf044, 0xf05a, 0xf05b, 0xf0aa, # triangles - (0xf051, 0xf053), # small stuff - 0xf071, 0xf09f, 0xf0a0, 0xf0a1, # small arrows - 0xf078, 0xf0a2, 0xf0a3, 0xf0a4, # chevrons - 0xf0ca, # dash - ]} + OCTI_SCALE_LIST = {'ScaleGroups': [ + [*range(0xf03d, 0xf040 + 1), 0xf019, 0xf030, 0xf04a, 0xf050, 0xf071, 0xf08c ], # arrows + [0xF0E7, # Smily and ... + 0xf044, 0xf05a, 0xf05b, 0xf0aa, # triangles + 0xf052, 0xf053, 0x296, 0xf2f0, # small stuff + 0xf078, 0xf0a2, 0xf0a3, 0xf0a4, # chevrons + 0xf0ca, 0xf081, 0xf092, # dash, X, github-text + ], + [0xf09c, 0xf09f, 0xf0de], # bells + range(0xf2c2, 0xf2c5 + 1), # move to + [0xf07b, 0xf0a1, 0xf0d6], # bookmarks + ]} + WEATH_SCALE_LIST = {'ScaleGroups': [ + [0xf03c, 0xf042, 0xf045 ], # degree signs + [0xf043, 0xf044, 0xf048, 0xf04b, 0xf04c, 0xf04d, 0xf057, 0xf058, 0xf087, 0xf088], # arrows + range(0xf053, 0xf055 + 1), # thermometers + [*range(0xf059, 0xf061 + 1), 0xf0b1], # wind directions + range(0xf089, 0xf094 + 1), # clocks + range(0xf095, 0xf0b0 + 1), # moon phases + range(0xf0b7, 0xf0c3 + 1), # wind strengths + [0xf06e, 0xf070 ], # solar/lunar eclipse + # Note: Codepoints listed before that are also in the following range + # will take the scaling of the previous group (the ScaleGroups are + # searched through in definition order). + # But be careful, the combined bounding box for the following group + # _will_ include all glyphs in its definition: Make sure the exempt + # glyphs from above are smaller (do not extend) the combined bounding + # box of this range: + range(0xf000, 0xf0cb + 1), # lots of clouds and other (Please read note above!) + ]} + MDI_SCALE_LIST = None # Maybe later add some selected ScaleGroups + # Define the character ranges # Symbol font ranges self.patch_set = [ - {'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE5AA, 'SrcStart': 0xE5FA, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, - {'Enabled': True, 'Name': "Devicons", 'Filename': "devicons.ttf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE6C5, 'SrcStart': 0xE700, 'ScaleGlyph': DEVI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, - {'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0A0, 'SymEnd': 0xE0A2, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, - {'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0B0, 'SymEnd': 0xE0B3, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, - {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0A3, 'SymEnd': 0xE0A3, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, - {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, - {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, - {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D4, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE}, - {'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, - {'Enabled': self.args.fontawesome, 'Name': "Font Awesome", 'Filename': "font-awesome/FontAwesome.otf", 'Exact': True, 'SymStart': 0xF000, 'SymEnd': 0xF2E0, 'SrcStart': None, 'ScaleGlyph': FONTA_SCALE_LIST, 'Attributes': SYM_ATTR_FONTA}, - {'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off) - {'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleGlyph': None, '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, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, - {'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, - {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Desktop - {'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, - {'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': CUSTOM_ATTR} + {'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE5FF, 'SrcStart': 0xE5FA, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': True, 'Name': "Heavy Angle Brackets", 'Filename': "extraglyphs.sfd", 'Exact': True, 'SymStart': 0x276C, 'SymEnd': 0x2771, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_HEAVYBRACKETS}, + {'Enabled': box_enabled, 'Name': "Box Drawing", 'Filename': "extraglyphs.sfd", 'Exact': True, 'SymStart': 0x2500, 'SymEnd': 0x259F, 'SrcStart': None, 'ScaleRules': BOX_SCALE_LIST, 'Attributes': SYM_ATTR_BOX}, + {'Enabled': True, 'Name': "Devicons", 'Filename': "devicons.ttf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE6C5, 'SrcStart': 0xE700, 'ScaleRules': DEVI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0A0, 'SymEnd': 0xE0A2, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0B0, 'SymEnd': 0xE0B3, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0A3, 'SymEnd': 0xE0A3, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D4, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0x2630, 'SymEnd': 0x2630, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_TRIGRAPH}, + {'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.fontawesome, 'Name': "Font Awesome", 'Filename': "font-awesome/FontAwesome.otf", 'Exact': True, 'SymStart': 0xF000, 'SymEnd': 0xF2E0, 'SrcStart': None, 'ScaleRules': FONTA_SCALE_LIST, 'Attributes': SYM_ATTR_FONTA}, + {'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': 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/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} ] - def setup_line_dimensions(self): - # win_ascent and win_descent are used to set the line height for windows fonts. - # hhead_ascent and hhead_descent are used to set the line height for mac fonts. - # + def improve_line_dimensions(self): # Make the total line size even. This seems to make the powerline separators # center more evenly. if self.args.adjustLineHeight: if (self.sourceFont.os2_winascent + self.sourceFont.os2_windescent) % 2 != 0: + # All three are equal before due to get_sourcefont_dimensions() + self.sourceFont.hhea_ascent += 1 + self.sourceFont.os2_typoascent += 1 self.sourceFont.os2_winascent += 1 - # Make the line size identical for windows and mac - # ! This is broken because hhea* is changed but os2_typo* is not - # ! On the other hand we need intact (i.e. original) typo values - # ! in get_sourcefont_dimensions() @TODO FIXME - self.sourceFont.hhea_ascent = self.sourceFont.os2_winascent - self.sourceFont.hhea_descent = -self.sourceFont.os2_windescent + def add_glyphrefs_to_essential(self, unicode): + self.essential.add(unicode) + # According to fontforge spec, altuni is either None or a tuple of tuples + # Those tuples contained in altuni are of the following "format": + # (unicode-value, variation-selector, reserved-field) + altuni = self.sourceFont[unicode].altuni + if altuni is not None: + for altcode in [ v for v, s, r in altuni if v >= 0 ]: + # If alternate unicode already exists in self.essential, + # that means it has gone through this function before. + # Therefore we skip it to avoid infinite loop. + # A unicode value of -1 basically means unused and is also worth skipping. + if altcode not in self.essential: + self.add_glyphrefs_to_essential(altcode) + # From fontforge documentation: + # glyph.references return a tuple of tuples containing, for each reference in foreground, + # a glyph name, a transformation matrix, and (depending on ff version) whether the + # reference is currently selected. + references = self.sourceFont[unicode].references + for refcode in [ self.sourceFont[n].unicode for n, *_ in references ]: # tuple of 2 or 3 depending on ff version + if refcode not in self.essential and refcode >= 0: + self.add_glyphrefs_to_essential(refcode) def get_essential_references(self): """Find glyphs that are needed for the basic glyphs""" @@ -821,31 +1085,76 @@ class font_patcher: # Find out which other glyphs are also needed to keep the basic # glyphs intact. # 0x00-0x17f is the Latin Extended-A range - for glyph in range(0x21, 0x17f): + basic_glyphs = set() + # Collect substitution destinations + for glyph in range(0x21, 0x17f + 1): if not glyph in self.sourceFont: continue - for r in self.sourceFont[glyph].references: - self.essential.add(self.sourceFont[r[0]].unicode) + basic_glyphs.add(glyph) + for possub in self.sourceFont[glyph].getPosSub('*'): + if possub[1] == 'Substitution' or possub[1] == 'Ligature': + basic_glyphs.add(self.sourceFont[possub[2]].unicode) + basic_glyphs.discard(-1) # the .notdef glyph + for glyph in basic_glyphs: + self.add_glyphrefs_to_essential(glyph) def get_sourcefont_dimensions(self): - # Initial font dimensions - self.font_dim = { - 'xmin' : 0, - 'ymin' : -self.sourceFont.os2_windescent, - 'xmax' : 0, - 'ymax' : self.sourceFont.os2_winascent, - 'width' : 0, - 'height': 0, - } - if self.sourceFont.os2_use_typo_metrics: - self.font_dim['ymin'] = self.sourceFont.os2_typodescent - self.font_dim['ymax'] = self.sourceFont.os2_typoascent + """ This gets the font dimensions (cell width and height), and makes them equal on all platforms """ + # Step 1 + # There are three ways to discribe the baseline to baseline distance + # (a.k.a. line spacing) of a font. That is all a kuddelmuddel + # and we try to sort this out here + # See also https://glyphsapp.com/learn/vertical-metrics + # See also https://github.com/source-foundry/font-line + (hhea_btb, typo_btb, win_btb, win_gap) = get_btb_metrics(self.sourceFont) + use_typo = self.sourceFont.os2_use_typo_metrics != 0 + + Metric = Enum('Metric', ['HHEA', 'TYPO', 'WIN']) + + # We use either TYPO (1) or WIN (2) and compare with HHEA + # and use HHEA (0) if the fonts seems broken - no WIN, see #1056 + 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: + # Try the other metric + our_btb = typo_btb if not use_typo else win_btb + if our_btb == hhea_btb: + use_typo = not use_typo + logger.warning("Font vertical metrics probably wrong USE TYPO METRICS, assume opposite (i.e. %s)", repr(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)) + + self.font_dim = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0, 'width' : 0, 'height': 0} + + if metrics == Metric.HHEA: + self.font_dim['ymin'] = self.sourceFont.hhea_descent - half_gap(self.sourceFont.hhea_linegap, False) + self.font_dim['ymax'] = self.sourceFont.hhea_ascent + half_gap(self.sourceFont.hhea_linegap, True) + elif metrics == Metric.TYPO: + self.font_dim['ymin'] = self.sourceFont.os2_typodescent - half_gap(self.sourceFont.os2_typolinegap, False) + self.font_dim['ymax'] = self.sourceFont.os2_typoascent + half_gap(self.sourceFont.os2_typolinegap, True) + elif metrics == Metric.WIN: + self.font_dim['ymin'] = -self.sourceFont.os2_windescent - half_gap(win_gap, False) + self.font_dim['ymax'] = self.sourceFont.os2_winascent + half_gap(win_gap, True) + else: + pass # Will fail the metrics check some line later # Calculate font height self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax'] 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, @@ -854,62 +1163,110 @@ class font_patcher: 'width' : self.sourceFont.em, 'height': self.sourceFont.descent + self.sourceFont.ascent, } + our_btb = self.sourceFont.descent + self.sourceFont.ascent + elif self.font_dim['height'] < 0: + logger.critical("Can not detect sane font height") + sys.exit(1) - # Line gap add extra space on the bottom of the line which - # doesn't allow the powerline glyphs to fill the entire line. - # Put half of the gap into the 'cell', each top and bottom - gap = max(self.sourceFont.hhea_linegap, self.sourceFont.os2_typolinegap) # TODO probably wrong - if self.sourceFont.os2_use_typo_metrics: - gap = self.sourceFont.os2_typolinegap - self.sourceFont.hhea_linegap = 0 + # Make all metrics equal self.sourceFont.os2_typolinegap = 0 - if gap > 0: - gap_top = int(gap / 2) - gap_bottom = gap - gap_top - self.font_dim['ymin'] -= gap_bottom - self.font_dim['ymax'] += gap_top - self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax'] - self.sourceFont.os2_typoascent = self.sourceFont.os2_typoascent + gap_top - self.sourceFont.os2_typodescent = self.sourceFont.os2_typodescent - gap_bottom - # TODO Check what to do with win and hhea values + self.sourceFont.os2_typoascent = self.font_dim['ymax'] + self.sourceFont.os2_typodescent = self.font_dim['ymin'] + self.sourceFont.os2_winascent = self.sourceFont.os2_typoascent + self.sourceFont.os2_windescent = -self.sourceFont.os2_typodescent + self.sourceFont.hhea_ascent = self.sourceFont.os2_typoascent + self.sourceFont.hhea_descent = self.sourceFont.os2_typodescent + self.sourceFont.hhea_linegap = self.sourceFont.os2_typolinegap + 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: + logger.critical("Error in baseline to baseline code detected") + sys.exit(1) - # Find the biggest char width - # Ignore the y-values, os2_winXXXXX values set above are used for line height - # + # Step 2 + # Find the biggest char width and advance width # 0x00-0x17f is the Latin Extended-A range + warned1 = self.args.nonmono # Do not warn if proportional target + warned2 = warned1 for glyph in range(0x21, 0x17f): - if glyph in range(0x7F, 0xBF): - continue # ignore special characters like '1/4' etc + if glyph in range(0x7F, 0xBF) or glyph in [ + 0x132, 0x133, # IJ, ij (in Overpass Mono) + 0x022, 0x027, 0x060, # Single and double quotes in Inconsolata LGC + 0x0D0, 0x10F, 0x110, 0x111, 0x127, 0x13E, 0x140, 0x165, # Eth and others with stroke or caron in RobotoMono + 0x02D, # hyphen for Monofur + ]: + continue # ignore special characters like '1/4' etc and some specifics try: (_, _, xmax, _) = self.sourceFont[glyph].boundingBox() except TypeError: continue + # print("WIDTH {:X} {} ({} {})".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) if self.font_dim['width'] < self.sourceFont[glyph].width: self.font_dim['width'] = self.sourceFont[glyph].width - # print("New MAXWIDTH-A {} {} {}".format(glyph, self.sourceFont[glyph].width, xmax)) + if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z + 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 - # print("New MAXWIDTH-B {} {} {}".format(glyph, self.sourceFont[glyph].width, xmax)) + if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z + 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']: + 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_scale_factor(self, sym_dim): - scale_ratio = 1 + def get_target_width(self, stretch): + """ Get the target width (1 or 2 'cell') for a given stretch parameter """ + # For monospaced fonts all chars need to be maximum 'one' space wide + # other fonts allows double width glyphs for 'pa' or if requested with '2' + if self.args.single or ('pa' not in stretch and '2' not in stretch) or '1' in stretch: + return 1 + return 2 - # We want to preserve x/y aspect ratio, so find biggest scale factor that allows symbol to fit - scale_ratio_x = self.font_dim['width'] / sym_dim['width'] - scale_ratio_y = self.font_dim['height'] / sym_dim['height'] - if scale_ratio_x > scale_ratio_y: - scale_ratio = scale_ratio_y + def get_scale_factors(self, sym_dim, stretch): + """ Get scale in x and y as tuple """ + # It is possible to have empty glyphs, so we need to skip those. + if not sym_dim['width'] or not sym_dim['height']: + return (1.0, 1.0) + + target_width = self.font_dim['width'] * self.get_target_width(stretch) + scale_ratio_x = target_width / sym_dim['width'] + + # font_dim['height'] represents total line height, keep our symbols sized based upon font's em + # Use the font_dim['height'] only for explicit 'y' scaling (not 'pa') + target_height = self.font_dim['height'] + scale_ratio_y = target_height / sym_dim['height'] + + if 'pa' in stretch: + # We want to preserve x/y aspect ratio, so find biggest scale factor that allows symbol to fit + scale_ratio_x = min(scale_ratio_x, scale_ratio_y) + if not self.args.single and not '!' in stretch: + # non monospaced fonts just scale down on 'pa', not up + scale_ratio_x = min(scale_ratio_x, 1.0) + scale_ratio_y = scale_ratio_x else: - scale_ratio = scale_ratio_x - return scale_ratio + # Keep the not-stretched direction + if not 'x' in stretch: + scale_ratio_x = 1.0 + if not 'y' in stretch: + scale_ratio_y = 1.0 + + return (scale_ratio_x, scale_ratio_y) - def copy_glyphs(self, sourceFontStart, symbolFont, symbolFontStart, symbolFontEnd, exactEncoding, scaleGlyph, setName, attributes): + def copy_glyphs(self, sourceFontStart, symbolFont, symbolFontStart, symbolFontEnd, exactEncoding, scaleRules, setName, attributes): """ Copies symbol glyphs into self.sourceFont """ progressText = '' careful = False - glyphSetLength = 0 sourceFontCounter = 0 if self.args.careful: @@ -925,23 +1282,27 @@ class font_patcher: else: symbolFont.selection.select((str("ranges"), str("unicode")), symbolFontStart, symbolFontEnd) - # Get number of selected non-empty glyphs - symbolFontSelection = list(symbolFont.selection.byGlyphs) + # Get number of selected non-empty glyphs with codes >=0 (i.e. not -1 == notdef) + symbolFontSelection = [ x for x in symbolFont.selection.byGlyphs if x.unicode >= 0 ] glyphSetLength = len(symbolFontSelection) - if self.args.quiet is False: - sys.stdout.write("Adding " + str(max(1, glyphSetLength)) + " Glyphs from " + setName + " Set \n") + if not self.args.quiet: + modify = attributes['default']['params'].get('dont_copy') + sys.stdout.write("{} {} Glyphs from {} Set\n".format( + "Adding" if not modify else "Rescaling", glyphSetLength, setName)) currentSourceFontGlyph = -1 # initialize for the exactEncoding case + width_warning = False for index, sym_glyph in enumerate(symbolFontSelection): - index = max(1, index) - - try: - sym_attr = attributes[sym_glyph.unicode] - except KeyError: + sym_attr = attributes.get(sym_glyph.unicode) + if sym_attr is None: sym_attr = attributes['default'] + if self.font_extrawide: + # Do not allow 'xy2' scaling + sym_attr['stretch'] = sym_attr['stretch'].replace('2', '') + if exactEncoding: # Use the exact same hex values for the source font as for the symbol font. # Problem is we do not know the codepoint of the sym_glyph and because it @@ -953,13 +1314,20 @@ class font_patcher: possible_codes += [ sym_glyph.unicode ] if sym_glyph.altuni: possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ] + if len(possible_codes) == 0: + logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode) + continue currentSourceFontGlyph = min(possible_codes) else: # use source font defined hex values based on passed in start (fills gaps; symbols are packed) currentSourceFontGlyph = sourceFontStart + sourceFontCounter sourceFontCounter += 1 - if self.args.quiet is False: + # For debugging process only limited glyphs + # if currentSourceFontGlyph != 0xe7bd: + # continue + + if not self.args.quiet: if self.args.progressbars: update_progress(round(float(index + 1) / glyphSetLength, 2)) else: @@ -970,9 +1338,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 self.args.quiet is False: - 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: @@ -981,64 +1348,69 @@ class font_patcher: if currentSourceFontGlyph in self.sourceFont: self.sourceFont[currentSourceFontGlyph].removePosSub("*") - # Select and copy symbol from its encoding point - # We need to do this select after the careful check, this way we don't - # reset our selection before starting the next loop - symbolFont.selection.select(sym_glyph.encoding) - symbolFont.copy() + dont_copy = sym_attr['params'].get('dont_copy') - # Paste it - self.sourceFont.selection.select(currentSourceFontGlyph) - self.sourceFont.paste() - self.sourceFont[currentSourceFontGlyph].glyphname = sym_glyph.glyphname - scale_ratio_x = 1 - scale_ratio_y = 1 + if dont_copy: + # Just prepare scaling of existing glyphs + glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, self.sourceFont, currentSourceFontGlyph) if scaleRules is not None else None + else: + # This will destroy any content currently in currentSourceFontGlyph, so do it first + glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, symbolFont, currentSourceFontGlyph) if scaleRules is not None else None + + # Select and copy symbol from its encoding point + # We need to do this select after the careful check, this way we don't + # reset our selection before starting the next loop + symbolFont.selection.select(sym_glyph.encoding) + symbolFont.copy() + + # Paste it + self.sourceFont.selection.select(currentSourceFontGlyph) + self.sourceFont.paste() + self.sourceFont[currentSourceFontGlyph].glyphname = sym_glyph.glyphname + self.sourceFont[currentSourceFontGlyph].manualHints = True # No autohints for symbols # Prepare symbol glyph dimensions sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph]) - - # Now that we have copy/pasted the glyph, if we are creating a monospace - # font we need to scale and move the glyphs. It is possible to have - # empty glyphs, so we need to skip those. - if self.args.single and sym_dim['width'] and sym_dim['height']: - # If we want to preserve that aspect ratio of the glyphs we need to - # find the largest possible scaling factor that will allow the glyph - # to fit in both the x and y directions - if sym_attr['stretch'] == 'pa': - scale_ratio_x = False - if scaleGlyph: - # We want to preserve the relative size of each glyph in a glyph group - scale_ratio_x = self.get_glyph_scale(sym_glyph.unicode, scaleGlyph, symbolFont) - if scale_ratio_x is False: - # In the remaining cases, each glyph is sized independently to each other - scale_ratio_x = self.get_scale_factor(sym_dim) - scale_ratio_y = scale_ratio_x + if glyph_scale_data is not None: + if glyph_scale_data[1] is not None: + sym_dim = glyph_scale_data[1] # Use combined bounding box + (scale_ratio_x, scale_ratio_y) = self.get_scale_factors(sym_dim, sym_attr['stretch']) else: - if 'x' in sym_attr['stretch']: - # Stretch the glyph horizontally to fit the entire available width - scale_ratio_x = self.font_dim['width'] / sym_dim['width'] - # end if single width - - # non-monospace (double width glyphs) - # elif sym_dim['width'] and sym_dim['height']: - # any special logic we want to apply for double-width variation - # would go here - - if 'y' in sym_attr['stretch']: - # Stretch the glyph vertically to total line height (good for powerline separators) - # Currently stretching vertically for both monospace and double-width - scale_ratio_y = self.font_dim['height'] / sym_dim['height'] + # This is roughly alike get_scale_factors(glyph_scale_data[1], 'pa') + # Except we do not have glyph_scale_data[1] always... + (scale_ratio_x, scale_ratio_y) = (glyph_scale_data[0], glyph_scale_data[0]) + else: + (scale_ratio_x, scale_ratio_y) = self.get_scale_factors(sym_dim, sym_attr['stretch']) overlap = sym_attr['params'].get('overlap') + if overlap: + scale_ratio_x *= 1.0 + (self.font_dim['width'] / (sym_dim['width'] * scale_ratio_x)) * overlap + y_overlap = min(0.01, overlap) # never aggressive vertical overlap + scale_ratio_y *= 1.0 + (self.font_dim['height'] / (sym_dim['height'] * scale_ratio_y)) * y_overlap - if scale_ratio_x != 1 or scale_ratio_y != 1: - if overlap: - scale_ratio_x *= 1 + overlap - scale_ratio_y *= 1 + overlap + # Size in x to size in y ratio limit (to prevent over-wide glyphs) + xy_ratio_max = sym_attr['params'].get('xy-ratio') + if (xy_ratio_max): + xy_ratio = sym_dim['width'] * scale_ratio_x / (sym_dim['height'] * scale_ratio_y) + if xy_ratio > xy_ratio_max: + scale_ratio_x = scale_ratio_x * xy_ratio_max / xy_ratio + + if scale_ratio_x != 1.0 or scale_ratio_y != 1.0: self.sourceFont[currentSourceFontGlyph].transform(psMat.scale(scale_ratio_x, scale_ratio_y)) - # Use the dimensions from the newly pasted and stretched glyph + # We pasted and scaled now we want to align/move + # Use the dimensions from the newly pasted and stretched glyph to avoid any rounding errors sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph]) + # Use combined bounding box? + if glyph_scale_data is not None and glyph_scale_data[1] is not None: + scaleglyph_dim = scale_bounding_box(glyph_scale_data[1], scale_ratio_x, scale_ratio_y) + if scaleglyph_dim['advance'] is None: + # On monospaced symbol collections use their advance with, otherwise align horizontally individually + scaleglyph_dim['xmin'] = sym_dim['xmin'] + scaleglyph_dim['xmax'] = sym_dim['xmax'] + scaleglyph_dim['width'] = sym_dim['width'] + sym_dim = scaleglyph_dim + y_align_distance = 0 if sym_attr['valign'] == 'c': # Center the symbol vertically by matching the center of the line height and center of symbol @@ -1048,7 +1420,11 @@ class font_patcher: # Handle glyph l/r/c alignment x_align_distance = 0 - if sym_attr['align']: + if self.args.nonmono and sym_dim['advance'] is None: + # Remove left side bearing + # (i.e. do not remove left side bearing when combined BB is in use) + x_align_distance = -self.sourceFont[currentSourceFontGlyph].left_side_bearing + elif sym_attr['align']: # First find the baseline x-alignment (left alignment amount) x_align_distance = self.font_dim['xmin'] - sym_dim['xmin'] if sym_attr['align'] == 'c': @@ -1056,14 +1432,24 @@ class font_patcher: x_align_distance += (self.font_dim['width'] / 2) - (sym_dim['width'] / 2) elif sym_attr['align'] == 'r': # Right align - x_align_distance += self.font_dim['width'] - sym_dim['width'] + x_align_distance += self.font_dim['width'] * self.get_target_width(sym_attr['stretch']) - sym_dim['width'] + # If symbol glyph is wider than target font cell, just left-align + x_align_distance = max(self.font_dim['xmin'] - sym_dim['xmin'], x_align_distance) if overlap: overlap_width = self.font_dim['width'] * overlap if sym_attr['align'] == 'l': x_align_distance -= overlap_width - if sym_attr['align'] == 'r': - x_align_distance += overlap_width + elif sym_attr['align'] == 'c': + if overlap_width > 0: + x_align_distance -= overlap_width / 2 + elif sym_attr['align'] == 'r': + # Check and correct overlap; it can go wrong if we have a xy-ratio limit + target_xmax = (self.font_dim['xmin'] + self.font_dim['width']) * self.get_target_width(sym_attr['stretch']) + target_xmax += overlap_width + glyph_xmax = sym_dim['xmax'] + x_align_distance + correction = target_xmax - glyph_xmax + x_align_distance += correction align_matrix = psMat.translate(x_align_distance, y_align_distance) self.sourceFont[currentSourceFontGlyph].transform(align_matrix) @@ -1078,22 +1464,35 @@ class font_patcher: # same width for all character glyphs. This needs to be done for all glyphs, # even the ones that are empty and didn't go through the scaling operations. # It should come after setting the glyph bearings - self.set_glyph_width_mono(self.sourceFont[currentSourceFontGlyph]) - - # Re-remove negative bearings for target font with variable advance width - if self.args.nonmono: - self.remove_glyph_neg_bearings(self.sourceFont[currentSourceFontGlyph]) + if not self.args.nonmono: + self.set_glyph_width_mono(self.sourceFont[currentSourceFontGlyph]) + else: + # Target font with variable advance width get the icons with their native widths + # and keeping possible (right and/or negative) bearings in effect + if sym_dim['advance'] is not None: + # 'Width' from monospaced scale group + width = sym_dim['advance'] + else: + width = sym_dim['width'] + # If we have overlap we need to subtract that to keep/get negative bearings + if overlap and (sym_attr['align'] == 'l' or sym_attr['align'] == 'r'): + width -= overlap_width + # Fontforge handles the width change like this: + # - Keep existing left_side_bearing + # - Set width + # - Calculate and set new right_side_bearing + self.sourceFont[currentSourceFontGlyph].width = int(width) # Check if the inserted glyph is scaled correctly for monospace 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 %s))", + currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], repr(overlap)) # end for - if self.args.quiet is False or self.args.progressbars: + if not self.args.quiet: sys.stdout.write("\n") @@ -1142,55 +1541,98 @@ class font_patcher: except: pass - def prepareScaleGlyph(self, scaleGlyph, symbolFont, destGlyph): - """ Prepare raw ScaleGlyph data for use """ - # The GlyphData is a dict with these (possible) entries: - # 'GlyphsToScale': List of ((lists of glyph codes) or (ranges of glyph codes)) that shall be scaled - # 'scales': List of associated scale factors, one for each entry in 'GlyphsToScale' (generated by this function) + def prepareScaleRules(self, scaleRules, symbolFont, destGlyph): + """ Prepare raw ScaleRules data for use """ + # The scaleRules is/will be a dict with these (possible) entries: + # 'ScaleGroups': List of ((lists of glyph codes) or (ranges of glyph codes)) that shall be scaled + # 'scales': List of associated scale factors, one for each entry in 'ScaleGroups' (generated by this function) + # 'bbdims': List of associated sym_dim dicts, one for each entry in 'ScaleGroups' (generated by this function) + # Each dim_dict describes the combined bounding box of all glyphs in one ScaleGroups group # Example: - # { 'GlyphsToScale': [ range(1, 3), [ 7, 10 ], ], - # 'scales': [ 1.23, 1.33, ] } + # { 'ScaleGroups': [ range(1, 3), [ 7, 10 ], ], + # 'scales': [ 1.23, 1.33, ], + # 'bbdims': [ dim_dict1, dim_dict2, ] } # - # Each item in 'GlyphsToScale' (a range or an explicit list) forms a group of glyphs that shall be - # as rescaled all with the same and maximum possible (for the included glyphs) factor. + # Each item in 'ScaleGroups' (a range or an explicit list) forms a group of glyphs that shall be + # as rescaled all with the same and maximum possible (for the included glyphs) 'pa' factor. + # If the 'bbdims' is present they all shall be shifted in the same way. # # Previously this structure has been used: # 'ScaleGlyph' Lead glyph, which scaling factor is taken - # 'GlyphsToScale': List of (glyph code) or (list of two glyph codes that form a closed range)) that shall be scaled + # 'GlyphsToScale': List of ((glyph code) or (tuple of two glyph codes that form a closed range)) that shall be scaled # Note that this allows only one group for the whle symbol font, and that the scaling factor is defined by # a specific character, which needs to be manually selected (on each symbol font update). # Previous entries are automatically rewritten to the new style. - if 'scales' in scaleGlyph: + # + # Note that scaleRules is overwritten with the added data. + if 'scales' in scaleRules: # Already prepared... must not happen, ignore call return - if 'ScaleGlyph' in scaleGlyph: - # old method. Rewrite to new. - flat_list = [] - for i in scaleGlyph['GlyphsToScale']: + + scaleRules['scales'] = [] + scaleRules['bbdims'] = [] + if 'ScaleGroups' not in scaleRules: + scaleRules['ScaleGroups'] = [] + for group in scaleRules['ScaleGroups']: + sym_dim = get_multiglyph_boundingBox([ symbolFont[g] if g in symbolFont else None for g in group ], destGlyph) + scale = self.get_scale_factors(sym_dim, 'pa')[0] + scaleRules['scales'].append(scale) + scaleRules['bbdims'].append(sym_dim) + + if 'ScaleGlyph' in scaleRules: + # Rewrite to equivalent ScaleGroup + group_list = [] + if 'GlyphsToScale+' in scaleRules: + key = 'GlyphsToScale+' + plus = True + else: + key = 'GlyphsToScale' + plus = False + for i in scaleRules[key]: if isinstance(i, tuple): - flat_list += list(range(i[0], i[1] + 1)) + group_list.append(range(i[0], i[1] + 1)) else: - flat_list.append(i) - scaleGlyph['GlyphsToScale'] = [ flat_list ] - sym_dim = get_glyph_dimensions(symbolFont[scaleGlyph['ScaleGlyph']]) - scaleGlyph['scales'] = [ self.get_scale_factor(sym_dim) ] - else: - scaleGlyph['scales'] = [] - for group in scaleGlyph['GlyphsToScale']: - sym_dim = get_multiglyph_boundingBox([ symbolFont[g] if g in symbolFont else None for g in group ], destGlyph) - scaleGlyph['scales'].append(self.get_scale_factor(sym_dim)) + group_list.append(i) + sym_dim = get_glyph_dimensions(symbolFont[scaleRules['ScaleGlyph']]) + scale = self.get_scale_factors(sym_dim, 'pa')[0] + scaleRules['ScaleGroups'].append(group_list) + scaleRules['scales'].append(scale) + if plus: + scaleRules['bbdims'].append(sym_dim) + else: + scaleRules['bbdims'].append(None) # The 'old' style keeps just the scale, not the positioning - def get_glyph_scale(self, unicode_value, scaleGlyph, symbolFont): - """ Determines whether or not to use scaled glyphs for glyphs in passed glyph_list """ - # Potentially destorys the contents of self.sourceFont[unicode_value] - if not 'scales' in scaleGlyph: - self.prepareScaleGlyph(scaleGlyph, symbolFont, self.sourceFont[unicode_value]) - for glyph_list, scale in zip(scaleGlyph['GlyphsToScale'], scaleGlyph['scales']): - if unicode_value in glyph_list: - return scale - return False + def get_glyph_scale(self, symbol_unicode, scaleRules, symbolFont, dest_unicode): + """ Determines whether or not to use scaled glyphs for glyph in passed symbol_unicode """ + # Potentially destorys the contents of self.sourceFont[dest_unicode] + if not 'scales' in scaleRules: + if not dest_unicode in self.sourceFont: + self.sourceFont.createChar(dest_unicode) + self.prepareScaleRules(scaleRules, symbolFont, self.sourceFont[dest_unicode]) + for glyph_list, scale, box in zip(scaleRules['ScaleGroups'], scaleRules['scales'], scaleRules['bbdims']): + for e in glyph_list: + if isinstance(e, range): + if symbol_unicode in e: + return (scale, box) + elif symbol_unicode == e: + return (scale, box) + return None +def half_gap(gap, top): + """ Divides integer value into two new integers """ + # Line gap add extra space on the bottom of the line which + # doesn't allow the powerline glyphs to fill the entire line. + # Put half of the gap into the 'cell', each top and bottom + if gap <= 0: + return 0 + gap_top = int(gap / 2) + gap_bottom = gap - gap_top + if top: + logger.info("Redistributing line gap of %d (%d top and %d bottom)", gap, gap_top, gap_bottom) + return gap_top + return gap_bottom + def replace_font_name(font_name, replacement_dict): """ Replaces all keys with vals from replacement_dict in font_name. """ for key, val in replacement_dict.items(): @@ -1206,40 +1648,83 @@ def make_sure_path_exists(path): if exception.errno != errno.EEXIST: raise +def sanitize_filename(filename, allow_dirs = False): + """ Enforces to not use forbitten characters in a filename/path. """ + if filename == '.' and not allow_dirs: + return '_' + trans = filename.maketrans('<>:"|?*', '_______') + for i in range(0x00, 0x20): + trans[i] = ord('_') + if not allow_dirs: + trans[ord('/')] = ord('_') + trans[ord('\\')] = ord('_') + else: + trans[ord('\\')] = ord('/') # We use posix paths + return filename.translate(trans) + def get_multiglyph_boundingBox(glyphs, destGlyph = None): """ Returns dict of the dimensions of multiple glyphs combined(, as if they are copied into destGlyph) """ # If destGlyph is given the glyph(s) are first copied over into that # glyph and measured in that font (to avoid rounding errors) # Leaves the destGlyph in unknown state! - bbox = [ None, None, None, None ] + bbox = [ None, None, None, None, None ] for glyph in glyphs: if glyph is None: # Glyph has been in defining range but is not in the actual font continue - if destGlyph: + if destGlyph and glyph.font != destGlyph.font: glyph.font.selection.select(glyph) glyph.font.copy() destGlyph.font.selection.select(destGlyph) destGlyph.font.paste() glyph = destGlyph gbb = glyph.boundingBox() + gadvance = glyph.width + if len(glyphs) > 1 and gbb[0] == gbb[2] and gbb[1] == gbb[3]: + # Ignore empty glyphs if we examine more than one glyph + continue bbox[0] = gbb[0] if bbox[0] is None or bbox[0] > gbb[0] else bbox[0] bbox[1] = gbb[1] if bbox[1] is None or bbox[1] > gbb[1] else bbox[1] bbox[2] = gbb[2] if bbox[2] is None or bbox[2] < gbb[2] else bbox[2] bbox[3] = gbb[3] if bbox[3] is None or bbox[3] < gbb[3] else bbox[3] + if not bbox[4]: + bbox[4] = -gadvance # Negative for one/first glyph + else: + if abs(bbox[4]) != gadvance: + bbox[4] = -1 # Marker for not-monospaced + else: + bbox[4] = gadvance # Positive for 2 or more glyphs + if bbox[4] and bbox[4] < 0: + # Not monospaced when only one glyph is used or multiple glyphs with different advance widths + bbox[4] = None return { - 'xmin' : bbox[0], - 'ymin' : bbox[1], - 'xmax' : bbox[2], - 'ymax' : bbox[3], - 'width' : bbox[2] + (-bbox[0]), - 'height': bbox[3] + (-bbox[1]), + 'xmin' : bbox[0], + 'ymin' : bbox[1], + 'xmax' : bbox[2], + 'ymax' : bbox[3], + 'width' : bbox[2] + (-bbox[0]), + 'height' : bbox[3] + (-bbox[1]), + 'advance': bbox[4], # advance width if monospaced } def get_glyph_dimensions(glyph): """ Returns dict of the dimesions of the glyph passed to it. """ return get_multiglyph_boundingBox([ glyph ]) +def scale_bounding_box(bbox, scale_x, scale_y): + """ Return a scaled version of a glyph dimensions dict """ + # Simulate scaling on combined bounding box, round values for better simulation + new_dim = { + 'xmin' : int(bbox['xmin'] * scale_x), + 'ymin' : int(bbox['ymin'] * scale_y), + 'xmax' : int(bbox['xmax'] * scale_x), + 'ymax' : int(bbox['ymax'] * scale_y), + 'advance': int(bbox['advance'] * scale_x) if bbox['advance'] is not None else None, + } + new_dim['width'] = new_dim['xmax'] + (-new_dim['xmin']) + new_dim['height'] = new_dim['ymax'] + (-new_dim['ymin']) + return new_dim + def update_progress(progress): """ Updates progress bar length. @@ -1269,10 +1754,38 @@ 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): + """ Upgraded the version to the current git tag version (starting with 'v') """ + git = subprocess.run("git describe --tags", + cwd=os.path.dirname(__file__), + shell=True, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ).stdout.decode('utf-8') + if len(git) == 0: + return False + tag = git.strip() + if len(tag) == 0 or not tag.startswith('v'): + return False + tag = tag[1:] + r = re.search('(.*?)(-[0-9]+)-g[0-9a-fA-F]+$', tag) + if r: + tag = r.group(1) + patchlevel = r.group(2) + else: + patchlevel = "" + # Inspired by Phaxmohdem's versiontuple https://stackoverflow.com/a/28568003 + + versiontuple = lambda v: tuple( p.zfill(8) for p in v.split(".") ) + if versiontuple(tag) > versiontuple(version): + return tag + patchlevel + if versiontuple(tag) == versiontuple(version) and len(patchlevel) > 0: + return tag + patchlevel + return False + def setup_arguments(): parser = argparse.ArgumentParser( description=( @@ -1290,7 +1803,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') @@ -1300,15 +1812,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=0, type=int, nargs='?', help='Verbose mode (optional: 1=just to file; 2*=just to terminal; 3=display and file)', const=2, choices=range(0, 3 + 1)) + 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') @@ -1326,8 +1857,9 @@ def setup_arguments(): args = parser.parse_args() - if args.makegroups and not FontnameParserOK: - sys.exit(projectName + ": 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: @@ -1356,23 +1888,34 @@ def setup_arguments(): for alias in sym_font_arg_aliases: if alias in sys.argv: found = True - if found is not True: + if not found: font_complete = False args.complete = font_complete - if args.alsowindows: - args.windows = False - if args.nonmono and args.single: - print("Warniung: 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: + logging.warning("Source font is a variable open type font (VF), opening might fail...") + except: + args.is_variable = False + finally: + try: + source_font_test.close() + except: + pass if args.extension == "": args.extension = os.path.splitext(args.font)[1] @@ -1380,44 +1923,79 @@ 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 def main(): - print("{} Patcher v{} ({}) executing".format(projectName, version, script_version)) + global version + git_version = check_version_with_git(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) + log_to_file = (args.debugmode & 1 == 1) + if log_to_file: + try: + f_handler = logging.FileHandler('font-patcher-log.txt') + f_handler.setFormatter(logging.Formatter('%(levelname)s: %(name)s %(message)s')) + logger.addHandler(f_handler) + except: + log_to_file = False + 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 & 2 == 2): + c_handler.setLevel(logging.INFO) + logger.addHandler(c_handler) + if (args.debugmode & 1 == 1) and not log_to_file: + logger.info("Can not write logfile, disabling") + 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): - print("\n{}: Processing {} ({}/{})".format(projectName, subfont, i + 1, len(all_fonts))) + if len(all_fonts) > 1: + 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]) - print("\nDone with Patch Sets, generating font...\n") + print("Done with Patch Sets, generating font...") for f in sourceFonts: 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/vendor/glyphs/NerdFontsSymbols 1000 EM Nerd Font Complete Blank.sfd b/vendor/glyphs/NerdFontsSymbols 1000 EM Nerd Font Complete Blank.sfd deleted file mode 100644 index 3b24766..0000000 --- a/vendor/glyphs/NerdFontsSymbols 1000 EM Nerd Font Complete Blank.sfd +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:90e2066ba1d77a5f444d5271230c2a49121f4ac8d14ca12f3a0be5ff3145b847 -size 1205 diff --git a/vendor/glyphs/NerdFontsSymbols 2048 EM Nerd Font Complete Blank.sfd b/vendor/glyphs/NerdFontsSymbols 2048 EM Nerd Font Complete Blank.sfd deleted file mode 100644 index c95144d..0000000 --- a/vendor/glyphs/NerdFontsSymbols 2048 EM Nerd Font Complete Blank.sfd +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1b55f9fde86b1036280dd9bb2bb23a6762f1551635a1a74ca07975dcc75bcdc6 -size 1209 diff --git a/vendor/glyphs/PowerlineExtraSymbols.otf b/vendor/glyphs/PowerlineExtraSymbols.otf index 25a1d52..85366b9 100644 --- a/vendor/glyphs/PowerlineExtraSymbols.otf +++ b/vendor/glyphs/PowerlineExtraSymbols.otf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10b2ee1ea656010c6d8bfe2224c5dcddda32cd288970f0f7aec0464ac619e46c -size 84852 +oid sha256:6936b882072a367232cb214860a2a8426b35645c8c99a26a3f5a58b8cdba614a +size 84952 diff --git a/vendor/glyphs/README.md b/vendor/glyphs/README.md new file mode 100644 index 0000000..eabb6c7 --- /dev/null +++ b/vendor/glyphs/README.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8538467a6f1f67b9d7fb7f95da492796ac1f02046121aa4138c07026e85c5fb2 +size 378 diff --git a/vendor/glyphs/Symbols-1000-em Nerd Font Complete.ttf b/vendor/glyphs/Symbols-1000-em Nerd Font Complete.ttf deleted file mode 100644 index 4e1a742..0000000 --- a/vendor/glyphs/Symbols-1000-em Nerd Font Complete.ttf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:118ce5dad7c258fc166e5febfe8f189da88848c16301b0063b9a1043f2947882 -size 777880 diff --git a/vendor/glyphs/Symbols-2048-em Nerd Font Complete.ttf b/vendor/glyphs/Symbols-2048-em Nerd Font Complete.ttf deleted file mode 100644 index 0151d35..0000000 --- a/vendor/glyphs/Symbols-2048-em Nerd Font Complete.ttf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:df6be58f7707c0e8c9bc3aa230fc98fe8fa78cfba1cad43321e59281eca49600 -size 841552 diff --git a/vendor/glyphs/extraglyphs.sfd b/vendor/glyphs/extraglyphs.sfd new file mode 100644 index 0000000..b1d704c --- /dev/null +++ b/vendor/glyphs/extraglyphs.sfd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38397d66a6204db56b6cdaf4d0a1ad5f70b2be0e2202f6927ca32ed5844722e3 +size 15109 diff --git a/vendor/glyphs/font-logos.ttf b/vendor/glyphs/font-logos.ttf index e1bae6d..84a3b6b 100644 --- a/vendor/glyphs/font-logos.ttf +++ b/vendor/glyphs/font-logos.ttf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f85b3dc86c7af9d81222ea934590cd4293249e5d9075573367707763200bc05 -size 29116 +oid sha256:528de590d622c8b84b7176e963fa3b59e516199ba7251c77074cbed46e1e7e3d +size 28864 diff --git a/vendor/glyphs/materialdesign/LICENSE b/vendor/glyphs/materialdesign/LICENSE new file mode 100644 index 0000000..6c15d5d --- /dev/null +++ b/vendor/glyphs/materialdesign/LICENSE @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bae6305bcaa6d10219b999f65c342034c2d40b1c0e8567fa08605e73ccba829 +size 992 diff --git a/vendor/glyphs/materialdesign/MaterialDesignIconsDesktop.ttf b/vendor/glyphs/materialdesign/MaterialDesignIconsDesktop.ttf new file mode 100644 index 0000000..36d9ed1 --- /dev/null +++ b/vendor/glyphs/materialdesign/MaterialDesignIconsDesktop.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a10085e807b1df0e311402659b04a8468be37f382dbf4edfe7cb18a4ed3233f3 +size 1109996 diff --git a/vendor/glyphs/materialdesign/MaterialDesignIconsDesktop_orig.ttf b/vendor/glyphs/materialdesign/MaterialDesignIconsDesktop_orig.ttf new file mode 100644 index 0000000..f79493f --- /dev/null +++ b/vendor/glyphs/materialdesign/MaterialDesignIconsDesktop_orig.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5c8a4a157f53a70a22716d80b7f4d2df5a2a0ee92773e405f7ca6f2572c7ad3 +size 1440704 diff --git a/vendor/glyphs/materialdesign/README.md b/vendor/glyphs/materialdesign/README.md new file mode 100644 index 0000000..b4c7559 --- /dev/null +++ b/vendor/glyphs/materialdesign/README.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2076ba03ba69cc76fc426efac792dc329ada4710be1311ccff769ee780052744 +size 978 diff --git a/vendor/glyphs/octicons.ttf b/vendor/glyphs/octicons.ttf deleted file mode 100644 index 4f7a3f6..0000000 --- a/vendor/glyphs/octicons.ttf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b30d5a3432340aa4d6a05c0097f068cac4a7cd5ac9a9fa114b38b7cbafca2423 -size 43920 diff --git a/vendor/glyphs/octicons/LICENSE b/vendor/glyphs/octicons/LICENSE new file mode 100644 index 0000000..775decc --- /dev/null +++ b/vendor/glyphs/octicons/LICENSE @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f886642f9e8d99eeadd484acdac011c3fde8a338faf52f5ca1408c13b9a9b63 +size 1068 diff --git a/vendor/glyphs/octicons/analyze_octicons b/vendor/glyphs/octicons/analyze_octicons new file mode 100644 index 0000000..ada8e7e --- /dev/null +++ b/vendor/glyphs/octicons/analyze_octicons @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14bb76c16e83fcb4c1f99366e238ad6b2d29a63898bd9b83b798e5741fab2a6e +size 726 diff --git a/vendor/glyphs/octicons/generate b/vendor/glyphs/octicons/generate new file mode 100644 index 0000000..662cd93 --- /dev/null +++ b/vendor/glyphs/octicons/generate @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cea9fad3ad41f4080af07fb1a2b525029624a7c75dba18a6c10d720024916d79 +size 5738 diff --git a/vendor/glyphs/octicons/mapping b/vendor/glyphs/octicons/mapping new file mode 100644 index 0000000..c7397fb --- /dev/null +++ b/vendor/glyphs/octicons/mapping @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f72305fd4fa3d5b5b2f4a420fcba7f619e3046a2fd4982cad29ffcf2e749d27 +size 6266 diff --git a/vendor/glyphs/octicons/octicons.ttf b/vendor/glyphs/octicons/octicons.ttf new file mode 100644 index 0000000..bb99005 --- /dev/null +++ b/vendor/glyphs/octicons/octicons.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10e6e416de7aae8cfc0e7553c0551270a383ec6338014e5421256bf40a115619 +size 76520 diff --git a/vendor/glyphs/original-source.otf b/vendor/glyphs/original-source.otf index a003454..2acb988 100644 --- a/vendor/glyphs/original-source.otf +++ b/vendor/glyphs/original-source.otf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c27a5545918c8be0d3cdca5a3cbb20e3055955c5cbb7f89713ae1f66f4cd0fb0 -size 150164 +oid sha256:4ab9089e95a5d26bc98c62229abb60537855b836cdaac16b3af579e5e25fad30 +size 151836