2017-04-08 22:26:15 +02:00
#!/usr/bin/env python
2015-11-14 23:50:16 +02:00
# coding=utf8
2023-06-04 22:32:30 +02:00
# Nerd Fonts Version: 3.0.2
2022-08-23 17:36:16 +02:00
# Script version is further down
2014-12-05 06:29:54 +02:00
2017-04-08 22:26:15 +02:00
from __future__ import absolute_import, print_function, unicode_literals
2022-08-23 17:36:16 +02:00
# Change the script version when you edit this script:
2023-06-06 08:12:56 +02:00
script_version = "4.4.1"
2022-08-23 17:36:16 +02:00
2023-06-04 22:32:30 +02:00
version = "3.0.2"
2016-03-19 05:16:46 +02:00
projectName = "Nerd Fonts"
projectNameAbbreviation = "NF"
projectNameSingular = projectName[:-1]
2014-12-05 06:29:54 +02:00
import sys
import re
2015-11-16 01:05:57 +02:00
import os
2015-03-01 18:24:05 +02:00
import argparse
2016-11-06 04:55:27 +02:00
from argparse import RawTextHelpFormatter
2015-11-16 01:05:57 +02:00
import errno
2016-12-03 21:57:11 +02:00
import subprocess
2018-02-19 02:02:54 +02:00
import json
2023-02-12 17:35:20 +02:00
from enum import Enum
2023-04-13 12:11:13 +02:00
import logging
2018-02-19 02:02:54 +02:00
try:
2018-08-04 12:39:21 +02:00
import configparser
2018-02-19 02:02:54 +02:00
except ImportError:
2018-08-04 12:39:21 +02:00
sys.exit(projectName + ": configparser module is probably not installed. Try `pip install configparser` or equivalent")
2014-12-05 06:29:54 +02:00
try:
2022-02-28 09:56:12 +02:00
import psMat
2016-03-19 05:16:46 +02:00
import fontforge
2014-12-05 06:29:54 +02:00
except ImportError:
2018-08-04 12:39:21 +02:00
sys.exit(
projectName + (
": FontForge module could not be loaded. Try installing fontforge python bindings "
2022-02-28 09:56:12 +02:00
"[e.g. on Linux Debian or Ubuntu: `sudo apt install fontforge python3-fontforge`]"
2018-08-04 12:39:21 +02:00
)
)
2022-02-06 13:10:07 +02:00
sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/bin/scripts/name_parser/')
2022-02-06 16:30:43 +02:00
try:
from FontnameParser import FontnameParser
from FontnameTools import FontnameTools
FontnameParserOK = True
except ImportError:
FontnameParserOK = False
2018-08-04 12:39:21 +02:00
2022-01-11 04:37:30 +02:00
class TableHEADWriter:
""" Access to the HEAD table without external dependencies """
def getlong(self, pos = None):
""" Get four bytes from the font file as integer number """
if pos:
self.goto(pos)
return (ord(self.f.read(1)) << 24) + (ord(self.f.read(1)) << 16) + (ord(self.f.read(1)) << 8) + ord(self.f.read(1))
def getshort(self, pos = None):
""" Get two bytes from the font file as integer number """
if pos:
self.goto(pos)
return (ord(self.f.read(1)) << 8) + ord(self.f.read(1))
def putlong(self, num, pos = None):
""" Put number as four bytes into font file """
if pos:
self.goto(pos)
self.f.write(bytearray([(num >> 24) & 0xFF, (num >> 16) & 0xFF ,(num >> 8) & 0xFF, num & 0xFF]))
self.modified = True
def putshort(self, num, pos = None):
""" Put number as two bytes into font file """
if pos:
self.goto(pos)
self.f.write(bytearray([(num >> 8) & 0xFF, num & 0xFF]))
self.modified = True
def calc_checksum(self, start, end, checksum = 0):
""" Calculate a font table checksum, optionally ignoring another embedded checksum value (for table 'head') """
self.f.seek(start)
for i in range(start, end - 4, 4):
checksum += self.getlong()
checksum &= 0xFFFFFFFF
i += 4
extra = 0
for j in range(4):
2022-10-10 16:12:52 +02:00
extra = extra << 8
2022-01-11 04:37:30 +02:00
if i + j <= end:
extra += ord(self.f.read(1))
checksum = (checksum + extra) & 0xFFFFFFFF
return checksum
2022-10-10 12:23:43 +02:00
def find_table(self, tablenames, idx):
""" Search all tables for one of the tables in tablenames and store its metadata """
2022-09-23 09:26:11 +02:00
# 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)
2022-01-11 04:37:30 +02:00
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()
2022-10-10 12:23:43 +02:00
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))
2022-01-11 04:37:30 +02:00
def goto(self, where):
""" Go to a named location in the file or to the specified index """
2023-02-21 16:08:59 +02:00
if isinstance(where, str):
2022-01-11 04:37:30 +02:00
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,
2023-04-12 15:19:52 +02:00
'avgWidth': 2,
2022-01-11 04:37:30 +02:00
}
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')
2022-09-23 09:26:11 +02:00
self.find_head_table(0)
2022-01-11 04:37:30 +02:00
self.flags = self.getshort('flags')
self.lowppem = self.getshort('lowestRecPPEM')
self.checksum_adj = self.getlong('checksumAdjustment')
2022-09-04 19:55:24 +02:00
def check_panose_monospaced(font):
""" Check if the font's Panose flags say it is monospaced """
# https://forum.high-logic.com/postedfiles/Panose.pdf
panose = list(font.os2_panose)
if panose[0] < 2 or panose[0] > 5:
return -1 # invalid Panose info
panose_mono = ((panose[0] == 2 and panose[3] == 9) or
(panose[0] == 3 and panose[3] == 3))
return 1 if panose_mono else 0
2023-01-21 18:31:15 +02:00
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 "")
2023-02-02 12:34:13 +02:00
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))
2022-09-04 19:55:24 +02:00
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
2023-01-22 15:01:13 +02:00
for glyph in [ 0x49, 0x4D, 0x57, 0x61, 0x69, 0x6d, 0x2E ]: # wide and slim glyphs 'I', 'M', 'W', 'a', 'i', 'm', '.'
2022-09-04 19:55:24 +02:00
if not glyph in font:
# A 'strange' font, believe Panose
2023-01-22 15:01:13 +02:00
return (check_panose_monospaced(font) == 1, None)
2022-09-04 19:55:24 +02:00
# 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
2023-01-22 15:01:13 +02:00
return (width_mono, None if width_mono else glyph)
2022-09-04 19:55:24 +02:00
2023-02-02 12:34:13 +02:00
def force_panose_monospaced(font):
""" Forces the Panose flag to monospaced if they are unset or halfway ok already """
2023-02-01 19:19:19 +02:00
# 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
2023-04-13 12:11:13 +02:00
logger.info("Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')")
2023-02-01 19:19:19 +02:00
font.os2_panose = tuple(panose)
if panose[0] == 2 and panose[3] != 9:
2023-04-13 12:11:13 +02:00
logger.info("Setting Panose 'Proportion' to 'Monospaced' (was '%s')", panose_proportion_to_text(panose[3]))
2023-02-02 12:34:13 +02:00
panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced
2023-02-01 19:19:19 +02:00
font.os2_panose = tuple(panose)
2022-09-04 19:55:24 +02:00
def get_advance_width(font, extended, minimum):
""" Get the maximum/minimum advance width in the extended(?) range """
width = 0
2023-05-24 12:45:36 +02:00
if not extended:
r = range(0x021, 0x07e)
2022-09-04 19:55:24 +02:00
else:
2023-05-24 12:45:36 +02:00
r = range(0x07f, 0x17f)
for glyph in r:
2022-09-04 19:55:24 +02:00
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
2023-01-21 18:31:15 +02:00
def report_advance_widths(font):
return "Advance widths (base/extended): {} - {} / {} - {}".format(
2023-05-24 12:45:36 +02:00
get_advance_width(font, False, True), get_advance_width(font, False, False),
get_advance_width(font, True, True), get_advance_width(font, True, False))
2023-01-21 18:31:15 +02:00
2023-02-12 00:22:29 +02:00
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)
2023-04-12 15:19:52 +02:00
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:
2023-04-13 12:11:13 +02:00
logger.critical("Can not determine ancient style xAvgCharWidth")
sys.exit(1)
2023-04-12 15:19:52 +02:00
s += font[g].width * weights[g]
return int(s / 1000)
2023-04-07 09:09:45 +02:00
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(' ', '')
2022-01-11 04:37:30 +02:00
2018-08-04 12:39:21 +02:00
class font_patcher:
2022-02-09 15:56:39 +02:00
def __init__(self, args):
self.args = args # class 'argparse.Namespace'
2018-08-07 13:06:39 +02:00
self.sym_font_args = []
2018-08-04 12:39:21 +02:00
self.config = None # class 'configparser.ConfigParser'
self.sourceFont = None # class 'fontforge.font'
self.patch_set = None # class 'list'
self.font_dim = None # class 'dict'
2023-01-12 17:37:09 +02:00
self.font_extrawide = False
2023-03-08 18:43:17 +02:00
self.source_monospaced = None # Later True or False
2023-05-07 09:52:24 +02:00
self.symbolsonly = False # Are we generating the SymbolsOnly font?
2018-08-04 12:39:21 +02:00
self.onlybitmaps = 0
2022-09-06 09:12:09 +02:00
self.essential = set()
2018-08-04 12:39:21 +02:00
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
2023-04-12 15:19:52 +02:00
self.xavgwidth = [] # list of ints
2022-02-09 17:04:32 +02:00
def patch(self, font):
self.sourceFont = font
2022-09-01 09:43:01 +02:00
self.setup_version()
2022-09-06 09:12:09 +02:00
self.get_essential_references()
2023-03-08 18:43:17 +02:00
self.assert_monospace()
2018-08-04 12:39:21 +02:00
self.remove_ligatures()
self.get_sourcefont_dimensions()
2023-05-07 09:52:24 +02:00
self.setup_patch_set()
2023-01-20 17:29:40 +02:00
self.improve_line_dimensions()
2018-08-04 12:39:21 +02:00
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()
2023-01-12 17:37:09 +02:00
# 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:
2023-04-13 12:11:13 +02:00
logger.warning("Very wide and short font, disabling 2 cell Powerline glyphs")
2023-01-12 17:37:09 +02:00
self.font_extrawide = True
2018-08-04 12:39:21 +02:00
# Prevent opening and closing the fontforge font. Makes things faster when patching
# multiple ranges using the same symbol font.
PreviousSymbolFilename = ""
symfont = None
2021-12-27 16:41:56 +02:00
if not os.path.isdir(self.args.glyphdir):
2023-04-13 12:11:13 +02:00
logger.critical("Can not find symbol glyph directory %s "
"(probably you need to download the src/glyphs/ directory?)", self.args.glyphdir)
sys.exit(1)
2021-12-27 16:41:56 +02:00
2023-04-20 17:20:31 +02:00
if self.args.dry_run:
return
2018-08-04 12:39:21 +02:00
for patch in self.patch_set:
if patch['Enabled']:
if PreviousSymbolFilename != patch['Filename']:
# We have a new symbol font, so close the previous one if it exists
if symfont:
symfont.close()
symfont = None
2023-05-09 12:50:33 +02:00
symfont_file = os.path.join(self.args.glyphdir, patch['Filename'])
if not os.path.isfile(symfont_file):
2023-04-13 12:11:13 +02:00
logger.critical("Can not find symbol source for '%s' (i.e. %s)",
2023-05-09 12:50:33 +02:00
patch['Name'], symfont_file)
2023-04-13 12:11:13 +02:00
sys.exit(1)
2023-05-09 12:50:33 +02:00
if not os.access(symfont_file, os.R_OK):
2023-04-13 12:11:13 +02:00
logger.critical("Can not open symbol source for '%s' (i.e. %s)",
2023-05-09 12:50:33 +02:00
patch['Name'], symfont_file)
2023-04-13 12:11:13 +02:00
sys.exit(1)
2023-05-09 12:50:33 +02:00
symfont = fontforge.open(symfont_file)
2023-02-15 18:02:01 +02:00
symfont.encoding = 'UnicodeFull'
2018-08-04 12:39:21 +02:00
# Match the symbol font size to the source font size
symfont.em = self.sourceFont.em
PreviousSymbolFilename = patch['Filename']
2022-02-04 12:33:38 +02:00
# If patch table doesn't include a source start, re-use the symbol font values
2018-08-04 12:39:21 +02:00
SrcStart = patch['SrcStart']
if not SrcStart:
SrcStart = patch['SymStart']
2023-01-03 20:26:15 +02:00
self.copy_glyphs(SrcStart, symfont, patch['SymStart'], patch['SymEnd'], patch['Exact'], patch['ScaleRules'], patch['Name'], patch['Attributes'])
2018-08-04 12:39:21 +02:00
if symfont:
symfont.close()
2022-07-07 12:33:31 +02:00
# 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"
2022-09-01 09:43:01 +02:00
2022-02-09 19:39:47 +02:00
def generate(self, sourceFonts):
sourceFont = sourceFonts[0]
2023-01-04 21:30:21 +02:00
# 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'))
2022-02-09 19:39:47 +02:00
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
2022-12-22 10:34:20 +02:00
outfile = os.path.normpath(os.path.join(
sanitize_filename(self.args.outputdir, True),
2023-04-07 09:09:45 +02:00
sanitize_filename(create_filename(sourceFonts)) + ".ttc"))
2023-01-04 21:30:21 +02:00
sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer)
2023-01-05 16:13:40 +02:00
message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile)
2020-02-12 06:25:36 +02:00
else:
2023-04-07 09:09:45 +02:00
fontname = create_filename(sourceFonts)
2022-02-09 19:39:47 +02:00
if not fontname:
fontname = sourceFont.cidfontname
2022-12-22 10:34:20 +02:00
outfile = os.path.normpath(os.path.join(
sanitize_filename(self.args.outputdir, True),
sanitize_filename(fontname) + self.args.extension))
font-patcher: Preserve existing bitmaps
[why]
Bitmaps existing in a font are discarded. The font-patcher script just
cares about outline fonts.
[how]
If the sourcefont has bitmaps, do save them into the patched font. Note
that this does not generate bitmaps for the patched-in glyphs (icons).
[note]
Fonts probably affected in src/unpatched-fonts:
bitmap (11,) (gohufont-11)
bitmap (14,) (gohufont-14)
bitmap (11,) (gohufont-uni-11)
bitmap (14,) (gohufont-uni-14)
bitmap (7, 9, 10, 12, 14, 18, 24) (ProFontIIx)
bitmap (12, 14, 16, 18, 20, 22, 24, 28, 32) (TerminusTTF-Bold Italic-4.40.1)
bitmap (12, 14, 16, 18, 20, 22, 24, 28, 32) (TerminusTTF-Bold-4.40.1)
bitmap (12, 14, 16, 18, 20, 22, 24, 28, 32) (TerminusTTF-Italic-4.40.1)
bitmap (12, 14, 16, 18, 20, 22, 24, 28, 32) (TerminusTTF-4.40.1)
Fixes: #86
Reported-by: DoctorKnowsBetter
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-09-23 07:43:51 +02:00
bitmaps = str()
2023-06-04 10:02:24 +02:00
if len(sourceFont.bitmapSizes):
logger.debug("Preserving bitmaps %s", repr(sourceFont.bitmapSizes))
font-patcher: Preserve existing bitmaps
[why]
Bitmaps existing in a font are discarded. The font-patcher script just
cares about outline fonts.
[how]
If the sourcefont has bitmaps, do save them into the patched font. Note
that this does not generate bitmaps for the patched-in glyphs (icons).
[note]
Fonts probably affected in src/unpatched-fonts:
bitmap (11,) (gohufont-11)
bitmap (14,) (gohufont-14)
bitmap (11,) (gohufont-uni-11)
bitmap (14,) (gohufont-uni-14)
bitmap (7, 9, 10, 12, 14, 18, 24) (ProFontIIx)
bitmap (12, 14, 16, 18, 20, 22, 24, 28, 32) (TerminusTTF-Bold Italic-4.40.1)
bitmap (12, 14, 16, 18, 20, 22, 24, 28, 32) (TerminusTTF-Bold-4.40.1)
bitmap (12, 14, 16, 18, 20, 22, 24, 28, 32) (TerminusTTF-Italic-4.40.1)
bitmap (12, 14, 16, 18, 20, 22, 24, 28, 32) (TerminusTTF-4.40.1)
Fixes: #86
Reported-by: DoctorKnowsBetter
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-09-23 07:43:51 +02:00
bitmaps = str('otf') # otf/ttf, both is bf_ttf
2023-04-20 17:20:31 +02:00
if self.args.dry_run:
2023-05-03 07:10:09 +02:00
logger.debug("=====> Filename '%s'", outfile)
2023-04-20 17:20:31 +02:00
return
2023-01-04 21:30:21 +02:00
sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags)
2023-06-04 10:02:24 +02:00
message = " {}\n \===> '{}'".format(sourceFont.fullname, outfile)
2022-01-11 04:37:30 +02:00
# Adjust flags that can not be changed via fontforge
2023-01-05 16:13:40 +02:00
if re.search('\\.[ot]tf$', self.args.font, re.IGNORECASE) and re.search('\\.[ot]tf$', outfile, re.IGNORECASE):
2022-08-19 18:47:50 +02:00
try:
2023-01-05 16:13:40 +02:00
source_font = TableHEADWriter(self.args.font)
dest_font = TableHEADWriter(outfile)
for idx in range(source_font.num_fonts):
2023-04-13 12:11:13 +02:00
logger.debug("Tweaking %d/%d", idx + 1, source_font.num_fonts)
2023-04-12 15:19:52 +02:00
xwidth_s = ''
2023-06-04 10:08:53 +02:00
xwidth = self.xavgwidth[idx] if len(self.xavgwidth) > idx else None
2023-04-12 15:19:52 +02:00
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:
2023-04-13 12:11:13 +02:00
logger.debug("Changing xAvgCharWidth from %d to %d%s", d_xwidth, xwidth, xwidth_s)
2023-04-12 18:17:34 +02:00
dest_font.putshort(xwidth, 'avgWidth')
dest_font.reset_table_checksum()
2023-01-05 16:13:40 +02:00
source_font.find_head_table(idx)
dest_font.find_head_table(idx)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
2023-04-13 12:11:13 +02:00
logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08)
2023-01-05 16:13:40 +02:00
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
if source_font.lowppem != dest_font.lowppem:
2023-04-13 12:11:13 +02:00
logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem)
2023-01-05 16:13:40 +02:00
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified:
dest_font.reset_table_checksum()
2023-04-12 15:31:28 +02:00
if dest_font.modified:
dest_font.reset_full_checksum()
2023-01-05 16:13:40 +02:00
except Exception as error:
2023-04-13 12:11:13 +02:00
logger.error("Can not handle font flags (%s)", repr(error))
2023-01-05 16:13:40 +02:00
finally:
try:
source_font.close()
dest_font.close()
except:
pass
2022-10-10 12:23:43 +02:00
if self.args.is_variable:
2023-04-24 15:35:49 +02:00
logger.critical("Source font is a variable open type font (VF) and the patch results will most likely not be what you want")
2022-01-11 04:37:30 +02:00
print(message)
2018-08-04 12:39:21 +02:00
if self.args.postprocess:
2022-01-11 04:08:21 +02:00
subprocess.call([self.args.postprocess, outfile])
2023-04-13 12:11:13 +02:00
print("\n")
logger.info("Post Processed: %s", outfile)
2018-08-04 12:39:21 +02:00
2022-02-09 19:39:47 +02:00
def setup_name_backup(self, font):
2022-09-01 10:21:16 +02:00
""" Store the original font names to be able to rename the font multiple times """
2022-02-09 19:39:47 +02:00
font.persistent = {
"fontname": font.fontname,
"fullname": font.fullname,
"familyname": font.familyname,
}
2022-09-01 10:21:16 +02:00
2022-02-09 19:39:47 +02:00
def setup_font_names(self, font):
font.fontname = font.persistent["fontname"]
2023-02-21 16:08:59 +02:00
if isinstance(font.persistent["fullname"], str):
font.fullname = font.persistent["fullname"]
if isinstance(font.persistent["familyname"], str):
font.familyname = font.persistent["familyname"]
2022-12-21 22:24:40 +02:00
verboseAdditionalFontNameSuffix = ""
additionalFontNameSuffix = ""
2018-08-07 13:27:48 +02:00
if not self.args.complete:
2018-08-07 13:06:39 +02:00
# NOTE not all symbol fonts have appended their suffix here
if self.args.fontawesome:
additionalFontNameSuffix += " A"
verboseAdditionalFontNameSuffix += " Plus Font Awesome"
if self.args.fontawesomeextension:
additionalFontNameSuffix += " AE"
verboseAdditionalFontNameSuffix += " Plus Font Awesome Extension"
if self.args.octicons:
additionalFontNameSuffix += " O"
verboseAdditionalFontNameSuffix += " Plus Octicons"
if self.args.powersymbols:
additionalFontNameSuffix += " PS"
verboseAdditionalFontNameSuffix += " Plus Power Symbols"
2021-12-05 00:34:09 +02:00
if self.args.codicons:
additionalFontNameSuffix += " C"
verboseAdditionalFontNameSuffix += " Plus Codicons"
2018-08-07 13:06:39 +02:00
if self.args.pomicons:
additionalFontNameSuffix += " P"
verboseAdditionalFontNameSuffix += " Plus Pomicons"
2022-09-13 07:06:41 +02:00
if self.args.fontlogos:
2018-08-07 13:06:39 +02:00
additionalFontNameSuffix += " L"
2022-09-13 07:06:41 +02:00
verboseAdditionalFontNameSuffix += " Plus Font Logos"
2018-08-07 13:06:39 +02:00
if self.args.material:
additionalFontNameSuffix += " MDI"
verboseAdditionalFontNameSuffix += " Plus Material Design Icons"
if self.args.weather:
additionalFontNameSuffix += " WEA"
verboseAdditionalFontNameSuffix += " Plus Weather Icons"
2018-08-04 12:39:21 +02:00
2022-12-21 22:24:40 +02:00
# add mono signifier to beginning of name suffix
2018-08-04 12:39:21 +02:00
if self.args.single:
2023-03-31 11:54:18 +02:00
variant_abbrev = "M"
variant_full = " Mono"
2023-01-15 17:34:02 +02:00
elif self.args.nonmono and not self.symbolsonly:
2023-03-31 11:54:18 +02:00
variant_abbrev = "P"
variant_full = " Propo"
else:
variant_abbrev = ""
variant_full = ""
2022-12-21 22:24:40 +02:00
2023-04-07 11:02:28 +02:00
ps_suffix = projectNameAbbreviation + variant_abbrev + additionalFontNameSuffix
2022-12-21 22:24:40 +02:00
# add 'Nerd Font' to beginning of name suffix
2023-03-31 11:54:18 +02:00
verboseAdditionalFontNameSuffix = " " + projectNameSingular + variant_full + verboseAdditionalFontNameSuffix
2023-04-20 16:13:59 +02:00
additionalFontNameSuffix = " " + projectNameSingular + variant_full + additionalFontNameSuffix
2018-08-04 12:39:21 +02:00
2023-04-07 22:50:19 +02:00
if FontnameParserOK and self.args.makegroups > 0:
2023-02-21 16:08:59 +02:00
use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse
2021-12-02 23:29:54 +02:00
# Use fullname if it is 'equal' to the fontname
2022-02-09 19:39:47 +02:00
if font.fullname:
use_fullname |= font.fontname.lower() == FontnameTools.postscript_char_filter(font.fullname).lower()
2021-12-02 23:29:54 +02:00
# Use fullname for any of these source fonts (that are impossible to disentangle from the fontname, we need the blanks)
for hit in [ 'Meslo' ]:
2022-02-09 19:39:47 +02:00
use_fullname |= font.fontname.lower().startswith(hit.lower())
parser_name = font.fullname if use_fullname else font.fontname
2021-12-02 23:29:54 +02:00
# 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]
2023-04-13 12:11:13 +02:00
n = FontnameParser(parser_name, logger)
2021-12-02 23:29:54 +02:00
if not n.parse_ok:
2023-04-20 15:59:11 +02:00
logger.warning("Have only minimal naming information, check resulting name. Maybe specify --makegroups 0")
2021-12-02 23:29:54 +02:00
n.drop_for_powerline()
2023-04-19 15:57:50 +02:00
n.enable_short_families(True, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ])
2023-04-24 15:12:37 +02:00
if not n.set_expect_no_italic(self.args.noitalic):
logger.critical("Detected 'Italic' slant but --has-no-italic specified")
sys.exit(1)
2021-12-02 23:29:54 +02:00
2022-02-06 13:44:53 +02:00
# All the following stuff is ignored in makegroups-mode
2021-12-02 23:29:54 +02:00
2018-08-04 12:39:21 +02:00
# basically split the font name around the dash "-" to get the fontname and the style (e.g. Bold)
# this does not seem very reliable so only use the style here as a fallback if the font does not
# have an internal style defined (in sfnt_names)
# using '([^-]*?)' to get the item before the first dash "-"
# using '([^-]*(?!.*-))' to get the item after the last dash "-"
2022-02-09 19:39:47 +02:00
fontname, fallbackStyle = re.match("^([^-]*).*?([^-]*(?!.*-))$", font.fontname).groups()
2018-08-04 12:39:21 +02:00
2022-02-09 19:39:47 +02:00
# dont trust 'font.familyname'
2018-08-04 12:39:21 +02:00
familyname = fontname
# fullname (filename) can always use long/verbose font name, even in windows
2022-02-09 19:39:47 +02:00
if font.fullname != None:
fullname = font.fullname + verboseAdditionalFontNameSuffix
2020-02-12 06:25:36 +02:00
else:
2022-02-09 19:39:47 +02:00
fullname = font.cidfontname + verboseAdditionalFontNameSuffix
2020-02-12 06:25:36 +02:00
2018-08-04 12:39:21 +02:00
fontname = fontname + additionalFontNameSuffix.replace(" ", "")
# let us try to get the 'style' from the font info in sfnt_names and fallback to the
# parse fontname if it fails:
try:
# search tuple:
2022-02-09 19:39:47 +02:00
subFamilyTupleIndex = [x[1] for x in font.sfnt_names].index("SubFamily")
2018-08-04 12:39:21 +02:00
# String ID is at the second index in the Tuple lists
sfntNamesStringIDIndex = 2
# now we have the correct item:
2022-02-09 19:39:47 +02:00
subFamily = font.sfnt_names[subFamilyTupleIndex][sfntNamesStringIDIndex]
2018-08-04 12:39:21 +02:00
except IndexError:
sys.stderr.write("{}: Could not find 'SubFamily' for given font, falling back to parsed fontname\n".format(projectName))
subFamily = fallbackStyle
# some fonts have inaccurate 'SubFamily', if it is Regular let us trust the filename more:
2023-01-13 08:53:22 +02:00
if subFamily == "Regular" and len(fallbackStyle):
2018-08-04 12:39:21 +02:00
subFamily = fallbackStyle
2020-09-16 08:38:11 +02:00
2020-12-01 07:00:54 +02:00
# This is meant to cover the case where the SubFamily is "Italic" and the filename is *-BoldItalic.
2020-09-16 08:38:11 +02:00
if len(subFamily) < len(fallbackStyle):
subFamily = fallbackStyle
2023-01-13 08:53:22 +02:00
if len(subFamily) == 0:
subFamily = "Regular"
2023-04-20 16:56:43 +02:00
familyname += " " + projectNameSingular + variant_full
2018-08-04 12:39:21 +02:00
# Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with
2022-04-10 15:33:11 +02:00
# 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(' ', '')
2016-10-14 18:23:56 +02:00
2019-07-05 21:32:06 +02:00
# rename font
#
# comply with SIL Open Font License (OFL)
reservedFontNameReplacements = {
2020-01-26 07:22:11 +02:00
'source' : 'sauce',
'Source' : 'Sauce',
2023-04-27 20:24:58 +02:00
'Bitstream Vera Sans Mono' : 'Bitstrom Wera',
'BitstreamVeraSansMono' : 'BitstromWera',
'bitstream vera sans mono' : 'bitstrom wera',
'bitstreamverasansmono' : 'bitstromwera',
2020-01-26 07:22:11 +02:00
'hermit' : 'hurmit',
'Hermit' : 'Hurmit',
'hasklig' : 'hasklug',
'Hasklig' : 'Hasklug',
'Share' : 'Shure',
'share' : 'shure',
'IBMPlex' : 'Blex',
'ibmplex' : 'blex',
'IBM-Plex' : 'Blex',
'IBM Plex' : 'Blex',
'terminus' : 'terminess',
'Terminus' : 'Terminess',
'liberation' : 'literation',
'Liberation' : 'Literation',
'iAWriter' : 'iMWriting',
'iA Writer' : 'iM Writing',
'iA-Writer' : 'iM-Writing',
'Anka/Coder' : 'AnaConder',
'anka/coder' : 'anaconder',
'Cascadia Code' : 'Caskaydia Cove',
'cascadia code' : 'caskaydia cove',
'CascadiaCode' : 'CaskaydiaCove',
2021-11-12 06:16:12 +02:00
'cascadiacode' : 'caskaydiacove',
2021-11-12 06:01:18 +02:00
'Cascadia Mono' : 'Caskaydia Mono',
'cascadia mono' : 'caskaydia mono',
'CascadiaMono' : 'CaskaydiaMono',
2021-12-19 21:12:08 +02:00
'cascadiamono' : 'caskaydiamono',
'Fira Mono' : 'Fura Mono',
'Fira Sans' : 'Fura Sans',
'FiraMono' : 'FuraMono',
'FiraSans' : 'FuraSans',
'fira mono' : 'fura mono',
'fira sans' : 'fura sans',
'firamono' : 'furamono',
'firasans' : 'furasans',
2019-07-05 21:32:06 +02:00
}
2015-08-11 00:33:21 +02:00
2018-08-04 12:39:21 +02:00
# remove overly verbose font names
# particularly regarding Powerline sourced Fonts (https://github.com/powerline/fonts)
additionalFontNameReplacements = {
'for Powerline': '',
'ForPowerline': ''
}
additionalFontNameReplacements2 = {
'Powerline': ''
}
projectInfo = (
"Patched with '" + projectName + " Patcher' (https://github.com/ryanoasis/nerd-fonts)\n\n"
"* Website: https://www.nerdfonts.com\n"
"* Version: " + version + "\n"
"* Development Website: https://github.com/ryanoasis/nerd-fonts\n"
2022-10-17 11:33:40 +02:00
"* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/-/changelog.md"
2018-08-04 12:39:21 +02:00
)
familyname = replace_font_name(familyname, reservedFontNameReplacements)
fullname = replace_font_name(fullname, reservedFontNameReplacements)
fontname = replace_font_name(fontname, reservedFontNameReplacements)
familyname = replace_font_name(familyname, additionalFontNameReplacements)
fullname = replace_font_name(fullname, additionalFontNameReplacements)
fontname = replace_font_name(fontname, additionalFontNameReplacements)
familyname = replace_font_name(familyname, additionalFontNameReplacements2)
fullname = replace_font_name(fullname, additionalFontNameReplacements2)
fontname = replace_font_name(fontname, additionalFontNameReplacements2)
2023-06-04 12:23:11 +02:00
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):
2021-12-02 23:29:54 +02:00
# replace any extra whitespace characters:
2022-02-09 19:39:47 +02:00
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)
2021-12-02 23:29:54 +02:00
else:
2023-04-19 15:57:50 +02:00
short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full
2023-04-07 19:52:48 +02:00
# inject_suffix(family, ps_fontname, short_family)
2023-04-07 22:50:19 +02:00
n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family)
2022-02-09 19:39:47 +02:00
n.rename_font(font)
2018-08-04 12:39:21 +02:00
2022-02-09 19:39:47 +02:00
font.comment = projectInfo
font.fontlog = projectInfo
2018-08-04 12:39:21 +02:00
2022-09-01 09:43:01 +02:00
def setup_version(self):
""" Add the Nerd Font version to the original version """
2018-08-07 13:06:39 +02:00
# print("Version was {}".format(sourceFont.version))
2020-02-12 06:25:36 +02:00
if self.sourceFont.version != None:
self.sourceFont.version += ";" + projectName + " " + version
else:
self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version
2021-11-29 11:04:40 +02:00
self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge
self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version)
2023-03-27 17:36:32 +02:00
# The Version SFNT name is later reused by the NameParser for UniqueID
2018-08-07 13:06:39 +02:00
# print("Version now is {}".format(sourceFont.version))
2018-08-04 12:39:21 +02:00
def remove_ligatures(self):
# let's deal with ligatures (mostly for monospaced fonts)
Prepatched fonts: Revive some ligature removal
[why]
Some sourcefonts, even that are monospaced, have a `fi` and/or `fl`
ligature that maps into one cell. That looks very strange.
[how]
Partially revert commit
148b0c445 Sunset ligature removal
for the cases that have a one-cell `fi`, `fl`, etc ligature, or a `ldot`
related ligature - that is active by default. Discretionary ligatures or
Stylistic Sets are not changed.
Do the removal on all patched fonts for consistency, not just `Nerd Font Mono`.
[note]
On Noto different subtables are needed for Sans, Serif and Sans-Mono. We
can not set up different configs for each, so all are tried in all fonts
and might fail (this is normal).
Same holds for OpenDyslexic Alta, Regular, Mono, Bold...
Fixes: #1187
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2023-05-02 13:23:54 +02:00
# Usually removes 'fi' ligs that end up being only one cell wide, and 'ldot'
2018-08-04 12:39:21 +02:00
if self.args.configfile and self.config.read(self.args.configfile):
if self.args.removeligatures:
2023-04-13 12:11:13 +02:00
logger.info("Removing ligatures from configfile `Subtables` section")
2018-08-04 12:39:21 +02:00
ligature_subtables = json.loads(self.config.get("Subtables", "ligatures"))
for subtable in ligature_subtables:
2023-04-13 12:11:13 +02:00
logger.debug("Removing subtable: %s", subtable)
2022-09-22 16:50:41 +02:00
try:
self.sourceFont.removeLookupSubtable(subtable)
2023-04-13 12:11:13 +02:00
logger.debug("Successfully removed subtable: %s", subtable)
2022-09-22 16:50:41 +02:00
except Exception:
2023-04-13 12:11:13 +02:00
logger.error("Failed to remove subtable: %s", subtable)
2021-07-25 23:24:27 +02:00
elif self.args.removeligatures:
2023-04-13 12:11:13 +02:00
logger.error("Unable to read configfile, unable to remove ligatures")
2018-08-04 12:39:21 +02:00
2022-09-04 19:55:24 +02:00
def assert_monospace(self):
# Check if the sourcefont is monospaced
2023-01-22 15:01:13 +02:00
width_mono, offending_char = is_monospaced(self.sourceFont)
2023-03-08 18:43:17 +02:00
self.source_monospaced = width_mono
if self.args.nonmono:
return
2022-09-04 19:55:24 +02:00
panose_mono = check_panose_monospaced(self.sourceFont)
2023-05-24 12:45:36 +02:00
logger.debug("Monospace check: %s; glyph-width-mono %s",
panose_check_to_text(panose_mono, self.sourceFont.os2_panose), repr(width_mono))
2022-09-04 19:55:24 +02:00
# 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):
2023-04-13 12:11:13 +02:00
logger.warning("Monospaced check: Panose assumed to be wrong")
2023-05-24 12:45:36 +02:00
logger.warning("Monospaced check: %s and %s",
2023-01-21 18:31:15 +02:00
report_advance_widths(self.sourceFont),
2023-04-13 12:11:13 +02:00
panose_check_to_text(panose_mono, self.sourceFont.os2_panose))
2023-02-01 19:19:19 +02:00
if self.args.single and not width_mono:
2023-05-24 12:45:36 +02:00
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 "")
2022-09-04 19:55:24 +02:00
if self.args.single <= 1:
2023-04-13 12:11:13 +02:00
logger.critical("Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching")
sys.exit(1)
2023-02-01 19:19:19 +02:00
if width_mono:
2023-02-02 12:34:13 +02:00
force_panose_monospaced(self.sourceFont)
2022-09-04 19:55:24 +02:00
2018-08-04 12:39:21 +02:00
def setup_patch_set(self):
""" Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """
2023-02-15 19:38:34 +02:00
2023-05-07 09:52:24 +02:00
box_enabled = self.source_monospaced and not self.symbolsonly # Box glyph only for monospaced and not for Symbols Only
2023-04-14 06:52:17 +02:00
box_keep = False
2023-03-08 18:43:17 +02:00
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:
2023-04-14 06:52:17 +02:00
# Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing)
2023-04-13 12:11:13 +02:00
if box_glyphs_current > 0:
logger.debug("%d/%d box drawing glyphs will be replaced",
box_glyphs_current, box_glyphs_target)
2023-03-08 18:43:17 +02:00
box_enabled = True
else:
2023-04-14 06:52:17 +02:00
# Sourcefont does have all of these glyphs
# box_keep = True # just scale do not copy (need to scale to fit new cell size)
2023-03-08 18:43:17 +02:00
box_enabled = False # Cowardly not scaling existing glyphs, although the code would allow this
2023-02-15 19:38:34 +02:00
2023-02-23 12:08:27 +02:00
# Stretch 'xz' or 'pa' (preserve aspect ratio)
2023-05-11 21:45:00 +02:00
# Supported params: overlap | careful | xy-ratio | dont_copy | ypadding
2022-10-14 17:00:16 +02:00
# Overlap value is used horizontally but vertically limited to 0.01
2023-02-15 17:48:56 +02:00
# Careful does not overwrite/modify existing glyphs
2023-02-12 20:29:52 +02:00
# 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')
2023-02-23 12:25:29 +02:00
# '!' means do the 'pa' scaling even with non mono fonts (else it just scales down, never up)
2023-02-15 19:33:11 +02:00
# Dont_copy does not overwrite existing glyphs but rescales the preexisting ones
2023-05-12 11:33:16 +02:00
#
# Be careful, stretch may not change within a ScaleRule!
2023-02-15 17:48:56 +02:00
2023-02-23 12:08:27 +02:00
SYM_ATTR_DEFAULT = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}
}
2018-08-04 12:39:21 +02:00
SYM_ATTR_POWERLINE = {
2022-02-07 16:05:54 +02:00
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
2018-08-04 12:39:21 +02:00
# Arrow tips
2023-01-04 18:07:46 +02:00
0xe0b0: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}},
2023-01-12 12:26:35 +02:00
0xe0b1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.7}},
2023-01-04 18:07:46 +02:00
0xe0b2: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}},
2023-01-12 12:26:35 +02:00
0xe0b3: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.7}},
2018-08-04 12:39:21 +02:00
# Rounded arcs
2023-01-04 18:07:46 +02:00
0xe0b4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01, 'xy-ratio': 0.59}},
2023-01-12 12:26:35 +02:00
0xe0b5: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.5}},
2023-01-04 18:07:46 +02:00
0xe0b6: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01, 'xy-ratio': 0.59}},
2023-01-12 12:26:35 +02:00
0xe0b7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'xy-ratio': 0.5}},
2018-08-04 12:39:21 +02:00
# Bottom Triangles
2023-02-12 20:29:52 +02:00
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': {}},
2018-08-04 12:39:21 +02:00
# Top Triangles
2023-02-12 20:29:52 +02:00
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': {}},
2018-08-04 12:39:21 +02:00
# Flames
2023-01-12 12:31:27 +02:00
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': {}},
2018-08-04 12:39:21 +02:00
# Small squares
2023-02-12 20:29:52 +02:00
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}},
2018-08-04 12:39:21 +02:00
# Bigger squares
2023-02-12 20:29:52 +02:00
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}},
2018-08-04 12:39:21 +02:00
# Waveform
2023-01-12 12:31:27 +02:00
0xe0c8: {'align': 'l', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}},
0xe0ca: {'align': 'r', 'valign': 'c', 'stretch': 'xy2', 'params': {'overlap': 0.01}},
2018-08-04 12:39:21 +02:00
# Hexagons
2023-02-12 20:29:52 +02:00
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}},
2018-08-04 12:39:21 +02:00
# Legos
2023-02-12 20:29:52 +02:00
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': {}},
2018-08-04 12:39:21 +02:00
# Top and bottom trapezoid
2023-01-12 18:09:43 +02:00
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}}
2018-08-04 12:39:21 +02:00
}
2023-02-23 12:25:29 +02:00
SYM_ATTR_TRIGRAPH = {
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa1!', 'params': {'overlap': -0.10, 'careful': True}}
}
2018-08-04 12:39:21 +02:00
SYM_ATTR_FONTA = {
# 'pa' == preserve aspect ratio
2022-02-07 16:05:54 +02:00
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}},
2018-08-04 12:39:21 +02:00
# Don't center these arrows vertically
2022-02-07 16:05:54 +02:00
0xf0dc: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}},
0xf0dd: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}},
0xf0de: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}}
2018-08-04 12:39:21 +02:00
}
2023-02-10 17:24:54 +02:00
SYM_ATTR_HEAVYBRACKETS = {
2023-05-11 21:45:00 +02:00
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa1!', 'params': {'ypadding': 0.3, 'careful': True}}
2023-02-10 17:24:54 +02:00
}
2023-02-15 17:40:41 +02:00
SYM_ATTR_BOX = {
2023-03-07 23:26:12 +02:00
'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}},
2023-02-15 17:40:41 +02:00
}
2018-08-04 12:39:21 +02:00
CUSTOM_ATTR = {
2023-05-09 12:40:24 +02:00
# previous custom scaling => do not touch the icons
# 'default': {'align': 'c', 'valign': '', 'stretch': '', 'params': {}}
2023-05-09 13:37:02 +02:00
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {'careful': self.args.careful}}
2018-08-04 12:39:21 +02:00
}
2023-01-04 17:09:44 +02:00
# 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.
2023-01-07 00:33:57 +02:00
# Shifting in addition to scaling can be selected too (see below).
2023-01-04 17:09:44 +02:00
# - 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.
2023-05-30 21:36:26 +02:00
# (- ScaleGroupsVert: Removed with this commit)
2023-01-04 17:09:44 +02:00
#
# 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
2023-01-07 00:33:57 +02:00
# handled (scaled) like the reference glyph.
2023-01-04 17:09:44 +02:00
# 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
# ]}
2023-01-07 00:33:57 +02:00
# 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.
2023-01-04 17:09:44 +02:00
#
# 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'.
2022-09-07 15:56:14 +02:00
# The codepoints mentioned here are symbol-font-codepoints.
2023-01-04 17:09:44 +02:00
2023-02-15 17:40:41 +02:00
BOX_SCALE_LIST = {'ScaleGroups': [
[*range(0x2500, 0x2570 + 1), *range(0x2574, 0x257f + 1)], # box drawing
range(0x2571, 0x2573 + 1), # diagonals
2023-03-07 23:26:12 +02:00
[*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!)
2023-02-15 17:40:41 +02:00
]}
2023-05-30 21:43:20 +02:00
CODI_SCALE_LIST = {'ScaleGroups': [
2023-03-02 15:18:14 +02:00
range(0xeb6e, 0xeb71 + 1), # triangles
range(0xeab4, 0xeab7 + 1), # chevrons
]}
2023-02-15 17:48:56 +02:00
DEVI_SCALE_LIST = {'ScaleGlyph': 0xE60E, # Android logo
2021-12-20 20:25:58 +02:00
'GlyphsToScale': [
(0xe6bd, 0xe6c3) # very small things
2023-03-02 15:18:14 +02:00
]}
2023-01-03 20:10:13 +02:00
FONTA_SCALE_LIST = {'ScaleGroups': [
2021-12-20 15:57:20 +02:00
[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
2022-10-13 12:39:47 +02:00
range(0xf0a4, 0xf0a7 + 1), # pointing hands
2022-10-12 18:44:16 +02:00
[0xf0d7, 0xf0d8, 0xf0d9, 0xf0da, 0xf0dc, 0xf0dd, 0xf0de], # caret all directions and same looking sort
2021-12-20 15:57:20 +02:00
range(0xf100, 0xf107 + 1), # angle
2022-10-13 12:39:47 +02:00
range(0xf130, 0xf131 + 1), # mic
2021-12-20 15:57:20 +02:00
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
]}
2023-05-11 21:45:00 +02:00
HEAVY_SCALE_LIST = {'ScaleGlyph': 0x2771, # widest bracket, horizontally
'GlyphsToScale': [
(0x276c, 0x2771) # all
]}
2023-05-03 09:38:52 +02:00
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
2023-05-08 12:26:30 +02:00
[0xf07b, 0xf0a1, 0xf0d6, 0xf306], # bookmarks
2023-03-02 15:18:14 +02:00
]}
2023-01-03 20:10:13 +02:00
WEATH_SCALE_LIST = {'ScaleGroups': [
2023-02-08 09:47:55 +02:00
[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
2022-09-07 15:56:14 +02:00
range(0xf095, 0xf0b0 + 1), # moon phases
range(0xf0b7, 0xf0c3 + 1), # wind strengths
2023-02-08 09:47:55 +02:00
[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!)
2022-09-07 15:56:14 +02:00
]}
2023-02-23 18:25:41 +02:00
MDI_SCALE_LIST = None # Maybe later add some selected ScaleGroups
2018-08-04 12:39:21 +02:00
2023-02-15 17:40:41 +02:00
2018-08-04 12:39:21 +02:00
# Define the character ranges
# Symbol font ranges
self.patch_set = [
2023-02-13 12:20:02 +02:00
{'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE5FF, 'SrcStart': 0xE5FA, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
2023-05-11 21:45:00 +02:00
{'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},
2023-03-08 13:44:07 +02:00
{'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},
2023-01-03 20:26:15 +02:00
{'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},
2023-02-23 12:25:29 +02:00
{'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},
2023-01-03 20:26:15 +02:00
{'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)
2023-04-24 21:11:59 +02:00
{'Enabled': False , 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
2023-01-06 19:18:56 +02:00
{'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},
2023-01-03 20:26:15 +02:00
{'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},
2023-03-25 14:20:22 +02:00
{'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
2023-05-08 12:26:30 +02:00
{'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},
2023-03-02 15:18:14 +02:00
{'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},
2023-01-03 20:26:15 +02:00
{'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR}
2018-08-04 12:39:21 +02:00
]
2023-01-20 17:18:34 +02:00
def improve_line_dimensions(self):
2018-08-04 12:39:21 +02:00
# 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:
2023-01-20 17:29:40 +02:00
# All three are equal before due to get_sourcefont_dimensions()
2023-01-20 17:18:34 +02:00
self.sourceFont.hhea_ascent += 1
self.sourceFont.os2_typoascent += 1
2018-08-04 12:39:21 +02:00
self.sourceFont.os2_winascent += 1
2023-01-20 00:13:37 +02:00
def add_glyphrefs_to_essential(self, unicode):
self.essential.add(unicode)
# According to fontforge spec, altuni is either None or a tuple of tuples
2023-01-21 22:39:13 +02:00
# 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 ]:
2023-01-20 00:13:37 +02:00
# 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.
2023-01-21 22:39:13 +02:00
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,
2023-03-10 11:43:55 +02:00
# a glyph name, a transformation matrix, and (depending on ff version) whether the
# reference is currently selected.
2023-01-21 22:39:13 +02:00
references = self.sourceFont[unicode].references
2023-03-10 11:43:55 +02:00
for refcode in [ self.sourceFont[n].unicode for n, *_ in references ]: # tuple of 2 or 3 depending on ff version
2023-01-21 22:39:13 +02:00
if refcode not in self.essential and refcode >= 0:
self.add_glyphrefs_to_essential(refcode)
2023-01-20 00:13:37 +02:00
2022-09-06 09:12:09 +02:00
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.
2023-05-10 11:09:16 +02:00
# 0x0000-0x017f is the Latin Extended-A range
# 0xfb00-0xfb06 are 'fi' and other ligatures
2023-01-28 13:26:27 +02:00
basic_glyphs = set()
# Collect substitution destinations
2023-05-10 11:09:16 +02:00
for glyph in [*range(0x21, 0x17f + 1), *range(0xfb00, 0xfb06 + 1)]:
2022-09-06 09:12:09 +02:00
if not glyph in self.sourceFont:
continue
2023-01-28 13:26:27 +02:00
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:
2023-01-20 00:13:37 +02:00
self.add_glyphrefs_to_essential(glyph)
2018-08-04 12:39:21 +02:00
def get_sourcefont_dimensions(self):
2023-01-20 17:29:40 +02:00
""" 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
2023-02-12 00:22:29 +02:00
(hhea_btb, typo_btb, win_btb, win_gap) = get_btb_metrics(self.sourceFont)
2023-01-20 17:29:40 +02:00
use_typo = self.sourceFont.os2_use_typo_metrics != 0
2023-02-12 17:35:20 +02:00
Metric = Enum('Metric', ['HHEA', 'TYPO', 'WIN'])
2023-01-20 17:29:40 +02:00
# We use either TYPO (1) or WIN (2) and compare with HHEA
2023-02-12 00:22:29 +02:00
# and use HHEA (0) if the fonts seems broken - no WIN, see #1056
2023-01-20 17:29:40 +02:00
our_btb = typo_btb if use_typo else win_btb
if our_btb == hhea_btb:
2023-02-12 17:35:20 +02:00
metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font
2023-04-30 23:18:23 +02:00
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
2023-01-20 17:29:40 +02:00
else:
2023-04-30 23:18:23 +02:00
# 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
2023-05-03 07:10:09 +02:00
logger.warning("Font vertical metrics probably wrong USE TYPO METRICS, assume opposite (i.e. %s)", repr(use_typo))
2023-04-30 23:18:23 +02:00
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
2023-01-20 17:29:40 +02:00
# print("FINI hhea {} typo {} win {} use {} {} {}".format(hhea_btb, typo_btb, win_btb, use_typo, our_btb != hhea_btb, self.sourceFont.fontname))
2023-05-11 21:45:00 +02:00
self.font_dim = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0, 'width' : 0, 'height': 0, 'ypadding': 0}
2023-01-20 17:29:40 +02:00
2023-02-12 17:35:20 +02:00
if metrics == Metric.HHEA:
2023-02-12 00:22:29 +02:00
self.font_dim['ymin'] = self.sourceFont.hhea_descent - half_gap(self.sourceFont.hhea_linegap, False)
2023-01-20 17:29:40 +02:00
self.font_dim['ymax'] = self.sourceFont.hhea_ascent + half_gap(self.sourceFont.hhea_linegap, True)
2023-02-12 17:35:20 +02:00
elif metrics == Metric.TYPO:
2023-02-12 00:22:29 +02:00
self.font_dim['ymin'] = self.sourceFont.os2_typodescent - half_gap(self.sourceFont.os2_typolinegap, False)
2023-01-20 17:29:40 +02:00
self.font_dim['ymax'] = self.sourceFont.os2_typoascent + half_gap(self.sourceFont.os2_typolinegap, True)
2023-02-12 17:35:20 +02:00
elif metrics == Metric.WIN:
2023-02-12 17:59:29 +02:00
self.font_dim['ymin'] = -self.sourceFont.os2_windescent - half_gap(win_gap, False)
2023-01-20 17:29:40 +02:00
self.font_dim['ymax'] = self.sourceFont.os2_winascent + half_gap(win_gap, True)
2023-02-12 17:35:20 +02:00
else:
pass # Will fail the metrics check some line later
2018-08-04 12:39:21 +02:00
2022-10-12 11:49:28 +02:00
# Calculate font height
self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax']
if self.font_dim['height'] == 0:
# This can only happen if the input font is empty
# Assume we are using our prepared templates
2023-01-15 17:34:02 +02:00
self.symbolsonly = True
2022-10-12 11:49:28 +02:00
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,
}
2023-02-12 20:35:05 +02:00
our_btb = self.sourceFont.descent + self.sourceFont.ascent
2023-01-20 17:29:40 +02:00
elif self.font_dim['height'] < 0:
2023-04-13 12:11:13 +02:00
logger.critical("Can not detect sane font height")
sys.exit(1)
2022-10-12 11:49:28 +02:00
2023-01-20 17:29:40 +02:00
# Make all metrics equal
2022-09-26 18:22:46 +02:00
self.sourceFont.os2_typolinegap = 0
2023-01-20 17:29:40 +02:00
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
2023-02-12 00:22:29 +02:00
(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:
2023-04-13 12:11:13 +02:00
logger.critical("Error in baseline to baseline code detected")
sys.exit(1)
2023-01-20 17:29:40 +02:00
# Step 2
2023-01-21 18:31:15 +02:00
# Find the biggest char width and advance width
2018-08-04 12:39:21 +02:00
# 0x00-0x17f is the Latin Extended-A range
2023-04-13 12:11:13 +02:00
warned1 = self.args.nonmono # Do not warn if proportional target
2023-03-08 12:53:59 +02:00
warned2 = warned1
2022-08-26 19:07:17 +02:00
for glyph in range(0x21, 0x17f):
2023-01-11 13:34:54 +02:00
if glyph in range(0x7F, 0xBF) or glyph in [
2023-01-21 13:29:54 +02:00
0x132, 0x133, # IJ, ij (in Overpass Mono)
2023-01-11 13:34:54 +02:00
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
2023-05-16 18:53:51 +02:00
0x149, # napostrophe in DaddyTimeMono
2023-01-22 15:50:43 +02:00
0x02D, # hyphen for Monofur
2023-01-11 13:34:54 +02:00
]:
continue # ignore special characters like '1/4' etc and some specifics
2018-08-04 12:39:21 +02:00
try:
(_, _, xmax, _) = self.sourceFont[glyph].boundingBox()
except TypeError:
continue
2023-01-11 13:34:54 +02:00
# print("WIDTH {:X} {} ({} {})".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
2018-08-04 12:39:21 +02:00
if self.font_dim['width'] < self.sourceFont[glyph].width:
self.font_dim['width'] = self.sourceFont[glyph].width
2023-03-08 12:53:59 +02:00
if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
2023-05-24 12:45:36 +02:00
logger.debug("Extended glyphs wider than basic glyphs, results might be useless")
logger.debug("%s", report_advance_widths(self.sourceFont))
2023-03-08 12:53:59 +02:00
warned1 = True
2023-01-22 15:50:43 +02:00
# print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
2018-08-04 12:39:21 +02:00
if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax
2023-03-08 12:53:59 +02:00
if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
2023-04-13 12:11:13 +02:00
logger.debug("Extended glyphs wider bounding box than basic glyphs")
2023-03-08 12:53:59 +02:00
warned2 = True
2023-01-22 15:50:43 +02:00
# print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
2023-03-08 12:53:59 +02:00
if self.font_dim['width'] < self.font_dim['xmax']:
2023-04-13 12:11:13 +02:00
logger.debug("Font has negative right side bearing in extended glyphs")
2023-02-15 17:50:29 +02:00
self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used
2023-05-24 12:45:36 +02:00
logger.debug("Final font cell dimensions %d w x %d h", self.font_dim['width'], self.font_dim['height'])
2017-07-30 03:41:42 +02:00
2023-04-12 15:19:52 +02:00
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)
2016-05-08 22:58:54 +02:00
2023-03-07 20:49:41 +02:00
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
2021-12-31 15:09:00 +02:00
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)
2023-03-07 20:49:41 +02:00
target_width = self.font_dim['width'] * self.get_target_width(stretch)
2021-12-31 15:09:00 +02:00
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')
2023-05-11 21:45:00 +02:00
target_height = self.font_dim['height'] * (1.0 - self.font_dim['ypadding'])
2021-12-31 15:09:00 +02:00
scale_ratio_y = target_height / sym_dim['height']
2023-02-12 20:29:52 +02:00
if 'pa' in stretch:
2021-12-31 15:09:00 +02:00
# 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)
2023-02-23 12:25:29 +02:00
if not self.args.single and not '!' in stretch:
2023-01-11 19:37:14 +02:00
# non monospaced fonts just scale down on 'pa', not up
scale_ratio_x = min(scale_ratio_x, 1.0)
2021-12-31 15:09:00 +02:00
scale_ratio_y = scale_ratio_x
2018-08-04 12:39:21 +02:00
else:
2021-12-31 15:09:00 +02:00
# Keep the not-stretched direction
2023-01-12 11:59:55 +02:00
if not 'x' in stretch:
2021-12-31 15:09:00 +02:00
scale_ratio_x = 1.0
if not 'y' in stretch:
scale_ratio_y = 1.0
return (scale_ratio_x, scale_ratio_y)
2018-08-04 12:39:21 +02:00
2023-01-03 20:26:15 +02:00
def copy_glyphs(self, sourceFontStart, symbolFont, symbolFontStart, symbolFontEnd, exactEncoding, scaleRules, setName, attributes):
2018-08-04 12:39:21 +02:00
""" Copies symbol glyphs into self.sourceFont """
progressText = ''
careful = False
2022-02-04 12:33:38 +02:00
sourceFontCounter = 0
2018-08-04 12:39:21 +02:00
if self.args.careful:
careful = True
# Create glyphs from symbol font
#
# If we are going to copy all Glyphs, then assume we want to be careful
# and only copy those that are not already contained in the source font
if symbolFontStart == 0:
symbolFont.selection.all()
careful = True
else:
symbolFont.selection.select((str("ranges"), str("unicode")), symbolFontStart, symbolFontEnd)
2016-10-25 02:05:15 +02:00
2022-11-26 16:03:51 +02:00
# 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 ]
2021-12-22 15:29:34 +02:00
glyphSetLength = len(symbolFontSelection)
2015-02-21 03:21:52 +02:00
2023-01-04 13:22:48 +02:00
if not self.args.quiet:
2023-02-15 19:33:11 +02:00
modify = attributes['default']['params'].get('dont_copy')
sys.stdout.write("{} {} Glyphs from {} Set\n".format(
"Adding" if not modify else "Rescaling", glyphSetLength, setName))
2015-02-21 03:21:52 +02:00
font-patcher: Allow glyphs with altuni for exactEncoding
[why]
Some symbol fonts might come with glyphs that have multiple codepoints.
When we want to patch them with `'Exact': true` (i.e. at their 'original'
codepoints) we want to patch them into the codepoint that has been used
in the selection process. That means between SymStart and SymEnd.
But this is not the case. We patch them in into their 'main' codepoint,
which can be outside the expected range of points.
This came up when patching with FontAwesome V6. It has for example these
glyphs:
Glyph 'music' has a main codepoint 0x1F3B5, but it is present in the
font also on codepoint 0xF001.
Glyph 'heard' has a main codepoint 0x1F9E1, but it is present in the
font also on codepoints 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ...
When doing a `'Exact': true` patch (i.e. exactEncoding = true) the
glyphs is patched into the target font at its (the glyph's) main
codepoint, regardless of our patch-codepoint-range.
[how]
We examine all codepoints that a glyph occupies in the symbol font. From
all these codepoints we take the nearest to the last glyph be patched
in. Nearest means from the possible codepoints the lowest that come
after the previous used codepoint.
For example the 'heard':
Last patched in codepoint was 0xF003.
Main codepoint: 0x1F9E1
Alternate codepoints: 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ...
-=> 0xF004
Later in the patching process we might encounter the same glyph again,
but this time the previous codepoint was 0xF089, so we need to take
0xF08A.
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-22 12:59:38 +02:00
currentSourceFontGlyph = -1 # initialize for the exactEncoding case
2023-01-12 17:37:09 +02:00
width_warning = False
font-patcher: Allow glyphs with altuni for exactEncoding
[why]
Some symbol fonts might come with glyphs that have multiple codepoints.
When we want to patch them with `'Exact': true` (i.e. at their 'original'
codepoints) we want to patch them into the codepoint that has been used
in the selection process. That means between SymStart and SymEnd.
But this is not the case. We patch them in into their 'main' codepoint,
which can be outside the expected range of points.
This came up when patching with FontAwesome V6. It has for example these
glyphs:
Glyph 'music' has a main codepoint 0x1F3B5, but it is present in the
font also on codepoint 0xF001.
Glyph 'heard' has a main codepoint 0x1F9E1, but it is present in the
font also on codepoints 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ...
When doing a `'Exact': true` patch (i.e. exactEncoding = true) the
glyphs is patched into the target font at its (the glyph's) main
codepoint, regardless of our patch-codepoint-range.
[how]
We examine all codepoints that a glyph occupies in the symbol font. From
all these codepoints we take the nearest to the last glyph be patched
in. Nearest means from the possible codepoints the lowest that come
after the previous used codepoint.
For example the 'heard':
Last patched in codepoint was 0xF003.
Main codepoint: 0x1F9E1
Alternate codepoints: 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ...
-=> 0xF004
Later in the patching process we might encounter the same glyph again,
but this time the previous codepoint was 0xF089, so we need to take
0xF08A.
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-22 12:59:38 +02:00
2021-12-22 15:29:34 +02:00
for index, sym_glyph in enumerate(symbolFontSelection):
2023-01-12 17:37:09 +02:00
sym_attr = attributes.get(sym_glyph.unicode)
if sym_attr is None:
2018-08-04 12:39:21 +02:00
sym_attr = attributes['default']
2015-02-21 03:21:52 +02:00
2023-01-12 17:37:09 +02:00
if self.font_extrawide:
# Do not allow 'xy2' scaling
sym_attr['stretch'] = sym_attr['stretch'].replace('2', '')
2018-08-04 12:39:21 +02:00
if exactEncoding:
font-patcher: Allow glyphs with altuni for exactEncoding
[why]
Some symbol fonts might come with glyphs that have multiple codepoints.
When we want to patch them with `'Exact': true` (i.e. at their 'original'
codepoints) we want to patch them into the codepoint that has been used
in the selection process. That means between SymStart and SymEnd.
But this is not the case. We patch them in into their 'main' codepoint,
which can be outside the expected range of points.
This came up when patching with FontAwesome V6. It has for example these
glyphs:
Glyph 'music' has a main codepoint 0x1F3B5, but it is present in the
font also on codepoint 0xF001.
Glyph 'heard' has a main codepoint 0x1F9E1, but it is present in the
font also on codepoints 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ...
When doing a `'Exact': true` patch (i.e. exactEncoding = true) the
glyphs is patched into the target font at its (the glyph's) main
codepoint, regardless of our patch-codepoint-range.
[how]
We examine all codepoints that a glyph occupies in the symbol font. From
all these codepoints we take the nearest to the last glyph be patched
in. Nearest means from the possible codepoints the lowest that come
after the previous used codepoint.
For example the 'heard':
Last patched in codepoint was 0xF003.
Main codepoint: 0x1F9E1
Alternate codepoints: 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ...
-=> 0xF004
Later in the patching process we might encounter the same glyph again,
but this time the previous codepoint was 0xF089, so we need to take
0xF08A.
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-22 12:59:38 +02:00
# Use the exact same hex values for the source font as for the symbol font.
# Problem is we do not know the codepoint of the sym_glyph and because it
# came from a selection.byGlyphs there might be skipped over glyphs.
# The iteration is still in the order of the selection by codepoint,
# so we take the next allowed codepoint of the current glyph
possible_codes = [ ]
if sym_glyph.unicode > currentSourceFontGlyph:
possible_codes += [ sym_glyph.unicode ]
if sym_glyph.altuni:
possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ]
2022-11-26 16:03:51 +02:00
if len(possible_codes) == 0:
2023-04-13 12:11:13 +02:00
logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode)
2022-11-26 16:03:51 +02:00
continue
font-patcher: Allow glyphs with altuni for exactEncoding
[why]
Some symbol fonts might come with glyphs that have multiple codepoints.
When we want to patch them with `'Exact': true` (i.e. at their 'original'
codepoints) we want to patch them into the codepoint that has been used
in the selection process. That means between SymStart and SymEnd.
But this is not the case. We patch them in into their 'main' codepoint,
which can be outside the expected range of points.
This came up when patching with FontAwesome V6. It has for example these
glyphs:
Glyph 'music' has a main codepoint 0x1F3B5, but it is present in the
font also on codepoint 0xF001.
Glyph 'heard' has a main codepoint 0x1F9E1, but it is present in the
font also on codepoints 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ...
When doing a `'Exact': true` patch (i.e. exactEncoding = true) the
glyphs is patched into the target font at its (the glyph's) main
codepoint, regardless of our patch-codepoint-range.
[how]
We examine all codepoints that a glyph occupies in the symbol font. From
all these codepoints we take the nearest to the last glyph be patched
in. Nearest means from the possible codepoints the lowest that come
after the previous used codepoint.
For example the 'heard':
Last patched in codepoint was 0xF003.
Main codepoint: 0x1F9E1
Alternate codepoints: 0x2665, 0x2764, 0xF004, 0xF08A, 0x1F499, ...
-=> 0xF004
Later in the patching process we might encounter the same glyph again,
but this time the previous codepoint was 0xF089, so we need to take
0xF08A.
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2022-02-22 12:59:38 +02:00
currentSourceFontGlyph = min(possible_codes)
2018-08-04 12:39:21 +02:00
else:
2022-02-04 12:33:38 +02:00
# use source font defined hex values based on passed in start (fills gaps; symbols are packed)
currentSourceFontGlyph = sourceFontStart + sourceFontCounter
2018-08-04 12:39:21 +02:00
sourceFontCounter += 1
2021-12-31 15:09:00 +02:00
# For debugging process only limited glyphs
# if currentSourceFontGlyph != 0xe7bd:
# continue
2023-05-11 21:45:00 +02:00
ypadding = sym_attr['params'].get('ypadding')
self.font_dim['ypadding'] = ypadding or 0.0
2023-01-04 13:22:48 +02:00
if not self.args.quiet:
2018-08-04 12:39:21 +02:00
if self.args.progressbars:
update_progress(round(float(index + 1) / glyphSetLength, 2))
else:
2021-12-22 22:27:35 +02:00
progressText = "\nUpdating glyph: {} {} putting at: {:X}".format(sym_glyph, sym_glyph.glyphname, currentSourceFontGlyph)
2018-08-04 12:39:21 +02:00
sys.stdout.write(progressText)
sys.stdout.flush()
# check if a glyph already exists in this location
2023-05-09 13:37:02 +02:00
do_careful = sym_attr['params'].get('careful', careful) # params take precedence
if do_careful or currentSourceFontGlyph in self.essential:
2021-12-22 22:27:35 +02:00
if currentSourceFontGlyph in self.sourceFont:
2023-04-13 12:11:13 +02:00
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph)
2018-08-04 12:39:21 +02:00
# We don't want to touch anything so move to next Glyph
continue
2021-12-13 17:55:48 +02:00
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)
2021-12-22 22:27:35 +02:00
if currentSourceFontGlyph in self.sourceFont:
self.sourceFont[currentSourceFontGlyph].removePosSub("*")
2018-08-04 12:39:21 +02:00
2023-05-12 11:33:16 +02:00
stretch = sym_attr['stretch']
2023-02-15 19:33:11 +02:00
dont_copy = sym_attr['params'].get('dont_copy')
2022-10-12 17:43:26 +02:00
2023-02-15 19:33:11 +02:00
if dont_copy:
# Just prepare scaling of existing glyphs
2023-05-12 11:33:16 +02:00
glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, stretch, self.sourceFont, currentSourceFontGlyph) if scaleRules is not None else None
2023-02-15 19:33:11 +02:00
else:
# This will destroy any content currently in currentSourceFontGlyph, so do it first
2023-05-12 11:33:16 +02:00
glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, stretch, symbolFont, currentSourceFontGlyph) if scaleRules is not None else None
2023-02-15 19:33:11 +02:00
# Select and copy symbol from its encoding point
# We need to do this select after the careful check, this way we don't
# reset our selection before starting the next loop
symbolFont.selection.select(sym_glyph.encoding)
symbolFont.copy()
2018-08-04 12:39:21 +02:00
2023-02-15 19:33:11 +02:00
# 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
2018-08-04 12:39:21 +02:00
2021-12-22 20:57:51 +02:00
# Prepare symbol glyph dimensions
sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph])
2021-12-31 15:09:00 +02:00
if glyph_scale_data is not None:
2023-01-12 12:53:40 +02:00
if glyph_scale_data[1] is not None:
sym_dim = glyph_scale_data[1] # Use combined bounding box
2023-05-12 11:33:16 +02:00
(scale_ratio_x, scale_ratio_y) = self.get_scale_factors(sym_dim, stretch)
2023-02-15 17:55:02 +02:00
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])
2021-12-31 15:09:00 +02:00
else:
2023-05-12 11:33:16 +02:00
(scale_ratio_x, scale_ratio_y) = self.get_scale_factors(sym_dim, stretch)
2021-12-31 15:09:00 +02:00
2022-02-07 16:05:54 +02:00
overlap = sym_attr['params'].get('overlap')
2023-05-11 21:45:00 +02:00
if overlap and ypadding:
logger.critical("Conflicting params: overlap and ypadding")
sys.exit(1)
2023-01-12 12:53:40 +02:00
if overlap:
scale_ratio_x *= 1.0 + (self.font_dim['width'] / (sym_dim['width'] * scale_ratio_x)) * overlap
2022-10-14 17:00:16 +02:00
y_overlap = min(0.01, overlap) # never aggressive vertical overlap
2023-01-12 17:55:38 +02:00
scale_ratio_y *= 1.0 + (self.font_dim['height'] / (sym_dim['height'] * scale_ratio_y)) * y_overlap
2021-12-22 09:54:01 +02:00
2023-01-12 12:53:40 +02:00
# 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
2023-01-04 18:07:46 +02:00
2023-01-12 12:53:40 +02:00
if scale_ratio_x != 1.0 or scale_ratio_y != 1.0:
2021-12-22 16:00:39 +02:00
self.sourceFont[currentSourceFontGlyph].transform(psMat.scale(scale_ratio_x, scale_ratio_y))
2018-08-04 12:39:21 +02:00
2022-10-13 12:28:37 +02:00
# We pasted and scaled now we want to align/move
# Use the dimensions from the newly pasted and stretched glyph to avoid any rounding errors
2018-08-04 12:39:21 +02:00
sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph])
2022-10-13 12:28:37 +02:00
# Use combined bounding box?
2023-01-03 20:26:15 +02:00
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)
2023-05-30 21:36:26 +02:00
if scaleglyph_dim['advance'] is None:
2022-10-13 12:28:37 +02:00
# On monospaced symbol collections use their advance with, otherwise align horizontally individually
scaleglyph_dim['xmin'] = sym_dim['xmin']
scaleglyph_dim['xmax'] = sym_dim['xmax']
scaleglyph_dim['width'] = sym_dim['width']
sym_dim = scaleglyph_dim
2018-08-04 12:39:21 +02:00
y_align_distance = 0
if sym_attr['valign'] == 'c':
# Center the symbol vertically by matching the center of the line height and center of symbol
sym_ycenter = sym_dim['ymax'] - (sym_dim['height'] / 2)
font_ycenter = self.font_dim['ymax'] - (self.font_dim['height'] / 2)
y_align_distance = font_ycenter - sym_ycenter
# Handle glyph l/r/c alignment
x_align_distance = 0
2023-01-04 11:50:37 +02:00
if self.args.nonmono and sym_dim['advance'] is None:
2023-01-02 17:17:22 +02:00
# Remove left side bearing
2023-01-04 11:50:37 +02:00
# (i.e. do not remove left side bearing when combined BB is in use)
2023-01-02 17:17:22 +02:00
x_align_distance = -self.sourceFont[currentSourceFontGlyph].left_side_bearing
2022-12-21 19:35:27 +02:00
elif sym_attr['align']:
2018-08-04 12:39:21 +02:00
# 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
2023-05-12 11:33:16 +02:00
x_align_distance += self.font_dim['width'] * self.get_target_width(stretch) - sym_dim['width']
2023-02-08 12:27:44 +02:00
# If symbol glyph is wider than target font cell, just left-align
2023-03-02 21:06:31 +02:00
x_align_distance = max(self.font_dim['xmin'] - sym_dim['xmin'], x_align_distance)
2015-02-21 03:21:52 +02:00
2022-02-07 16:05:54 +02:00
if overlap:
2021-12-22 09:54:01 +02:00
overlap_width = self.font_dim['width'] * overlap
2018-08-04 12:39:21 +02:00
if sym_attr['align'] == 'l':
x_align_distance -= overlap_width
2023-03-02 21:35:19 +02:00
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
2023-05-12 11:33:16 +02:00
target_xmax = (self.font_dim['xmin'] + self.font_dim['width']) * self.get_target_width(stretch)
2023-03-07 20:49:41 +02:00
target_xmax += overlap_width
glyph_xmax = sym_dim['xmax'] + x_align_distance
correction = target_xmax - glyph_xmax
2023-03-02 21:35:19 +02:00
x_align_distance += correction
2018-08-04 12:39:21 +02:00
align_matrix = psMat.translate(x_align_distance, y_align_distance)
2021-12-22 16:00:39 +02:00
self.sourceFont[currentSourceFontGlyph].transform(align_matrix)
2018-08-04 12:39:21 +02:00
2022-09-01 07:38:00 +02:00
# Ensure after horizontal adjustments and centering that the glyph
# does not overlap the bearings (edges)
2022-02-07 16:11:08 +02:00
if not overlap:
self.remove_glyph_neg_bearings(self.sourceFont[currentSourceFontGlyph])
2022-09-01 07:38:00 +02:00
2018-08-04 12:39:21 +02:00
# Needed for setting 'advance width' on each glyph so they do not overlap,
# also ensures the font is considered monospaced on Windows by setting the
# same width for all character glyphs. This needs to be done for all glyphs,
# even the ones that are empty and didn't go through the scaling operations.
2022-09-01 07:38:00 +02:00
# It should come after setting the glyph bearings
2023-01-02 17:17:22 +02:00
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
2023-01-04 11:50:37 +02:00
# 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']
2023-01-02 17:17:22 +02:00
# 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)
2022-09-01 08:43:55 +02:00
2021-12-22 09:54:01 +02:00
# Check if the inserted glyph is scaled correctly for monospace
if self.args.single:
(xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox()
2022-02-07 16:05:54 +02:00
if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)):
2023-05-03 07:10:09 +02:00
logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %s))",
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], repr(overlap))
2021-12-22 09:54:01 +02:00
2018-08-04 12:39:21 +02:00
# end for
2015-02-21 03:21:52 +02:00
2023-01-28 16:26:34 +02:00
if not self.args.quiet:
2018-08-04 12:39:21 +02:00
sys.stdout.write("\n")
2015-02-21 03:21:52 +02:00
2015-02-25 21:45:58 +02:00
2018-08-04 12:39:21 +02:00
def set_sourcefont_glyph_widths(self):
""" Makes self.sourceFont monospace compliant """
for glyph in self.sourceFont.glyphs():
2019-10-24 08:20:12 +02:00
if (glyph.width == self.font_dim['width']):
2020-04-12 07:59:16 +02:00
# Don't touch the (negative) bearings if the width is ok
2019-10-24 08:20:12 +02:00
# Ligartures will have these.
continue
font-patcher: Fix broken diacritic glyphs in monospaced font
[why]
Some glyphs are just used as overlays for 'real' glyphs. These can be
for example U+0300 .. U+036F (the 'COMBINING ...' diactritics) like
U+0300, gravecomb, COMBINING GRAVE ACCENT
U+0301, acutecomb, COMBINING ACUTE ACCENT
U+0308, uni0308, COMBINING DIAERESIS
They are never used on their own, at least they are overlayed over a
blanc (U+0020, space).
For the font rendering engine they need to have the correct negative
bearings, so they are shifted to take no space by themselves.
The font-patcher script does not allow negative bearings in monospaced
fonts. This makes sense if every glyph is in itself a 'letter' that
should not reach beyond it's allotted (monospaced) space.
[how]
In the font-patcher script we do not touch the bearings of such overlay
glyphs. They can be identified by their width of zero.
For Windows to detect this font as 'monospaced' we need to change the
width to the standard width, though.
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
2019-10-24 08:21:02 +02:00
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)
2018-08-04 12:39:21 +02:00
self.set_glyph_width_mono(glyph)
def remove_glyph_neg_bearings(self, glyph):
2020-04-22 12:52:07 +02:00
""" Sets passed glyph's bearings 0 if they are negative. """
2018-08-04 03:47:20 +02:00
try:
2020-04-22 12:52:07 +02:00
if glyph.left_side_bearing < 0:
glyph.left_side_bearing = 0
if glyph.right_side_bearing < 0:
glyph.right_side_bearing = 0
2018-08-04 03:47:20 +02:00
except:
pass
2016-10-14 09:35:44 +02:00
2018-08-04 12:39:21 +02:00
def set_glyph_width_mono(self, glyph):
""" Sets passed glyph.width to self.font_dim.width.
2016-10-15 08:19:10 +02:00
2018-08-04 12:39:21 +02:00
self.font_dim.width is set with self.get_sourcefont_dimensions().
"""
try:
2022-09-01 08:43:55 +02:00
# Fontforge handles the width change like this:
# - Keep existing left_side_bearing
# - Set width
# - Calculate and set new right_side_bearing
2018-08-04 12:39:21 +02:00
glyph.width = self.font_dim['width']
except:
pass
2023-05-12 11:33:16 +02:00
def prepareScaleRules(self, scaleRules, stretch, symbolFont, destGlyph):
2023-01-03 20:26:15 +02:00
""" Prepare raw ScaleRules data for use """
2023-01-04 17:09:44 +02:00
# The scaleRules is/will be a dict with these (possible) entries:
2023-01-03 20:10:13 +02:00
# '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)
2023-01-04 17:09:44 +02:00
# Each dim_dict describes the combined bounding box of all glyphs in one ScaleGroups group
2021-12-20 15:26:03 +02:00
# Example:
2023-01-03 20:10:13 +02:00
# { 'ScaleGroups': [ range(1, 3), [ 7, 10 ], ],
# 'scales': [ 1.23, 1.33, ],
# 'bbdims': [ dim_dict1, dim_dict2, ] }
2021-12-20 15:26:03 +02:00
#
2023-01-03 20:10:13 +02:00
# Each item in 'ScaleGroups' (a range or an explicit list) forms a group of glyphs that shall be
2021-12-31 15:09:00 +02:00
# as rescaled all with the same and maximum possible (for the included glyphs) 'pa' factor.
2022-06-02 13:29:56 +02:00
# If the 'bbdims' is present they all shall be shifted in the same way.
2021-12-20 15:26:03 +02:00
#
# Previously this structure has been used:
# 'ScaleGlyph' Lead glyph, which scaling factor is taken
2023-01-04 17:09:44 +02:00
# 'GlyphsToScale': List of ((glyph code) or (tuple of two glyph codes that form a closed range)) that shall be scaled
2021-12-20 15:26:03 +02:00
# 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.
2023-01-04 17:09:44 +02:00
#
# Note that scaleRules is overwritten with the added data.
2023-01-03 20:26:15 +02:00
if 'scales' in scaleRules:
2021-12-20 15:26:03 +02:00
# Already prepared... must not happen, ignore call
return
2023-01-04 17:06:54 +02:00
scaleRules['scales'] = []
scaleRules['bbdims'] = []
if 'ScaleGroups' not in scaleRules:
scaleRules['ScaleGroups'] = []
2023-05-30 21:36:26 +02:00
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)
2023-01-04 17:06:54 +02:00
2023-01-03 20:26:15 +02:00
if 'ScaleGlyph' in scaleRules:
2023-01-04 17:09:44 +02:00
# Rewrite to equivalent ScaleGroup
2023-01-07 00:40:38 +02:00
group_list = []
2023-01-07 00:33:57 +02:00
if 'GlyphsToScale+' in scaleRules:
key = 'GlyphsToScale+'
plus = True
else:
key = 'GlyphsToScale'
plus = False
for i in scaleRules[key]:
2021-12-20 15:26:03 +02:00
if isinstance(i, tuple):
2023-01-07 00:40:38 +02:00
group_list.append(range(i[0], i[1] + 1))
2021-12-20 15:26:03 +02:00
else:
2023-01-07 00:40:38 +02:00
group_list.append(i)
2023-01-03 20:26:15 +02:00
sym_dim = get_glyph_dimensions(symbolFont[scaleRules['ScaleGlyph']])
2023-05-12 11:33:16 +02:00
scale = self.get_scale_factors(sym_dim, stretch)[0]
2023-01-07 00:40:38 +02:00
scaleRules['ScaleGroups'].append(group_list)
2023-01-04 17:06:54 +02:00
scaleRules['scales'].append(scale)
2023-01-07 00:33:57 +02:00
if plus:
scaleRules['bbdims'].append(sym_dim)
else:
scaleRules['bbdims'].append(None) # The 'old' style keeps just the scale, not the positioning
2021-12-20 15:26:03 +02:00
2023-05-12 11:33:16 +02:00
def get_glyph_scale(self, symbol_unicode, scaleRules, stretch, symbolFont, dest_unicode):
2023-01-03 20:26:15 +02:00
""" Determines whether or not to use scaled glyphs for glyph in passed symbol_unicode """
2022-10-12 17:43:26 +02:00
# Potentially destorys the contents of self.sourceFont[dest_unicode]
2023-01-03 20:26:15 +02:00
if not 'scales' in scaleRules:
2022-10-12 17:43:26 +02:00
if not dest_unicode in self.sourceFont:
self.sourceFont.createChar(dest_unicode)
2023-05-12 11:33:16 +02:00
self.prepareScaleRules(scaleRules, stretch, symbolFont, self.sourceFont[dest_unicode])
2023-01-03 20:26:15 +02:00
for glyph_list, scale, box in zip(scaleRules['ScaleGroups'], scaleRules['scales'], scaleRules['bbdims']):
2023-01-07 00:40:38 +02:00
for e in glyph_list:
if isinstance(e, range):
if symbol_unicode in e:
return (scale, box)
elif symbol_unicode == e:
return (scale, box)
2023-01-02 19:00:13 +02:00
return None
2021-12-20 15:26:03 +02:00
2018-08-04 12:39:21 +02:00
2023-01-20 17:29:40 +02:00
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:
2023-04-13 12:11:13 +02:00
logger.info("Redistributing line gap of %d (%d top and %d bottom)", gap, gap_top, gap_bottom)
2023-01-20 17:29:40 +02:00
return gap_top
return gap_bottom
2018-08-04 12:39:21 +02:00
def replace_font_name(font_name, replacement_dict):
""" Replaces all keys with vals from replacement_dict in font_name. """
for key, val in replacement_dict.items():
font_name = font_name.replace(key, val)
return font_name
def make_sure_path_exists(path):
""" Verifies path passed to it exists. """
try:
os.makedirs(path)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise
2022-12-22 10:34:20 +02:00
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)
2022-09-08 17:09:58 +02:00
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!
2022-10-13 12:18:24 +02:00
bbox = [ None, None, None, None, None ]
2021-12-20 15:26:03 +02:00
for glyph in glyphs:
if glyph is None:
# Glyph has been in defining range but is not in the actual font
continue
2023-02-15 19:33:11 +02:00
if destGlyph and glyph.font != destGlyph.font:
2022-09-08 17:09:58 +02:00
glyph.font.selection.select(glyph)
glyph.font.copy()
destGlyph.font.selection.select(destGlyph)
destGlyph.font.paste()
glyph = destGlyph
2021-12-20 15:26:03 +02:00
gbb = glyph.boundingBox()
2022-10-13 12:18:24 +02:00
gadvance = glyph.width
2022-06-02 13:23:54 +02:00
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
2021-12-20 15:26:03 +02:00
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]
2022-10-13 12:18:24 +02:00
if not bbox[4]:
2023-01-03 10:58:10 +02:00
bbox[4] = -gadvance # Negative for one/first glyph
2022-10-13 12:18:24 +02:00
else:
2023-01-03 10:58:10 +02:00
if abs(bbox[4]) != gadvance:
2022-10-13 12:18:24 +02:00
bbox[4] = -1 # Marker for not-monospaced
2023-01-03 10:58:10 +02:00
else:
bbox[4] = gadvance # Positive for 2 or more glyphs
2022-10-13 12:18:24 +02:00
if bbox[4] and bbox[4] < 0:
2023-01-03 10:58:10 +02:00
# Not monospaced when only one glyph is used or multiple glyphs with different advance widths
2022-10-13 12:18:24 +02:00
bbox[4] = None
2018-08-04 12:39:21 +02:00
return {
2022-10-13 12:34:00 +02:00
'xmin' : bbox[0],
'ymin' : bbox[1],
'xmax' : bbox[2],
'ymax' : bbox[3],
'width' : bbox[2] + (-bbox[0]),
'height' : bbox[3] + (-bbox[1]),
2022-10-13 12:18:24 +02:00
'advance': bbox[4], # advance width if monospaced
2018-08-04 12:39:21 +02:00
}
2021-12-20 15:26:03 +02:00
def get_glyph_dimensions(glyph):
""" Returns dict of the dimesions of the glyph passed to it. """
return get_multiglyph_boundingBox([ glyph ])
2018-08-04 12:39:21 +02:00
2022-10-13 12:28:37 +02:00
def scale_bounding_box(bbox, scale_x, scale_y):
""" Return a scaled version of a glyph dimensions dict """
2023-01-03 12:32:38 +02:00
# Simulate scaling on combined bounding box, round values for better simulation
2022-10-13 12:28:37 +02:00
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),
2023-01-03 12:32:38 +02:00
'advance': int(bbox['advance'] * scale_x) if bbox['advance'] is not None else None,
2022-10-13 12:28:37 +02:00
}
new_dim['width'] = new_dim['xmax'] + (-new_dim['xmin'])
new_dim['height'] = new_dim['ymax'] + (-new_dim['ymin'])
return new_dim
2017-04-21 04:22:24 +02:00
def update_progress(progress):
2018-08-04 12:39:21 +02:00
""" Updates progress bar length.
Accepts a float between 0.0 and 1.0. Any int will be converted to a float.
A value at 1 or bigger represents 100%
modified from: https://stackoverflow.com/questions/3160699/python-progress-bar
"""
barLength = 40 # Modify this to change the length of the progress bar
2016-11-06 04:55:27 +02:00
if isinstance(progress, int):
progress = float(progress)
if progress >= 1:
progress = 1
2018-08-04 12:39:21 +02:00
status = "Done...\r\n" # NOTE: status initialized and never used
block = int(round(barLength * progress))
text = "\r╢{0}╟ {1}%".format("█" * block + "░" * (barLength - block), int(progress * 100))
2016-11-06 04:55:27 +02:00
sys.stdout.write(text)
sys.stdout.flush()
2018-08-04 12:39:21 +02:00
def check_fontforge_min_version():
""" Verifies installed FontForge version meets minimum requirement. """
minimumVersion = 20141231
actualVersion = int(fontforge.version())
2016-11-06 04:55:27 +02:00
2018-08-04 12:39:21 +02:00
# un-comment following line for testing invalid version error handling
2018-08-07 13:06:39 +02:00
# actualVersion = 20120731
2016-11-06 04:55:27 +02:00
2018-08-04 12:39:21 +02:00
# versions tested: 20150612, 20150824
if actualVersion < minimumVersion:
2023-04-13 12:11:13 +02:00
logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion)
logger.critical("Please use at least version: %d", minimumVersion)
2018-08-04 12:39:21 +02:00
sys.exit(1)
2016-10-22 06:59:40 +02:00
2023-01-27 13:10:14 +02:00
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
2022-02-09 15:56:39 +02:00
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'
2022-10-17 11:33:40 +02:00
'* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/-/changelog.md'),
2022-02-09 15:56:39 +02:00
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 + ")")
2022-09-04 19:55:24 +02:00
parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='count', help='Whether to generate the glyphs as single-width not double-width (default is double-width)')
2022-02-09 15:56:39 +02:00
parser.add_argument('-l', '--adjust-line-height', dest='adjustLineHeight', default=False, action='store_true', help='Whether to adjust line heights (attempt to center powerline separators more evenly)')
parser.add_argument('-q', '--quiet', '--shutup', dest='quiet', default=False, action='store_true', help='Do not generate verbose output')
parser.add_argument('-c', '--complete', dest='complete', default=False, action='store_true', help='Add all available Glyphs')
parser.add_argument('--careful', dest='careful', default=False, action='store_true', help='Do not overwrite existing glyphs if detected')
parser.add_argument('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specificed in JSON configuration file')
parser.add_argument('--postprocess', dest='postprocess', default=False, type=str, nargs='?', help='Specify a Script for Post Processing')
parser.add_argument('--configfile', dest='configfile', default=False, type=str, nargs='?', help='Specify a file path for JSON configuration file (see sample: src/config.sample.json)')
2023-05-09 12:50:33 +02:00
parser.add_argument('--custom', dest='custom', default=False, type=str, nargs='?', help='Specify a custom symbol font, all glyphs will be copied; absolute path suggested')
2022-02-09 15:56:39 +02:00
parser.add_argument('-ext', '--extension', dest='extension', default="", type=str, nargs='?', help='Change font file type to create (e.g., ttf, otf)')
parser.add_argument('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to')
parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching')
2023-06-04 12:23:11 +02:00
parser.add_argument('--makegroups', dest='makegroups', default=1, type=int, nargs='?', help='Use alternative method to name patched fonts (recommended)', const=1, choices=range(-1, 6 + 1))
2023-04-19 15:57:50 +02:00
# --makegroup has an additional undocumented numeric specifier. '--makegroup' is in fact '--makegroup 1'.
# Original font name: Hugo Sans Mono ExtraCondensed Light Italic
2023-06-04 12:23:11 +02:00
# 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]
2023-04-19 15:57:50 +02:00
2022-02-09 15:56:39 +02:00
parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")')
2023-04-24 15:12:37 +02:00
parser.add_argument('--has-no-italic', dest='noitalic', default=False, action='store_true', help='Font family does not have Italic (but Oblique)')
2022-02-09 15:56:39 +02:00
# progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
progressbars_group_parser = parser.add_mutually_exclusive_group(required=False)
2023-04-07 11:02:28 +02:00
progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set (default)')
2022-02-09 15:56:39 +02:00
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)
2023-05-06 08:55:04 +02:00
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))
2023-04-20 17:20:31 +02:00
parser.add_argument('--dry', dest='dry_run', default=False, action='store_true', help='Do neither patch nor store the font, to check naming')
2023-04-12 15:19:52 +02:00
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
2022-02-09 15:56:39 +02:00
# 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()
2023-04-07 22:50:19 +02:00
if args.makegroups > 0 and not FontnameParserOK:
2023-06-06 08:12:56 +02:00
logger.critical("FontnameParser module missing (bin/scripts/name_parser/Fontname*), specify --makegroups 0")
2023-04-13 12:11:13 +02:00
sys.exit(1)
2022-02-09 15:56:39 +02:00
# 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
2023-01-04 13:22:48 +02:00
if not found:
2022-02-09 15:56:39 +02:00
font_complete = False
args.complete = font_complete
if args.nonmono and args.single:
2023-06-06 08:12:56 +02:00
logger.warning("Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.")
2022-02-09 15:56:39 +02:00
args.nonmono = False
2022-02-09 16:18:22 +02:00
make_sure_path_exists(args.outputdir)
if not os.path.isfile(args.font):
2023-06-06 08:12:56 +02:00
logger.critical("Font file does not exist: %s", args.font)
2023-04-13 12:11:13 +02:00
sys.exit(1)
2022-02-09 16:18:22 +02:00
if not os.access(args.font, os.R_OK):
2023-06-06 08:12:56 +02:00
logger.critical("Can not open font file for reading: %s", args.font)
2023-04-13 12:11:13 +02:00
sys.exit(1)
2022-02-09 19:39:47 +02:00
is_ttc = len(fontforge.fontsInFile(args.font)) > 1
2022-10-10 12:23:43 +02:00
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:
2023-06-06 08:12:56 +02:00
logger.warning("Source font is a variable open type font (VF), opening might fail...")
2022-10-10 12:23:43 +02:00
except:
args.is_variable = False
finally:
try:
source_font_test.close()
except:
pass
2022-02-09 16:18:22 +02:00
if args.extension == "":
args.extension = os.path.splitext(args.font)[1]
else:
args.extension = '.' + args.extension
if re.match("\.ttc$", args.extension, re.IGNORECASE):
2022-02-09 19:39:47 +02:00
if not is_ttc:
2023-06-06 08:12:56 +02:00
logger.critical("Can not create True Type Collections from single font files")
2023-04-13 12:11:13 +02:00
sys.exit(1)
2022-02-09 19:39:47 +02:00
else:
if is_ttc:
2023-06-06 08:12:56 +02:00
logger.critical("Can not create single font files from True Type Collections")
2023-04-13 12:11:13 +02:00
sys.exit(1)
2022-02-09 16:18:22 +02:00
2023-04-12 15:19:52 +02:00
if isinstance(args.xavgwidth, int) and not isinstance(args.xavgwidth, bool):
if args.xavgwidth < 0:
2023-06-06 08:12:56 +02:00
logger.critical("--xavgcharwidth takes no negative numbers")
2023-04-13 12:11:13 +02:00
sys.exit(2)
2023-04-12 15:19:52 +02:00
if args.xavgwidth > 16384:
2023-06-06 08:12:56 +02:00
logger.critical("--xavgcharwidth takes only numbers up to 16384")
2023-04-13 12:11:13 +02:00
sys.exit(2)
2023-04-12 15:19:52 +02:00
2022-02-09 15:56:39 +02:00
return args
2018-08-04 12:39:21 +02:00
def main():
2023-06-06 08:12:56 +02:00
global logger
logging.basicConfig(format='%(levelname)s: %(message)s')
logger = logging # Use root logger until we can set up something sane
2023-01-27 13:10:14 +02:00
global version
git_version = check_version_with_git(version)
2023-04-13 12:11:13 +02:00
allversions = "Patcher v{} ({}) (ff {})".format(
git_version if git_version else version, script_version, fontforge.version())
print("{} {}".format(projectName, allversions))
2023-01-27 13:10:14 +02:00
if git_version:
version = git_version
2018-08-04 12:39:21 +02:00
check_fontforge_min_version()
2022-02-09 15:56:39 +02:00
args = setup_arguments()
2023-04-13 12:11:13 +02:00
logger = logging.getLogger(os.path.basename(args.font))
logger.setLevel(logging.DEBUG)
2023-05-06 08:55:04 +02:00
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:]))
2023-04-13 12:11:13 +02:00
c_handler = logging.StreamHandler(stream=sys.stdout)
c_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
2023-05-06 08:55:04 +02:00
if not (args.debugmode & 2 == 2):
2023-04-13 12:11:13 +02:00
c_handler.setLevel(logging.INFO)
logger.addHandler(c_handler)
2023-05-06 08:55:04 +02:00
if (args.debugmode & 1 == 1) and not log_to_file:
2023-05-06 08:22:42 +02:00
logger.info("Can not write logfile, disabling")
2023-04-20 17:25:02 +02:00
logger.debug("Naming mode %d", args.makegroups)
2023-04-13 12:11:13 +02:00
2022-02-09 15:56:39 +02:00
patcher = font_patcher(args)
2022-02-09 17:04:32 +02:00
2022-02-09 19:39:47 +02:00
sourceFonts = []
2022-09-23 09:26:11 +02:00
all_fonts = fontforge.fontsInFile(args.font)
for i, subfont in enumerate(all_fonts):
2023-01-05 16:13:40 +02:00
if len(all_fonts) > 1:
2023-04-13 12:11:13 +02:00
print("\n")
logger.info("Processing %s (%d/%d)", subfont, i + 1, len(all_fonts))
2022-02-09 19:39:47 +02:00
try:
sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",))
except Exception:
2023-04-13 12:11:13 +02:00
logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information",
subfont)
sys.exit(1)
2022-02-09 17:04:32 +02:00
2023-06-04 09:59:11 +02:00
patcher.setup_name_backup(sourceFonts[-1])
2022-02-09 19:39:47 +02:00
patcher.patch(sourceFonts[-1])
2022-02-09 17:04:32 +02:00
2023-01-05 16:13:40 +02:00
print("Done with Patch Sets, generating font...")
2022-02-09 19:39:47 +02:00
for f in sourceFonts:
patcher.setup_font_names(f)
patcher.generate(sourceFonts)
for f in sourceFonts:
f.close()
2017-04-22 01:11:11 +02:00
2018-08-04 12:39:21 +02:00
if __name__ == "__main__":
__dir__ = os.path.dirname(os.path.abspath(__file__))
main()