From ec4cb0a8c54e3b9942be8336471f1b2505ef2d5f Mon Sep 17 00:00:00 2001 From: Fini Jastrow Date: Wed, 9 Feb 2022 14:56:39 +0100 Subject: [PATCH 1/6] font-patcher: Pull argument parser out of patcher object [why] Parsing the command line arguments has nothing to do with the actual patching. If we want to patch more than one font we need to separate the partching from unrelated work. [how] Just do the argument processing in main() and hand the information over to the patcher object. [note] No functional change. Lines copied over (almost *) 1:1. (*) Exceptions: self.sym_font_args is now only a local variable, as it is only used in this one function. The startup message is shown on ... startup (i.e. main()). Signed-off-by: Fini Jastrow --- font-patcher | 220 +++++++++++++++++++++++---------------------------- 1 file changed, 101 insertions(+), 119 deletions(-) diff --git a/font-patcher b/font-patcher index 435c2ed00e..e1a9936a32 100755 --- a/font-patcher +++ b/font-patcher @@ -154,8 +154,8 @@ class TableHEADWriter: class font_patcher: - def __init__(self): - self.args = None # class 'argparse.Namespace' + def __init__(self, args): + self.args = args # class 'argparse.Namespace' self.sym_font_args = [] self.config = None # class 'configparser.ConfigParser' self.sourceFont = None # class 'fontforge.font' @@ -165,7 +165,6 @@ class font_patcher: self.onlybitmaps = 0 self.extension = "" self.essential = set() - self.setup_arguments() self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) if not os.path.isfile(self.args.font): sys.exit("{}: Font file does not exist: {}".format(projectName, self.args.font)) @@ -198,9 +197,6 @@ class font_patcher: def patch(self): - - print("{} Patcher v{} ({}) executing\n".format(projectName, version, script_version)) - if self.args.single: # 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. @@ -301,118 +297,6 @@ class font_patcher: print("\nPost Processed: {}".format(outfile)) - def setup_arguments(self): - parser = argparse.ArgumentParser( - description=( - 'Nerd Fonts Font Patcher: patches a given font with programming and development related glyphs\n\n' - '* Website: https://www.nerdfonts.com\n' - '* Version: ' + version + '\n' - '* Development Website: https://github.com/ryanoasis/nerd-fonts\n' - '* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/master/changelog.md'), - formatter_class=RawTextHelpFormatter - ) - - # 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('-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') - parser.add_argument('--postprocess', dest='postprocess', default=False, type=str, nargs='?', help='Specify a Script for Post Processing') - parser.add_argument('--configfile', dest='configfile', default=False, type=str, nargs='?', help='Specify a file path for JSON configuration file (see sample: src/config.sample.json)') - parser.add_argument('--custom', dest='custom', default=False, type=str, nargs='?', help='Specify a custom symbol font. All new glyphs will be copied, with no scaling applied.') - 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('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")') - - # 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('--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') - - # symbol fonts to include arguments - sym_font_group = parser.add_argument_group('Symbol Fonts') - sym_font_group.add_argument('--fontawesome', dest='fontawesome', default=False, action='store_true', help='Add Font Awesome Glyphs (http://fontawesome.io/)') - sym_font_group.add_argument('--fontawesomeextension', dest='fontawesomeextension', default=False, action='store_true', help='Add Font Awesome Extension Glyphs (https://andrelzgava.github.io/font-awesome-extension/)') - sym_font_group.add_argument('--fontlogos', '--fontlinux', dest='fontlogos', default=False, action='store_true', help='Add Font Logos Glyphs (https://github.com/Lukas-W/font-logos)') - sym_font_group.add_argument('--octicons', dest='octicons', default=False, action='store_true', help='Add Octicons Glyphs (https://octicons.github.com)') - sym_font_group.add_argument('--codicons', dest='codicons', default=False, action='store_true', help='Add Codicons Glyphs (https://github.com/microsoft/vscode-codicons)') - sym_font_group.add_argument('--powersymbols', dest='powersymbols', default=False, action='store_true', help='Add IEC Power Symbols (https://unicodepowersymbol.com/)') - sym_font_group.add_argument('--pomicons', dest='pomicons', default=False, action='store_true', help='Add Pomicon Glyphs (https://github.com/gabrielelana/pomicons)') - sym_font_group.add_argument('--powerline', dest='powerline', default=False, action='store_true', help='Add Powerline Glyphs') - sym_font_group.add_argument('--powerlineextra', dest='powerlineextra', default=False, action='store_true', help='Add Powerline Glyphs (https://github.com/ryanoasis/powerline-extra-symbols)') - sym_font_group.add_argument('--material', '--materialdesignicons', '--mdi', dest='material', default=False, action='store_true', help='Add Material Design Icons (https://github.com/templarian/MaterialDesign)') - sym_font_group.add_argument('--weather', '--weathericons', dest='weather', default=False, action='store_true', help='Add Weather Icons (https://github.com/erikflowers/weather-icons)') - - self.args = parser.parse_args() - - if self.args.makegroups and not FontnameParserOK: - sys.exit(projectName + ": FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName)) - - # if you add a new font, set it to True here inside the if condition - if self.args.complete: - self.args.fontawesome = True - self.args.fontawesomeextension = True - self.args.fontlogos = True - self.args.octicons = True - self.args.codicons = True - self.args.powersymbols = True - self.args.pomicons = True - self.args.powerline = True - self.args.powerlineextra = True - self.args.material = True - self.args.weather = True - - if not self.args.complete: - # add the list of arguments for each symbol font to the list self.sym_font_args - for action in sym_font_group._group_actions: - self.sym_font_args.append(action.__dict__['option_strings']) - - # determine whether or not all symbol fonts are to be used - font_complete = True - for sym_font_arg_aliases in self.sym_font_args: - found = False - for alias in sym_font_arg_aliases: - if alias in sys.argv: - found = True - if found is not True: - font_complete = False - self.args.complete = font_complete - - if self.args.alsowindows: - self.args.windows = False - - if self.args.nonmono and self.args.single: - print("Warniung: Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.") - self.args.nonmono = False - - # this one also works but it needs to be updated every time a font is added - # it was a conditional in self.setup_font_names() before, but it was missing - # a symbol font, so it would name the font complete without being so sometimes. - # that's why i did the above. - # - # if you add a new font, put it in here too, as the others are - # self.args.complete = all([ - # self.args.fontawesome is True, - # self.args.fontawesomeextension is True, - # self.args.fontlogos is True, - # self.args.octicons is True, - # self.args.powersymbols is True, - # self.args.pomicons is True, - # self.args.powerline is True, - # self.args.powerlineextra is True, - # self.args.material is True, - # self.args.weather is True - # ]) - def setup_name_backup(self): """ Store the original font names to be able to rename the font multiple times """ @@ -1292,10 +1176,108 @@ def check_fontforge_min_version(): sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion)) sys.exit(1) +def setup_arguments(): + parser = argparse.ArgumentParser( + description=( + 'Nerd Fonts Font Patcher: patches a given font with programming and development related glyphs\n\n' + '* Website: https://www.nerdfonts.com\n' + '* Version: ' + version + '\n' + '* Development Website: https://github.com/ryanoasis/nerd-fonts\n' + '* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/master/changelog.md'), + formatter_class=RawTextHelpFormatter + ) + + # 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('-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') + parser.add_argument('--postprocess', dest='postprocess', default=False, type=str, nargs='?', help='Specify a Script for Post Processing') + parser.add_argument('--configfile', dest='configfile', default=False, type=str, nargs='?', help='Specify a file path for JSON configuration file (see sample: src/config.sample.json)') + parser.add_argument('--custom', dest='custom', default=False, type=str, nargs='?', help='Specify a custom symbol font. All new glyphs will be copied, with no scaling applied.') + 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('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")') + + # 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('--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') + + # symbol fonts to include arguments + sym_font_group = parser.add_argument_group('Symbol Fonts') + sym_font_group.add_argument('--fontawesome', dest='fontawesome', default=False, action='store_true', help='Add Font Awesome Glyphs (http://fontawesome.io/)') + sym_font_group.add_argument('--fontawesomeextension', dest='fontawesomeextension', default=False, action='store_true', help='Add Font Awesome Extension Glyphs (https://andrelzgava.github.io/font-awesome-extension/)') + sym_font_group.add_argument('--fontlogos', '--fontlinux', dest='fontlogos', default=False, action='store_true', help='Add Font Logos Glyphs (https://github.com/Lukas-W/font-logos)') + sym_font_group.add_argument('--octicons', dest='octicons', default=False, action='store_true', help='Add Octicons Glyphs (https://octicons.github.com)') + sym_font_group.add_argument('--codicons', dest='codicons', default=False, action='store_true', help='Add Codicons Glyphs (https://github.com/microsoft/vscode-codicons)') + sym_font_group.add_argument('--powersymbols', dest='powersymbols', default=False, action='store_true', help='Add IEC Power Symbols (https://unicodepowersymbol.com/)') + sym_font_group.add_argument('--pomicons', dest='pomicons', default=False, action='store_true', help='Add Pomicon Glyphs (https://github.com/gabrielelana/pomicons)') + sym_font_group.add_argument('--powerline', dest='powerline', default=False, action='store_true', help='Add Powerline Glyphs') + sym_font_group.add_argument('--powerlineextra', dest='powerlineextra', default=False, action='store_true', help='Add Powerline Glyphs (https://github.com/ryanoasis/powerline-extra-symbols)') + sym_font_group.add_argument('--material', '--materialdesignicons', '--mdi', dest='material', default=False, action='store_true', help='Add Material Design Icons (https://github.com/templarian/MaterialDesign)') + sym_font_group.add_argument('--weather', '--weathericons', dest='weather', default=False, action='store_true', help='Add Weather Icons (https://github.com/erikflowers/weather-icons)') + + 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 you add a new font, set it to True here inside the if condition + if args.complete: + args.fontawesome = True + args.fontawesomeextension = True + args.fontlogos = True + args.octicons = True + args.codicons = True + args.powersymbols = True + args.pomicons = True + args.powerline = True + args.powerlineextra = True + args.material = True + args.weather = True + + if not args.complete: + sym_font_args = [] + # add the list of arguments for each symbol font to the list sym_font_args + for action in sym_font_group._group_actions: + sym_font_args.append(action.__dict__['option_strings']) + + # determine whether or not all symbol fonts are to be used + font_complete = True + for sym_font_arg_aliases in sym_font_args: + found = False + for alias in sym_font_arg_aliases: + if alias in sys.argv: + found = True + if found is not True: + 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.") + args.nonmono = False + + return args + def main(): + print("{} Patcher v{} ({}) executing\n".format(projectName, version, script_version)) check_fontforge_min_version() - patcher = font_patcher() + args = setup_arguments() + patcher = font_patcher(args) patcher.patch() print("\nDone with Patch Sets, generating font...\n") patcher.setup_font_names() From 2432a5700a0ba1bf98a860ef712c1a76238001b3 Mon Sep 17 00:00:00 2001 From: Fini Jastrow Date: Wed, 9 Feb 2022 15:18:22 +0100 Subject: [PATCH 2/6] font-patcher: Handle argument checks with arguments [why] The extension handling is a bit out-of-place and could be handled by the arguments handling, which simplifies the code. Somes goes for other argument validity checks. [how] Put argument checks into setup_arguments(). Dropping self.extensions in favour of self.args.extensions. No functional change. Signed-off-by: Fini Jastrow --- font-patcher | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/font-patcher b/font-patcher index e1a9936a32..5306b71393 100755 --- a/font-patcher +++ b/font-patcher @@ -163,16 +163,8 @@ class font_patcher: self.patch_set = None # class 'list' self.font_dim = None # class 'dict' self.onlybitmaps = 0 - self.extension = "" self.essential = set() self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) - if not os.path.isfile(self.args.font): - sys.exit("{}: Font file does not exist: {}".format(projectName, self.args.font)) - if not os.access(self.args.font, os.R_OK): - sys.exit("{}: Can not open font file for reading: {}".format(projectName, self.args.font)) - if len(fontforge.fontsInFile(self.args.font)) > 1: - sys.exit("{}: Font file contains {} fonts, can only handle single font files".format(projectName, - len(fontforge.fontsInFile(self.args.font)))) try: self.sourceFont = fontforge.open(self.args.font, 1) # 1 = ("fstypepermitted",)) except Exception: @@ -181,19 +173,12 @@ class font_patcher: self.get_essential_references() self.setup_name_backup() self.remove_ligatures() - make_sure_path_exists(self.args.outputdir) self.check_position_conflicts() self.setup_patch_set() self.setup_line_dimensions() self.get_sourcefont_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 - if self.args.extension == "": - self.extension = os.path.splitext(self.args.font)[1] - else: - self.extension = '.' + self.args.extension - if re.match("\.ttc$", self.extension, re.IGNORECASE): - sys.exit(projectName + ": Can not create True Type Collections") def patch(self): @@ -261,11 +246,11 @@ class font_patcher: def generate(self): # the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'. if self.sourceFont.fullname != None: - outfile = self.args.outputdir + "/" + self.sourceFont.fullname + self.extension + outfile = self.args.outputdir + "/" + self.sourceFont.fullname + self.args.extension self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments'))) message = "\nGenerated: {} in '{}'".format(self.sourceFont.fullname, outfile) else: - outfile = self.args.outputdir + "/" + self.sourceFont.cidfontname + self.extension + outfile = self.args.outputdir + "/" + self.sourceFont.cidfontname + self.args.extension self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments'))) message = "\nGenerated: {} in '{}'".format(self.sourceFont.fontname, outfile) @@ -1270,6 +1255,22 @@ def setup_arguments(): print("Warniung: 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)) + if not os.access(args.font, os.R_OK): + sys.exit("{}: Can not open font file for reading: {}".format(projectName, args.font)) + if len(fontforge.fontsInFile(args.font)) > 1: + sys.exit("{}: Font file contains {} fonts, can only handle single font files".format(projectName, + len(fontforge.fontsInFile(args.font)))) + + if args.extension == "": + args.extension = os.path.splitext(args.font)[1] + else: + args.extension = '.' + args.extension + if re.match("\.ttc$", args.extension, re.IGNORECASE): + sys.exit(projectName + ": Can not create True Type Collections") + return args From 1b8c9e276867437a246cc31ec85e98e2b4159900 Mon Sep 17 00:00:00 2001 From: Fini Jastrow Date: Wed, 9 Feb 2022 16:04:32 +0100 Subject: [PATCH 3/6] font-patcher: Pull out opening and generating the font [why] These operations are also not strictly patching. When we want to handle more than one font we need to have this outside the patch() function. [how] Put opening and exporting (previously in __init__() and patch() into main() outside the patcher object. No functional change (except the sourceFont is now closed :->) Signed-off-by: Fini Jastrow --- font-patcher | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/font-patcher b/font-patcher index 5306b71393..8d1b525336 100755 --- a/font-patcher +++ b/font-patcher @@ -165,10 +165,9 @@ class font_patcher: self.onlybitmaps = 0 self.essential = set() self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) - try: - self.sourceFont = fontforge.open(self.args.font, 1) # 1 = ("fstypepermitted",)) - except Exception: - sys.exit(projectName + ": Can not open font, try to open with fontforge interactively to get more information") + + def patch(self, font): + self.sourceFont = font self.setup_version() self.get_essential_references() self.setup_name_backup() @@ -180,8 +179,6 @@ class font_patcher: 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 - - def patch(self): if self.args.single: # 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. @@ -1279,7 +1276,14 @@ def main(): check_fontforge_min_version() args = setup_arguments() patcher = font_patcher(args) - patcher.patch() + + try: + sourceFont = fontforge.open(args.font, 1) # 1 = ("fstypepermitted",)) + except Exception: + sys.exit(projectName + ": Can not open font, try to open with fontforge interactively to get more information") + + patcher.patch(sourceFont) + print("\nDone with Patch Sets, generating font...\n") patcher.setup_font_names() patcher.generate() From e515cccab907205747f21202fddf89f2dfb70d55 Mon Sep 17 00:00:00 2001 From: Fini Jastrow Date: Wed, 9 Feb 2022 18:39:47 +0100 Subject: [PATCH 4/6] font-patcher: Allow patching of True Type Collections [why] Someone might want to patch a whole lot of fonts that come in a ttc. [how] Just open all fonts that the input file contains (1 or more) and create a single font or collection font file. The automatic layer detection does not work in all cases for me, so we need to manually search for the foreground layer (usually '1'). Code inspiration taken from https://github.com/powerline/fontpatcher/pull/6 [note] Changed output in the end to the filename (before it was the font name), so that one can easily copy&paste or open that file. Reported-by: Lily Ballard Signed-off-by: Fini Jastrow --- font-patcher | 136 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 80 insertions(+), 56 deletions(-) diff --git a/font-patcher b/font-patcher index 8d1b525336..18d4ae8564 100755 --- a/font-patcher +++ b/font-patcher @@ -170,7 +170,7 @@ class font_patcher: self.sourceFont = font self.setup_version() self.get_essential_references() - self.setup_name_backup() + self.setup_name_backup(font) self.remove_ligatures() self.check_position_conflicts() self.setup_patch_set() @@ -240,16 +240,27 @@ class font_patcher: self.sourceFont["grave"].glyphclass="baseglyph" - def generate(self): - # the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'. - if self.sourceFont.fullname != None: - outfile = self.args.outputdir + "/" + self.sourceFont.fullname + self.args.extension - self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments'))) - message = "\nGenerated: {} in '{}'".format(self.sourceFont.fullname, outfile) + def generate(self, sourceFonts): + sourceFont = sourceFonts[0] + if len(sourceFonts) > 1: + layer = None + # use first non-background layer + for l in sourceFont.layers: + 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) else: - outfile = self.args.outputdir + "/" + self.sourceFont.cidfontname + self.args.extension - self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments'))) - message = "\nGenerated: {} in '{}'".format(self.sourceFont.fontname, outfile) + fontname = sourceFont.fullname + 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'. + sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments'))) + message = "\nGenerated: {} in '{}'".format(self.sourceFont.fullname, outfile) # Adjust flags that can not be changed via fontforge try: @@ -279,18 +290,19 @@ class font_patcher: print("\nPost Processed: {}".format(outfile)) - - def setup_name_backup(self): + def setup_name_backup(self, font): """ Store the original font names to be able to rename the font multiple times """ - self.original_fontname = self.sourceFont.fontname - self.original_fullname = self.sourceFont.fullname - self.original_familyname = self.sourceFont.familyname + font.persistent = { + "fontname": font.fontname, + "fullname": font.fullname, + "familyname": font.familyname, + } - def setup_font_names(self): - self.sourceFont.fontname = self.original_fontname - self.sourceFont.fullname = self.original_fullname - self.sourceFont.familyname = self.original_familyname + 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 @@ -337,14 +349,14 @@ class font_patcher: verboseAdditionalFontNameSuffix += " Mono" if FontnameParserOK and self.args.makegroups: - use_fullname = type(self.sourceFont.fullname) == str # Usually the fullname is better to parse + use_fullname = type(font.fullname) == str # Usually the fullname is better to parse # Use fullname if it is 'equal' to the fontname - if self.sourceFont.fullname: - use_fullname |= self.sourceFont.fontname.lower() == FontnameTools.postscript_char_filter(self.sourceFont.fullname).lower() + if font.fullname: + use_fullname |= font.fontname.lower() == FontnameTools.postscript_char_filter(font.fullname).lower() # Use fullname for any of these source fonts (that are impossible to disentangle from the fontname, we need the blanks) for hit in [ 'Meslo' ]: - use_fullname |= self.sourceFont.fontname.lower().startswith(hit.lower()) - parser_name = self.sourceFont.fullname if use_fullname else self.sourceFont.fontname + use_fullname |= font.fontname.lower().startswith(hit.lower()) + parser_name = font.fullname if use_fullname else font.fontname # 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] @@ -362,16 +374,16 @@ class font_patcher: # have an internal style defined (in sfnt_names) # using '([^-]*?)' to get the item before the first dash "-" # using '([^-]*(?!.*-))' to get the item after the last dash "-" - fontname, fallbackStyle = re.match("^([^-]*).*?([^-]*(?!.*-))$", self.sourceFont.fontname).groups() + fontname, fallbackStyle = re.match("^([^-]*).*?([^-]*(?!.*-))$", font.fontname).groups() - # dont trust 'sourceFont.familyname' + # dont trust 'font.familyname' familyname = fontname # fullname (filename) can always use long/verbose font name, even in windows - if self.sourceFont.fullname != None: - fullname = self.sourceFont.fullname + verboseAdditionalFontNameSuffix + if font.fullname != None: + fullname = font.fullname + verboseAdditionalFontNameSuffix else: - fullname = self.sourceFont.cidfontname + verboseAdditionalFontNameSuffix + fullname = font.cidfontname + verboseAdditionalFontNameSuffix fontname = fontname + additionalFontNameSuffix.replace(" ", "") @@ -379,13 +391,13 @@ class font_patcher: # parse fontname if it fails: try: # search tuple: - subFamilyTupleIndex = [x[1] for x in self.sourceFont.sfnt_names].index("SubFamily") + subFamilyTupleIndex = [x[1] for x in font.sfnt_names].index("SubFamily") # String ID is at the second index in the Tuple lists sfntNamesStringIDIndex = 2 # now we have the correct item: - subFamily = self.sourceFont.sfnt_names[subFamilyTupleIndex][sfntNamesStringIDIndex] + subFamily = font.sfnt_names[subFamilyTupleIndex][sfntNamesStringIDIndex] except IndexError: sys.stderr.write("{}: Could not find 'SubFamily' for given font, falling back to parsed fontname\n".format(projectName)) subFamily = fallbackStyle @@ -495,22 +507,22 @@ class font_patcher: if not (FontnameParserOK and self.args.makegroups): # replace any extra whitespace characters: - self.sourceFont.familyname = " ".join(familyname.split()) - self.sourceFont.fullname = " ".join(fullname.split()) - self.sourceFont.fontname = " ".join(fontname.split()) - - self.sourceFont.appendSFNTName(str('English (US)'), str('Preferred Family'), self.sourceFont.familyname) - self.sourceFont.appendSFNTName(str('English (US)'), str('Family'), self.sourceFont.familyname) - self.sourceFont.appendSFNTName(str('English (US)'), str('Compatible Full'), self.sourceFont.fullname) - self.sourceFont.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) + font.familyname = " ".join(familyname.split()) + font.fullname = " ".join(fullname.split()) + font.fontname = " ".join(fontname.split()) + + font.appendSFNTName(str('English (US)'), str('Preferred Family'), font.familyname) + font.appendSFNTName(str('English (US)'), str('Family'), font.familyname) + 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) - n.rename_font(self.sourceFont) + n.rename_font(font) - self.sourceFont.comment = projectInfo - self.sourceFont.fontlog = projectInfo + font.comment = projectInfo + font.fontlog = projectInfo def setup_version(self): @@ -1257,41 +1269,53 @@ def setup_arguments(): sys.exit("{}: Font file does not exist: {}".format(projectName, args.font)) if not os.access(args.font, os.R_OK): sys.exit("{}: Can not open font file for reading: {}".format(projectName, args.font)) - if len(fontforge.fontsInFile(args.font)) > 1: - sys.exit("{}: Font file contains {} fonts, can only handle single font files".format(projectName, - len(fontforge.fontsInFile(args.font)))) + is_ttc = len(fontforge.fontsInFile(args.font)) > 1 if args.extension == "": args.extension = os.path.splitext(args.font)[1] else: args.extension = '.' + args.extension if re.match("\.ttc$", args.extension, re.IGNORECASE): - sys.exit(projectName + ": Can not create True Type Collections") + if not is_ttc: + sys.exit(projectName + ": Can not create True Type Collections from single font files") + else: + if is_ttc: + sys.exit(projectName + ": Can not create single font files from True Type Collections") return args def main(): - print("{} Patcher v{} ({}) executing\n".format(projectName, version, script_version)) + print("{} Patcher v{} ({}) executing".format(projectName, version, script_version)) check_fontforge_min_version() args = setup_arguments() patcher = font_patcher(args) - try: - sourceFont = fontforge.open(args.font, 1) # 1 = ("fstypepermitted",)) - except Exception: - sys.exit(projectName + ": Can not open font, try to open with fontforge interactively to get more information") + sourceFonts = [] + for subfont in fontforge.fontsInFile(args.font): + print("\n{}: Processing {}".format(projectName, subfont)) + 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)) - patcher.patch(sourceFont) + patcher.patch(sourceFonts[-1]) print("\nDone with Patch Sets, generating font...\n") - patcher.setup_font_names() - patcher.generate() + 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 - patcher.setup_font_names() - patcher.generate() + for f in sourceFonts: + patcher.setup_font_names(f) + patcher.generate(sourceFonts) + + for f in sourceFonts: + f.close() if __name__ == "__main__": From e8a17c71bf0e08f57bfe7ab2ec131a5928963144 Mon Sep 17 00:00:00 2001 From: Fini Jastrow Date: Wed, 14 Sep 2022 17:30:18 +0200 Subject: [PATCH 5/6] font-patcher: Increase script version [why] With the new TTC feature we might increase the minor number ;-) This is not just a bugfix. Signed-off-by: Fini Jastrow --- font-patcher | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/font-patcher b/font-patcher index 18d4ae8564..a21c7cbe44 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.0.6" +script_version = "3.1.0" version = "2.2.2" projectName = "Nerd Fonts" From 7c5c838122ade9751399589f6cfd3f9bd87799b1 Mon Sep 17 00:00:00 2001 From: Fini Jastrow Date: Fri, 23 Sep 2022 09:26:11 +0200 Subject: [PATCH 6/6] font-patcher: Enable lowestRecPPEM fix in TTCs [why] The font flags and PPEM fix does not work with font collection files, because it does not know how to handle them. It assumes a ttf or otf font with the specified table structure. The fix (for single font files) has been introduced with commit 40138bee9 font-patcher: Handle lowestRecPPEM [how] Check if the file is of type 'ttcf', and if so fast forward to the given single font index into the collection. This can be rather slow... Signed-off-by: Fini Jastrow --- font-patcher | 52 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/font-patcher b/font-patcher index a21c7cbe44..f7cefc060f 100755 --- a/font-patcher +++ b/font-patcher @@ -88,9 +88,26 @@ class TableHEADWriter: checksum = (checksum + extra) & 0xFFFFFFFF return checksum - def find_head_table(self): + def find_head_table(self, idx): """ Search all tables for the HEAD table and store its metadata """ - self.f.seek(4) + # Use font with index idx if this is a font collection file + self.f.seek(0, 0) + tag = self.f.read(4) + if tag == b'ttcf': + self.f.seek(2*2, 1) + self.num_fonts = self.getlong() + if (idx >= self.num_fonts): + raise Exception('Trying to access subfont index {} but have only {} fonts'.format(idx, num_fonts)) + for _ in range(idx + 1): + offset = self.getlong() + self.f.seek(offset, 0) + elif idx != 0: + raise Exception('Trying to access subfont but file is no collection') + else: + self.f.seek(0, 0) + self.num_fonts = 1 + + self.f.seek(4, 1) numtables = self.getshort() self.f.seek(3*2, 1) @@ -102,7 +119,7 @@ class TableHEADWriter: self.tab_length = self.getlong() if tab_name == b'head': return - raise Exception('No HEAD table 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 """ @@ -146,7 +163,7 @@ class TableHEADWriter: self.modified = False self.f = open(filename, 'r+b') - self.find_head_table() + self.find_head_table(0) self.flags = self.getshort('flags') self.lowppem = self.getshort('lowestRecPPEM') @@ -266,15 +283,19 @@ class font_patcher: try: source_font = TableHEADWriter(self.args.font) dest_font = TableHEADWriter(outfile) - 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() + 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: @@ -1292,8 +1313,9 @@ def main(): patcher = font_patcher(args) sourceFonts = [] - for subfont in fontforge.fontsInFile(args.font): - print("\n{}: Processing {}".format(projectName, subfont)) + all_fonts = fontforge.fontsInFile(args.font) + for i, subfont in enumerate(all_fonts): + print("\n{}: Processing {} ({}/{})".format(projectName, subfont, i + 1, len(all_fonts))) try: sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",)) except Exception: