chore: change batteries

This commit is contained in:
github-actions 2023-05-01 01:24:04 +00:00
parent c70c84402e
commit 5b946b0aca
6 changed files with 733 additions and 140 deletions

View file

@ -1,14 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf8 # coding=utf8
# Nerd Fonts Version: 2.3.3 # Nerd Fonts Version: 3.0.0
# Script version is further down # Script version is further down
from __future__ import absolute_import, print_function, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
# Change the script version when you edit this script: # Change the script version when you edit this script:
script_version = "3.7.1" script_version = "4.1.1"
version = "2.3.3" version = "3.0.0"
projectName = "Nerd Fonts" projectName = "Nerd Fonts"
projectNameAbbreviation = "NF" projectNameAbbreviation = "NF"
projectNameSingular = projectName[:-1] projectNameSingular = projectName[:-1]
@ -22,6 +22,7 @@ import errno
import subprocess import subprocess
import json import json
from enum import Enum from enum import Enum
import logging
try: try:
import configparser import configparser
except ImportError: except ImportError:
@ -239,10 +240,10 @@ def force_panose_monospaced(font):
panose = list(font.os2_panose) panose = list(font.os2_panose)
if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default) if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default)
panose[0] = 2 # make kind latin text and display panose[0] = 2 # make kind latin text and display
print(" Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')") logger.info("Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')")
font.os2_panose = tuple(panose) font.os2_panose = tuple(panose)
if panose[0] == 2 and panose[3] != 9: if panose[0] == 2 and panose[3] != 9:
print(" Setting Panose 'Proportion' to 'Monospaced' (was '{}')".format(panose_proportion_to_text(panose[3]))) logger.info("Setting Panose 'Proportion' to 'Monospaced' (was '%s')", panose_proportion_to_text(panose[3]))
panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced
font.os2_panose = tuple(panose) font.os2_panose = tuple(panose)
@ -296,10 +297,22 @@ def get_old_average_x_width(font):
} }
for g in weights: for g in weights:
if g not in font: if g not in font:
sys.exit("{}: Can not determine ancient style xAvgCharWidth".format(projectName)) logger.critical("Can not determine ancient style xAvgCharWidth")
sys.exit(1)
s += font[g].width * weights[g] s += font[g].width * weights[g]
return int(s / 1000) 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: class font_patcher:
def __init__(self, args): def __init__(self, args):
@ -311,6 +324,7 @@ class font_patcher:
self.font_dim = None # class 'dict' self.font_dim = None # class 'dict'
self.font_extrawide = False self.font_extrawide = False
self.source_monospaced = None # Later True or False self.source_monospaced = None # Later True or False
self.symbolsonly = False
self.onlybitmaps = 0 self.onlybitmaps = 0
self.essential = set() self.essential = set()
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
@ -336,7 +350,7 @@ class font_patcher:
# For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs # 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: if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2:
print("Very wide and short font, disabling 2 cell Powerline glyphs") logger.warning("Very wide and short font, disabling 2 cell Powerline glyphs")
self.font_extrawide = True self.font_extrawide = True
# Prevent opening and closing the fontforge font. Makes things faster when patching # Prevent opening and closing the fontforge font. Makes things faster when patching
@ -345,8 +359,12 @@ class font_patcher:
symfont = None symfont = None
if not os.path.isdir(self.args.glyphdir): if not os.path.isdir(self.args.glyphdir):
sys.exit("{}: Can not find symbol glyph directory {} " logger.critical("Can not find symbol glyph directory %s "
"(probably you need to download the src/glyphs/ directory?)".format(projectName, self.args.glyphdir)) "(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: for patch in self.patch_set:
if patch['Enabled']: if patch['Enabled']:
@ -356,11 +374,13 @@ class font_patcher:
symfont.close() symfont.close()
symfont = None symfont = None
if not os.path.isfile(self.args.glyphdir + patch['Filename']): if not os.path.isfile(self.args.glyphdir + patch['Filename']):
sys.exit("{}: Can not find symbol source for '{}'\n{:>{}} (i.e. {})".format( logger.critical("Can not find symbol source for '%s' (i.e. %s)",
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename'])) patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK): if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK):
sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format( logger.critical("Can not open symbol source for '%s' (i.e. %s)",
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename'])) patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename'])) symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename']))
symfont.encoding = 'UnicodeFull' symfont.encoding = 'UnicodeFull'
@ -402,11 +422,11 @@ class font_patcher:
break break
outfile = os.path.normpath(os.path.join( outfile = os.path.normpath(os.path.join(
sanitize_filename(self.args.outputdir, True), sanitize_filename(self.args.outputdir, True),
sanitize_filename(sourceFont.familyname) + ".ttc")) sanitize_filename(create_filename(sourceFonts)) + ".ttc"))
sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer) sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer)
message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile) message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile)
else: else:
fontname = sourceFont.fullname fontname = create_filename(sourceFonts)
if not fontname: if not fontname:
fontname = sourceFont.cidfontname fontname = sourceFont.cidfontname
outfile = os.path.normpath(os.path.join( outfile = os.path.normpath(os.path.join(
@ -414,9 +434,11 @@ class font_patcher:
sanitize_filename(fontname) + self.args.extension)) sanitize_filename(fontname) + self.args.extension))
bitmaps = str() bitmaps = str()
if len(self.sourceFont.bitmapSizes): if len(self.sourceFont.bitmapSizes):
if not self.args.quiet: logger.debug("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
bitmaps = str('otf') # otf/ttf, both is bf_ttf bitmaps = str('otf') # otf/ttf, both is bf_ttf
if self.args.dry_run:
logger.debug("=====> Filename '{}'".format(outfile))
return
sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags) sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags)
message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile) message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile)
@ -426,8 +448,7 @@ class font_patcher:
source_font = TableHEADWriter(self.args.font) source_font = TableHEADWriter(self.args.font)
dest_font = TableHEADWriter(outfile) dest_font = TableHEADWriter(outfile)
for idx in range(source_font.num_fonts): for idx in range(source_font.num_fonts):
if not self.args.quiet: logger.debug("Tweaking %d/%d", idx + 1, source_font.num_fonts)
print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts))
xwidth_s = '' xwidth_s = ''
xwidth = self.xavgwidth[idx] xwidth = self.xavgwidth[idx]
if isinstance(xwidth, int): if isinstance(xwidth, int):
@ -438,26 +459,23 @@ class font_patcher:
dest_font.find_table([b'OS/2'], idx) dest_font.find_table([b'OS/2'], idx)
d_xwidth = dest_font.getshort('avgWidth') d_xwidth = dest_font.getshort('avgWidth')
if d_xwidth != xwidth: if d_xwidth != xwidth:
if not self.args.quiet: logger.debug("Changing xAvgCharWidth from %d to %d%s", d_xwidth, xwidth, xwidth_s)
print("Changing xAvgCharWidth from {} to {}{}".format(d_xwidth, xwidth, xwidth_s))
dest_font.putshort(xwidth, 'avgWidth') dest_font.putshort(xwidth, 'avgWidth')
dest_font.reset_table_checksum() dest_font.reset_table_checksum()
source_font.find_head_table(idx) source_font.find_head_table(idx)
dest_font.find_head_table(idx) dest_font.find_head_table(idx)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0: if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
if not self.args.quiet: logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08)
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' dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
if source_font.lowppem != dest_font.lowppem: if source_font.lowppem != dest_font.lowppem:
if not self.args.quiet: logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem)
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM') dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified: if dest_font.modified:
dest_font.reset_table_checksum() dest_font.reset_table_checksum()
if dest_font.modified: if dest_font.modified:
dest_font.reset_full_checksum() dest_font.reset_full_checksum()
except Exception as error: except Exception as error:
print("Can not handle font flags ({})".format(repr(error))) logger.error("Can not handle font flags (%s)", repr(error))
finally: finally:
try: try:
source_font.close() source_font.close()
@ -465,12 +483,13 @@ class font_patcher:
except: except:
pass pass
if self.args.is_variable: if self.args.is_variable:
print("Warning: Source font is a variable open type font (VF) and the patch results will most likely not be what you want") logger.critical("Source font is a variable open type font (VF) and the patch results will most likely not be what you want")
print(message) print(message)
if self.args.postprocess: if self.args.postprocess:
subprocess.call([self.args.postprocess, outfile]) 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): def setup_name_backup(self, font):
@ -488,11 +507,8 @@ class font_patcher:
font.fullname = font.persistent["fullname"] font.fullname = font.persistent["fullname"]
if isinstance(font.persistent["familyname"], str): if isinstance(font.persistent["familyname"], str):
font.familyname = font.persistent["familyname"] font.familyname = font.persistent["familyname"]
verboseAdditionalFontNameSuffix = " " + projectNameSingular verboseAdditionalFontNameSuffix = ""
if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later additionalFontNameSuffix = ""
additionalFontNameSuffix = " " + projectNameAbbreviation
else:
additionalFontNameSuffix = verboseAdditionalFontNameSuffix
if not self.args.complete: if not self.args.complete:
# NOTE not all symbol fonts have appended their suffix here # NOTE not all symbol fonts have appended their suffix here
if self.args.fontawesome: if self.args.fontawesome:
@ -523,17 +539,24 @@ class font_patcher:
additionalFontNameSuffix += " WEA" additionalFontNameSuffix += " WEA"
verboseAdditionalFontNameSuffix += " Plus Weather Icons" verboseAdditionalFontNameSuffix += " Plus Weather Icons"
# if all source glyphs included simplify the name # add mono signifier to beginning of name suffix
else:
additionalFontNameSuffix = " " + projectNameSingular + " Complete"
verboseAdditionalFontNameSuffix = " " + projectNameSingular + " Complete"
# add mono signifier to end of name
if self.args.single: if self.args.single:
additionalFontNameSuffix += " M" variant_abbrev = "M"
verboseAdditionalFontNameSuffix += " Mono" variant_full = " Mono"
elif self.args.nonmono and not self.symbolsonly:
variant_abbrev = "P"
variant_full = " Propo"
else:
variant_abbrev = ""
variant_full = ""
if FontnameParserOK and self.args.makegroups: ps_suffix = projectNameAbbreviation + variant_abbrev + additionalFontNameSuffix
# add 'Nerd Font' to beginning of name suffix
verboseAdditionalFontNameSuffix = " " + projectNameSingular + variant_full + verboseAdditionalFontNameSuffix
additionalFontNameSuffix = " " + projectNameSingular + variant_full + additionalFontNameSuffix
if FontnameParserOK and self.args.makegroups > 0:
use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse
# Use fullname if it is 'equal' to the fontname # Use fullname if it is 'equal' to the fontname
if font.fullname: if font.fullname:
@ -545,12 +568,14 @@ class font_patcher:
# Gohu fontnames hide the weight, but the file names are ok... # Gohu fontnames hide the weight, but the file names are ok...
if parser_name.startswith('Gohu'): if parser_name.startswith('Gohu'):
parser_name = os.path.splitext(os.path.basename(self.args.font))[0] 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: 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.drop_for_powerline()
n.enable_short_families(True, "Noto") n.enable_short_families(True, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ])
n.set_for_windows(self.args.windows) 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 # All the following stuff is ignored in makegroups-mode
@ -598,23 +623,7 @@ class font_patcher:
if len(subFamily) == 0: if len(subFamily) == 0:
subFamily = "Regular" subFamily = "Regular"
if self.args.windows: familyname += " " + projectNameSingular + variant_full
maxFamilyLength = 31
maxFontLength = maxFamilyLength - len('-' + subFamily)
familyname += " " + projectNameAbbreviation
if self.args.single:
familyname += "M"
fullname += " Windows Compatible"
# now make sure less than 32 characters name length
if len(fontname) > maxFontLength:
fontname = fontname[:maxFontLength]
if len(familyname) > maxFamilyLength:
familyname = familyname[:maxFamilyLength]
else:
familyname += " " + projectNameSingular
if self.args.single:
familyname += " Mono"
# Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with # 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 # the same name as the same font, even if subFamily is different. Make sure to
@ -627,6 +636,10 @@ class font_patcher:
reservedFontNameReplacements = { reservedFontNameReplacements = {
'source' : 'sauce', 'source' : 'sauce',
'Source' : 'Sauce', 'Source' : 'Sauce',
'Bitstream Vera Sans Mono' : 'Bitstrom Wera',
'BitstreamVeraSansMono' : 'BitstromWera',
'bitstream vera sans mono' : 'bitstrom wera',
'bitstreamverasansmono' : 'bitstromwera',
'hermit' : 'hurmit', 'hermit' : 'hurmit',
'Hermit' : 'Hurmit', 'Hermit' : 'Hurmit',
'hasklig' : 'hasklug', 'hasklig' : 'hasklug',
@ -693,7 +706,7 @@ class font_patcher:
fullname = replace_font_name(fullname, additionalFontNameReplacements2) fullname = replace_font_name(fullname, additionalFontNameReplacements2)
fontname = replace_font_name(fontname, 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: # replace any extra whitespace characters:
font.familyname = " ".join(familyname.split()) font.familyname = " ".join(familyname.split())
font.fullname = " ".join(fullname.split()) font.fullname = " ".join(fullname.split())
@ -704,13 +717,9 @@ class font_patcher:
font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname) font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname)
font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily)
else: else:
fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full
if self.args.single: # inject_suffix(family, ps_fontname, short_family)
if self.args.windows: n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family)
fam_suffix += 'M'
else:
fam_suffix += ' Mono'
n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix)
n.rename_font(font) n.rename_font(font)
font.comment = projectInfo font.comment = projectInfo
@ -726,6 +735,7 @@ class font_patcher:
self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version
self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge
self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version) 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)) # print("Version now is {}".format(sourceFont.version))
@ -734,17 +744,17 @@ class font_patcher:
# the tables have been removed from the repo with >this< commit # the tables have been removed from the repo with >this< commit
if self.args.configfile and self.config.read(self.args.configfile): if self.args.configfile and self.config.read(self.args.configfile):
if self.args.removeligatures: 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")) ligature_subtables = json.loads(self.config.get("Subtables", "ligatures"))
for subtable in ligature_subtables: for subtable in ligature_subtables:
print("Removing subtable:", subtable) logger.debug("Removing subtable: %s", subtable)
try: try:
self.sourceFont.removeLookupSubtable(subtable) self.sourceFont.removeLookupSubtable(subtable)
print("Successfully removed subtable:", subtable) logger.debug("Successfully removed subtable: %s", subtable)
except Exception: except Exception:
print("Failed to remove subtable:", subtable) logger.error("Failed to remove subtable: %s", subtable)
elif self.args.removeligatures: elif self.args.removeligatures:
print("Unable to read configfile, unable to remove ligatures") logger.error("Unable to read configfile, unable to remove ligatures")
def assert_monospace(self): def assert_monospace(self):
@ -756,16 +766,17 @@ class font_patcher:
panose_mono = check_panose_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' # 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): if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1):
print(" Warning: Monospaced check: Panose assumed to be wrong") logger.warning("Monospaced check: Panose assumed to be wrong")
print(" {} and {}".format( logger.warning(" %s and %s",
report_advance_widths(self.sourceFont), report_advance_widths(self.sourceFont),
panose_check_to_text(panose_mono, self.sourceFont.os2_panose))) panose_check_to_text(panose_mono, self.sourceFont.os2_panose))
if self.args.single and not width_mono: if self.args.single and not width_mono:
print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless") logger.warning("Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless")
if offending_char is not None: if offending_char is not None:
print(" Offending char: 0x{:X}".format(offending_char)) logger.warning(" Offending char: %X", offending_char)
if self.args.single <= 1: 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: if width_mono:
force_panose_monospaced(self.sourceFont) force_panose_monospaced(self.sourceFont)
@ -781,9 +792,9 @@ class font_patcher:
box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs)) box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs))
if box_glyphs_target > box_glyphs_current: if box_glyphs_target > box_glyphs_current:
# Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing) # Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing)
if not self.args.quiet and box_glyphs_current > 0: if box_glyphs_current > 0:
print("INFO: {}/{} box drawing glyphs will be replaced".format( logger.debug("%d/%d box drawing glyphs will be replaced",
box_glyphs_current, box_glyphs_target)) box_glyphs_current, box_glyphs_target)
box_enabled = True box_enabled = True
else: else:
# Sourcefont does have all of these glyphs # Sourcefont does have all of these glyphs
@ -1020,14 +1031,14 @@ class font_patcher:
{'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize {'Enabled': self.args.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': 0x23FB, 'SymEnd': 0x23FE, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Power, Power On/Off, Power On, Sleep
{'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off) {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off)
{'Enabled': self.args.material, 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': False , 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesign/MaterialDesignIconsDesktop.ttf", 'Exact': True, 'SymStart': 0xF0001,'SymEnd': 0xF1AF0,'SrcStart': None, 'ScaleRules': MDI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.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.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.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF27C, 'SrcStart': 0xF4A9, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Desktop {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 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.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} {'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR}
] ]
@ -1102,9 +1113,20 @@ class font_patcher:
our_btb = typo_btb if use_typo else win_btb our_btb = typo_btb if use_typo else win_btb
if our_btb == hhea_btb: if our_btb == hhea_btb:
metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font 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:
logger.warning("Font vertical metrics probably wrong USE TYPO METRICS, assume opposite (i.e. %s)", 'True' if not use_typo else 'False')
use_typo = not use_typo
self.sourceFont.os2_use_typo_metrics = 1 if use_typo else 0
metrics = Metric.TYPO if use_typo else Metric.WIN
else: else:
# We trust the WIN metric more, see experiments in #1056 # 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)) logger.warning("Font vertical metrics inconsistent (HHEA %d / TYPO %d / WIN %d), using WIN", hhea_btb, typo_btb, win_btb)
our_btb = win_btb our_btb = win_btb
metrics = Metric.WIN metrics = Metric.WIN
@ -1129,6 +1151,7 @@ class font_patcher:
if self.font_dim['height'] == 0: if self.font_dim['height'] == 0:
# This can only happen if the input font is empty # This can only happen if the input font is empty
# Assume we are using our prepared templates # Assume we are using our prepared templates
self.symbolsonly = True
self.font_dim = { self.font_dim = {
'xmin' : 0, 'xmin' : 0,
'ymin' : -self.sourceFont.descent, 'ymin' : -self.sourceFont.descent,
@ -1139,7 +1162,8 @@ class font_patcher:
} }
our_btb = self.sourceFont.descent + self.sourceFont.ascent our_btb = self.sourceFont.descent + self.sourceFont.ascent
elif self.font_dim['height'] < 0: elif self.font_dim['height'] < 0:
sys.exit("{}: Can not detect sane font height".format(projectName)) logger.critical("Can not detect sane font height")
sys.exit(1)
# Make all metrics equal # Make all metrics equal
self.sourceFont.os2_typolinegap = 0 self.sourceFont.os2_typolinegap = 0
@ -1153,12 +1177,13 @@ class font_patcher:
self.sourceFont.os2_use_typo_metrics = 1 self.sourceFont.os2_use_typo_metrics = 1
(check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont) (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: if check_hhea_btb != check_typo_btb or check_typo_btb != check_win_btb or check_win_btb != our_btb:
sys.exit("{}: Error in baseline to baseline code detected".format(projectName)) logger.critical("Error in baseline to baseline code detected")
sys.exit(1)
# Step 2 # Step 2
# Find the biggest char width and advance width # Find the biggest char width and advance width
# 0x00-0x17f is the Latin Extended-A range # 0x00-0x17f is the Latin Extended-A range
warned1 = self.args.quiet or self.args.nonmono # Do not warn if quiet or proportional target warned1 = self.args.nonmono # Do not warn if proportional target
warned2 = warned1 warned2 = warned1
for glyph in range(0x21, 0x17f): for glyph in range(0x21, 0x17f):
if glyph in range(0x7F, 0xBF) or glyph in [ if glyph in range(0x7F, 0xBF) or glyph in [
@ -1176,19 +1201,18 @@ class font_patcher:
if self.font_dim['width'] < self.sourceFont[glyph].width: if self.font_dim['width'] < self.sourceFont[glyph].width:
self.font_dim['width'] = self.sourceFont[glyph].width self.font_dim['width'] = self.sourceFont[glyph].width
if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Warning: Extended glyphs wider than basic glyphs, results might be useless\n {}".format( logger.debug("Extended glyphs wider than basic glyphs, results might be useless\n %s",
report_advance_widths(self.sourceFont))) report_advance_widths(self.sourceFont))
warned1 = True warned1 = True
# print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) # print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if xmax > self.font_dim['xmax']: if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax self.font_dim['xmax'] = xmax
if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Info: Extended glyphs wider bounding box than basic glyphs") logger.debug("Extended glyphs wider bounding box than basic glyphs")
warned2 = True warned2 = True
# print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) # print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if self.font_dim['width'] < self.font_dim['xmax']: if self.font_dim['width'] < self.font_dim['xmax']:
if not self.args.quiet: logger.debug("Font has negative right side bearing in extended glyphs")
print("Warning: Font has negative right side bearing in extended glyphs")
self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used
# print("FINAL", self.font_dim) # print("FINAL", self.font_dim)
@ -1288,7 +1312,7 @@ class font_patcher:
if sym_glyph.altuni: if sym_glyph.altuni:
possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ] possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ]
if len(possible_codes) == 0: if len(possible_codes) == 0:
print(" Can not determine codepoint of {:X}. Skipping...".format(sym_glyph.unicode)) logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode)
continue continue
currentSourceFontGlyph = min(possible_codes) currentSourceFontGlyph = min(possible_codes)
else: else:
@ -1311,9 +1335,8 @@ class font_patcher:
# check if a glyph already exists in this location # check if a glyph already exists in this location
if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential: if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential:
if currentSourceFontGlyph in self.sourceFont: if currentSourceFontGlyph in self.sourceFont:
if not self.args.quiet:
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing' careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
print(" Found {} Glyph at {:X}. Skipping...".format(careful_type, currentSourceFontGlyph)) logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph)
# We don't want to touch anything so move to next Glyph # We don't want to touch anything so move to next Glyph
continue continue
else: else:
@ -1461,8 +1484,8 @@ class font_patcher:
if self.args.single: if self.args.single:
(xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox() (xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox()
if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)): 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( logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %f))",
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)) currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)
# end for # end for
@ -1603,7 +1626,7 @@ def half_gap(gap, top):
gap_top = int(gap / 2) gap_top = int(gap / 2)
gap_bottom = gap - gap_top gap_bottom = gap - gap_top
if top: if top:
print("Redistributing line gap of {} ({} top and {} bottom)".format(gap, gap_top, gap_bottom)) logger.info("Redistributing line gap of %d (%d top and %d bottom)", gap, gap_top, gap_bottom)
return gap_top return gap_top
return gap_bottom return gap_bottom
@ -1728,8 +1751,8 @@ def check_fontforge_min_version():
# versions tested: 20150612, 20150824 # versions tested: 20150612, 20150824
if actualVersion < minimumVersion: if actualVersion < minimumVersion:
sys.stderr.write("{}: You seem to be using an unsupported (old) version of fontforge: {}\n".format(projectName, actualVersion)) logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion)
sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion)) logger.critical("Please use at least version: %d", minimumVersion)
sys.exit(1) sys.exit(1)
def check_version_with_git(version): def check_version_with_git(version):
@ -1777,7 +1800,6 @@ def setup_arguments():
parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='count', help='Whether to generate the glyphs as single-width not double-width (default is double-width)') parser.add_argument('-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('-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('-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('-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('--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('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specificed in JSON configuration file')
@ -1787,15 +1809,28 @@ 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('-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('-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('--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('--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 # 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 = 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') 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.set_defaults(progressbars=True)
parser.add_argument('--also-windows', dest='alsowindows', default=False, action='store_true', help='Create two fonts, the normal and the --windows version') parser.add_argument('--debug', dest='debugmode', default=False, action='store_true', help='Verbose mode')
parser.add_argument('--dry', dest='dry_run', default=False, action='store_true', help='Do neither patch nor store the font, to check naming')
parser.add_argument('--xavgcharwidth', dest='xavgwidth', default=None, type=int, nargs='?', help='Adjust xAvgCharWidth (optional: concrete value)', const=True) 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 # --xavgcharwidth for compatibility with old applications like notepad and non-latin fonts
# Possible values with examples: # Possible values with examples:
@ -1819,8 +1854,9 @@ def setup_arguments():
args = parser.parse_args() args = parser.parse_args()
if args.makegroups and not FontnameParserOK: if args.makegroups > 0 and not FontnameParserOK:
sys.exit("{}: FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName)) 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 you add a new font, set it to True here inside the if condition
if args.complete: if args.complete:
@ -1853,24 +1889,23 @@ def setup_arguments():
font_complete = False font_complete = False
args.complete = font_complete args.complete = font_complete
if args.alsowindows:
args.windows = False
if args.nonmono and args.single: if args.nonmono and args.single:
print("Warning: Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.") logging.warning("Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.")
args.nonmono = False args.nonmono = False
make_sure_path_exists(args.outputdir) make_sure_path_exists(args.outputdir)
if not os.path.isfile(args.font): 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): 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 is_ttc = len(fontforge.fontsInFile(args.font)) > 1
try: try:
source_font_test = TableHEADWriter(args.font) 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) 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: if args.is_variable:
print(" Warning: Source font is a variable open type font (VF), opening might fail...") logging.warning("Source font is a variable open type font (VF), opening might fail...")
except: except:
args.is_variable = False args.is_variable = False
finally: finally:
@ -1885,16 +1920,20 @@ def setup_arguments():
args.extension = '.' + args.extension args.extension = '.' + args.extension
if re.match("\.ttc$", args.extension, re.IGNORECASE): if re.match("\.ttc$", args.extension, re.IGNORECASE):
if not is_ttc: 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: else:
if is_ttc: 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 isinstance(args.xavgwidth, int) and not isinstance(args.xavgwidth, bool):
if args.xavgwidth < 0: if args.xavgwidth < 0:
sys.exit(projectName + ": --xavgcharwidth takes no negative numbers") logging.critical("--xavgcharwidth takes no negative numbers")
sys.exit(2)
if args.xavgwidth > 16384: if args.xavgwidth > 16384:
sys.exit(projectName + ": --xavgcharwidth takes only numbers up to 16384") logging.critical("--xavgcharwidth takes only numbers up to 16384")
sys.exit(2)
return args return args
@ -1902,24 +1941,43 @@ def setup_arguments():
def main(): def main():
global version global version
git_version = check_version_with_git(version) git_version = check_version_with_git(version)
print("{} Patcher v{} ({}) (ff {}) executing".format( allversions = "Patcher v{} ({}) (ff {})".format(
projectName, git_version if git_version else version, script_version, fontforge.version())) git_version if git_version else version, script_version, fontforge.version())
print("{} {}".format(projectName, allversions))
if git_version: if git_version:
version = git_version version = git_version
check_fontforge_min_version() check_fontforge_min_version()
args = setup_arguments() args = setup_arguments()
global logger
logger = logging.getLogger(os.path.basename(args.font))
logger.setLevel(logging.DEBUG)
f_handler = logging.FileHandler('font-patcher-log.txt')
f_handler.setFormatter(logging.Formatter('%(levelname)s: %(name)s %(message)s'))
logger.addHandler(f_handler)
logger.debug(allversions)
logger.debug("Options %s", repr(sys.argv[1:]))
c_handler = logging.StreamHandler(stream=sys.stdout)
c_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
if not args.debugmode:
c_handler.setLevel(logging.INFO)
logger.addHandler(c_handler)
logger.debug("Naming mode %d", args.makegroups)
patcher = font_patcher(args) patcher = font_patcher(args)
sourceFonts = [] sourceFonts = []
all_fonts = fontforge.fontsInFile(args.font) all_fonts = fontforge.fontsInFile(args.font)
for i, subfont in enumerate(all_fonts): for i, subfont in enumerate(all_fonts):
if len(all_fonts) > 1: if len(all_fonts) > 1:
print("\n{}: Processing {} ({}/{})".format(projectName, subfont, i + 1, len(all_fonts))) print("\n")
logger.info("Processing %s (%d/%d)", subfont, i + 1, len(all_fonts))
try: try:
sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",)) sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",))
except Exception: except Exception:
sys.exit("{}: Can not open font '{}', try to open with fontforge interactively to get more information".format( logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information",
projectName, subfont)) subfont)
sys.exit(1)
patcher.patch(sourceFonts[-1]) patcher.patch(sourceFonts[-1])
@ -1928,13 +1986,6 @@ def main():
patcher.setup_font_names(f) patcher.setup_font_names(f)
patcher.generate(sourceFonts) 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: for f in sourceFonts:
f.close() f.close()

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python3
# coding=utf8
# This extracts the names and source and destination codepoints
# of the old octicons font file to keep their codepoints stable
#
# You do not need to redo it, the result is in the repo
#
# Usage:
# fontforge analyze_octicons > mapping
import fontforge
octi_orig = "octicons.ttf"
current_cp = 0xF400
print('# Examining {}'.format(octi_orig))
font = fontforge.open(octi_orig)
for glyph in font.glyphs('encoding'):
point = glyph.unicode
if point < 0:
continue
desti = glyph.unicode
if point < 0xF000:
desti = point
else:
desti = current_cp
current_cp += 1
print("{:X} {:X} {}".format(point, desti, glyph.glyphname))
font.close()

180
src/glyphs/octicons/generate Executable file
View file

@ -0,0 +1,180 @@
#!/usr/bin/env python3
# coding=utf8
import sys
import os
import re
import subprocess
import fontforge
# Double-quotes required here, for version-bump.sh:
version = "2.3.3"
archive = 'v18.3.0.tar.gz'
vectorsdir = 'icons'
fontdir = '.'
fontfile = 'octicons.ttf'
glyphsetfile = 'i_oct.sh'
glyphsetsdir = '../../../bin/scripts/lib'
subset = '-16' # use 16 px subset if possible
subset_other = '-24' # use 24 px subset otherwise
def renamer(old_name):
""" Return new equivalent icon name """
return {
'trashcan' : 'trash',
'cloud-download' : 'download',
'cloud-upload' : 'upload',
'clippy' : 'paste',
'mail-read' : 'read',
'primitive-dot' : 'dot-fill',
'primitive-square' : 'square-fill',
'settings' : 'sliders',
'dashboard' : 'meter',
'paintcan' : 'paintbrush',
}.get(old_name, old_name)
def addIcon(codepoint, name, filename):
""" Add one outline file and rescale/move """
dBB = [120, 0, 1000-120, 900] # just some nice sizes
filename = os.path.join(vectorsdir, filename)
glyph = font.createChar(codepoint, name)
glyph.importOutlines(filename)
glyph.manualHints = True
def createGlyphInfo(icon_datasets, filepathname, into):
""" Write the glyphinfo file """
with open(filepathname, 'w', encoding = 'utf8') as f:
f.write(u'#!/usr/bin/env bash\n')
f.write(intro)
f.write(u'# Script Version: (autogenerated)\n')
f.write(u'test -n "$__i_oct_loaded" && return || __i_oct_loaded=1\n')
for _, codepoint, name in icon_datasets:
codepoint = int(codepoint, 16)
f.write(u"i='{}' i_oct_{}=$i\n".format(chr(codepoint), name.replace('-', '_')))
f.write(u'unset i\n')
print('\nReading mapping file')
old_mapping = []
with open('mapping', 'r') as f:
for line in f.readlines():
if line.startswith('#'):
continue
old_mapping.append(tuple(re.split(' +', line.strip())))
print('Found {} entries'.format(len(old_mapping)))
old_mapping.sort(key=(lambda x: x[0]))
print('Fetching octicons archive "{}"\n'.format(archive))
if subprocess.call('curl -OL https://github.com/primer/octicons/archive/' + archive, shell=True):
sys.exit('Error fetching octicons archive')
print('\nUnpacking octicons archive')
if subprocess.call('rm -rf icons octicons-* && tar zxf *.gz && mv octicons-*/icons . && rm -rf octicons-*', shell=True):
sys.exit('Error unpacking archive')
svgs = os.listdir(vectorsdir)
print('Found {} svgs'.format(len(svgs)))
names = { s[0:-len('-xx.svg')] for s in svgs if s.endswith(subset + '.svg') or s.endswith(subset_other + '.svg') }
print('Found {} icons after de-duplicating\n'.format(len(names)))
num_found = 0
num_missing = 0
misslist = ''
renamelist = ''
freeslots = []
new_mapping = []
for i, j, old_n in old_mapping:
if old_n in names:
names.remove(old_n)
new_mapping.append((i, j, old_n))
num_found += 1
continue
new_n = renamer(old_n)
if new_n in names:
renamelist += 'Renamed {} -> {}\n'.format(old_n, new_n)
names.remove(new_n)
new_mapping.append((i, j, new_n))
num_found += 1
continue
misslist += 'Missing {}\n'.format(old_n)
freeslots.append((i, j))
num_missing += 1
print(renamelist)
print(misslist)
print('Found {} (of {}, missing {}) and new {}'.format(num_found, len(old_mapping), num_missing, len(names)))
names = list(names)
names.sort()
for n in list(names):
if len(freeslots) == 0:
break
i, j = freeslots[0]
new_mapping.append((i, j, n))
names.remove(n)
freeslots = freeslots[1:]
print('Filled in missing, remaining new {}'.format(len(names)))
i_max = 0
j_max = 0
for i, j, _ in new_mapping:
i = int(i, 16)
j = int(j, 16)
if i > i_max:
i_max = i
if j > j_max:
j_max = j
for n in names:
i_max += 1
j_max += 1
new_mapping.append(('{:X}'.format(i_max), '{:X}'.format(j_max), n))
print('Appended remaining new, total new mapping {}'.format(len(new_mapping)))
new_mapping.sort(key=(lambda x: x[0]))
with open('mapping', 'w') as f:
for i, j, n in new_mapping:
f.write('{} {} {}\n'.format(i, j, n))
font = fontforge.font()
font.fontname = 'OcticonsNerdFont-Regular'
font.fullname = 'Octicons Nerd Font Regular'
font.familyname = 'Octicons Nerd Font'
font.em = 2048
font.encoding = 'UnicodeFull'
# Add valid space glyph to avoid "unknown character" box on IE11
glyph = font.createChar(32)
glyph.width = 200
font.sfntRevision = None # Auto-set (refreshed) by fontforge
font.version = version
font.copyright = 'GitHub Inc.'
font.appendSFNTName('English (US)', 'Version', archive + '; ' + version)
font.appendSFNTName('English (US)', 'Vendor URL', 'https://github.com/ryanoasis/nerd-fonts')
font.appendSFNTName('English (US)', 'Copyright', 'GitHub Inc.')
for codepoint, _, name in new_mapping:
codepoint = int(codepoint, 16)
filename = name + subset + '.svg'
if filename not in svgs:
filename = name + subset_other + '.svg'
addIcon(codepoint, name, filename)
num_icons = len(new_mapping)
print('Generating {} with {} glyphs'.format(fontfile, num_icons))
font.generate(os.path.join(fontdir, fontfile), flags=("no-FFTM-table",))
codepoints = [ int(p, 16) for _, p, _ in new_mapping ]
intro = u'# Octicons ({} icons)\n'.format(num_icons)
intro += u'# Codepoints: {:X}-{:X} with gaps\n'.format(min(codepoints), max(codepoints))
intro += u'# Nerd Fonts Version: {}\n'.format(version)
print('Generating GlyphInfo {}'.format(glyphsetfile))
createGlyphInfo(new_mapping, os.path.join(glyphsetsdir, glyphsetfile), intro)
print('Finished')

309
src/glyphs/octicons/mapping Normal file
View file

@ -0,0 +1,309 @@
2665 2665 heart
26A1 26A1 zap
F000 F400 light-bulb
F001 F401 repo
F002 F402 repo-forked
F005 F403 repo-push
F006 F404 repo-pull
F007 F405 book
F008 F406 accessibility
F009 F407 git-pull-request
F00A F408 mark-github
F00B F409 download
F00C F40A upload
F00D F40B accessibility-inset
F00E F40C alert-fill
F010 F40D file-code
F011 F40E apps
F012 F40F file-media
F013 F410 file-zip
F014 F411 archive
F015 F412 tag
F016 F413 file-directory
F017 F414 file-submodule
F018 F415 person
F019 F416 arrow-both
F01F F417 git-commit
F020 F418 git-branch
F023 F419 git-merge
F024 F41A mirror
F026 F41B issue-opened
F027 F41C issue-reopened
F028 F41D issue-closed
F02A F41E star
F02B F41F comment
F02C F420 question
F02D F421 alert
F02E F422 search
F02F F423 gear
F030 F424 arrow-down-left
F031 F425 tools
F032 F426 sign-out
F033 F427 rocket
F034 F428 rss
F035 F429 paste
F036 F42A sign-in
F037 F42B organization
F038 F42C device-mobile
F039 F42D unfold
F03A F42E check
F03B F42F mail
F03C F430 read
F03D F431 arrow-up
F03E F432 arrow-right
F03F F433 arrow-down
F040 F434 arrow-left
F041 F435 pin
F042 F436 gift
F043 F437 graph
F044 F438 triangle-left
F045 F439 credit-card
F046 F43A clock
F047 F43B ruby
F048 F43C broadcast
F049 F43D key
F04A F43E arrow-down-right
F04C F43F repo-clone
F04D F440 diff
F04E F441 eye
F04F F442 comment-discussion
F051 F443 arrow-switch
F052 F444 dot-fill
F053 F445 square-fill
F056 F446 device-camera
F057 F447 device-camera-video
F058 F448 pencil
F059 F449 info
F05A F44A triangle-right
F05B F44B triangle-down
F05C F44C link
F05D F44D plus
F05E F44E three-bars
F05F F44F code
F060 F450 location
F061 F451 list-unordered
F062 F452 list-ordered
F063 F453 quote
F064 F454 versions
F068 F455 calendar
F06A F456 lock
F06B F457 diff-added
F06C F458 diff-removed
F06D F459 diff-modified
F06E F45A diff-renamed
F070 F45B horizontal-rule
F071 F45C arrow-up-left
F075 F45D milestone
F076 F45E checklist
F077 F45F megaphone
F078 F460 chevron-right
F07B F461 bookmark
F07C F462 sliders
F07D F463 meter
F07E F464 history
F07F F465 link-external
F080 F466 mute
F081 F467 x
F084 F468 circle-slash
F085 F469 pulse
F087 F46A sync
F088 F46B telescope
F08C F46C arrow-up-right
F08D F46D home
F08F F46E stop
F091 F46F bug
F092 F470 logo-github
F094 F471 file-binary
F096 F472 database
F097 F473 server
F099 F474 diff-ignored
F09A F475 ellipsis
F09C F476 bell-fill
F09D F477 hubot
F09F F478 bell-slash
F0A0 F479 blocked
F0A1 F47A bookmark-fill
F0A2 F47B chevron-up
F0A3 F47C chevron-down
F0A4 F47D chevron-left
F0AA F47E triangle-up
F0AC F47F git-compare
F0AD F480 logo-gist
F0B0 F481 file-symlink-file
F0B1 F482 bookmark-slash
F0B2 F483 squirrel
F0B6 F484 globe
F0BA F485 unmute
F0BE F486 mention
F0C4 F487 package
F0C5 F488 browser
F0C8 F489 terminal
F0C9 F48A markdown
F0CA F48B dash
F0CC F48C fold
F0CF F48D inbox
F0D0 F48E trash
F0D1 F48F paintbrush
F0D2 F490 flame
F0D3 F491 briefcase
F0D4 F492 plug
F0D6 F493 bookmark-slash-fill
F0D7 F494 mortar-board
F0D8 F495 law
F0DA F496 thumbsup
F0DB F497 thumbsdown
F0DC F498 desktop-download
F0DD F499 beaker
F0DE F49A bell
F0E0 F49B cache
F0E1 F49C shield
F0E2 F49D bold
F0E3 F49E check-circle
F0E4 F49F italic
F0E5 F4A0 tasklist
F0E6 F4A1 verified
F0E7 F4A2 smiley
F0E8 F4A3 unverified
F101 F4A4 check-circle-fill
F102 F4A5 file
F103 F4A6 grabber
F104 F4A7 checkbox
F105 F4A8 reply
F27C F4A9 device-desktop
F27D F4AA circle
F27E F4AB clock-fill
F27F F4AC cloud
F280 F4AD cloud-offline
F281 F4AE code-of-conduct
F282 F4AF code-review
F283 F4B0 code-square
F284 F4B1 codescan
F285 F4B2 codescan-checkmark
F286 F4B3 codespaces
F287 F4B4 columns
F288 F4B5 command-palette
F289 F4B6 commit
F28A F4B7 container
F28B F4B8 copilot
F28C F4B9 copilot-error
F28D F4BA copilot-warning
F28E F4BB copy
F28F F4BC cpu
F290 F4BD cross-reference
F291 F4BE dependabot
F292 F4BF diamond
F293 F4C0 discussion-closed
F294 F4C1 discussion-duplicate
F295 F4C2 discussion-outdated
F296 F4C3 dot
F297 F4C4 duplicate
F298 F4C5 eye-closed
F299 F4C6 feed-discussion
F29A F4C7 feed-forked
F29B F4C8 feed-heart
F29C F4C9 feed-merged
F29D F4CA feed-person
F29E F4CB feed-repo
F29F F4CC feed-rocket
F2A0 F4CD feed-star
F2A1 F4CE feed-tag
F2A2 F4CF feed-trophy
F2A3 F4D0 file-added
F2A4 F4D1 file-badge
F2A5 F4D2 file-diff
F2A6 F4D3 file-directory-fill
F2A7 F4D4 file-directory-open-fill
F2A8 F4D5 file-moved
F2A9 F4D6 file-removed
F2AA F4D7 filter
F2AB F4D8 fiscal-host
F2AC F4D9 fold-down
F2AD F4DA fold-up
F2AE F4DB git-merge-queue
F2AF F4DC git-pull-request-closed
F2B0 F4DD git-pull-request-draft
F2B1 F4DE goal
F2B2 F4DF hash
F2B3 F4E0 heading
F2B4 F4E1 heart-fill
F2B5 F4E2 home-fill
F2B6 F4E3 hourglass
F2B7 F4E4 id-badge
F2B8 F4E5 image
F2B9 F4E6 infinity
F2BA F4E7 issue-draft
F2BB F4E8 issue-tracked-by
F2BC F4E9 issue-tracks
F2BD F4EA iterations
F2BE F4EB kebab-horizontal
F2BF F4EC key-asterisk
F2C0 F4ED log
F2C1 F4EE moon
F2C2 F4EF move-to-bottom
F2C3 F4F0 move-to-end
F2C4 F4F1 move-to-start
F2C5 F4F2 move-to-top
F2C6 F4F3 multi-select
F2C7 F4F4 no-entry
F2C8 F4F5 north-star
F2C9 F4F6 note
F2CA F4F7 number
F2CB F4F8 package-dependencies
F2CC F4F9 package-dependents
F2CD F4FA paper-airplane
F2CE F4FB paperclip
F2CF F4FC passkey-fill
F2D0 F4FD people
F2D1 F4FE person-add
F2D2 F4FF person-fill
F2D3 F500 play
F2D4 F501 plus-circle
F2D5 F502 project
F2D6 F503 project-roadmap
F2D7 F504 project-symlink
F2D8 F505 project-template
F2D9 F506 rel-file-path
F2DA F507 repo-deleted
F2DB F508 repo-locked
F2DC F509 repo-template
F2DD F50A report
F2DE F50B rows
F2DF F50C screen-full
F2E0 F50D screen-normal
F2E1 F50E share
F2E2 F50F share-android
F2E3 F510 shield-check
F2E4 F511 shield-lock
F2E5 F512 shield-slash
F2E6 F513 shield-x
F2E7 F514 sidebar-collapse
F2E8 F515 sidebar-expand
F2E9 F516 single-select
F2EA F517 skip
F2EB F518 skip-fill
F2EC F519 sort-asc
F2ED F51A sort-desc
F2EE F51B sparkle-fill
F2EF F51C sponsor-tiers
F2F0 F51D square
F2F1 F51E stack
F2F2 F51F star-fill
F2F3 F520 stopwatch
F2F4 F521 strikethrough
F2F5 F522 sun
F2F6 F523 tab
F2F7 F524 tab-external
F2F8 F525 table
F2F9 F526 telescope-fill
F2FA F527 trophy
F2FB F528 typography
F2FC F529 unlink
F2FD F52A unlock
F2FE F52B unread
F2FF F52C video
F300 F52D webhook
F301 F52E workflow
F302 F52F x-circle
F303 F530 x-circle-fill
F304 F531 zoom-in
F305 F532 zoom-out

Binary file not shown.