From feafa89bb38150c169bd599822165e19d966ede2 Mon Sep 17 00:00:00 2001 From: Fini Jastrow Date: Sun, 4 Sep 2022 19:55:24 +0200 Subject: [PATCH] font-patcher: Prevent --mono on proportional fonts [why] When the source font is proportional we can not really create a monospaced (patched) font from it. The glyph width is for example very small for 'i' but wide for 'W'. The glyphs are all left aligned, leaving very strange separation between smallish glyphs. Even if we would center the glyphs, the look would be strange and completely differenmt from the source font's look. [how] For proportional fonts do not allow to patch with `--mono`. The fact if a source font is monospaced is determined by examining some (very few) glyphs. But testing all our source fonts in the repo shows that it is sufficient. Furthermore the Panose flag is checked and differences between the flag and what the glyph examination found are reported. The user can enforce `Nerd Font Mono` generation with double specifying the command line option `--mono --mono`. Still a warning will be issued. [note] Because `gotta-patch-em-all-font-patcher!.sh` does not really count the variations but calculates them in a separate loop it does not know anymore how many variations are created per family. The numbers are wrong. But probably we should count the result font files in the end anyhow. Because the information is not needed (in an automated manner) this is not corrected here. It seems wrong anyhow: total_variation_count=$((total_variation_count+combination_count)) total_count=$((total_count+complete_variations_per_family+combination_count)) Signed-off-by: Fini Jastrow --- .../gotta-patch-em-all-font-patcher!.sh | 4 +- bin/scripts/name_parser/query_monospace | 94 +++++++++++++++++++ font-patcher | 82 +++++++++++++++- 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100755 bin/scripts/name_parser/query_monospace diff --git a/bin/scripts/gotta-patch-em-all-font-patcher!.sh b/bin/scripts/gotta-patch-em-all-font-patcher!.sh index 366a4364c..38c390fae 100755 --- a/bin/scripts/gotta-patch-em-all-font-patcher!.sh +++ b/bin/scripts/gotta-patch-em-all-font-patcher!.sh @@ -145,11 +145,11 @@ function patch_font { echo "fontforge -quiet -script ${PWD}/font-patcher "$f" -q --also-windows $powerline $post_process --complete --no-progressbars --outputdir "${patched_font_dir}complete/" $config_patch_flags" { OUT=$(fontforge -quiet -script ${PWD}/font-patcher "$f" -q --also-windows $powerline $post_process --complete --no-progressbars \ --outputdir "${patched_font_dir}complete/" $config_patch_flags 2>&1 1>&3 3>&- ); } 3>&1 - if [ $? -ne 0 ]; then printf "$OUT\n"; fi + if [ $? -ne 0 ]; then printf "$OUT\nPatcher run aborted!\n\n"; fi echo "fontforge -quiet -script ${PWD}/font-patcher "$f" -q -s ${font_config} --also-windows $powerline $post_process --complete --no-progressbars --outputdir "${patched_font_dir}complete/" $config_patch_flags" { OUT=$(fontforge -quiet -script ${PWD}/font-patcher "$f" -q -s ${font_config} --also-windows $powerline $post_process --complete --no-progressbars \ --outputdir "${patched_font_dir}complete/" $config_patch_flags 2>&1 1>&3 3>&- ); } 3>&1 - if [ $? -ne 0 ]; then printf "$OUT\n"; fi + if [ $? -ne 0 ]; then printf "$OUT\nPatcher run aborted!\n\n"; fi # wait for this group of background processes to finish to avoid forking too many processes # that can add up quickly with the number of combinations #wait diff --git a/bin/scripts/name_parser/query_monospace b/bin/scripts/name_parser/query_monospace new file mode 100755 index 000000000..3132ffaa3 --- /dev/null +++ b/bin/scripts/name_parser/query_monospace @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# coding=utf8 + +import sys +import os.path +import fontforge + +###### Some helpers (code from font-patcher) + +def check_panose_monospaced(font): + """ Check if the font's Panose flags say it is monospaced """ + # https://forum.high-logic.com/postedfiles/Panose.pdf + panose = list(font.os2_panose) + if panose[0] < 2 or panose[0] > 5: + return -1 # invalid Panose info + panose_mono = ((panose[0] == 2 and panose[3] == 9) or + (panose[0] == 3 and panose[3] == 3)) + return 1 if panose_mono else 0 + +def is_monospaced(font): + """ Check if a font is probably monospaced """ + # Some fonts lie (or have not any Panose flag set), spot check monospaced: + width = -1 + width_mono = True + for glyph in [ 0x49, 0x4D, 0x57, 0x61, 0x69, 0x2E ]: # wide and slim glyphs 'I', 'M', 'W', 'a', 'i', '.' + if not glyph in font: + # A 'strange' font, believe Panose + return check_panose_monospaced(font) == 1 + # print(" -> {} {}".format(glyph, font[glyph].width)) + if width < 0: + width = font[glyph].width + continue + if font[glyph].width != width: + # Exception for fonts like Code New Roman Regular or Hermit Light/Bold: + # Allow small 'i' and dot to be smaller than normal + # I believe the source fonts are buggy + if glyph in [ 0x69, 0x2E ]: + if width > font[glyph].width: + continue + (xmin, _, xmax, _) = font[glyph].boundingBox() + if width > xmax - xmin: + continue + width_mono = False + break + # We believe our own check more then Panose ;-D + return width_mono + +def get_advance_width(font, extended, minimum): + """ Get the maximum/minimum advance width in the extended(?) range """ + width = 0 + if extended: + end = 0x17f + else: + end = 0x07e + for glyph in range(0x21, end): + if not glyph in font: + continue + if glyph in range(0x7F, 0xBF): + continue # ignore special characters like '1/4' etc + if width == 0: + width = font[glyph].width + continue + if not minimum and width < font[glyph].width: + width = font[glyph].width + elif minimum and width > font[glyph].width: + width = font[glyph].width + return width + + +###### Let's go! + +if len(sys.argv) < 2: + print('Usage: {} font_name [font_name ...]\n'.format(sys.argv[0])) + sys.exit(1) + +print('Examining {} font files'.format(len(sys.argv) - 1)) + + +for filename in sys.argv[1:]: + fullfile = os.path.basename(filename) + fname = os.path.splitext(fullfile)[0] + + font = fontforge.open(filename, 1) + width_mono = is_monospaced(font) + panose_mono = check_panose_monospaced(font) + if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1): + print('[{:50.50}] Warning: Monospaced check: Panose assumed to be wrong; Glyph widths {} / {} - {} and Panose says "monospace {}" ({})'.format(fullfile, get_advance_width(font, False, True), + get_advance_width(font, False, False), get_advance_width(font, True, False), panose_mono, list(font.os2_panose))) + if not width_mono: + print('[{:50.50}] Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless; Glyph widths {} / {} - {}'.format(fullfile, get_advance_width(font, False, True), + get_advance_width(font, False, False), get_advance_width(font, True, False), panose_mono, list(font.os2_panose))) + else: + print('[{:50.50}] OK'.format(fullfile)) + font.close() diff --git a/font-patcher b/font-patcher index ac5d05745..011fca276 100755 --- a/font-patcher +++ b/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.1.0" +script_version = "3.1.1" version = "2.2.2" projectName = "Nerd Fonts" @@ -169,6 +169,65 @@ class TableHEADWriter: self.lowppem = self.getshort('lowestRecPPEM') self.checksum_adj = self.getlong('checksumAdjustment') +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 + class font_patcher: def __init__(self, args): @@ -187,6 +246,8 @@ class font_patcher: self.setup_version() self.get_essential_references() self.setup_name_backup(font) + if self.args.single: + self.assert_monospace() self.remove_ligatures() self.setup_patch_set() self.setup_line_dimensions() @@ -576,6 +637,21 @@ class font_patcher: print("No configfile given, skipping configfile related actions") + def assert_monospace(self): + # Check if the sourcefont is monospaced + width_mono = is_monospaced(self.sourceFont) + 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") + 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") + + 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 @@ -782,8 +858,10 @@ class font_patcher: continue 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 xmax > self.font_dim['xmax']: self.font_dim['xmax'] = xmax + # print("New MAXWIDTH-B {} {} {}".format(glyph, self.sourceFont[glyph].width, xmax)) # Calculate font height self.font_dim['height'] = abs(self.font_dim['ymin']) + self.font_dim['ymax'] @@ -1195,7 +1273,7 @@ def setup_arguments(): # optional arguments parser.add_argument('font', help='The path to the font to patch (e.g., Inconsolata.otf)') parser.add_argument('-v', '--version', action='version', version=projectName + ": %(prog)s (" + version + ")") - parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='store_true', help='Whether to generate the glyphs as single-width not double-width (default is double-width)') + 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)')