mirror of
https://github.com/ryanoasis/nerd-fonts.git
synced 2024-12-25 20:18:01 +02:00
e97e7959d6
[why] Sometimes we set an empty string as SubFamily name. That ends up as 'Book' which is unexpected. [how] The translation from empty to "Book" is done by Fontforge, at least with version 20220308. Make sure we always have a SubFamily, and if we don't that must be a 'Regular'. [note] This was only a problem with the old naming engine. --makegroups got this right always. Fixes: #1046 Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
1643 lines
86 KiB
Python
Executable File
1643 lines
86 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# coding=utf8
|
|
# Nerd Fonts Version: 2.3.0-RC
|
|
# 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 = "3.4.1"
|
|
|
|
version = "2.3.0-RC"
|
|
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
|
|
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`]"
|
|
)
|
|
)
|
|
|
|
# This is for experimenting
|
|
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 type(where) is 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,
|
|
}
|
|
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 is_monospaced(font):
|
|
""" Check if a font is probably monospaced """
|
|
# Some fonts lie (or have not any Panose flag set), spot check monospaced:
|
|
width = -1
|
|
width_mono = True
|
|
for glyph in [ 0x49, 0x4D, 0x57, 0x61, 0x69, 0x2E ]: # wide and slim glyphs 'I', 'M', 'W', 'a', 'i', '.'
|
|
if not glyph in font:
|
|
# A 'strange' font, believe Panose
|
|
return check_panose_monospaced(font) == 1
|
|
# print(" -> {} {}".format(glyph, font[glyph].width))
|
|
if width < 0:
|
|
width = font[glyph].width
|
|
continue
|
|
if font[glyph].width != width:
|
|
# Exception for fonts like Code New Roman Regular or Hermit Light/Bold:
|
|
# Allow small 'i' and dot to be smaller than normal
|
|
# I believe the source fonts are buggy
|
|
if glyph in [ 0x69, 0x2E ]:
|
|
if width > font[glyph].width:
|
|
continue
|
|
(xmin, _, xmax, _) = font[glyph].boundingBox()
|
|
if width > xmax - xmin:
|
|
continue
|
|
width_mono = False
|
|
break
|
|
# We believe our own check more then Panose ;-D
|
|
return width_mono
|
|
|
|
def get_advance_width(font, extended, minimum):
|
|
""" Get the maximum/minimum advance width in the extended(?) range """
|
|
width = 0
|
|
if extended:
|
|
end = 0x17f
|
|
else:
|
|
end = 0x07e
|
|
for glyph in range(0x21, end):
|
|
if not glyph in font:
|
|
continue
|
|
if glyph in range(0x7F, 0xBF):
|
|
continue # ignore special characters like '1/4' etc
|
|
if width == 0:
|
|
width = font[glyph].width
|
|
continue
|
|
if not minimum and width < font[glyph].width:
|
|
width = font[glyph].width
|
|
elif minimum and width > font[glyph].width:
|
|
width = font[glyph].width
|
|
return width
|
|
|
|
|
|
class font_patcher:
|
|
def __init__(self, args):
|
|
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.onlybitmaps = 0
|
|
self.essential = set()
|
|
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
|
|
|
|
def patch(self, font):
|
|
self.sourceFont = font
|
|
self.setup_version()
|
|
self.get_essential_references()
|
|
self.setup_name_backup(font)
|
|
if self.args.single:
|
|
self.assert_monospace()
|
|
self.remove_ligatures()
|
|
self.setup_patch_set()
|
|
self.setup_line_dimensions()
|
|
self.get_sourcefont_dimensions()
|
|
self.sourceFont.encoding = 'UnicodeFull' # Update the font encoding to ensure that the Unicode glyphs are available
|
|
self.onlybitmaps = self.sourceFont.onlybitmaps # Fetch this property before adding outlines. NOTE self.onlybitmaps initialized and never used
|
|
|
|
if self.args.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 some Windows applications (e.g. 'cmd') that is not enough. But they seem to honour the Panose table
|
|
# https://forum.high-logic.com/postedfiles/Panose.pdf
|
|
panose = list(self.sourceFont.os2_panose)
|
|
if panose[0] == 0 or panose[0] == 2: # 0 (1st value) = family kind; 0 = any (default); 2 = latin text and display
|
|
panose[0] = 2 # Assert kind
|
|
panose[3] = 9 # 3 (4th value) = propotion; 9 = monospaced
|
|
self.sourceFont.os2_panose = tuple(panose)
|
|
|
|
# For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs
|
|
if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2:
|
|
print("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):
|
|
sys.exit("{}: Can not find symbol glyph directory {} "
|
|
"(probably you need to download the src/glyphs/ directory?)".format(projectName, self.args.glyphdir))
|
|
|
|
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']):
|
|
sys.exit("{}: Can not find symbol source for '{}'\n{:>{}} (i.e. {})".format(
|
|
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename']))
|
|
if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK):
|
|
sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format(
|
|
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename']))
|
|
symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename']))
|
|
|
|
# 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(sourceFont.familyname) + ".ttc"))
|
|
sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer)
|
|
message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile)
|
|
else:
|
|
fontname = sourceFont.fullname
|
|
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):
|
|
if not self.args.quiet:
|
|
print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
|
|
bitmaps = str('otf') # otf/ttf, both is bf_ttf
|
|
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):
|
|
if not self.args.quiet:
|
|
print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts))
|
|
source_font.find_head_table(idx)
|
|
dest_font.find_head_table(idx)
|
|
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
|
|
if not self.args.quiet:
|
|
print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08))
|
|
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
|
|
if source_font.lowppem != dest_font.lowppem:
|
|
if not self.args.quiet:
|
|
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
|
|
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
|
|
if dest_font.modified:
|
|
dest_font.reset_table_checksum()
|
|
dest_font.reset_full_checksum()
|
|
except Exception as error:
|
|
print("Can not handle font flags ({})".format(repr(error)))
|
|
finally:
|
|
try:
|
|
source_font.close()
|
|
dest_font.close()
|
|
except:
|
|
pass
|
|
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")
|
|
print(message)
|
|
|
|
if self.args.postprocess:
|
|
subprocess.call([self.args.postprocess, outfile])
|
|
print("\nPost Processed: {}".format(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"]
|
|
font.fullname = font.persistent["fullname"]
|
|
font.familyname = font.persistent["familyname"]
|
|
verboseAdditionalFontNameSuffix = " " + projectNameSingular
|
|
if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later
|
|
additionalFontNameSuffix = " " + projectNameAbbreviation
|
|
else:
|
|
additionalFontNameSuffix = verboseAdditionalFontNameSuffix
|
|
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"
|
|
|
|
# if all source glyphs included simplify the name
|
|
else:
|
|
additionalFontNameSuffix = " " + projectNameSingular + " Complete"
|
|
verboseAdditionalFontNameSuffix = " " + projectNameSingular + " Complete"
|
|
|
|
# add mono signifier to end of name
|
|
if self.args.single:
|
|
additionalFontNameSuffix += " M"
|
|
verboseAdditionalFontNameSuffix += " Mono"
|
|
|
|
if FontnameParserOK and self.args.makegroups:
|
|
use_fullname = type(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)
|
|
if not n.parse_ok:
|
|
print("Have only minimal naming information, check resulting name. Maybe omit --makegroups option")
|
|
n.drop_for_powerline()
|
|
n.enable_short_families(True, "Noto")
|
|
n.set_for_windows(self.args.windows)
|
|
|
|
# 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"
|
|
|
|
if self.args.windows:
|
|
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
|
|
# 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',
|
|
'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):
|
|
# 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:
|
|
fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation
|
|
fam_suffix += ' Mono' if self.args.single else ''
|
|
n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix)
|
|
n.rename_font(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)
|
|
# 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:
|
|
print("Removing ligatures from configfile `Subtables` section")
|
|
ligature_subtables = json.loads(self.config.get("Subtables", "ligatures"))
|
|
for subtable in ligature_subtables:
|
|
print("Removing subtable:", subtable)
|
|
try:
|
|
self.sourceFont.removeLookupSubtable(subtable)
|
|
print("Successfully removed subtable:", subtable)
|
|
except Exception:
|
|
print("Failed to remove subtable:", subtable)
|
|
elif self.args.removeligatures:
|
|
print("Unable to read configfile, unable to remove ligatures")
|
|
|
|
|
|
def assert_monospace(self):
|
|
# Check if the sourcefont is monospaced
|
|
width_mono = is_monospaced(self.sourceFont)
|
|
panose_mono = check_panose_monospaced(self.sourceFont)
|
|
# The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown'
|
|
if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1):
|
|
print(" Warning: Monospaced check: Panose assumed to be wrong")
|
|
print(" Glyph widths {} / {} - {} and Panose says \"monospace {}\" ({})".format(get_advance_width(self.sourceFont, False, True),
|
|
get_advance_width(self.sourceFont, False, False), get_advance_width(self.sourceFont, True, False), panose_mono, list(self.sourceFont.os2_panose)))
|
|
if not width_mono:
|
|
print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless")
|
|
if self.args.single <= 1:
|
|
sys.exit(projectName + ": Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching")
|
|
|
|
|
|
def setup_patch_set(self):
|
|
""" Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """
|
|
# Supported params: overlap | careful
|
|
# Overlap value is used horizontally but vertically limited to 0.01
|
|
# Powerline dividers
|
|
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': 'xy2', 'params': {'overlap': 0.02}},
|
|
0xe0b9: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {}},
|
|
0xe0ba: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02}},
|
|
0xe0bb: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {}},
|
|
|
|
# Top Triangles
|
|
0xe0bc: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02}},
|
|
0xe0bd: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {}},
|
|
0xe0be: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.02}},
|
|
0xe0bf: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', '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': 'xy', 'params': {}},
|
|
0xe0c5: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}},
|
|
|
|
# Bigger squares
|
|
0xe0c6: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
|
|
0xe0c7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}},
|
|
|
|
# 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': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0cd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
|
|
|
|
# Legos
|
|
0xe0ce: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
|
|
0xe0cf: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {}},
|
|
0xe0d1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
|
|
# 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_DEFAULT = {
|
|
# 'pa' == preserve aspect ratio
|
|
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}
|
|
}
|
|
|
|
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': {}}
|
|
}
|
|
|
|
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.
|
|
# - 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 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
|
|
# ]}
|
|
#
|
|
# 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.
|
|
|
|
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': [
|
|
range(0xf095, 0xf0b0 + 1), # moon phases
|
|
range(0xf0b7, 0xf0c3 + 1), # wind strengths
|
|
range(0xf053, 0xf055 + 1), # thermometer
|
|
[0xf06e, 0xf070 ], # solar eclipse
|
|
[0xf042, 0xf045 ], # degree sign
|
|
]}
|
|
|
|
# Define the character ranges
|
|
# Symbol font ranges
|
|
self.patch_set = [
|
|
{'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE5AA, 'SrcStart': 0xE5FA, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
|
|
{'Enabled': True, 'Name': "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.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': self.args.material, 'Name': "Material", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, '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.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.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.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleRules': None, '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 setup_line_dimensions(self):
|
|
# win_ascent and win_descent are used to set the line height for windows fonts.
|
|
# hhead_ascent and hhead_descent are used to set the line height for mac fonts.
|
|
#
|
|
# 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:
|
|
self.sourceFont.os2_winascent += 1
|
|
|
|
# Make the line size identical for windows and mac
|
|
# ! This is broken because hhea* is changed but os2_typo* is not
|
|
# ! On the other hand we need intact (i.e. original) typo values
|
|
# ! in get_sourcefont_dimensions() @TODO FIXME
|
|
self.sourceFont.hhea_ascent = self.sourceFont.os2_winascent
|
|
self.sourceFont.hhea_descent = -self.sourceFont.os2_windescent
|
|
|
|
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
|
|
for glyph in range(0x21, 0x17f):
|
|
if not glyph in self.sourceFont:
|
|
continue
|
|
for r in self.sourceFont[glyph].references:
|
|
self.essential.add(self.sourceFont[r[0]].unicode)
|
|
|
|
def get_sourcefont_dimensions(self):
|
|
# Initial font dimensions
|
|
self.font_dim = {
|
|
'xmin' : 0,
|
|
'ymin' : -self.sourceFont.os2_windescent,
|
|
'xmax' : 0,
|
|
'ymax' : self.sourceFont.os2_winascent,
|
|
'width' : 0,
|
|
'height': 0,
|
|
}
|
|
if self.sourceFont.os2_use_typo_metrics:
|
|
self.font_dim['ymin'] = self.sourceFont.os2_typodescent
|
|
self.font_dim['ymax'] = self.sourceFont.os2_typoascent
|
|
|
|
# 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.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,
|
|
}
|
|
|
|
# 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
|
|
gap = max(self.sourceFont.hhea_linegap, self.sourceFont.os2_typolinegap) # TODO probably wrong
|
|
if self.sourceFont.os2_use_typo_metrics:
|
|
gap = self.sourceFont.os2_typolinegap
|
|
self.sourceFont.hhea_linegap = 0
|
|
self.sourceFont.os2_typolinegap = 0
|
|
if gap > 0:
|
|
gap_top = int(gap / 2)
|
|
gap_bottom = gap - gap_top
|
|
print("Redistributing line gap of {} ({} top and {} bottom)".format(gap, gap_top, gap_bottom))
|
|
self.font_dim['ymin'] -= gap_bottom
|
|
self.font_dim['ymax'] += gap_top
|
|
self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax']
|
|
self.sourceFont.os2_typoascent = self.sourceFont.os2_typoascent + gap_top
|
|
self.sourceFont.os2_typodescent = self.sourceFont.os2_typodescent - gap_bottom
|
|
# TODO Check what to do with win and hhea values
|
|
|
|
# Find the biggest char width
|
|
# Ignore the y-values, os2_winXXXXX values set above are used for line height
|
|
#
|
|
# 0x00-0x17f is the Latin Extended-A range
|
|
warned = self.args.quiet or self.args.nonmono # Do not warn if quiet or proportional target
|
|
for glyph in range(0x21, 0x17f):
|
|
if glyph in range(0x7F, 0xBF) or glyph in [
|
|
0x132, 0x134, # 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
|
|
]:
|
|
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 warned and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
|
|
print("Extended glyphs wider than basic glyphs")
|
|
warned = True
|
|
# print("New MAXWIDTH-A {} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
|
|
if xmax > self.font_dim['xmax']:
|
|
self.font_dim['xmax'] = xmax
|
|
# print("New MAXWIDTH-B {} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
|
|
# print("FINAL", self.font_dim)
|
|
|
|
|
|
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)
|
|
|
|
# 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 (stretch != 'pa' and '2' not in stretch):
|
|
relative_width = 1.0
|
|
else:
|
|
relative_width = 2.0
|
|
target_width = self.font_dim['width'] * relative_width
|
|
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 stretch == 'pa':
|
|
# 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:
|
|
# 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
|
|
glyphSetLength = 0
|
|
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:
|
|
sys.stdout.write("Adding " + str(max(1, glyphSetLength)) + " Glyphs from " + setName + " Set \n")
|
|
|
|
currentSourceFontGlyph = -1 # initialize for the exactEncoding case
|
|
width_warning = False
|
|
|
|
for index, sym_glyph in enumerate(symbolFontSelection):
|
|
index = max(1, index)
|
|
|
|
sym_attr = attributes.get(sym_glyph.unicode)
|
|
if sym_attr is None:
|
|
sym_attr = attributes['default']
|
|
|
|
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:
|
|
print(" Can not determine codepoint of {:X}. Skipping...".format(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:
|
|
if not self.args.quiet:
|
|
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
|
|
print(" Found {} Glyph at {:X}. Skipping...".format(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("*")
|
|
|
|
# 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
|
|
|
|
# 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
|
|
# 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'] - sym_dim['width']
|
|
if not self.args.single and '2' in sym_attr['stretch']:
|
|
x_align_distance += self.font_dim['width']
|
|
|
|
if overlap:
|
|
overlap_width = self.font_dim['width'] * overlap
|
|
if sym_attr['align'] == 'l':
|
|
x_align_distance -= overlap_width
|
|
if sym_attr['align'] == 'r' and not self.args.nonmono:
|
|
# Nonmono is 'left aligned' per definition, translation does not help here
|
|
x_align_distance += overlap_width
|
|
|
|
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)):
|
|
print("\n Warning: Scaled glyph U+{:X} wider than one monospace width ({} / {} (overlap {}))".format(
|
|
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap))
|
|
|
|
# end for
|
|
|
|
if not self.args.quiet or self.args.progressbars:
|
|
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 Ä 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 = []
|
|
for i in scaleRules['GlyphsToScale']:
|
|
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)
|
|
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 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:
|
|
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:
|
|
sys.stderr.write("{}: You seem to be using an unsupported (old) version of fontforge: {}\n".format(projectName, actualVersion))
|
|
sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion))
|
|
sys.exit(1)
|
|
|
|
def setup_arguments():
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
'Nerd Fonts Font Patcher: patches a given font with programming and development related glyphs\n\n'
|
|
'* Website: https://www.nerdfonts.com\n'
|
|
'* Version: ' + version + '\n'
|
|
'* Development Website: https://github.com/ryanoasis/nerd-fonts\n'
|
|
'* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/-/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('-w', '--windows', dest='windows', default=False, action='store_true', help='Limit the internal font name to 31 characters (for Windows compatibility)')
|
|
parser.add_argument('-c', '--complete', dest='complete', default=False, action='store_true', help='Add all available Glyphs')
|
|
parser.add_argument('--careful', dest='careful', default=False, action='store_true', help='Do not overwrite existing glyphs if detected')
|
|
parser.add_argument('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specificed in JSON configuration file')
|
|
parser.add_argument('--postprocess', dest='postprocess', default=False, type=str, nargs='?', help='Specify a Script for Post Processing')
|
|
parser.add_argument('--configfile', dest='configfile', default=False, type=str, nargs='?', help='Specify a file path for JSON configuration file (see sample: src/config.sample.json)')
|
|
parser.add_argument('--custom', dest='custom', default=False, type=str, nargs='?', help='Specify a custom symbol font. All new glyphs will be copied, with no scaling applied.')
|
|
parser.add_argument('-ext', '--extension', dest='extension', default="", type=str, nargs='?', help='Change font file type to create (e.g., ttf, otf)')
|
|
parser.add_argument('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to')
|
|
parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching')
|
|
parser.add_argument('--makegroups', dest='makegroups', default=False, action='store_true', help='Use alternative method to name patched fonts (experimental)')
|
|
parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")')
|
|
|
|
# progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
|
|
progressbars_group_parser = parser.add_mutually_exclusive_group(required=False)
|
|
progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set')
|
|
progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set')
|
|
parser.set_defaults(progressbars=True)
|
|
parser.add_argument('--also-windows', dest='alsowindows', default=False, action='store_true', help='Create two fonts, the normal and the --windows version')
|
|
|
|
# symbol fonts to include arguments
|
|
sym_font_group = parser.add_argument_group('Symbol Fonts')
|
|
sym_font_group.add_argument('--fontawesome', dest='fontawesome', default=False, action='store_true', help='Add Font Awesome Glyphs (http://fontawesome.io/)')
|
|
sym_font_group.add_argument('--fontawesomeextension', dest='fontawesomeextension', default=False, action='store_true', help='Add Font Awesome Extension Glyphs (https://andrelzgava.github.io/font-awesome-extension/)')
|
|
sym_font_group.add_argument('--fontlogos', '--fontlinux', dest='fontlogos', default=False, action='store_true', help='Add Font Logos Glyphs (https://github.com/Lukas-W/font-logos)')
|
|
sym_font_group.add_argument('--octicons', dest='octicons', default=False, action='store_true', help='Add Octicons Glyphs (https://octicons.github.com)')
|
|
sym_font_group.add_argument('--codicons', dest='codicons', default=False, action='store_true', help='Add Codicons Glyphs (https://github.com/microsoft/vscode-codicons)')
|
|
sym_font_group.add_argument('--powersymbols', dest='powersymbols', default=False, action='store_true', help='Add IEC Power Symbols (https://unicodepowersymbol.com/)')
|
|
sym_font_group.add_argument('--pomicons', dest='pomicons', default=False, action='store_true', help='Add Pomicon Glyphs (https://github.com/gabrielelana/pomicons)')
|
|
sym_font_group.add_argument('--powerline', dest='powerline', default=False, action='store_true', help='Add Powerline Glyphs')
|
|
sym_font_group.add_argument('--powerlineextra', dest='powerlineextra', default=False, action='store_true', help='Add Powerline Glyphs (https://github.com/ryanoasis/powerline-extra-symbols)')
|
|
sym_font_group.add_argument('--material', '--materialdesignicons', '--mdi', dest='material', default=False, action='store_true', help='Add Material Design Icons (https://github.com/templarian/MaterialDesign)')
|
|
sym_font_group.add_argument('--weather', '--weathericons', dest='weather', default=False, action='store_true', help='Add Weather Icons (https://github.com/erikflowers/weather-icons)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.makegroups and not FontnameParserOK:
|
|
sys.exit(projectName + ": FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName))
|
|
|
|
# if you add a new font, set it to True here inside the if condition
|
|
if args.complete:
|
|
args.fontawesome = True
|
|
args.fontawesomeextension = True
|
|
args.fontlogos = True
|
|
args.octicons = True
|
|
args.codicons = True
|
|
args.powersymbols = True
|
|
args.pomicons = True
|
|
args.powerline = True
|
|
args.powerlineextra = True
|
|
args.material = True
|
|
args.weather = True
|
|
|
|
if not args.complete:
|
|
sym_font_args = []
|
|
# add the list of arguments for each symbol font to the list sym_font_args
|
|
for action in sym_font_group._group_actions:
|
|
sym_font_args.append(action.__dict__['option_strings'])
|
|
|
|
# determine whether or not all symbol fonts are to be used
|
|
font_complete = True
|
|
for sym_font_arg_aliases in sym_font_args:
|
|
found = False
|
|
for alias in sym_font_arg_aliases:
|
|
if alias in sys.argv:
|
|
found = True
|
|
if not found:
|
|
font_complete = False
|
|
args.complete = font_complete
|
|
|
|
if args.alsowindows:
|
|
args.windows = False
|
|
|
|
if args.nonmono and args.single:
|
|
print("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):
|
|
sys.exit("{}: Font file does not exist: {}".format(projectName, args.font))
|
|
if not os.access(args.font, os.R_OK):
|
|
sys.exit("{}: Can not open font file for reading: {}".format(projectName, args.font))
|
|
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:
|
|
print(" 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:
|
|
sys.exit(projectName + ": Can not create True Type Collections from single font files")
|
|
else:
|
|
if is_ttc:
|
|
sys.exit(projectName + ": Can not create single font files from True Type Collections")
|
|
|
|
return args
|
|
|
|
|
|
def main():
|
|
print("{} Patcher v{} ({}) executing".format(projectName, version, script_version))
|
|
check_fontforge_min_version()
|
|
args = setup_arguments()
|
|
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{}: Processing {} ({}/{})".format(projectName, subfont, i + 1, len(all_fonts)))
|
|
try:
|
|
sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",))
|
|
except Exception:
|
|
sys.exit("{}: Can not open font '{}', try to open with fontforge interactively to get more information".format(
|
|
projectName, subfont))
|
|
|
|
patcher.patch(sourceFonts[-1])
|
|
|
|
print("Done with Patch Sets, generating font...")
|
|
for f in sourceFonts:
|
|
patcher.setup_font_names(f)
|
|
patcher.generate(sourceFonts)
|
|
|
|
# This mainly helps to improve CI runtime
|
|
if patcher.args.alsowindows:
|
|
patcher.args.windows = True
|
|
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()
|