1
0
mirror of https://github.com/ryanoasis/nerd-fonts.git synced 2024-12-19 20:12:52 +02:00
nerd-fonts/font-patcher
Fini Jastrow 334238d030 Start release v3.0.0
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2023-04-27 21:57:22 +02:00

1985 lines
106 KiB
Python
Executable File

#!/usr/bin/env python
# coding=utf8
# Nerd Fonts Version: 3.0.0
# Script version is further down
from __future__ import absolute_import, print_function, unicode_literals
# Change the script version when you edit this script:
script_version = "4.1.0"
version = "3.0.0"
projectName = "Nerd Fonts"
projectNameAbbreviation = "NF"
projectNameSingular = projectName[:-1]
import sys
import re
import os
import argparse
from argparse import RawTextHelpFormatter
import errno
import subprocess
import json
from enum import Enum
import logging
try:
import configparser
except ImportError:
sys.exit(projectName + ": configparser module is probably not installed. Try `pip install configparser` or equivalent")
try:
import psMat
import fontforge
except ImportError:
sys.exit(
projectName + (
": FontForge module could not be loaded. Try installing fontforge python bindings "
"[e.g. on Linux Debian or Ubuntu: `sudo apt install fontforge python3-fontforge`]"
)
)
sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/bin/scripts/name_parser/')
try:
from FontnameParser import FontnameParser
from FontnameTools import FontnameTools
FontnameParserOK = True
except ImportError:
FontnameParserOK = False
class TableHEADWriter:
""" Access to the HEAD table without external dependencies """
def getlong(self, pos = None):
""" Get four bytes from the font file as integer number """
if pos:
self.goto(pos)
return (ord(self.f.read(1)) << 24) + (ord(self.f.read(1)) << 16) + (ord(self.f.read(1)) << 8) + ord(self.f.read(1))
def getshort(self, pos = None):
""" Get two bytes from the font file as integer number """
if pos:
self.goto(pos)
return (ord(self.f.read(1)) << 8) + ord(self.f.read(1))
def putlong(self, num, pos = None):
""" Put number as four bytes into font file """
if pos:
self.goto(pos)
self.f.write(bytearray([(num >> 24) & 0xFF, (num >> 16) & 0xFF ,(num >> 8) & 0xFF, num & 0xFF]))
self.modified = True
def putshort(self, num, pos = None):
""" Put number as two bytes into font file """
if pos:
self.goto(pos)
self.f.write(bytearray([(num >> 8) & 0xFF, num & 0xFF]))
self.modified = True
def calc_checksum(self, start, end, checksum = 0):
""" Calculate a font table checksum, optionally ignoring another embedded checksum value (for table 'head') """
self.f.seek(start)
for i in range(start, end - 4, 4):
checksum += self.getlong()
checksum &= 0xFFFFFFFF
i += 4
extra = 0
for j in range(4):
extra = extra << 8
if i + j <= end:
extra += ord(self.f.read(1))
checksum = (checksum + extra) & 0xFFFFFFFF
return checksum
def find_table(self, tablenames, idx):
""" Search all tables for one of the tables in tablenames and store its metadata """
# Use font with index idx if this is a font collection file
self.f.seek(0, 0)
tag = self.f.read(4)
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)
for i in range(numtables):
tab_name = self.f.read(4)
self.tab_check_offset = self.f.tell()
self.tab_check = self.getlong()
self.tab_offset = self.getlong()
self.tab_length = self.getlong()
if tab_name in tablenames:
return True
return False
def find_head_table(self, idx):
""" Search all tables for the HEAD table and store its metadata """
# Use font with index idx if this is a font collection file
found = self.find_table([ b'head' ], idx)
if not found:
raise Exception('No HEAD table found in font idx {}'.format(idx))
def goto(self, where):
""" Go to a named location in the file or to the specified index """
if isinstance(where, str):
positions = {'checksumAdjustment': 2+2+4,
'flags': 2+2+4+4+4,
'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2,
'avgWidth': 2,
}
where = self.tab_offset + positions[where]
self.f.seek(where)
def calc_full_checksum(self, check = False):
""" Calculate the whole file's checksum """
self.f.seek(0, 2)
self.end = self.f.tell()
full_check = self.calc_checksum(0, self.end, (-self.checksum_adj) & 0xFFFFFFFF)
if check and (0xB1B0AFBA - full_check) & 0xFFFFFFFF != self.checksum_adj:
sys.exit("Checksum of whole font is bad")
return full_check
def calc_table_checksum(self, check = False):
tab_check_new = self.calc_checksum(self.tab_offset, self.tab_offset + self.tab_length - 1, (-self.checksum_adj) & 0xFFFFFFFF)
if check and tab_check_new != self.tab_check:
sys.exit("Checksum of 'head' in font is bad")
return tab_check_new
def reset_table_checksum(self):
new_check = self.calc_table_checksum()
self.putlong(new_check, self.tab_check_offset)
def reset_full_checksum(self):
new_adj = (0xB1B0AFBA - self.calc_full_checksum()) & 0xFFFFFFFF
self.putlong(new_adj, 'checksumAdjustment')
def close(self):
self.f.close()
def __init__(self, filename):
self.modified = False
self.f = open(filename, 'r+b')
self.find_head_table(0)
self.flags = self.getshort('flags')
self.lowppem = self.getshort('lowestRecPPEM')
self.checksum_adj = self.getlong('checksumAdjustment')
def check_panose_monospaced(font):
""" Check if the font's Panose flags say it is monospaced """
# https://forum.high-logic.com/postedfiles/Panose.pdf
panose = list(font.os2_panose)
if panose[0] < 2 or panose[0] > 5:
return -1 # invalid Panose info
panose_mono = ((panose[0] == 2 and panose[3] == 9) or
(panose[0] == 3 and panose[3] == 3))
return 1 if panose_mono else 0
def panose_check_to_text(value, panose = False):
""" Convert value from check_panose_monospaced() to human readable string """
if value == 0:
return "Panose says \"not monospaced\""
if value == 1:
return "Panose says \"monospaced\""
return "Panose is invalid" + (" ({})".format(list(panose)) if panose else "")
def panose_proportion_to_text(value):
""" Interpret a Panose proportion value (4th value) for family 2 (latin text) """
proportion = {
0: "Any", 1: "No Fit", 2: "Old Style", 3: "Modern", 4: "Even Width",
5: "Extended", 6: "Condensed", 7: "Very Extended", 8: "Very Condensed",
9: "Monospaced" }
return proportion.get(value, "??? {}".format(value))
def is_monospaced(font):
""" Check if a font is probably monospaced """
# Some fonts lie (or have not any Panose flag set), spot check monospaced:
width = -1
width_mono = True
for glyph in [ 0x49, 0x4D, 0x57, 0x61, 0x69, 0x6d, 0x2E ]: # wide and slim glyphs 'I', 'M', 'W', 'a', 'i', 'm', '.'
if not glyph in font:
# A 'strange' font, believe Panose
return (check_panose_monospaced(font) == 1, None)
# print(" -> {} {}".format(glyph, font[glyph].width))
if width < 0:
width = font[glyph].width
continue
if font[glyph].width != width:
# Exception for fonts like Code New Roman Regular or Hermit Light/Bold:
# Allow small 'i' and dot to be smaller than normal
# I believe the source fonts are buggy
if glyph in [ 0x69, 0x2E ]:
if width > font[glyph].width:
continue
(xmin, _, xmax, _) = font[glyph].boundingBox()
if width > xmax - xmin:
continue
width_mono = False
break
# We believe our own check more then Panose ;-D
return (width_mono, None if width_mono else glyph)
def force_panose_monospaced(font):
""" Forces the Panose flag to monospaced if they are unset or halfway ok already """
# For some Windows applications (e.g. 'cmd'), they seem to honour the Panose table
# https://forum.high-logic.com/postedfiles/Panose.pdf
panose = list(font.os2_panose)
if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default)
panose[0] = 2 # make kind latin text and display
logger.info("Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')")
font.os2_panose = tuple(panose)
if panose[0] == 2 and panose[3] != 9:
logger.info("Setting Panose 'Proportion' to 'Monospaced' (was '%s')", panose_proportion_to_text(panose[3]))
panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced
font.os2_panose = tuple(panose)
def get_advance_width(font, extended, minimum):
""" Get the maximum/minimum advance width in the extended(?) range """
width = 0
if extended:
end = 0x17f
else:
end = 0x07e
for glyph in range(0x21, end):
if not glyph in font:
continue
if glyph in range(0x7F, 0xBF):
continue # ignore special characters like '1/4' etc
if width == 0:
width = font[glyph].width
continue
if not minimum and width < font[glyph].width:
width = font[glyph].width
elif minimum and width > font[glyph].width:
width = font[glyph].width
return width
def report_advance_widths(font):
return "Advance widths (base/extended): {} - {} / {} - {}".format(
get_advance_width(font, True, True), get_advance_width(font, False, True),
get_advance_width(font, False, False), get_advance_width(font, True, False))
def get_btb_metrics(font):
""" Get the baseline to baseline distance for all three metrics """
hhea_height = font.hhea_ascent - font.hhea_descent
typo_height = font.os2_typoascent - font.os2_typodescent
win_height = font.os2_winascent + font.os2_windescent
win_gap = max(0, font.hhea_linegap - win_height + hhea_height)
hhea_btb = hhea_height + font.hhea_linegap
typo_btb = typo_height + font.os2_typolinegap
win_btb = win_height + win_gap
return (hhea_btb, typo_btb, win_btb, win_gap)
def get_old_average_x_width(font):
""" Determine xAvgCharWidth of the OS/2 table """
# Fontforge can not create fonts with old (i.e. prior to OS/2 version 3)
# table values, but some very old applications do need them sometimes
# https://learn.microsoft.com/en-us/typography/opentype/spec/os2#xavgcharwidth
s = 0
weights = {
'a': 64, 'b': 14, 'c': 27, 'd': 35, 'e': 100, 'f': 20, 'g': 14, 'h': 42, 'i': 63,
'j': 3, 'k': 6, 'l': 35, 'm': 20, 'n': 56, 'o': 56, 'p': 17, 'q': 4, 'r': 49,
's': 56, 't': 71, 'u': 31, 'v': 10, 'w': 18, 'x': 3, 'y': 18, 'z': 2, 32: 166,
}
for g in weights:
if g not in font:
logger.critical("Can not determine ancient style xAvgCharWidth")
sys.exit(1)
s += font[g].width * weights[g]
return int(s / 1000)
def create_filename(fonts):
""" Determine filename from font object(s) """
sfnt = { k: v for l, k, v in fonts[0].sfnt_names }
sfnt_pfam = sfnt.get('Preferred Family', sfnt['Family'])
sfnt_psubfam = sfnt.get('Preferred Styles', sfnt['SubFamily'])
if len(fonts) > 1:
return sfnt_pfam
if len(sfnt_psubfam) > 0:
sfnt_psubfam = '-' + sfnt_psubfam
return (sfnt_pfam + sfnt_psubfam).replace(' ', '')
class font_patcher:
def __init__(self, args):
self.args = args # class 'argparse.Namespace'
self.sym_font_args = []
self.config = None # class 'configparser.ConfigParser'
self.sourceFont = None # class 'fontforge.font'
self.patch_set = None # class 'list'
self.font_dim = None # class 'dict'
self.font_extrawide = False
self.source_monospaced = None # Later True or False
self.symbolsonly = False
self.onlybitmaps = 0
self.essential = set()
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
self.xavgwidth = [] # list of ints
def patch(self, font):
self.sourceFont = font
self.setup_version()
self.get_essential_references()
self.setup_name_backup(font)
self.assert_monospace()
self.remove_ligatures()
self.setup_patch_set()
self.get_sourcefont_dimensions()
self.improve_line_dimensions()
self.sourceFont.encoding = 'UnicodeFull' # Update the font encoding to ensure that the Unicode glyphs are available
self.onlybitmaps = self.sourceFont.onlybitmaps # Fetch this property before adding outlines. NOTE self.onlybitmaps initialized and never used
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.
self.set_sourcefont_glyph_widths()
# For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs
if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2:
logger.warning("Very wide and short font, disabling 2 cell Powerline glyphs")
self.font_extrawide = True
# Prevent opening and closing the fontforge font. Makes things faster when patching
# multiple ranges using the same symbol font.
PreviousSymbolFilename = ""
symfont = None
if not os.path.isdir(self.args.glyphdir):
logger.critical("Can not find symbol glyph directory %s "
"(probably you need to download the src/glyphs/ directory?)", self.args.glyphdir)
sys.exit(1)
if self.args.dry_run:
return
for patch in self.patch_set:
if patch['Enabled']:
if PreviousSymbolFilename != patch['Filename']:
# We have a new symbol font, so close the previous one if it exists
if symfont:
symfont.close()
symfont = None
if not os.path.isfile(self.args.glyphdir + patch['Filename']):
logger.critical("Can not find symbol source for '%s' (i.e. %s)",
patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK):
logger.critical("Can not open symbol source for '%s' (i.e. %s)",
patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename']))
symfont.encoding = 'UnicodeFull'
# Match the symbol font size to the source font size
symfont.em = self.sourceFont.em
PreviousSymbolFilename = patch['Filename']
# If patch table doesn't include a source start, re-use the symbol font values
SrcStart = patch['SrcStart']
if not SrcStart:
SrcStart = patch['SymStart']
self.copy_glyphs(SrcStart, symfont, patch['SymStart'], patch['SymEnd'], patch['Exact'], patch['ScaleRules'], patch['Name'], patch['Attributes'])
if symfont:
symfont.close()
# The grave accent and fontforge:
# If the type is 'auto' fontforge changes it to 'mark' on export.
# We can not prevent this. So set it to 'baseglyph' instead, as
# that resembles the most common expectations.
# This is not needed with fontforge March 2022 Release anymore.
if "grave" in self.sourceFont:
self.sourceFont["grave"].glyphclass="baseglyph"
def generate(self, sourceFonts):
sourceFont = sourceFonts[0]
# the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'.
if int(fontforge.version()) >= 20201107:
gen_flags = (str('opentype'), str('PfEd-comments'), str('no-FFTM-table'))
else:
gen_flags = (str('opentype'), str('PfEd-comments'))
if len(sourceFonts) > 1:
layer = None
# use first non-background layer
for l in sourceFont.layers:
if not sourceFont.layers[l].is_background:
layer = l
break
outfile = os.path.normpath(os.path.join(
sanitize_filename(self.args.outputdir, True),
sanitize_filename(create_filename(sourceFonts)) + ".ttc"))
sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer)
message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile)
else:
fontname = create_filename(sourceFonts)
if not fontname:
fontname = sourceFont.cidfontname
outfile = os.path.normpath(os.path.join(
sanitize_filename(self.args.outputdir, True),
sanitize_filename(fontname) + self.args.extension))
bitmaps = str()
if len(self.sourceFont.bitmapSizes):
logger.debug("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
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)
message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile)
# Adjust flags that can not be changed via fontforge
if re.search('\\.[ot]tf$', self.args.font, re.IGNORECASE) and re.search('\\.[ot]tf$', outfile, re.IGNORECASE):
try:
source_font = TableHEADWriter(self.args.font)
dest_font = TableHEADWriter(outfile)
for idx in range(source_font.num_fonts):
logger.debug("Tweaking %d/%d", idx + 1, source_font.num_fonts)
xwidth_s = ''
xwidth = self.xavgwidth[idx]
if isinstance(xwidth, int):
if isinstance(xwidth, bool) and xwidth:
source_font.find_table([b'OS/2'], idx)
xwidth = source_font.getshort('avgWidth')
xwidth_s = ' (copied from source)'
dest_font.find_table([b'OS/2'], idx)
d_xwidth = dest_font.getshort('avgWidth')
if d_xwidth != xwidth:
logger.debug("Changing xAvgCharWidth from %d to %d%s", d_xwidth, xwidth, xwidth_s)
dest_font.putshort(xwidth, 'avgWidth')
dest_font.reset_table_checksum()
source_font.find_head_table(idx)
dest_font.find_head_table(idx)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08)
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
if source_font.lowppem != dest_font.lowppem:
logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem)
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified:
dest_font.reset_table_checksum()
if dest_font.modified:
dest_font.reset_full_checksum()
except Exception as error:
logger.error("Can not handle font flags (%s)", repr(error))
finally:
try:
source_font.close()
dest_font.close()
except:
pass
if self.args.is_variable:
logger.critical("Source font is a variable open type font (VF) and the patch results will most likely not be what you want")
print(message)
if self.args.postprocess:
subprocess.call([self.args.postprocess, outfile])
print("\n")
logger.info("Post Processed: %s", outfile)
def setup_name_backup(self, font):
""" Store the original font names to be able to rename the font multiple times """
font.persistent = {
"fontname": font.fontname,
"fullname": font.fullname,
"familyname": font.familyname,
}
def setup_font_names(self, font):
font.fontname = font.persistent["fontname"]
if isinstance(font.persistent["fullname"], str):
font.fullname = font.persistent["fullname"]
if isinstance(font.persistent["familyname"], str):
font.familyname = font.persistent["familyname"]
verboseAdditionalFontNameSuffix = ""
additionalFontNameSuffix = ""
if not self.args.complete:
# NOTE not all symbol fonts have appended their suffix here
if self.args.fontawesome:
additionalFontNameSuffix += " A"
verboseAdditionalFontNameSuffix += " Plus Font Awesome"
if self.args.fontawesomeextension:
additionalFontNameSuffix += " AE"
verboseAdditionalFontNameSuffix += " Plus Font Awesome Extension"
if self.args.octicons:
additionalFontNameSuffix += " O"
verboseAdditionalFontNameSuffix += " Plus Octicons"
if self.args.powersymbols:
additionalFontNameSuffix += " PS"
verboseAdditionalFontNameSuffix += " Plus Power Symbols"
if self.args.codicons:
additionalFontNameSuffix += " C"
verboseAdditionalFontNameSuffix += " Plus Codicons"
if self.args.pomicons:
additionalFontNameSuffix += " P"
verboseAdditionalFontNameSuffix += " Plus Pomicons"
if self.args.fontlogos:
additionalFontNameSuffix += " L"
verboseAdditionalFontNameSuffix += " Plus Font Logos"
if self.args.material:
additionalFontNameSuffix += " MDI"
verboseAdditionalFontNameSuffix += " Plus Material Design Icons"
if self.args.weather:
additionalFontNameSuffix += " WEA"
verboseAdditionalFontNameSuffix += " Plus Weather Icons"
# add mono signifier to beginning of name suffix
if self.args.single:
variant_abbrev = "M"
variant_full = " Mono"
elif self.args.nonmono and not self.symbolsonly:
variant_abbrev = "P"
variant_full = " Propo"
else:
variant_abbrev = ""
variant_full = ""
ps_suffix = projectNameAbbreviation + variant_abbrev + additionalFontNameSuffix
# add 'Nerd Font' to beginning of name suffix
verboseAdditionalFontNameSuffix = " " + projectNameSingular + variant_full + verboseAdditionalFontNameSuffix
additionalFontNameSuffix = " " + projectNameSingular + variant_full + additionalFontNameSuffix
if FontnameParserOK and self.args.makegroups > 0:
use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse
# Use fullname if it is 'equal' to the fontname
if font.fullname:
use_fullname |= font.fontname.lower() == FontnameTools.postscript_char_filter(font.fullname).lower()
# 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 |= 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]
n = FontnameParser(parser_name, logger)
if not n.parse_ok:
logger.warning("Have only minimal naming information, check resulting name. Maybe specify --makegroups 0")
n.drop_for_powerline()
n.enable_short_families(True, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ])
if not n.set_expect_no_italic(self.args.noitalic):
logger.critical("Detected 'Italic' slant but --has-no-italic specified")
sys.exit(1)
# All the following stuff is ignored in makegroups-mode
# basically split the font name around the dash "-" to get the fontname and the style (e.g. Bold)
# this does not seem very reliable so only use the style here as a fallback if the font does not
# 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("^([^-]*).*?([^-]*(?!.*-))$", font.fontname).groups()
# dont trust 'font.familyname'
familyname = fontname
# fullname (filename) can always use long/verbose font name, even in windows
if font.fullname != None:
fullname = font.fullname + verboseAdditionalFontNameSuffix
else:
fullname = font.cidfontname + verboseAdditionalFontNameSuffix
fontname = fontname + additionalFontNameSuffix.replace(" ", "")
# let us try to get the 'style' from the font info in sfnt_names and fallback to the
# parse fontname if it fails:
try:
# search tuple:
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 = 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
# some fonts have inaccurate 'SubFamily', if it is Regular let us trust the filename more:
if subFamily == "Regular" and len(fallbackStyle):
subFamily = fallbackStyle
# This is meant to cover the case where the SubFamily is "Italic" and the filename is *-BoldItalic.
if len(subFamily) < len(fallbackStyle):
subFamily = fallbackStyle
if len(subFamily) == 0:
subFamily = "Regular"
familyname += " " + projectNameSingular + variant_full
# Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with
# the same name as the same font, even if subFamily is different. Make sure to
# keep the resulting fontname (PostScript name) valid by removing spaces.
fontname += '-' + subFamily.replace(' ', '')
# rename font
#
# comply with SIL Open Font License (OFL)
reservedFontNameReplacements = {
'source' : 'sauce',
'Source' : 'Sauce',
'Bitstream Vera Sans Mono' : 'Bitstrom Wera',
'BitstreamVeraSansMono' : 'BitstromWera',
'bitstream vera sans mono' : 'bitstrom wera',
'bitstreamverasansmono' : 'bitstromwera',
'hermit' : 'hurmit',
'Hermit' : 'Hurmit',
'hasklig' : 'hasklug',
'Hasklig' : 'Hasklug',
'Share' : 'Shure',
'share' : 'shure',
'IBMPlex' : 'Blex',
'ibmplex' : 'blex',
'IBM-Plex' : 'Blex',
'IBM Plex' : 'Blex',
'terminus' : 'terminess',
'Terminus' : 'Terminess',
'liberation' : 'literation',
'Liberation' : 'Literation',
'iAWriter' : 'iMWriting',
'iA Writer' : 'iM Writing',
'iA-Writer' : 'iM-Writing',
'Anka/Coder' : 'AnaConder',
'anka/coder' : 'anaconder',
'Cascadia Code' : 'Caskaydia Cove',
'cascadia code' : 'caskaydia cove',
'CascadiaCode' : 'CaskaydiaCove',
'cascadiacode' : 'caskaydiacove',
'Cascadia Mono' : 'Caskaydia Mono',
'cascadia mono' : 'caskaydia mono',
'CascadiaMono' : 'CaskaydiaMono',
'cascadiamono' : 'caskaydiamono',
'Fira Mono' : 'Fura Mono',
'Fira Sans' : 'Fura Sans',
'FiraMono' : 'FuraMono',
'FiraSans' : 'FuraSans',
'fira mono' : 'fura mono',
'fira sans' : 'fura sans',
'firamono' : 'furamono',
'firasans' : 'furasans',
}
# remove overly verbose font names
# particularly regarding Powerline sourced Fonts (https://github.com/powerline/fonts)
additionalFontNameReplacements = {
'for Powerline': '',
'ForPowerline': ''
}
additionalFontNameReplacements2 = {
'Powerline': ''
}
projectInfo = (
"Patched with '" + projectName + " Patcher' (https://github.com/ryanoasis/nerd-fonts)\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/-/changelog.md"
)
familyname = replace_font_name(familyname, reservedFontNameReplacements)
fullname = replace_font_name(fullname, reservedFontNameReplacements)
fontname = replace_font_name(fontname, reservedFontNameReplacements)
familyname = replace_font_name(familyname, additionalFontNameReplacements)
fullname = replace_font_name(fullname, additionalFontNameReplacements)
fontname = replace_font_name(fontname, additionalFontNameReplacements)
familyname = replace_font_name(familyname, additionalFontNameReplacements2)
fullname = replace_font_name(fullname, additionalFontNameReplacements2)
fontname = replace_font_name(fontname, additionalFontNameReplacements2)
if not (FontnameParserOK and self.args.makegroups > 0):
# replace any extra whitespace characters:
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:
short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full
# inject_suffix(family, ps_fontname, short_family)
n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family)
n.rename_font(font)
font.comment = projectInfo
font.fontlog = projectInfo
def setup_version(self):
""" Add the Nerd Font version to the original version """
# print("Version was {}".format(sourceFont.version))
if self.sourceFont.version != None:
self.sourceFont.version += ";" + projectName + " " + version
else:
self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version
self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge
self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version)
# The Version SFNT name is later reused by the NameParser for UniqueID
# print("Version now is {}".format(sourceFont.version))
def remove_ligatures(self):
# let's deal with ligatures (mostly for monospaced fonts)
# the tables have been removed from the repo with >this< commit
if self.args.configfile and self.config.read(self.args.configfile):
if self.args.removeligatures:
logger.info("Removing ligatures from configfile `Subtables` section")
ligature_subtables = json.loads(self.config.get("Subtables", "ligatures"))
for subtable in ligature_subtables:
logger.debug("Removing subtable: %s", subtable)
try:
self.sourceFont.removeLookupSubtable(subtable)
logger.debug("Successfully removed subtable: %s", subtable)
except Exception:
logger.error("Failed to remove subtable: %s", subtable)
elif self.args.removeligatures:
logger.error("Unable to read configfile, unable to remove ligatures")
def assert_monospace(self):
# Check if the sourcefont is monospaced
width_mono, offending_char = is_monospaced(self.sourceFont)
self.source_monospaced = width_mono
if self.args.nonmono:
return
panose_mono = check_panose_monospaced(self.sourceFont)
# The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown'
if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1):
logger.warning("Monospaced check: Panose assumed to be wrong")
logger.warning(" %s and %s",
report_advance_widths(self.sourceFont),
panose_check_to_text(panose_mono, self.sourceFont.os2_panose))
if self.args.single and not width_mono:
logger.warning("Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless")
if offending_char is not None:
logger.warning(" Offending char: %X", offending_char)
if self.args.single <= 1:
logger.critical("Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching")
sys.exit(1)
if width_mono:
force_panose_monospaced(self.sourceFont)
def setup_patch_set(self):
""" Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """
box_enabled = self.source_monospaced # Box glyph only for monospaced
box_keep = False
if box_enabled:
self.sourceFont.selection.select(("ranges",), 0x2500, 0x259f)
box_glyphs_target = len(list(self.sourceFont.selection))
box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs))
if box_glyphs_target > box_glyphs_current:
# Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing)
if box_glyphs_current > 0:
logger.debug("%d/%d box drawing glyphs will be replaced",
box_glyphs_current, box_glyphs_target)
box_enabled = True
else:
# Sourcefont does have all of these glyphs
# box_keep = True # just scale do not copy (need to scale to fit new cell size)
box_enabled = False # Cowardly not scaling existing glyphs, although the code would allow this
# Stretch 'xz' or 'pa' (preserve aspect ratio)
# Supported params: overlap | careful | xy-ratio | dont_copy
# Overlap value is used horizontally but vertically limited to 0.01
# Careful does not overwrite/modify existing glyphs
# The xy-ratio limits the x-scale for a given y-scale to make the ratio <= this value (to prevent over-wide glyphs)
# '1' means occupu 1 cell (default for 'xy')
# '2' means occupy 2 cells (default for 'pa')
# '!' means do the 'pa' scaling even with non mono fonts (else it just scales down, never up)
# Dont_copy does not overwrite existing glyphs but rescales the preexisting ones
SYM_ATTR_DEFAULT = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}
}
SYM_ATTR_POWERLINE = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
# Arrow tips
0xe0b0: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}},
0xe0b1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.7}},
0xe0b2: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}},
0xe0b3: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.7}},
# Rounded arcs
0xe0b4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01, 'xy-ratio': 0.59}},
0xe0b5: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.5}},
0xe0b6: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01, 'xy-ratio': 0.59}},
0xe0b7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.5}},
# Bottom Triangles
0xe0b8: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0b9: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
0xe0ba: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0bb: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}},
# Top Triangles
0xe0bc: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0bd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
0xe0be: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0bf: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}},
# Flames
0xe0c0: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}},
0xe0c1: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {}},
0xe0c2: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}},
0xe0c3: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {}},
# Small squares
0xe0c4: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.86}},
0xe0c5: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.86}},
# Bigger squares
0xe0c6: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.78}},
0xe0c7: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.78}},
# Waveform
0xe0c8: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}},
0xe0ca: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}},
# Hexagons
0xe0cc: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02, 'xy-ratio': 0.85}},
0xe0cd: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'xy-ratio': 0.865}},
# Legos
0xe0ce: {'align': 'l', 'valign': 'c', 'stretch': 'pa', 'params': {}},
0xe0cf: {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
0xe0d0: {'align': 'l', 'valign': 'c', 'stretch': 'pa', 'params': {}},
0xe0d1: {'align': 'l', 'valign': 'c', 'stretch': 'pa', 'params': {}},
# Top and bottom trapezoid
0xe0d2: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}},
0xe0d4: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}}
}
SYM_ATTR_TRIGRAPH = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa1!', 'params': {'overlap': -0.10, 'careful': True}}
}
SYM_ATTR_FONTA = {
# 'pa' == preserve aspect ratio
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
# Don't center these arrows vertically
0xf0dc: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}},
0xf0dd: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}},
0xf0de: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}}
}
SYM_ATTR_HEAVYBRACKETS = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {'careful': True}}
}
SYM_ATTR_BOX = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'dont_copy': box_keep}},
# No overlap with checkered greys (commented out because that raises problems on rescaling clients)
# 0x2591: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}},
# 0x2592: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}},
# 0x2593: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}},
}
CUSTOM_ATTR = {
# 'pa' == preserve aspect ratio
'default': {'align': 'c', 'valign': '', 'stretch': '', 'params': {}}
}
# Most glyphs we want to maximize (individually) during the scale
# However, there are some that need to be small or stay relative in
# size to each other.
# The glyph-specific behavior can be given as ScaleRules in the patch-set.
#
# ScaleRules can contain two different kind of rules (possibly in parallel):
# - ScaleGlyph:
# Here one specific glyph is used as 'scale blueprint'. Other glyphs are
# scaled by the same factor as this glyph. This is useful if you have one
# 'biggest' glyph and all others should stay relatively in size.
# Shifting in addition to scaling can be selected too (see below).
# - ScaleGroups:
# Here you specify a group of glyphs that should be handled together
# with the same scaling and shifting. The basis for it is a 'combined
# bounding box' of all glyphs in that group. All glyphs are handled as
# if they fill that combined bounding box.
#
# The ScaleGlyph method: You set 'ScaleGlyph' to the unicode of the reference glyph.
# Note that there can be only one per patch-set.
# Additionally you set 'GlyphsToScale' that contains all the glyphs that shall be
# handled (scaled) like the reference glyph.
# It is a List of: ((glyph code) or (tuple of two glyph codes that form a closed range))
# 'GlyphsToScale': [
# 0x0100, 0x0300, 0x0400, # The single glyphs 0x0100, 0x0300, and 0x0400
# (0x0200, 0x0210), # All glyphs 0x0200 to 0x0210 including both 0x0200 and 0x0210
# ]}
# If you want to not only scale but also shift as the refenerce glyph you give the
# data as 'GlyphsToScale+'. Note that only one set is used and the plus version is preferred.
#
# For the ScaleGroup method you define any number groups of glyphs and each group is
# handled separately. The combined bounding box of all glyphs in the group is determined
# and based on that the scale and shift for all the glyphs in the group.
# You define the groups as value of 'ScaleGroups'.
# It is a List of: ((lists of glyph codes) or (ranges of glyph codes))
# 'ScaleGroups': [
# [0x0100, 0x0300, 0x0400], # One group consists of glyphs 0x0100, 0x0300, and 0x0400
# range(0x0200, 0x0210 + 1), # Another group contains glyphs 0x0200 to 0x0210 incl.
#
# Note the subtle differences: tuple vs. range; closed vs open range; etc
# See prepareScaleRules() for some more details.
# For historic reasons ScaleGroups is sometimes called 'new method' and ScaleGlyph 'old'.
# The codepoints mentioned here are symbol-font-codepoints.
BOX_SCALE_LIST = {'ScaleGroups': [
[*range(0x2500, 0x2570 + 1), *range(0x2574, 0x257f + 1)], # box drawing
range(0x2571, 0x2573 + 1), # diagonals
[*range(0x2580, 0x2590 + 1), 0x2594, 0x2595], # blocks
range(0x2591, 0x2593 + 1), # greys
range(0x2594, 0x259f + 1), # quards (Note: quard 2597 in Hack is wrong, scales like block!)
]}
CODI_SCALE_LIST = {'ScaleGroups': [
range(0xea99, 0xeaa1 + 1), # arrows
range(0xeb6e, 0xeb71 + 1), # triangles
range(0xeab4, 0xeab7 + 1), # chevrons
[0xea71, *range(0xeaa6, 0xeaab + 1), 0xeabc, 0xeb18, 0xeb87, 0xeb88, 0xeb8a, 0xeb8c, 0xebb4], # cicles
[0xeacc, 0xeaba], # dash
]}
DEVI_SCALE_LIST = {'ScaleGlyph': 0xE60E, # Android logo
'GlyphsToScale': [
(0xe6bd, 0xe6c3) # very small things
]}
FONTA_SCALE_LIST = {'ScaleGroups': [
[0xf005, 0xf006, 0xf089], # star, star empty, half star
range(0xf026, 0xf028 + 1), # volume off, down, up
range(0xf02b, 0xf02c + 1), # tag, tags
range(0xf031, 0xf035 + 1), # font et al
range(0xf044, 0xf046 + 1), # edit, share, check (boxes)
range(0xf048, 0xf052 + 1), # multimedia buttons
range(0xf060, 0xf063 + 1), # arrows
[0xf053, 0xf054, 0xf077, 0xf078], # chevron all directions
range(0xf07d, 0xf07e + 1), # resize
range(0xf0a4, 0xf0a7 + 1), # pointing hands
[0xf0d7, 0xf0d8, 0xf0d9, 0xf0da, 0xf0dc, 0xf0dd, 0xf0de], # caret all directions and same looking sort
range(0xf100, 0xf107 + 1), # angle
range(0xf130, 0xf131 + 1), # mic
range(0xf141, 0xf142 + 1), # ellipsis
range(0xf153, 0xf15a + 1), # currencies
range(0xf175, 0xf178 + 1), # long arrows
range(0xf182, 0xf183 + 1), # male and female
range(0xf221, 0xf22d + 1), # gender or so
range(0xf255, 0xf25b + 1), # hand symbols
]}
OCTI_SCALE_LIST = {'ScaleGlyph': 0xF02E, # looking glass (probably biggest glyph?)
'GlyphsToScale': [
(0xf03d, 0xf040), # arrows
0xf044, 0xf05a, 0xf05b, 0xf0aa, # triangles
(0xf051, 0xf053), # small stuff
0xf071, 0xf09f, 0xf0a0, 0xf0a1, # small arrows
0xf078, 0xf0a2, 0xf0a3, 0xf0a4, # chevrons
0xf0ca, # dash
]}
WEATH_SCALE_LIST = {'ScaleGroups': [
[0xf03c, 0xf042, 0xf045 ], # degree signs
[0xf043, 0xf044, 0xf048, 0xf04b, 0xf04c, 0xf04d, 0xf057, 0xf058, 0xf087, 0xf088], # arrows
range(0xf053, 0xf055 + 1), # thermometers
[*range(0xf059, 0xf061 + 1), 0xf0b1], # wind directions
range(0xf089, 0xf094 + 1), # clocks
range(0xf095, 0xf0b0 + 1), # moon phases
range(0xf0b7, 0xf0c3 + 1), # wind strengths
[0xf06e, 0xf070 ], # solar/lunar eclipse
# Note: Codepoints listed before that are also in the following range
# will take the scaling of the previous group (the ScaleGroups are
# searched through in definition order).
# But be careful, the combined bounding box for the following group
# _will_ include all glyphs in its definition: Make sure the exempt
# glyphs from above are smaller (do not extend) the combined bounding
# box of this range:
range(0xf000, 0xf0cb + 1), # lots of clouds and other (Please read note above!)
]}
MDI_SCALE_LIST = None # Maybe later add some selected ScaleGroups
# Define the character ranges
# Symbol font ranges
self.patch_set = [
{'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE5FF, 'SrcStart': 0xE5FA, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': True, 'Name': "Heavy Angle Brackets", 'Filename': "extraglyphs.sfd", 'Exact': True, 'SymStart': 0x276C, 'SymEnd': 0x2771, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_HEAVYBRACKETS},
{'Enabled': box_enabled, 'Name': "Box Drawing", 'Filename': "extraglyphs.sfd", 'Exact': True, 'SymStart': 0x2500, 'SymEnd': 0x259F, 'SrcStart': None, 'ScaleRules': BOX_SCALE_LIST, 'Attributes': SYM_ATTR_BOX},
{'Enabled': True, 'Name': "Devicons", 'Filename': "devicons.ttf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE6C5, 'SrcStart': 0xE700, 'ScaleRules': DEVI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0A0, 'SymEnd': 0xE0A2, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0B0, 'SymEnd': 0xE0B3, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0A3, 'SymEnd': 0xE0A3, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D4, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0x2630, 'SymEnd': 0x2630, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_TRIGRAPH},
{'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.fontawesome, 'Name': "Font Awesome", 'Filename': "font-awesome/FontAwesome.otf", 'Exact': True, 'SymStart': 0xF000, 'SymEnd': 0xF2E0, 'SrcStart': None, 'ScaleRules': FONTA_SCALE_LIST, 'Attributes': SYM_ATTR_FONTA},
{'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize
{'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x23FB, 'SymEnd': 0x23FE, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Power, Power On/Off, Power On, Sleep
{'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off)
{'Enabled': False , 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesign/MaterialDesignIconsDesktop.ttf", 'Exact': True, 'SymStart': 0xF0001,'SymEnd': 0xF1AF0,'SrcStart': None, 'ScaleRules': MDI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.weather, 'Name': "Weather Icons", 'Filename': "weather-icons/weathericons-regular-webfont.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF0EB, 'SrcStart': 0xE300, 'ScaleRules': WEATH_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF305, 'SrcStart': 0xF4A9, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleRules': CODI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR}
]
def improve_line_dimensions(self):
# Make the total line size even. This seems to make the powerline separators
# center more evenly.
if self.args.adjustLineHeight:
if (self.sourceFont.os2_winascent + self.sourceFont.os2_windescent) % 2 != 0:
# All three are equal before due to get_sourcefont_dimensions()
self.sourceFont.hhea_ascent += 1
self.sourceFont.os2_typoascent += 1
self.sourceFont.os2_winascent += 1
def add_glyphrefs_to_essential(self, unicode):
self.essential.add(unicode)
# According to fontforge spec, altuni is either None or a tuple of tuples
# Those tuples contained in altuni are of the following "format":
# (unicode-value, variation-selector, reserved-field)
altuni = self.sourceFont[unicode].altuni
if altuni is not None:
for altcode in [ v for v, s, r in altuni if v >= 0 ]:
# If alternate unicode already exists in self.essential,
# that means it has gone through this function before.
# Therefore we skip it to avoid infinite loop.
# A unicode value of -1 basically means unused and is also worth skipping.
if altcode not in self.essential:
self.add_glyphrefs_to_essential(altcode)
# From fontforge documentation:
# glyph.references return a tuple of tuples containing, for each reference in foreground,
# a glyph name, a transformation matrix, and (depending on ff version) whether the
# reference is currently selected.
references = self.sourceFont[unicode].references
for refcode in [ self.sourceFont[n].unicode for n, *_ in references ]: # tuple of 2 or 3 depending on ff version
if refcode not in self.essential and refcode >= 0:
self.add_glyphrefs_to_essential(refcode)
def get_essential_references(self):
"""Find glyphs that are needed for the basic glyphs"""
# Sometimes basic glyphs are constructed from multiple other glyphs.
# Find out which other glyphs are also needed to keep the basic
# glyphs intact.
# 0x00-0x17f is the Latin Extended-A range
basic_glyphs = set()
# Collect substitution destinations
for glyph in range(0x21, 0x17f + 1):
if not glyph in self.sourceFont:
continue
basic_glyphs.add(glyph)
for possub in self.sourceFont[glyph].getPosSub('*'):
if possub[1] == 'Substitution' or possub[1] == 'Ligature':
basic_glyphs.add(self.sourceFont[possub[2]].unicode)
basic_glyphs.discard(-1) # the .notdef glyph
for glyph in basic_glyphs:
self.add_glyphrefs_to_essential(glyph)
def get_sourcefont_dimensions(self):
""" This gets the font dimensions (cell width and height), and makes them equal on all platforms """
# Step 1
# There are three ways to discribe the baseline to baseline distance
# (a.k.a. line spacing) of a font. That is all a kuddelmuddel
# and we try to sort this out here
# See also https://glyphsapp.com/learn/vertical-metrics
# See also https://github.com/source-foundry/font-line
(hhea_btb, typo_btb, win_btb, win_gap) = get_btb_metrics(self.sourceFont)
use_typo = self.sourceFont.os2_use_typo_metrics != 0
Metric = Enum('Metric', ['HHEA', 'TYPO', 'WIN'])
# We use either TYPO (1) or WIN (2) and compare with HHEA
# and use HHEA (0) if the fonts seems broken - no WIN, see #1056
our_btb = typo_btb if use_typo else win_btb
if our_btb == hhea_btb:
metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font
else:
# We trust the WIN metric more, see experiments in #1056
logger.warning("Font vertical metrics inconsistent (HHEA %d / TYPO %d / WIN %d), using WIN", hhea_btb, typo_btb, win_btb)
our_btb = win_btb
metrics = Metric.WIN
# print("FINI hhea {} typo {} win {} use {} {} {}".format(hhea_btb, typo_btb, win_btb, use_typo, our_btb != hhea_btb, self.sourceFont.fontname))
self.font_dim = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0, 'width' : 0, 'height': 0}
if metrics == Metric.HHEA:
self.font_dim['ymin'] = self.sourceFont.hhea_descent - half_gap(self.sourceFont.hhea_linegap, False)
self.font_dim['ymax'] = self.sourceFont.hhea_ascent + half_gap(self.sourceFont.hhea_linegap, True)
elif metrics == Metric.TYPO:
self.font_dim['ymin'] = self.sourceFont.os2_typodescent - half_gap(self.sourceFont.os2_typolinegap, False)
self.font_dim['ymax'] = self.sourceFont.os2_typoascent + half_gap(self.sourceFont.os2_typolinegap, True)
elif metrics == Metric.WIN:
self.font_dim['ymin'] = -self.sourceFont.os2_windescent - half_gap(win_gap, False)
self.font_dim['ymax'] = self.sourceFont.os2_winascent + half_gap(win_gap, True)
else:
pass # Will fail the metrics check some line later
# Calculate font height
self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax']
if self.font_dim['height'] == 0:
# This can only happen if the input font is empty
# Assume we are using our prepared templates
self.symbolsonly = True
self.font_dim = {
'xmin' : 0,
'ymin' : -self.sourceFont.descent,
'xmax' : self.sourceFont.em,
'ymax' : self.sourceFont.ascent,
'width' : self.sourceFont.em,
'height': self.sourceFont.descent + self.sourceFont.ascent,
}
our_btb = self.sourceFont.descent + self.sourceFont.ascent
elif self.font_dim['height'] < 0:
logger.critical("Can not detect sane font height")
sys.exit(1)
# Make all metrics equal
self.sourceFont.os2_typolinegap = 0
self.sourceFont.os2_typoascent = self.font_dim['ymax']
self.sourceFont.os2_typodescent = self.font_dim['ymin']
self.sourceFont.os2_winascent = self.sourceFont.os2_typoascent
self.sourceFont.os2_windescent = -self.sourceFont.os2_typodescent
self.sourceFont.hhea_ascent = self.sourceFont.os2_typoascent
self.sourceFont.hhea_descent = self.sourceFont.os2_typodescent
self.sourceFont.hhea_linegap = self.sourceFont.os2_typolinegap
self.sourceFont.os2_use_typo_metrics = 1
(check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont)
if check_hhea_btb != check_typo_btb or check_typo_btb != check_win_btb or check_win_btb != our_btb:
logger.critical("Error in baseline to baseline code detected")
sys.exit(1)
# Step 2
# Find the biggest char width and advance width
# 0x00-0x17f is the Latin Extended-A range
warned1 = self.args.nonmono # Do not warn if proportional target
warned2 = warned1
for glyph in range(0x21, 0x17f):
if glyph in range(0x7F, 0xBF) or glyph in [
0x132, 0x133, # IJ, ij (in Overpass Mono)
0x022, 0x027, 0x060, # Single and double quotes in Inconsolata LGC
0x0D0, 0x10F, 0x110, 0x111, 0x127, 0x13E, 0x140, 0x165, # Eth and others with stroke or caron in RobotoMono
0x02D, # hyphen for Monofur
]:
continue # ignore special characters like '1/4' etc and some specifics
try:
(_, _, xmax, _) = self.sourceFont[glyph].boundingBox()
except TypeError:
continue
# print("WIDTH {:X} {} ({} {})".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if self.font_dim['width'] < self.sourceFont[glyph].width:
self.font_dim['width'] = self.sourceFont[glyph].width
if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
logger.debug("Extended glyphs wider than basic glyphs, results might be useless\n %s",
report_advance_widths(self.sourceFont))
warned1 = True
# print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax
if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
logger.debug("Extended glyphs wider bounding box than basic glyphs")
warned2 = True
# print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if self.font_dim['width'] < self.font_dim['xmax']:
logger.debug("Font has negative right side bearing in extended glyphs")
self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used
# print("FINAL", self.font_dim)
self.xavgwidth.append(self.args.xavgwidth)
if isinstance(self.xavgwidth[-1], int) and self.xavgwidth[-1] == 0:
self.xavgwidth[-1] = get_old_average_x_width(self.sourceFont)
def get_target_width(self, stretch):
""" Get the target width (1 or 2 'cell') for a given stretch parameter """
# For monospaced fonts all chars need to be maximum 'one' space wide
# other fonts allows double width glyphs for 'pa' or if requested with '2'
if self.args.single or ('pa' not in stretch and '2' not in stretch) or '1' in stretch:
return 1
return 2
def get_scale_factors(self, sym_dim, stretch):
""" Get scale in x and y as tuple """
# It is possible to have empty glyphs, so we need to skip those.
if not sym_dim['width'] or not sym_dim['height']:
return (1.0, 1.0)
target_width = self.font_dim['width'] * self.get_target_width(stretch)
scale_ratio_x = target_width / sym_dim['width']
# font_dim['height'] represents total line height, keep our symbols sized based upon font's em
# Use the font_dim['height'] only for explicit 'y' scaling (not 'pa')
target_height = self.font_dim['height']
scale_ratio_y = target_height / sym_dim['height']
if 'pa' in stretch:
# We want to preserve x/y aspect ratio, so find biggest scale factor that allows symbol to fit
scale_ratio_x = min(scale_ratio_x, scale_ratio_y)
if not self.args.single and not '!' in stretch:
# non monospaced fonts just scale down on 'pa', not up
scale_ratio_x = min(scale_ratio_x, 1.0)
scale_ratio_y = scale_ratio_x
else:
# Keep the not-stretched direction
if not 'x' in stretch:
scale_ratio_x = 1.0
if not 'y' in stretch:
scale_ratio_y = 1.0
return (scale_ratio_x, scale_ratio_y)
def copy_glyphs(self, sourceFontStart, symbolFont, symbolFontStart, symbolFontEnd, exactEncoding, scaleRules, setName, attributes):
""" Copies symbol glyphs into self.sourceFont """
progressText = ''
careful = False
sourceFontCounter = 0
if self.args.careful:
careful = True
# Create glyphs from symbol font
#
# If we are going to copy all Glyphs, then assume we want to be careful
# and only copy those that are not already contained in the source font
if symbolFontStart == 0:
symbolFont.selection.all()
careful = True
else:
symbolFont.selection.select((str("ranges"), str("unicode")), symbolFontStart, symbolFontEnd)
# Get number of selected non-empty glyphs with codes >=0 (i.e. not -1 == notdef)
symbolFontSelection = [ x for x in symbolFont.selection.byGlyphs if x.unicode >= 0 ]
glyphSetLength = len(symbolFontSelection)
if not self.args.quiet:
modify = attributes['default']['params'].get('dont_copy')
sys.stdout.write("{} {} Glyphs from {} Set\n".format(
"Adding" if not modify else "Rescaling", glyphSetLength, setName))
currentSourceFontGlyph = -1 # initialize for the exactEncoding case
width_warning = False
for index, sym_glyph in enumerate(symbolFontSelection):
sym_attr = attributes.get(sym_glyph.unicode)
if sym_attr is None:
sym_attr = attributes['default']
if self.font_extrawide:
# Do not allow 'xy2' scaling
sym_attr['stretch'] = sym_attr['stretch'].replace('2', '')
if exactEncoding:
# Use the exact same hex values for the source font as for the symbol font.
# Problem is we do not know the codepoint of the sym_glyph and because it
# came from a selection.byGlyphs there might be skipped over glyphs.
# The iteration is still in the order of the selection by codepoint,
# so we take the next allowed codepoint of the current glyph
possible_codes = [ ]
if sym_glyph.unicode > currentSourceFontGlyph:
possible_codes += [ sym_glyph.unicode ]
if sym_glyph.altuni:
possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ]
if len(possible_codes) == 0:
logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode)
continue
currentSourceFontGlyph = min(possible_codes)
else:
# use source font defined hex values based on passed in start (fills gaps; symbols are packed)
currentSourceFontGlyph = sourceFontStart + sourceFontCounter
sourceFontCounter += 1
# For debugging process only limited glyphs
# if currentSourceFontGlyph != 0xe7bd:
# continue
if not self.args.quiet:
if self.args.progressbars:
update_progress(round(float(index + 1) / glyphSetLength, 2))
else:
progressText = "\nUpdating glyph: {} {} putting at: {:X}".format(sym_glyph, sym_glyph.glyphname, currentSourceFontGlyph)
sys.stdout.write(progressText)
sys.stdout.flush()
# check if a glyph already exists in this location
if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential:
if currentSourceFontGlyph in self.sourceFont:
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph)
# We don't want to touch anything so move to next Glyph
continue
else:
# If we overwrite an existing glyph all subtable entries regarding it will be wrong
# (Probably; at least if we add a symbol and do not substitude a ligature or such)
if currentSourceFontGlyph in self.sourceFont:
self.sourceFont[currentSourceFontGlyph].removePosSub("*")
dont_copy = sym_attr['params'].get('dont_copy')
if dont_copy:
# Just prepare scaling of existing glyphs
glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, self.sourceFont, currentSourceFontGlyph) if scaleRules is not None else None
else:
# This will destroy any content currently in currentSourceFontGlyph, so do it first
glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, symbolFont, currentSourceFontGlyph) if scaleRules is not None else None
# Select and copy symbol from its encoding point
# We need to do this select after the careful check, this way we don't
# reset our selection before starting the next loop
symbolFont.selection.select(sym_glyph.encoding)
symbolFont.copy()
# Paste it
self.sourceFont.selection.select(currentSourceFontGlyph)
self.sourceFont.paste()
self.sourceFont[currentSourceFontGlyph].glyphname = sym_glyph.glyphname
self.sourceFont[currentSourceFontGlyph].manualHints = True # No autohints for symbols
# Prepare symbol glyph dimensions
sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph])
if glyph_scale_data is not None:
if glyph_scale_data[1] is not None:
sym_dim = glyph_scale_data[1] # Use combined bounding box
(scale_ratio_x, scale_ratio_y) = self.get_scale_factors(sym_dim, sym_attr['stretch'])
else:
# This is roughly alike get_scale_factors(glyph_scale_data[1], 'pa')
# Except we do not have glyph_scale_data[1] always...
(scale_ratio_x, scale_ratio_y) = (glyph_scale_data[0], glyph_scale_data[0])
else:
(scale_ratio_x, scale_ratio_y) = self.get_scale_factors(sym_dim, sym_attr['stretch'])
overlap = sym_attr['params'].get('overlap')
if overlap:
scale_ratio_x *= 1.0 + (self.font_dim['width'] / (sym_dim['width'] * scale_ratio_x)) * overlap
y_overlap = min(0.01, overlap) # never aggressive vertical overlap
scale_ratio_y *= 1.0 + (self.font_dim['height'] / (sym_dim['height'] * scale_ratio_y)) * y_overlap
# Size in x to size in y ratio limit (to prevent over-wide glyphs)
xy_ratio_max = sym_attr['params'].get('xy-ratio')
if (xy_ratio_max):
xy_ratio = sym_dim['width'] * scale_ratio_x / (sym_dim['height'] * scale_ratio_y)
if xy_ratio > xy_ratio_max:
scale_ratio_x = scale_ratio_x * xy_ratio_max / xy_ratio
if scale_ratio_x != 1.0 or scale_ratio_y != 1.0:
self.sourceFont[currentSourceFontGlyph].transform(psMat.scale(scale_ratio_x, scale_ratio_y))
# We pasted and scaled now we want to align/move
# Use the dimensions from the newly pasted and stretched glyph to avoid any rounding errors
sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph])
# Use combined bounding box?
if glyph_scale_data is not None and glyph_scale_data[1] is not None:
scaleglyph_dim = scale_bounding_box(glyph_scale_data[1], scale_ratio_x, scale_ratio_y)
if scaleglyph_dim['advance'] is None:
# On monospaced symbol collections use their advance with, otherwise align horizontally individually
scaleglyph_dim['xmin'] = sym_dim['xmin']
scaleglyph_dim['xmax'] = sym_dim['xmax']
scaleglyph_dim['width'] = sym_dim['width']
sym_dim = scaleglyph_dim
y_align_distance = 0
if sym_attr['valign'] == 'c':
# Center the symbol vertically by matching the center of the line height and center of symbol
sym_ycenter = sym_dim['ymax'] - (sym_dim['height'] / 2)
font_ycenter = self.font_dim['ymax'] - (self.font_dim['height'] / 2)
y_align_distance = font_ycenter - sym_ycenter
# Handle glyph l/r/c alignment
x_align_distance = 0
if self.args.nonmono and sym_dim['advance'] is None:
# Remove left side bearing
# (i.e. do not remove left side bearing when combined BB is in use)
x_align_distance = -self.sourceFont[currentSourceFontGlyph].left_side_bearing
elif sym_attr['align']:
# First find the baseline x-alignment (left alignment amount)
x_align_distance = self.font_dim['xmin'] - sym_dim['xmin']
if sym_attr['align'] == 'c':
# Center align
x_align_distance += (self.font_dim['width'] / 2) - (sym_dim['width'] / 2)
elif sym_attr['align'] == 'r':
# Right align
x_align_distance += self.font_dim['width'] * self.get_target_width(sym_attr['stretch']) - sym_dim['width']
# If symbol glyph is wider than target font cell, just left-align
x_align_distance = max(self.font_dim['xmin'] - sym_dim['xmin'], x_align_distance)
if overlap:
overlap_width = self.font_dim['width'] * overlap
if sym_attr['align'] == 'l':
x_align_distance -= overlap_width
elif sym_attr['align'] == 'c':
if overlap_width > 0:
x_align_distance -= overlap_width / 2
elif sym_attr['align'] == 'r':
# Check and correct overlap; it can go wrong if we have a xy-ratio limit
target_xmax = (self.font_dim['xmin'] + self.font_dim['width']) * self.get_target_width(sym_attr['stretch'])
target_xmax += overlap_width
glyph_xmax = sym_dim['xmax'] + x_align_distance
correction = target_xmax - glyph_xmax
x_align_distance += correction
align_matrix = psMat.translate(x_align_distance, y_align_distance)
self.sourceFont[currentSourceFontGlyph].transform(align_matrix)
# Ensure after horizontal adjustments and centering that the glyph
# does not overlap the bearings (edges)
if not overlap:
self.remove_glyph_neg_bearings(self.sourceFont[currentSourceFontGlyph])
# Needed for setting 'advance width' on each glyph so they do not overlap,
# also ensures the font is considered monospaced on Windows by setting the
# same width for all character glyphs. This needs to be done for all glyphs,
# even the ones that are empty and didn't go through the scaling operations.
# It should come after setting the glyph bearings
if not self.args.nonmono:
self.set_glyph_width_mono(self.sourceFont[currentSourceFontGlyph])
else:
# Target font with variable advance width get the icons with their native widths
# and keeping possible (right and/or negative) bearings in effect
if sym_dim['advance'] is not None:
# 'Width' from monospaced scale group
width = sym_dim['advance']
else:
width = sym_dim['width']
# If we have overlap we need to subtract that to keep/get negative bearings
if overlap and (sym_attr['align'] == 'l' or sym_attr['align'] == 'r'):
width -= overlap_width
# Fontforge handles the width change like this:
# - Keep existing left_side_bearing
# - Set width
# - Calculate and set new right_side_bearing
self.sourceFont[currentSourceFontGlyph].width = int(width)
# Check if the inserted glyph is scaled correctly for monospace
if self.args.single:
(xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox()
if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)):
logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %f))",
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)
# end for
if not self.args.quiet:
sys.stdout.write("\n")
def set_sourcefont_glyph_widths(self):
""" Makes self.sourceFont monospace compliant """
for glyph in self.sourceFont.glyphs():
if (glyph.width == self.font_dim['width']):
# Don't touch the (negative) bearings if the width is ok
# Ligartures will have these.
continue
if (glyph.width != 0):
# If the width is zero this glyph is intened to be printed on top of another one.
# In this case we need to keep the negative bearings to shift it 'left'.
# Things like &Auml; have these: composed of U+0041 'A' and U+0308 'double dot above'
#
# If width is not zero, correct the bearings such that they are within the width:
self.remove_glyph_neg_bearings(glyph)
self.set_glyph_width_mono(glyph)
def remove_glyph_neg_bearings(self, glyph):
""" Sets passed glyph's bearings 0 if they are negative. """
try:
if glyph.left_side_bearing < 0:
glyph.left_side_bearing = 0
if glyph.right_side_bearing < 0:
glyph.right_side_bearing = 0
except:
pass
def set_glyph_width_mono(self, glyph):
""" Sets passed glyph.width to self.font_dim.width.
self.font_dim.width is set with self.get_sourcefont_dimensions().
"""
try:
# Fontforge handles the width change like this:
# - Keep existing left_side_bearing
# - Set width
# - Calculate and set new right_side_bearing
glyph.width = self.font_dim['width']
except:
pass
def prepareScaleRules(self, scaleRules, symbolFont, destGlyph):
""" Prepare raw ScaleRules data for use """
# The scaleRules is/will be a dict with these (possible) entries:
# 'ScaleGroups': List of ((lists of glyph codes) or (ranges of glyph codes)) that shall be scaled
# 'scales': List of associated scale factors, one for each entry in 'ScaleGroups' (generated by this function)
# 'bbdims': List of associated sym_dim dicts, one for each entry in 'ScaleGroups' (generated by this function)
# Each dim_dict describes the combined bounding box of all glyphs in one ScaleGroups group
# Example:
# { 'ScaleGroups': [ range(1, 3), [ 7, 10 ], ],
# 'scales': [ 1.23, 1.33, ],
# 'bbdims': [ dim_dict1, dim_dict2, ] }
#
# Each item in 'ScaleGroups' (a range or an explicit list) forms a group of glyphs that shall be
# as rescaled all with the same and maximum possible (for the included glyphs) 'pa' factor.
# If the 'bbdims' is present they all shall be shifted in the same way.
#
# Previously this structure has been used:
# 'ScaleGlyph' Lead glyph, which scaling factor is taken
# 'GlyphsToScale': List of ((glyph code) or (tuple of two glyph codes that form a closed range)) that shall be scaled
# Note that this allows only one group for the whle symbol font, and that the scaling factor is defined by
# a specific character, which needs to be manually selected (on each symbol font update).
# Previous entries are automatically rewritten to the new style.
#
# Note that scaleRules is overwritten with the added data.
if 'scales' in scaleRules:
# Already prepared... must not happen, ignore call
return
scaleRules['scales'] = []
scaleRules['bbdims'] = []
if 'ScaleGroups' not in scaleRules:
scaleRules['ScaleGroups'] = []
for group in scaleRules['ScaleGroups']:
sym_dim = get_multiglyph_boundingBox([ symbolFont[g] if g in symbolFont else None for g in group ], destGlyph)
scale = self.get_scale_factors(sym_dim, 'pa')[0]
scaleRules['scales'].append(scale)
scaleRules['bbdims'].append(sym_dim)
if 'ScaleGlyph' in scaleRules:
# Rewrite to equivalent ScaleGroup
group_list = []
if 'GlyphsToScale+' in scaleRules:
key = 'GlyphsToScale+'
plus = True
else:
key = 'GlyphsToScale'
plus = False
for i in scaleRules[key]:
if isinstance(i, tuple):
group_list.append(range(i[0], i[1] + 1))
else:
group_list.append(i)
sym_dim = get_glyph_dimensions(symbolFont[scaleRules['ScaleGlyph']])
scale = self.get_scale_factors(sym_dim, 'pa')[0]
scaleRules['ScaleGroups'].append(group_list)
scaleRules['scales'].append(scale)
if plus:
scaleRules['bbdims'].append(sym_dim)
else:
scaleRules['bbdims'].append(None) # The 'old' style keeps just the scale, not the positioning
def get_glyph_scale(self, symbol_unicode, scaleRules, symbolFont, dest_unicode):
""" Determines whether or not to use scaled glyphs for glyph in passed symbol_unicode """
# Potentially destorys the contents of self.sourceFont[dest_unicode]
if not 'scales' in scaleRules:
if not dest_unicode in self.sourceFont:
self.sourceFont.createChar(dest_unicode)
self.prepareScaleRules(scaleRules, symbolFont, self.sourceFont[dest_unicode])
for glyph_list, scale, box in zip(scaleRules['ScaleGroups'], scaleRules['scales'], scaleRules['bbdims']):
for e in glyph_list:
if isinstance(e, range):
if symbol_unicode in e:
return (scale, box)
elif symbol_unicode == e:
return (scale, box)
return None
def half_gap(gap, top):
""" Divides integer value into two new integers """
# Line gap add extra space on the bottom of the line which
# doesn't allow the powerline glyphs to fill the entire line.
# Put half of the gap into the 'cell', each top and bottom
if gap <= 0:
return 0
gap_top = int(gap / 2)
gap_bottom = gap - gap_top
if top:
logger.info("Redistributing line gap of %d (%d top and %d bottom)", gap, gap_top, gap_bottom)
return gap_top
return gap_bottom
def replace_font_name(font_name, replacement_dict):
""" Replaces all keys with vals from replacement_dict in font_name. """
for key, val in replacement_dict.items():
font_name = font_name.replace(key, val)
return font_name
def make_sure_path_exists(path):
""" Verifies path passed to it exists. """
try:
os.makedirs(path)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise
def sanitize_filename(filename, allow_dirs = False):
""" Enforces to not use forbitten characters in a filename/path. """
if filename == '.' and not allow_dirs:
return '_'
trans = filename.maketrans('<>:"|?*', '_______')
for i in range(0x00, 0x20):
trans[i] = ord('_')
if not allow_dirs:
trans[ord('/')] = ord('_')
trans[ord('\\')] = ord('_')
else:
trans[ord('\\')] = ord('/') # We use posix paths
return filename.translate(trans)
def get_multiglyph_boundingBox(glyphs, destGlyph = None):
""" Returns dict of the dimensions of multiple glyphs combined(, as if they are copied into destGlyph) """
# If destGlyph is given the glyph(s) are first copied over into that
# glyph and measured in that font (to avoid rounding errors)
# Leaves the destGlyph in unknown state!
bbox = [ None, None, None, None, None ]
for glyph in glyphs:
if glyph is None:
# Glyph has been in defining range but is not in the actual font
continue
if destGlyph and glyph.font != destGlyph.font:
glyph.font.selection.select(glyph)
glyph.font.copy()
destGlyph.font.selection.select(destGlyph)
destGlyph.font.paste()
glyph = destGlyph
gbb = glyph.boundingBox()
gadvance = glyph.width
if len(glyphs) > 1 and gbb[0] == gbb[2] and gbb[1] == gbb[3]:
# Ignore empty glyphs if we examine more than one glyph
continue
bbox[0] = gbb[0] if bbox[0] is None or bbox[0] > gbb[0] else bbox[0]
bbox[1] = gbb[1] if bbox[1] is None or bbox[1] > gbb[1] else bbox[1]
bbox[2] = gbb[2] if bbox[2] is None or bbox[2] < gbb[2] else bbox[2]
bbox[3] = gbb[3] if bbox[3] is None or bbox[3] < gbb[3] else bbox[3]
if not bbox[4]:
bbox[4] = -gadvance # Negative for one/first glyph
else:
if abs(bbox[4]) != gadvance:
bbox[4] = -1 # Marker for not-monospaced
else:
bbox[4] = gadvance # Positive for 2 or more glyphs
if bbox[4] and bbox[4] < 0:
# Not monospaced when only one glyph is used or multiple glyphs with different advance widths
bbox[4] = None
return {
'xmin' : bbox[0],
'ymin' : bbox[1],
'xmax' : bbox[2],
'ymax' : bbox[3],
'width' : bbox[2] + (-bbox[0]),
'height' : bbox[3] + (-bbox[1]),
'advance': bbox[4], # advance width if monospaced
}
def get_glyph_dimensions(glyph):
""" Returns dict of the dimesions of the glyph passed to it. """
return get_multiglyph_boundingBox([ glyph ])
def scale_bounding_box(bbox, scale_x, scale_y):
""" Return a scaled version of a glyph dimensions dict """
# Simulate scaling on combined bounding box, round values for better simulation
new_dim = {
'xmin' : int(bbox['xmin'] * scale_x),
'ymin' : int(bbox['ymin'] * scale_y),
'xmax' : int(bbox['xmax'] * scale_x),
'ymax' : int(bbox['ymax'] * scale_y),
'advance': int(bbox['advance'] * scale_x) if bbox['advance'] is not None else None,
}
new_dim['width'] = new_dim['xmax'] + (-new_dim['xmin'])
new_dim['height'] = new_dim['ymax'] + (-new_dim['ymin'])
return new_dim
def update_progress(progress):
""" Updates progress bar length.
Accepts a float between 0.0 and 1.0. Any int will be converted to a float.
A value at 1 or bigger represents 100%
modified from: https://stackoverflow.com/questions/3160699/python-progress-bar
"""
barLength = 40 # Modify this to change the length of the progress bar
if isinstance(progress, int):
progress = float(progress)
if progress >= 1:
progress = 1
status = "Done...\r\n" # NOTE: status initialized and never used
block = int(round(barLength * progress))
text = "\r{0}{1}%".format("" * block + "" * (barLength - block), int(progress * 100))
sys.stdout.write(text)
sys.stdout.flush()
def check_fontforge_min_version():
""" Verifies installed FontForge version meets minimum requirement. """
minimumVersion = 20141231
actualVersion = int(fontforge.version())
# un-comment following line for testing invalid version error handling
# actualVersion = 20120731
# versions tested: 20150612, 20150824
if actualVersion < minimumVersion:
logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion)
logger.critical("Please use at least version: %d", minimumVersion)
sys.exit(1)
def check_version_with_git(version):
""" Upgraded the version to the current git tag version (starting with 'v') """
git = subprocess.run("git describe --tags",
cwd=os.path.dirname(__file__),
shell=True,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
).stdout.decode('utf-8')
if len(git) == 0:
return False
tag = git.strip()
if len(tag) == 0 or not tag.startswith('v'):
return False
tag = tag[1:]
r = re.search('(.*?)(-[0-9]+)-g[0-9a-fA-F]+$', tag)
if r:
tag = r.group(1)
patchlevel = r.group(2)
else:
patchlevel = ""
# Inspired by Phaxmohdem's versiontuple https://stackoverflow.com/a/28568003
versiontuple = lambda v: tuple( p.zfill(8) for p in v.split(".") )
if versiontuple(tag) > versiontuple(version):
return tag + patchlevel
if versiontuple(tag) == versiontuple(version) and len(patchlevel) > 0:
return tag + patchlevel
return False
def setup_arguments():
parser = argparse.ArgumentParser(
description=(
'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/-/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='count', help='Whether to generate the glyphs as single-width not double-width (default is double-width)')
parser.add_argument('-l', '--adjust-line-height', dest='adjustLineHeight', default=False, action='store_true', help='Whether to adjust line heights (attempt to center powerline separators more evenly)')
parser.add_argument('-q', '--quiet', '--shutup', dest='quiet', default=False, action='store_true', help='Do not generate verbose output')
parser.add_argument('-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=1, type=int, nargs='?', help='Use alternative method to name patched fonts (recommended)', const=1, choices=range(0, 6 + 1))
# --makegroup has an additional undocumented numeric specifier. '--makegroup' is in fact '--makegroup 1'.
# Original font name: Hugo Sans Mono ExtraCondensed Light Italic
# NF Fam agg.
# 0 turned off, use old naming scheme [-] [-] [-]
# 1 HugoSansMono Nerd Font ExtraCondensed Light Italic [ ] [ ] [ ]
# 2 HugoSansMono Nerd Font ExtCn Light Italic [ ] [X] [ ]
# 3 HugoSansMono Nerd Font XCn Lt It [ ] [X] [X]
# 4 HugoSansMono NF ExtraCondensed Light Italic [X] [ ] [ ]
# 5 HugoSansMono NF ExtCn Light Italic [X] [X] [ ]
# 6 HugoSansMono NF XCn Lt It [X] [X] [X]
parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")')
parser.add_argument('--has-no-italic', dest='noitalic', default=False, action='store_true', help='Font family does not have Italic (but Oblique)')
# progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
progressbars_group_parser = parser.add_mutually_exclusive_group(required=False)
progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set (default)')
progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set')
parser.set_defaults(progressbars=True)
parser.add_argument('--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)
# --xavgcharwidth for compatibility with old applications like notepad and non-latin fonts
# Possible values with examples:
# <none> - copy from sourcefont (default)
# 0 - calculate from font according to OS/2-version-2
# 500 - set to 500
# symbol fonts to include arguments
sym_font_group = parser.add_argument_group('Symbol Fonts')
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 > 0 and not FontnameParserOK:
logger.critical("FontnameParser module missing (bin/scripts/name_parser/Fontname*), specify --makegroups 0")
sys.exit(1)
# if you add a new font, set it to True here inside the if condition
if args.complete:
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 not found:
font_complete = False
args.complete = font_complete
if args.nonmono and args.single:
logging.warning("Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.")
args.nonmono = False
make_sure_path_exists(args.outputdir)
if not os.path.isfile(args.font):
logging.critical("Font file does not exist: %s", args.font)
sys.exit(1)
if not os.access(args.font, os.R_OK):
logging.critical("Can not open font file for reading: %s", args.font)
sys.exit(1)
is_ttc = len(fontforge.fontsInFile(args.font)) > 1
try:
source_font_test = TableHEADWriter(args.font)
args.is_variable = source_font_test.find_table([b'avar', b'cvar', b'fvar', b'gvarb', b'HVAR', b'MVAR', b'VVAR'], 0)
if args.is_variable:
logging.warning("Source font is a variable open type font (VF), opening might fail...")
except:
args.is_variable = False
finally:
try:
source_font_test.close()
except:
pass
if args.extension == "":
args.extension = os.path.splitext(args.font)[1]
else:
args.extension = '.' + args.extension
if re.match("\.ttc$", args.extension, re.IGNORECASE):
if not is_ttc:
logging.critical("Can not create True Type Collections from single font files")
sys.exit(1)
else:
if is_ttc:
logging.critical("Can not create single font files from True Type Collections")
sys.exit(1)
if isinstance(args.xavgwidth, int) and not isinstance(args.xavgwidth, bool):
if args.xavgwidth < 0:
logging.critical("--xavgcharwidth takes no negative numbers")
sys.exit(2)
if args.xavgwidth > 16384:
logging.critical("--xavgcharwidth takes only numbers up to 16384")
sys.exit(2)
return args
def main():
global version
git_version = check_version_with_git(version)
allversions = "Patcher v{} ({}) (ff {})".format(
git_version if git_version else version, script_version, fontforge.version())
print("{} {}".format(projectName, allversions))
if git_version:
version = git_version
check_fontforge_min_version()
args = setup_arguments()
global logger
logger = logging.getLogger(os.path.basename(args.font))
logger.setLevel(logging.DEBUG)
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)
sourceFonts = []
all_fonts = fontforge.fontsInFile(args.font)
for i, subfont in enumerate(all_fonts):
if len(all_fonts) > 1:
print("\n")
logger.info("Processing %s (%d/%d)", subfont, i + 1, len(all_fonts))
try:
sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",))
except Exception:
logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information",
subfont)
sys.exit(1)
patcher.patch(sourceFonts[-1])
print("Done with Patch Sets, generating font...")
for f in sourceFonts:
patcher.setup_font_names(f)
patcher.generate(sourceFonts)
for f in sourceFonts:
f.close()
if __name__ == "__main__":
__dir__ = os.path.dirname(os.path.abspath(__file__))
main()