1
0
mirror of https://github.com/ryanoasis/nerd-fonts.git synced 2024-11-19 16:39:20 +02:00
nerd-fonts/font-patcher

1504 lines
79 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env python
# coding=utf8
2022-10-07 13:55:35 +02:00
# 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.2.2"
2022-10-07 13:55:35 +02:00
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:
2018-08-04 12:39:21 +02:00
import configparser
except ImportError:
2018-08-04 12:39:21 +02:00
sys.exit(projectName + ": configparser module is probably not installed. Try `pip install configparser` or equivalent")
try:
import psMat
import fontforge
except ImportError:
2018-08-04 12:39:21 +02:00
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`]"
2018-08-04 12:39:21 +02:00
)
)
# 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
2018-08-04 12:39:21 +02:00
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_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
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 == b'head':
return
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')
font-patcher: Prevent --mono on proportional fonts [why] When the source font is proportional we can not really create a monospaced (patched) font from it. The glyph width is for example very small for 'i' but wide for 'W'. The glyphs are all left aligned, leaving very strange separation between smallish glyphs. Even if we would center the glyphs, the look would be strange and completely differenmt from the source font's look. [how] For proportional fonts do not allow to patch with `--mono`. The fact if a source font is monospaced is determined by examining some (very few) glyphs. But testing all our source fonts in the repo shows that it is sufficient. Furthermore the Panose flag is checked and differences between the flag and what the glyph examination found are reported. The user can enforce `Nerd Font Mono` generation with double specifying the command line option `--mono --mono`. Still a warning will be issued. [note] Because `gotta-patch-em-all-font-patcher!.sh` does not really count the variations but calculates them in a separate loop it does not know anymore how many variations are created per family. The numbers are wrong. But probably we should count the result font files in the end anyhow. Because the information is not needed (in an automated manner) this is not corrected here. It seems wrong anyhow: total_variation_count=$((total_variation_count+combination_count)) total_count=$((total_count+complete_variations_per_family+combination_count)) Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-09-04 19:55:24 +02:00
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
2018-08-04 12:39:21 +02:00
class font_patcher:
def __init__(self, args):
self.args = args # class 'argparse.Namespace'
2018-08-07 13:06:39 +02:00
self.sym_font_args = []
2018-08-04 12:39:21 +02:00
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.onlybitmaps = 0
self.essential = set()
2018-08-04 12:39:21 +02:00
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)
font-patcher: Prevent --mono on proportional fonts [why] When the source font is proportional we can not really create a monospaced (patched) font from it. The glyph width is for example very small for 'i' but wide for 'W'. The glyphs are all left aligned, leaving very strange separation between smallish glyphs. Even if we would center the glyphs, the look would be strange and completely differenmt from the source font's look. [how] For proportional fonts do not allow to patch with `--mono`. The fact if a source font is monospaced is determined by examining some (very few) glyphs. But testing all our source fonts in the repo shows that it is sufficient. Furthermore the Panose flag is checked and differences between the flag and what the glyph examination found are reported. The user can enforce `Nerd Font Mono` generation with double specifying the command line option `--mono --mono`. Still a warning will be issued. [note] Because `gotta-patch-em-all-font-patcher!.sh` does not really count the variations but calculates them in a separate loop it does not know anymore how many variations are created per family. The numbers are wrong. But probably we should count the result font files in the end anyhow. Because the information is not needed (in an automated manner) this is not corrected here. It seems wrong anyhow: total_variation_count=$((total_variation_count+combination_count)) total_count=$((total_count+complete_variations_per_family+combination_count)) Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-09-04 19:55:24 +02:00
if self.args.single:
self.assert_monospace()
2018-08-04 12:39:21 +02:00
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)
2018-08-04 12:39:21 +02:00
# 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))
2018-08-04 12:39:21 +02:00
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']))
2018-08-04 12:39:21 +02:00
# Match the symbol font size to the source font size
symfont.em = self.sourceFont.em
PreviousSymbolFilename = patch['Filename']
font-patcher: Remove "SrcEnd" option [why] In patch set definitions we have for the source ranges SymStart and SymEnd and for the destination we can specify SrcStart and SrcEnd The SrcEnd can be automatically generated. For SrcEnd values that differ from the autogenerated value (are lower) the script would crash, or (are higher) ignore them anyhow. There are two modes: 'exact = True' and 'exact = False'. The SrcStart and SrcEnd values are ignored if exact is True, because the glyphs are patched into the same codepoint where they originate. This also means that gaps in the symbols are preserved - all patched in glyphs have the same codepoint as they have in the source (symbol) font. When exact is False on the other hand, all (non empty) glyphs are filled into the codepoints that start at SrcStart. Gaps (empty glyphs) are ignored and thus are not present as gap in the patched font anymore (*). The to-be-filled-next codepoint in the patched font just increases by 1 on every filled symbol. See note for the reason. This also makes maintining the patch set easier as noone needs to 'calculate' a SrcEnd value anymore. [how] Use directly the start value and the counter instead of filling an array with a range, that is then indexed by the counter. Before this commit: list_of_patched_font_codepoints = list(range(SrcStart, SrcEnd + 1)) current_codepoint = list_of_patched_font_codepoints[loop_counter] After this commit: current_codepoint = SrcStart + loop_counter [note] Maybe related to code removed with c728079b6. I guess the code uses the list, because it has been believed that glyphs can only be indexed by strings containing a hex number. The array supposedly contained that strings. But in fact the fontforge python module docu is like this: font.__getitem__(key) If key is an integer, then returns the glyph at that encoding. If a string then returns the glyph with that name. May not be assigned to. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-04 12:33:38 +02:00
# If patch table doesn't include a source start, re-use the symbol font values
2018-08-04 12:39:21 +02:00
SrcStart = patch['SrcStart']
if not SrcStart:
SrcStart = patch['SymStart']
font-patcher: Remove "SrcEnd" option [why] In patch set definitions we have for the source ranges SymStart and SymEnd and for the destination we can specify SrcStart and SrcEnd The SrcEnd can be automatically generated. For SrcEnd values that differ from the autogenerated value (are lower) the script would crash, or (are higher) ignore them anyhow. There are two modes: 'exact = True' and 'exact = False'. The SrcStart and SrcEnd values are ignored if exact is True, because the glyphs are patched into the same codepoint where they originate. This also means that gaps in the symbols are preserved - all patched in glyphs have the same codepoint as they have in the source (symbol) font. When exact is False on the other hand, all (non empty) glyphs are filled into the codepoints that start at SrcStart. Gaps (empty glyphs) are ignored and thus are not present as gap in the patched font anymore (*). The to-be-filled-next codepoint in the patched font just increases by 1 on every filled symbol. See note for the reason. This also makes maintining the patch set easier as noone needs to 'calculate' a SrcEnd value anymore. [how] Use directly the start value and the counter instead of filling an array with a range, that is then indexed by the counter. Before this commit: list_of_patched_font_codepoints = list(range(SrcStart, SrcEnd + 1)) current_codepoint = list_of_patched_font_codepoints[loop_counter] After this commit: current_codepoint = SrcStart + loop_counter [note] Maybe related to code removed with c728079b6. I guess the code uses the list, because it has been believed that glyphs can only be indexed by strings containing a hex number. The array supposedly contained that strings. But in fact the fontforge python module docu is like this: font.__getitem__(key) If key is an integer, then returns the glyph at that encoding. If a string then returns the glyph with that name. May not be assigned to. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-04 12:33:38 +02:00
self.copy_glyphs(SrcStart, symfont, patch['SymStart'], patch['SymEnd'], patch['Exact'], patch['ScaleGlyph'], patch['Name'], patch['Attributes'])
2018-08-04 12:39:21 +02:00
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]
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"))
# the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'.
sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=(str('opentype'), str('PfEd-comments')), layer=layer)
message = "\nGenerated: {} fonts in '{}'".format(len(sourceFonts), outfile)
else:
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))
# the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'.
bitmaps = str()
if len(self.sourceFont.bitmapSizes):
print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
bitmaps = str('otf') # otf/ttf, both is bf_ttf
sourceFont.generate(outfile, bitmap_type=bitmaps, flags=(str('opentype'), str('PfEd-comments')))
message = "\nGenerated: {} in '{}'".format(self.sourceFont.fullname, outfile)
# Adjust flags that can not be changed via fontforge
try:
source_font = TableHEADWriter(self.args.font)
dest_font = TableHEADWriter(outfile)
for idx in range(source_font.num_fonts):
print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts))
source_font.find_head_table(idx)
dest_font.find_head_table(idx)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08))
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
if source_font.lowppem != dest_font.lowppem:
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified:
dest_font.reset_table_checksum()
dest_font.reset_full_checksum()
except Exception as error:
print("Can not handle font flags ({})".format(repr(error)))
finally:
try:
source_font.close()
dest_font.close()
except:
pass
print(message)
2018-08-04 12:39:21 +02:00
if self.args.postprocess:
subprocess.call([self.args.postprocess, outfile])
print("\nPost Processed: {}".format(outfile))
2018-08-04 12:39:21 +02:00
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"]
2018-08-04 12:39:21 +02:00
verboseAdditionalFontNameSuffix = " " + projectNameSingular
if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later
additionalFontNameSuffix = " " + projectNameAbbreviation
else:
additionalFontNameSuffix = verboseAdditionalFontNameSuffix
2018-08-07 13:27:48 +02:00
if not self.args.complete:
2018-08-07 13:06:39 +02:00
# 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"
2021-12-05 00:34:09 +02:00
if self.args.codicons:
additionalFontNameSuffix += " C"
verboseAdditionalFontNameSuffix += " Plus Codicons"
2018-08-07 13:06:39 +02:00
if self.args.pomicons:
additionalFontNameSuffix += " P"
verboseAdditionalFontNameSuffix += " Plus Pomicons"
if self.args.fontlogos:
2018-08-07 13:06:39 +02:00
additionalFontNameSuffix += " L"
verboseAdditionalFontNameSuffix += " Plus Font Logos"
2018-08-07 13:06:39 +02:00
if self.args.material:
additionalFontNameSuffix += " MDI"
verboseAdditionalFontNameSuffix += " Plus Material Design Icons"
if self.args.weather:
additionalFontNameSuffix += " WEA"
verboseAdditionalFontNameSuffix += " Plus Weather Icons"
2018-08-04 12:39:21 +02:00
# if all source glyphs included simplify the name
2018-08-07 13:06:39 +02:00
else:
2018-08-04 12:39:21 +02:00
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
2018-08-04 12:39:21 +02:00
# 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()
2018-08-04 12:39:21 +02:00
# dont trust 'font.familyname'
2018-08-04 12:39:21 +02:00
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
2018-08-04 12:39:21 +02:00
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")
2018-08-04 12:39:21 +02:00
# 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]
2018-08-04 12:39:21 +02:00
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":
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
2018-08-04 12:39:21 +02:00
if self.args.windows:
maxFamilyLength = 31
maxFontLength = maxFamilyLength - len('-' + subFamily)
familyname += " " + projectNameAbbreviation
if self.args.single:
familyname += "M"
2018-08-04 12:39:21 +02:00
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(' ', '')
2019-07-05 21:32:06 +02:00
# 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',
2021-11-12 06:16:12 +02:00
'cascadiacode' : 'caskaydiacove',
'Cascadia Mono' : 'Caskaydia Mono',
'cascadia mono' : 'caskaydia mono',
'CascadiaMono' : 'CaskaydiaMono',
2021-12-19 21:12:08 +02:00
'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',
2019-07-05 21:32:06 +02:00
}
2018-08-04 12:39:21 +02:00
# 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"
2018-08-04 12:39:21 +02:00
)
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)
2018-08-04 12:39:21 +02:00
font.comment = projectInfo
font.fontlog = projectInfo
2018-08-04 12:39:21 +02:00
def setup_version(self):
""" Add the Nerd Font version to the original version """
2018-08-07 13:06:39 +02:00
# 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)
2018-08-07 13:06:39 +02:00
# print("Version now is {}".format(sourceFont.version))
2018-08-04 12:39:21 +02:00
def remove_ligatures(self):
# let's deal with ligatures (mostly for monospaced fonts)
# the tables have been removed from the repo with >this< commit
2018-08-04 12:39:21 +02:00
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)
2021-07-25 23:24:27 +02:00
elif self.args.removeligatures:
print("Unable to read configfile, unable to remove ligatures")
else:
print("No configfile given, skipping configfile related actions")
2018-08-04 12:39:21 +02:00
font-patcher: Prevent --mono on proportional fonts [why] When the source font is proportional we can not really create a monospaced (patched) font from it. The glyph width is for example very small for 'i' but wide for 'W'. The glyphs are all left aligned, leaving very strange separation between smallish glyphs. Even if we would center the glyphs, the look would be strange and completely differenmt from the source font's look. [how] For proportional fonts do not allow to patch with `--mono`. The fact if a source font is monospaced is determined by examining some (very few) glyphs. But testing all our source fonts in the repo shows that it is sufficient. Furthermore the Panose flag is checked and differences between the flag and what the glyph examination found are reported. The user can enforce `Nerd Font Mono` generation with double specifying the command line option `--mono --mono`. Still a warning will be issued. [note] Because `gotta-patch-em-all-font-patcher!.sh` does not really count the variations but calculates them in a separate loop it does not know anymore how many variations are created per family. The numbers are wrong. But probably we should count the result font files in the end anyhow. Because the information is not needed (in an automated manner) this is not corrected here. It seems wrong anyhow: total_variation_count=$((total_variation_count+combination_count)) total_count=$((total_count+complete_variations_per_family+combination_count)) Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-09-04 19:55:24 +02:00
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")
2018-08-04 12:39:21 +02:00
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
# Powerline dividers
SYM_ATTR_POWERLINE = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
2018-08-04 12:39:21 +02:00
# Arrow tips
0xe0b0: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0b1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0b2: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0b3: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
# Rounded arcs
0xe0b4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
0xe0b5: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
0xe0b6: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
0xe0b7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
# Bottom Triangles
0xe0b8: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0b9: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0ba: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0bb: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
# Top Triangles
0xe0bc: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0bd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0be: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
0xe0bf: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
# Flames
0xe0c0: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
0xe0c1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
0xe0c2: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
0xe0c3: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
# Small squares
0xe0c4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
0xe0c5: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}},
2018-08-04 12:39:21 +02:00
# Bigger squares
0xe0c6: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
0xe0c7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {}},
2018-08-04 12:39:21 +02:00
# Waveform
0xe0c8: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
# Hexagons
0xe0cc: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
0xe0cd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
2018-08-04 12:39:21 +02:00
# Legos
0xe0ce: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {}},
0xe0cf: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {}},
2018-08-04 12:39:21 +02:00
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}},
0xe0d4: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}
}
SYM_ATTR_DEFAULT = {
# 'pa' == preserve aspect ratio
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}
2018-08-04 12:39:21 +02:00
}
SYM_ATTR_FONTA = {
# 'pa' == preserve aspect ratio
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
2018-08-04 12:39:21 +02:00
# 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': {}}
2018-08-04 12:39:21 +02:00
}
CUSTOM_ATTR = {
# 'pa' == preserve aspect ratio
'default': {'align': 'c', 'valign': '', 'stretch': '', 'params': {}}
2018-08-04 12:39:21 +02:00
}
# Most glyphs we want to maximize during the scale. However, there are some
# that need to be small or stay relative in size to each other.
# The following list are those glyphs. A tuple represents a range.
DEVI_SCALE_LIST = {'ScaleGlyph': 0xE60E, # Android logo
'GlyphsToScale': [
(0xe6bd, 0xe6c3) # very small things
]}
FONTA_SCALE_LIST = {'GlyphsToScale': [
[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
]}
2018-08-04 12:39:21 +02:00
# 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, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': True, 'Name': "Devicons", 'Filename': "devicons.ttf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE6C5, 'SrcStart': 0xE700, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0A3, 'SymEnd': 0xE0A3, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D4, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
{'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': 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, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleGlyph': None, 'Attributes': CUSTOM_ATTR}
2018-08-04 12:39:21 +02:00
]
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
2018-08-04 12:39:21 +02:00
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)
2018-08-04 12:39:21 +02:00
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
2018-08-04 12:39:21 +02:00
# 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
2018-08-04 12:39:21 +02:00
# 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
for glyph in range(0x21, 0x17f):
if glyph in range(0x7F, 0xBF):
continue # ignore special characters like '1/4' etc
2018-08-04 12:39:21 +02:00
try:
(_, _, xmax, _) = self.sourceFont[glyph].boundingBox()
except TypeError:
continue
if self.font_dim['width'] < self.sourceFont[glyph].width:
self.font_dim['width'] = self.sourceFont[glyph].width
font-patcher: Prevent --mono on proportional fonts [why] When the source font is proportional we can not really create a monospaced (patched) font from it. The glyph width is for example very small for 'i' but wide for 'W'. The glyphs are all left aligned, leaving very strange separation between smallish glyphs. Even if we would center the glyphs, the look would be strange and completely differenmt from the source font's look. [how] For proportional fonts do not allow to patch with `--mono`. The fact if a source font is monospaced is determined by examining some (very few) glyphs. But testing all our source fonts in the repo shows that it is sufficient. Furthermore the Panose flag is checked and differences between the flag and what the glyph examination found are reported. The user can enforce `Nerd Font Mono` generation with double specifying the command line option `--mono --mono`. Still a warning will be issued. [note] Because `gotta-patch-em-all-font-patcher!.sh` does not really count the variations but calculates them in a separate loop it does not know anymore how many variations are created per family. The numbers are wrong. But probably we should count the result font files in the end anyhow. Because the information is not needed (in an automated manner) this is not corrected here. It seems wrong anyhow: total_variation_count=$((total_variation_count+combination_count)) total_count=$((total_count+complete_variations_per_family+combination_count)) Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-09-04 19:55:24 +02:00
# print("New MAXWIDTH-A {} {} {}".format(glyph, self.sourceFont[glyph].width, xmax))
2018-08-04 12:39:21 +02:00
if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax
font-patcher: Prevent --mono on proportional fonts [why] When the source font is proportional we can not really create a monospaced (patched) font from it. The glyph width is for example very small for 'i' but wide for 'W'. The glyphs are all left aligned, leaving very strange separation between smallish glyphs. Even if we would center the glyphs, the look would be strange and completely differenmt from the source font's look. [how] For proportional fonts do not allow to patch with `--mono`. The fact if a source font is monospaced is determined by examining some (very few) glyphs. But testing all our source fonts in the repo shows that it is sufficient. Furthermore the Panose flag is checked and differences between the flag and what the glyph examination found are reported. The user can enforce `Nerd Font Mono` generation with double specifying the command line option `--mono --mono`. Still a warning will be issued. [note] Because `gotta-patch-em-all-font-patcher!.sh` does not really count the variations but calculates them in a separate loop it does not know anymore how many variations are created per family. The numbers are wrong. But probably we should count the result font files in the end anyhow. Because the information is not needed (in an automated manner) this is not corrected here. It seems wrong anyhow: total_variation_count=$((total_variation_count+combination_count)) total_count=$((total_count+complete_variations_per_family+combination_count)) Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-09-04 19:55:24 +02:00
# print("New MAXWIDTH-B {} {} {}".format(glyph, self.sourceFont[glyph].width, xmax))
2018-08-04 12:39:21 +02:00
def get_scale_factor(self, sym_dim):
scale_ratio = 1
2018-08-04 12:39:21 +02:00
# We want to preserve x/y aspect ratio, so find biggest scale factor that allows symbol to fit
scale_ratio_x = self.font_dim['width'] / sym_dim['width']
scale_ratio_y = self.font_dim['height'] / sym_dim['height']
2018-08-04 12:39:21 +02:00
if scale_ratio_x > scale_ratio_y:
scale_ratio = scale_ratio_y
else:
scale_ratio = scale_ratio_x
return scale_ratio
font-patcher: Remove "SrcEnd" option [why] In patch set definitions we have for the source ranges SymStart and SymEnd and for the destination we can specify SrcStart and SrcEnd The SrcEnd can be automatically generated. For SrcEnd values that differ from the autogenerated value (are lower) the script would crash, or (are higher) ignore them anyhow. There are two modes: 'exact = True' and 'exact = False'. The SrcStart and SrcEnd values are ignored if exact is True, because the glyphs are patched into the same codepoint where they originate. This also means that gaps in the symbols are preserved - all patched in glyphs have the same codepoint as they have in the source (symbol) font. When exact is False on the other hand, all (non empty) glyphs are filled into the codepoints that start at SrcStart. Gaps (empty glyphs) are ignored and thus are not present as gap in the patched font anymore (*). The to-be-filled-next codepoint in the patched font just increases by 1 on every filled symbol. See note for the reason. This also makes maintining the patch set easier as noone needs to 'calculate' a SrcEnd value anymore. [how] Use directly the start value and the counter instead of filling an array with a range, that is then indexed by the counter. Before this commit: list_of_patched_font_codepoints = list(range(SrcStart, SrcEnd + 1)) current_codepoint = list_of_patched_font_codepoints[loop_counter] After this commit: current_codepoint = SrcStart + loop_counter [note] Maybe related to code removed with c728079b6. I guess the code uses the list, because it has been believed that glyphs can only be indexed by strings containing a hex number. The array supposedly contained that strings. But in fact the fontforge python module docu is like this: font.__getitem__(key) If key is an integer, then returns the glyph at that encoding. If a string then returns the glyph with that name. May not be assigned to. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-04 12:33:38 +02:00
def copy_glyphs(self, sourceFontStart, symbolFont, symbolFontStart, symbolFontEnd, exactEncoding, scaleGlyph, setName, attributes):
2018-08-04 12:39:21 +02:00
""" Copies symbol glyphs into self.sourceFont """
progressText = ''
careful = False
glyphSetLength = 0
font-patcher: Remove "SrcEnd" option [why] In patch set definitions we have for the source ranges SymStart and SymEnd and for the destination we can specify SrcStart and SrcEnd The SrcEnd can be automatically generated. For SrcEnd values that differ from the autogenerated value (are lower) the script would crash, or (are higher) ignore them anyhow. There are two modes: 'exact = True' and 'exact = False'. The SrcStart and SrcEnd values are ignored if exact is True, because the glyphs are patched into the same codepoint where they originate. This also means that gaps in the symbols are preserved - all patched in glyphs have the same codepoint as they have in the source (symbol) font. When exact is False on the other hand, all (non empty) glyphs are filled into the codepoints that start at SrcStart. Gaps (empty glyphs) are ignored and thus are not present as gap in the patched font anymore (*). The to-be-filled-next codepoint in the patched font just increases by 1 on every filled symbol. See note for the reason. This also makes maintining the patch set easier as noone needs to 'calculate' a SrcEnd value anymore. [how] Use directly the start value and the counter instead of filling an array with a range, that is then indexed by the counter. Before this commit: list_of_patched_font_codepoints = list(range(SrcStart, SrcEnd + 1)) current_codepoint = list_of_patched_font_codepoints[loop_counter] After this commit: current_codepoint = SrcStart + loop_counter [note] Maybe related to code removed with c728079b6. I guess the code uses the list, because it has been believed that glyphs can only be indexed by strings containing a hex number. The array supposedly contained that strings. But in fact the fontforge python module docu is like this: font.__getitem__(key) If key is an integer, then returns the glyph at that encoding. If a string then returns the glyph with that name. May not be assigned to. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-04 12:33:38 +02:00
sourceFontCounter = 0
2018-08-04 12:39:21 +02:00
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)
2018-08-04 12:39:21 +02:00
if self.args.quiet is False:
sys.stdout.write("Adding " + str(max(1, glyphSetLength)) + " Glyphs from " + setName + " Set \n")
font-patcher: Allow glyphs with altuni for exactEncoding [why] Some symbol fonts might come with glyphs that have multiple codepoints. When we want to patch them with `'Exact': true` (i.e. at their 'original' codepoints) we want to patch them into the codepoint that has been used in the selection process. That means between SymStart and SymEnd. But this is not the case. We patch them in into their 'main' codepoint, which can be outside the expected range of points. This came up when patching with FontAwesome V6. It has for example these glyphs: Glyph 'music' has a main codepoint 0x1F3B5, but it is present in the font also on codepoint 0xF001. Glyph 'heard' has a main codepoint 0x1F9E1, but it is present in the font also on codepoints 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ... When doing a `'Exact': true` patch (i.e. exactEncoding = true) the glyphs is patched into the target font at its (the glyph's) main codepoint, regardless of our patch-codepoint-range. [how] We examine all codepoints that a glyph occupies in the symbol font. From all these codepoints we take the nearest to the last glyph be patched in. Nearest means from the possible codepoints the lowest that come after the previous used codepoint. For example the 'heard': Last patched in codepoint was 0xF003. Main codepoint: 0x1F9E1 Alternate codepoints: 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ... -=> 0xF004 Later in the patching process we might encounter the same glyph again, but this time the previous codepoint was 0xF089, so we need to take 0xF08A. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-22 12:59:38 +02:00
currentSourceFontGlyph = -1 # initialize for the exactEncoding case
for index, sym_glyph in enumerate(symbolFontSelection):
2018-08-04 12:39:21 +02:00
index = max(1, index)
2018-08-04 12:39:21 +02:00
try:
sym_attr = attributes[sym_glyph.unicode]
except KeyError:
sym_attr = attributes['default']
2018-08-04 12:39:21 +02:00
if exactEncoding:
font-patcher: Allow glyphs with altuni for exactEncoding [why] Some symbol fonts might come with glyphs that have multiple codepoints. When we want to patch them with `'Exact': true` (i.e. at their 'original' codepoints) we want to patch them into the codepoint that has been used in the selection process. That means between SymStart and SymEnd. But this is not the case. We patch them in into their 'main' codepoint, which can be outside the expected range of points. This came up when patching with FontAwesome V6. It has for example these glyphs: Glyph 'music' has a main codepoint 0x1F3B5, but it is present in the font also on codepoint 0xF001. Glyph 'heard' has a main codepoint 0x1F9E1, but it is present in the font also on codepoints 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ... When doing a `'Exact': true` patch (i.e. exactEncoding = true) the glyphs is patched into the target font at its (the glyph's) main codepoint, regardless of our patch-codepoint-range. [how] We examine all codepoints that a glyph occupies in the symbol font. From all these codepoints we take the nearest to the last glyph be patched in. Nearest means from the possible codepoints the lowest that come after the previous used codepoint. For example the 'heard': Last patched in codepoint was 0xF003. Main codepoint: 0x1F9E1 Alternate codepoints: 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ... -=> 0xF004 Later in the patching process we might encounter the same glyph again, but this time the previous codepoint was 0xF089, so we need to take 0xF08A. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-22 12:59:38 +02:00
# 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
font-patcher: Allow glyphs with altuni for exactEncoding [why] Some symbol fonts might come with glyphs that have multiple codepoints. When we want to patch them with `'Exact': true` (i.e. at their 'original' codepoints) we want to patch them into the codepoint that has been used in the selection process. That means between SymStart and SymEnd. But this is not the case. We patch them in into their 'main' codepoint, which can be outside the expected range of points. This came up when patching with FontAwesome V6. It has for example these glyphs: Glyph 'music' has a main codepoint 0x1F3B5, but it is present in the font also on codepoint 0xF001. Glyph 'heard' has a main codepoint 0x1F9E1, but it is present in the font also on codepoints 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ... When doing a `'Exact': true` patch (i.e. exactEncoding = true) the glyphs is patched into the target font at its (the glyph's) main codepoint, regardless of our patch-codepoint-range. [how] We examine all codepoints that a glyph occupies in the symbol font. From all these codepoints we take the nearest to the last glyph be patched in. Nearest means from the possible codepoints the lowest that come after the previous used codepoint. For example the 'heard': Last patched in codepoint was 0xF003. Main codepoint: 0x1F9E1 Alternate codepoints: 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ... -=> 0xF004 Later in the patching process we might encounter the same glyph again, but this time the previous codepoint was 0xF089, so we need to take 0xF08A. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-22 12:59:38 +02:00
currentSourceFontGlyph = min(possible_codes)
2018-08-04 12:39:21 +02:00
else:
font-patcher: Remove "SrcEnd" option [why] In patch set definitions we have for the source ranges SymStart and SymEnd and for the destination we can specify SrcStart and SrcEnd The SrcEnd can be automatically generated. For SrcEnd values that differ from the autogenerated value (are lower) the script would crash, or (are higher) ignore them anyhow. There are two modes: 'exact = True' and 'exact = False'. The SrcStart and SrcEnd values are ignored if exact is True, because the glyphs are patched into the same codepoint where they originate. This also means that gaps in the symbols are preserved - all patched in glyphs have the same codepoint as they have in the source (symbol) font. When exact is False on the other hand, all (non empty) glyphs are filled into the codepoints that start at SrcStart. Gaps (empty glyphs) are ignored and thus are not present as gap in the patched font anymore (*). The to-be-filled-next codepoint in the patched font just increases by 1 on every filled symbol. See note for the reason. This also makes maintining the patch set easier as noone needs to 'calculate' a SrcEnd value anymore. [how] Use directly the start value and the counter instead of filling an array with a range, that is then indexed by the counter. Before this commit: list_of_patched_font_codepoints = list(range(SrcStart, SrcEnd + 1)) current_codepoint = list_of_patched_font_codepoints[loop_counter] After this commit: current_codepoint = SrcStart + loop_counter [note] Maybe related to code removed with c728079b6. I guess the code uses the list, because it has been believed that glyphs can only be indexed by strings containing a hex number. The array supposedly contained that strings. But in fact the fontforge python module docu is like this: font.__getitem__(key) If key is an integer, then returns the glyph at that encoding. If a string then returns the glyph with that name. May not be assigned to. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-04 12:33:38 +02:00
# use source font defined hex values based on passed in start (fills gaps; symbols are packed)
currentSourceFontGlyph = sourceFontStart + sourceFontCounter
2018-08-04 12:39:21 +02:00
sourceFontCounter += 1
if self.args.quiet is False:
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)
2018-08-04 12:39:21 +02:00
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:
2018-08-04 12:39:21 +02:00
if self.args.quiet is False:
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
print(" Found {} Glyph at {:X}. Skipping...".format(careful_type, currentSourceFontGlyph))
2018-08-04 12:39:21 +02:00
# 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("*")
2018-08-04 12:39:21 +02:00
# This will destroy any content currently in currentSourceFontGlyph, so do it first
scale_glyph_data = self.get_glyph_scale(sym_glyph.encoding, scaleGlyph, symbolFont, currentSourceFontGlyph) if scaleGlyph else None
2018-08-04 12:39:21 +02:00
# 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])
scale_ratio_x = 1
scale_ratio_y = 1
2018-08-04 12:39:21 +02:00
# Now that we have copy/pasted the glyph, if we are creating a monospace
# font we need to scale and move the glyphs. It is possible to have
# empty glyphs, so we need to skip those.
if self.args.single and sym_dim['width'] and sym_dim['height']:
# If we want to preserve that aspect ratio of the glyphs we need to
# find the largest possible scaling factor that will allow the glyph
# to fit in both the x and y directions
if sym_attr['stretch'] == 'pa':
scale_ratio_x = False
if scale_glyph_data:
# We want to preserve the relative size of each glyph in a glyph group
scale_ratio_x = scale_glyph_data[0]
if scale_ratio_x is False:
# In the remaining cases, each glyph is sized independently to each other
2018-08-04 12:39:21 +02:00
scale_ratio_x = self.get_scale_factor(sym_dim)
scale_ratio_y = scale_ratio_x
2018-08-04 12:39:21 +02:00
else:
if 'x' in sym_attr['stretch']:
# Stretch the glyph horizontally to fit the entire available width
scale_ratio_x = self.font_dim['width'] / sym_dim['width']
# end if single width
# non-monospace (double width glyphs)
# elif sym_dim['width'] and sym_dim['height']:
# any special logic we want to apply for double-width variation
# would go here
if 'y' in sym_attr['stretch']:
# Stretch the glyph vertically to total line height (good for powerline separators)
# Currently stretching vertically for both monospace and double-width
scale_ratio_y = self.font_dim['height'] / sym_dim['height']
overlap = sym_attr['params'].get('overlap')
2018-08-04 12:39:21 +02:00
if scale_ratio_x != 1 or scale_ratio_y != 1:
if overlap:
scale_ratio_x *= 1 + overlap
scale_ratio_y *= 1 + overlap
self.sourceFont[currentSourceFontGlyph].transform(psMat.scale(scale_ratio_x, scale_ratio_y))
2018-08-04 12:39:21 +02:00
# 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
2018-08-04 12:39:21 +02:00
sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph])
# Use combined bounding box?
if scale_glyph_data and scale_glyph_data[1] and (scale_ratio_x != 1 or scale_ratio_y != 1):
# Simulate scaling on combined bounding box
scaleglyph_dim = scale_bounding_box(scale_glyph_data[1], scale_ratio_x, scale_ratio_y)
if not scaleglyph_dim['advance']:
# 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
2018-08-04 12:39:21 +02:00
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 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 overlap:
overlap_width = self.font_dim['width'] * overlap
2018-08-04 12:39:21 +02:00
if sym_attr['align'] == 'l':
x_align_distance -= overlap_width
if sym_attr['align'] == 'r':
x_align_distance += overlap_width
align_matrix = psMat.translate(x_align_distance, y_align_distance)
self.sourceFont[currentSourceFontGlyph].transform(align_matrix)
2018-08-04 12:39:21 +02:00
# 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])
2018-08-04 12:39:21 +02:00
# 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
2018-08-04 12:39:21 +02:00
self.set_glyph_width_mono(self.sourceFont[currentSourceFontGlyph])
# Re-remove negative bearings for target font with variable advance width
if self.args.nonmono:
self.remove_glyph_neg_bearings(self.sourceFont[currentSourceFontGlyph])
# 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))
2018-08-04 12:39:21 +02:00
# end for
2018-08-04 12:39:21 +02:00
if self.args.quiet is False or self.args.progressbars:
sys.stdout.write("\n")
2018-08-04 12:39:21 +02:00
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)
2018-08-04 12:39:21 +02:00
self.set_glyph_width_mono(glyph)
def remove_glyph_neg_bearings(self, glyph):
""" Sets passed glyph's bearings 0 if they are negative. """
2018-08-04 03:47:20 +02:00
try:
if glyph.left_side_bearing < 0:
glyph.left_side_bearing = 0
if glyph.right_side_bearing < 0:
glyph.right_side_bearing = 0
2018-08-04 03:47:20 +02:00
except:
pass
2018-08-04 12:39:21 +02:00
def set_glyph_width_mono(self, glyph):
""" Sets passed glyph.width to self.font_dim.width.
2018-08-04 12:39:21 +02:00
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
2018-08-04 12:39:21 +02:00
glyph.width = self.font_dim['width']
except:
pass
def prepareScaleGlyph(self, scaleGlyph, symbolFont, destGlyph):
""" Prepare raw ScaleGlyph data for use """
# The GlyphData is a dict with these (possible) entries:
# 'GlyphsToScale': 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 'GlyphsToScale' (generated by this function)
# 'bbdims': List of associated sym_dim dicts, one for each entry in 'GlyphsToScale' (generated by this function)
# Each sym_dim dict describes the combined bounding box of all glyphs in GlyphsToScale
# Example:
# { 'GlyphsToScale': [ range(1, 3), [ 7, 10 ], ],
# 'scales': [ 1.23, 1.33, ],
# 'bbdims': [ dim_dict1, dim_dict2, ] }
#
# Each item in 'GlyphsToScale' (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) 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 (list 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.
if 'scales' in scaleGlyph:
# Already prepared... must not happen, ignore call
return
if 'ScaleGlyph' in scaleGlyph:
# old method. Rewrite to new.
flat_list = []
for i in scaleGlyph['GlyphsToScale']:
if isinstance(i, tuple):
flat_list += list(range(i[0], i[1] + 1))
else:
flat_list.append(i)
scaleGlyph['GlyphsToScale'] = [ flat_list ]
sym_dim = get_glyph_dimensions(symbolFont[scaleGlyph['ScaleGlyph']])
scale = self.get_scale_factor(sym_dim)
scaleGlyph['scales'] = [ scale ]
# The 'old' style keeps just the scale, not the positioning
scaleGlyph['bbdims'] = [ None ]
else:
scaleGlyph['scales'] = []
scaleGlyph['bbdims'] = []
for group in scaleGlyph['GlyphsToScale']:
sym_dim = get_multiglyph_boundingBox([ symbolFont[g] if g in symbolFont else None for g in group ], destGlyph)
scale = self.get_scale_factor(sym_dim)
scaleGlyph['scales'].append(scale)
scaleGlyph['bbdims'].append(sym_dim)
def get_glyph_scale(self, symbol_unicode, scaleGlyph, symbolFont, dest_unicode):
""" Determines whether or not to use scaled glyphs for glyphs in passed glyph_list """
# Potentially destorys the contents of self.sourceFont[dest_unicode]
if not 'scales' in scaleGlyph:
if not dest_unicode in self.sourceFont:
self.sourceFont.createChar(dest_unicode)
self.prepareScaleGlyph(scaleGlyph, symbolFont, self.sourceFont[dest_unicode])
for glyph_list, scale, box in zip(scaleGlyph['GlyphsToScale'], scaleGlyph['scales'], scaleGlyph['bbdims']):
if symbol_unicode in glyph_list:
return (scale, box)
return False
2018-08-04 12:39:21 +02:00
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
else:
if bbox[4] != gadvance:
bbox[4] = -1 # Marker for not-monospaced
if bbox[4] and bbox[4] < 0:
bbox[4] = None
2018-08-04 12:39:21 +02:00
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
2018-08-04 12:39:21 +02:00
}
def get_glyph_dimensions(glyph):
""" Returns dict of the dimesions of the glyph passed to it. """
return get_multiglyph_boundingBox([ glyph ])
2018-08-04 12:39:21 +02:00
def scale_bounding_box(bbox, scale_x, scale_y):
""" Return a scaled version of a glyph dimensions dict """
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_y) if bbox['advance'] 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):
2018-08-04 12:39:21 +02:00
""" 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
2018-08-04 12:39:21 +02:00
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()
2018-08-04 12:39:21 +02:00
def check_fontforge_min_version():
""" Verifies installed FontForge version meets minimum requirement. """
minimumVersion = 20141231
actualVersion = int(fontforge.version())
2018-08-04 12:39:21 +02:00
# un-comment following line for testing invalid version error handling
2018-08-07 13:06:39 +02:00
# actualVersion = 20120731
2018-08-04 12:39:21 +02:00
# 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 + ")")
font-patcher: Prevent --mono on proportional fonts [why] When the source font is proportional we can not really create a monospaced (patched) font from it. The glyph width is for example very small for 'i' but wide for 'W'. The glyphs are all left aligned, leaving very strange separation between smallish glyphs. Even if we would center the glyphs, the look would be strange and completely differenmt from the source font's look. [how] For proportional fonts do not allow to patch with `--mono`. The fact if a source font is monospaced is determined by examining some (very few) glyphs. But testing all our source fonts in the repo shows that it is sufficient. Furthermore the Panose flag is checked and differences between the flag and what the glyph examination found are reported. The user can enforce `Nerd Font Mono` generation with double specifying the command line option `--mono --mono`. Still a warning will be issued. [note] Because `gotta-patch-em-all-font-patcher!.sh` does not really count the variations but calculates them in a separate loop it does not know anymore how many variations are created per family. The numbers are wrong. But probably we should count the result font files in the end anyhow. Because the information is not needed (in an automated manner) this is not corrected here. It seems wrong anyhow: total_variation_count=$((total_variation_count+combination_count)) total_count=$((total_count+complete_variations_per_family+combination_count)) Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-09-04 19:55:24 +02:00
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 found is not True:
font_complete = False
args.complete = font_complete
if args.alsowindows:
args.windows = False
if args.nonmono and args.single:
print("Warniung: Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.")
args.nonmono = False
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
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
2018-08-04 12:39:21 +02:00
def main():
print("{} Patcher v{} ({}) executing".format(projectName, version, script_version))
2018-08-04 12:39:21 +02:00
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):
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("\nDone with Patch Sets, generating font...\n")
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()
2018-08-04 12:39:21 +02:00
if __name__ == "__main__":
__dir__ = os.path.dirname(os.path.abspath(__file__))
main()