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