diff --git a/bin/font-patcher b/bin/font-patcher index 1a54ea9..79bd27e 100755 --- a/bin/font-patcher +++ b/bin/font-patcher @@ -6,7 +6,7 @@ from __future__ import absolute_import, print_function, unicode_literals # Change the script version when you edit this script: -script_version = "3.5.2" +script_version = "3.5.8" version = "2.3.3" projectName = "Nerd Fonts" @@ -21,6 +21,7 @@ from argparse import RawTextHelpFormatter import errno import subprocess import json +from enum import Enum try: import configparser except ImportError: @@ -194,6 +195,14 @@ def panose_check_to_text(value, panose = False): 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: @@ -222,6 +231,20 @@ def is_monospaced(font): # We believe our own check more then Panose ;-D 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 + print(" Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')") + font.os2_panose = tuple(panose) + if panose[0] == 2 and panose[3] != 9: + print(" Setting Panose 'Proportion' to 'Monospaced' (was '{}')".format(panose_proportion_to_text(panose[3]))) + 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 """ width = 0 @@ -248,6 +271,17 @@ def report_advance_widths(font): 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) + class font_patcher: def __init__(self, args): @@ -267,7 +301,7 @@ class font_patcher: self.setup_version() self.get_essential_references() self.setup_name_backup(font) - if self.args.single: + if not self.args.nonmono: self.assert_monospace() self.remove_ligatures() self.setup_patch_set() @@ -280,13 +314,6 @@ 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: @@ -693,12 +720,14 @@ class font_patcher: print(" {} and {}".format( report_advance_widths(self.sourceFont), panose_check_to_text(panose_mono, self.sourceFont.os2_panose))) - if not width_mono: + if self.args.single and not width_mono: print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless") if offending_char is not None: print(" Offending char: 0x{:X}".format(offending_char)) 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") + if width_mono: + force_panose_monospaced(self.sourceFont) def setup_patch_set(self): @@ -779,6 +808,9 @@ 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}} + } CUSTOM_ATTR = { # 'pa' == preserve aspect ratio @@ -877,7 +909,8 @@ class font_patcher: # 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, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, + {'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': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_HEAVYBRACKETS}, {'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}, @@ -961,39 +994,37 @@ class font_patcher: # 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_height = self.sourceFont.hhea_ascent - self.sourceFont.hhea_descent - typo_height = self.sourceFont.os2_typoascent - self.sourceFont.os2_typodescent - win_height = self.sourceFont.os2_winascent + self.sourceFont.os2_windescent - win_gap = max(0, self.sourceFont.hhea_linegap - win_height + hhea_height) - hhea_btb = hhea_height + self.sourceFont.hhea_linegap - typo_btb = typo_height + self.sourceFont.os2_typolinegap - win_btb = win_height + win_gap + (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 + # 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 = 1 if use_typo else 2 # conforming font + metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font else: # We trust the WIN metric more, see experiments in #1056 print("{}: WARNING Font vertical metrics inconsistent (HHEA {} / TYPO {} / WIN {}), using WIN".format(projectName, hhea_btb, typo_btb, win_btb)) our_btb = win_btb - metrics = 1 + 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 == 0: - self.font_dim['ymin'] = self.sourceFont.hhea_descent + half_gap(self.sourceFont.hhea_linegap, False) + 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 == 1: - self.font_dim['ymin'] = self.sourceFont.os2_typodescent + half_gap(self.sourceFont.os2_typolinegap, False) + 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) - else: - self.font_dim['ymin'] = -self.sourceFont.os2_windescent + half_gap(win_gap, False) + 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'] @@ -1021,6 +1052,9 @@ class font_patcher: 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: + sys.exit("{}: Error in baseline to baseline code detected".format(projectName)) # Step 2 # Find the biggest char width and advance width @@ -1093,7 +1127,6 @@ class font_patcher: """ Copies symbol glyphs into self.sourceFont """ progressText = '' careful = False - glyphSetLength = 0 sourceFontCounter = 0 if self.args.careful: @@ -1114,14 +1147,12 @@ class font_patcher: glyphSetLength = len(symbolFontSelection) if not self.args.quiet: - sys.stdout.write("Adding " + str(max(1, glyphSetLength)) + " Glyphs from " + setName + " Set \n") + sys.stdout.write("Adding {} Glyphs from {} Set\n".format(glyphSetLength, setName)) currentSourceFontGlyph = -1 # initialize for the exactEncoding case width_warning = False for index, sym_glyph in enumerate(symbolFontSelection): - index = max(1, index) - sym_attr = attributes.get(sym_glyph.unicode) if sym_attr is None: sym_attr = attributes['default'] @@ -1571,6 +1602,34 @@ def check_fontforge_min_version(): sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, 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=( @@ -1699,7 +1758,12 @@ def setup_arguments(): def main(): - print("{} Patcher v{} ({}) executing".format(projectName, version, script_version)) + global version + git_version = check_version_with_git(version) + print("{} Patcher v{} ({}) (ff {}) executing".format( + projectName, git_version if git_version else version, script_version, fontforge.version())) + if git_version: + version = git_version check_fontforge_min_version() args = setup_arguments() patcher = font_patcher(args) diff --git a/src/glyphs/extraglyphs.sfd b/src/glyphs/extraglyphs.sfd new file mode 100644 index 0000000..45f63aa Binary files /dev/null and b/src/glyphs/extraglyphs.sfd differ