mirror of
https://github.com/ryanoasis/nerd-fonts.git
synced 2024-12-13 17:18:37 +02:00
74eca532f2
[why] A lot people expect the font-patcher to be a stand alone script. They even think that the source glyphs (symbols) to be added to be somehow magically there and one PR makes sure that they are fetched if missing. The same problem arises when we have a script distributed over multiple files. For maintenance reasons and code quality this is what one wants. But that might hinder easy use of the font-patcher. [how] Put all the code in the main script. That has an additional drawback: For the nameparser_test* scripts to work we need stand alone files for that classes. Now the code is duplicated and will get out of sync. I have no solution for that, and it all boils down what Nerd Font wants to do. One solution would be to have font-patcher properly set up / divided in many .py files, and to create one monolithic font-patcher from all the sources on demand (via github actions or manually when someone pushes changes to any of the constituends). That approach is taken by a lot of C++ 'header only libraries' that originally consist of a lot files but create one big 'all in one' file automatically from all the small files. For now I guess we can live with the duplication, but we need to think about a solution, as this will bite us sooner or later. Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
1811 lines
91 KiB
Python
Executable File
1811 lines
91 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# coding=utf8
|
|
# Nerd Fonts Version: 2.1.0
|
|
# script version: 3.0.1
|
|
|
|
from __future__ import absolute_import, print_function, unicode_literals
|
|
|
|
version = "2.1.0"
|
|
projectName = "Nerd Fonts"
|
|
projectNameAbbreviation = "NF"
|
|
projectNameSingular = projectName[:-1]
|
|
|
|
import sys
|
|
try:
|
|
import psMat
|
|
except ImportError:
|
|
sys.exit(projectName + ": FontForge module is probably not installed. [See: http://designwithfontforge.com/en-US/Installing_Fontforge.html]")
|
|
import re
|
|
import os
|
|
import argparse
|
|
from argparse import RawTextHelpFormatter
|
|
import errno
|
|
import subprocess
|
|
import json
|
|
try:
|
|
import configparser
|
|
except ImportError:
|
|
sys.exit(projectName + ": configparser module is probably not installed. Try `pip install configparser` or equivalent")
|
|
try:
|
|
import fontforge
|
|
except ImportError:
|
|
sys.exit(
|
|
projectName + (
|
|
": FontForge module could not be loaded. Try installing fontforge python bindings "
|
|
"[e.g. on Linux Debian or Ubuntu: `sudo apt install fontforge python-fontforge`]"
|
|
)
|
|
)
|
|
|
|
############################################
|
|
# bin/scripts/name_parser/FontnameTools.py
|
|
#
|
|
|
|
class FontnameTools:
|
|
"""Deconstruct a font filename to get standardized name parts"""
|
|
|
|
@staticmethod
|
|
def front_upper(word):
|
|
"""Capitalize a string (but keep case of subsequent chars)"""
|
|
return word[:1].upper() + word[1:]
|
|
|
|
@staticmethod
|
|
def camel_casify(word):
|
|
"""Remove blanks and use CamelCase for the new word"""
|
|
return ''.join(map(FontnameTools.front_upper, word.split(' ')))
|
|
|
|
@staticmethod
|
|
def camel_explode(word):
|
|
"""Explode CamelCase -> Camel Case"""
|
|
# But do not explode "JetBrains" etc at string start...
|
|
excludes = [
|
|
'JetBrains',
|
|
'DejaVu',
|
|
'OpenDyslexicAlta',
|
|
'OpenDyslexicMono',
|
|
'OpenDyslexic',
|
|
'DaddyTimeMono',
|
|
'InconsolataGo',
|
|
'ProFontWindows',
|
|
'ProFont',
|
|
'ProggyClean',
|
|
]
|
|
m = re.match('(' + '|'.join(excludes) + ')(.*)', word)
|
|
(prefix, word) = m.group(1,2) if m != None else ('', word)
|
|
if len(word) == 0:
|
|
return prefix
|
|
parts = re.split('(?<=[a-z0-9])(?=[A-Z])', word)
|
|
if len(prefix):
|
|
parts.insert(0, prefix)
|
|
return ' '.join(parts)
|
|
|
|
@staticmethod
|
|
def drop_empty(l):
|
|
"""Remove empty strings from list of strings"""
|
|
return [x for x in l if len(x) > 0]
|
|
|
|
@staticmethod
|
|
def concat(*all_things):
|
|
"""Flatten list of (strings or lists of strings) to a blank-separated string"""
|
|
all = []
|
|
for thing in all_things:
|
|
if type(thing) == str:
|
|
all.append(thing)
|
|
else:
|
|
all += thing
|
|
return ' '.join(FontnameTools.drop_empty(all))
|
|
|
|
@staticmethod
|
|
def unify_style_names(style_name):
|
|
"""Substitude some known token with standard wording"""
|
|
known_names = {
|
|
# Source of the table is the current sourcefonts
|
|
# Left side needs to be lower case
|
|
'-': '',
|
|
'book': '',
|
|
'text': '',
|
|
'ce': 'CE',
|
|
'(ttf)': '(TTF)',
|
|
#'semibold': 'Demi',
|
|
'ob': 'Oblique',
|
|
'it': 'Italic',
|
|
'i': 'Italic',
|
|
'b': 'Bold',
|
|
'normal': 'Regular',
|
|
'c': 'Condensed',
|
|
'r': 'Regular',
|
|
'm': 'Medium',
|
|
'l': 'Light',
|
|
}
|
|
if style_name in known_names:
|
|
return known_names[style_name.lower()]
|
|
return style_name
|
|
|
|
@staticmethod
|
|
def shorten_style_name(name):
|
|
"""Substitude some known styles to short form"""
|
|
known_names = {
|
|
# Chiefly from Noto
|
|
'SemiCondensed': 'SemCond',
|
|
'Condensed': 'Cond',
|
|
'ExtraCondensed': 'ExtCond',
|
|
'SemiBold': 'SemBd',
|
|
'ExtraBold': 'ExtBd',
|
|
'Medium': 'Med',
|
|
'ExtraLight': 'ExtLt',
|
|
'Black': 'Blk',
|
|
}
|
|
if name in known_names:
|
|
return known_names[name]
|
|
return name
|
|
|
|
@staticmethod
|
|
def short_styles(styles):
|
|
"""Shorten all style names in a list"""
|
|
return list(map(FontnameTools.shorten_style_name, styles))
|
|
@staticmethod
|
|
def make_oblique_style(weights, styles):
|
|
"""Move "Oblique" from weights to styles for font naming purposes"""
|
|
if 'Oblique' in weights:
|
|
weights = list(weights)
|
|
weights.remove('Oblique')
|
|
styles = list(styles)
|
|
styles.append('Oblique')
|
|
return (weights, styles)
|
|
|
|
@staticmethod
|
|
def get_name_token(name, tokens, allow_regex_token = False):
|
|
"""Try to find any case insensitive token from tokens in the name, return tuple with found token-list and rest"""
|
|
# The default mode (allow_regex_token = False) will try to find any verbatim string in the
|
|
# tokens list (case insensitive matching) and give that tokens list item back with
|
|
# unchanged case (i.e. [ 'Bold' ] will match "bold" and return it as [ 'Bold', ]
|
|
# In the regex mode (allow_regex_token = True) it will use the tokens elements as
|
|
# regexes and return the original (i.e. from name) case.
|
|
#
|
|
# Token are always used in a regex and may not capture, use non capturing
|
|
# grouping if needed (?: ... )
|
|
lower_tokens = [ t.lower() for t in tokens ]
|
|
not_matched = ""
|
|
all_tokens = []
|
|
j = 1
|
|
regex = re.compile('(.*?)(' + '|'.join(tokens) + ')(.*)', re.IGNORECASE)
|
|
while j:
|
|
j = regex.match(name)
|
|
if not j:
|
|
break
|
|
if len(j.groups()) != 3:
|
|
sys.exit('Malformed regex in FontnameTools.get_name_token()')
|
|
not_matched += ' ' + j.groups()[0] # Blanc prevents unwanted concatenation of unmatched substrings
|
|
tok = j.groups()[1].lower()
|
|
if tok in lower_tokens:
|
|
tok = tokens[lower_tokens.index(tok)]
|
|
tok = FontnameTools.unify_style_names(tok)
|
|
if len(tok):
|
|
all_tokens.append(tok)
|
|
name = j.groups()[2] # Recurse rest
|
|
not_matched += ' ' + name
|
|
return ( not_matched.strip(), all_tokens )
|
|
|
|
@staticmethod
|
|
def postscript_char_filter(name):
|
|
"""Filter out characters that are not allowed in Postscript names"""
|
|
# The name string must be restricted to the printable ASCII subset, codes 33 to 126,
|
|
# except for the 10 characters '[', ']', '(', ')', '{', '}', '<', '>', '/', '%'
|
|
out = ""
|
|
for c in name:
|
|
if c in '[](){}<>/%' or ord(c) < 33 or ord(c) > 126:
|
|
continue
|
|
out += c
|
|
return out
|
|
|
|
SIL_TABLE = [
|
|
( '(s)ource', r'\1auce' ),
|
|
( '(h)ermit', r'\1urmit' ),
|
|
( '(h)asklig', r'\1asklug' ),
|
|
( '(s)hare', r'\1hure' ),
|
|
( 'IBM[- ]?plex', r'Blex' ), # We do not keep the case here
|
|
( '(t)erminus', r'\1erminess' ),
|
|
( '(l)iberation', r'\1iteration' ),
|
|
( 'iA([- ]?)writer', r'iM\1Writing' ),
|
|
( '(a)nka/(c)oder', r'\1na\2onder' ),
|
|
( '(c)ascadia( ?)(c)ode', r'\1askaydia\2\3ove' ),
|
|
( '(c)ascadia( ?)(m)ono', r'\1askaydia\2\3ono' ),
|
|
( '(m)plus', r'\1+'), # Added this, because they use a plus symbol :->
|
|
( 'Gohufont', r'GohuFont'), # Correct to CamelCase
|
|
# Noone cares that font names starting with a digit are forbidden:
|
|
# ( '(3270)', r'Ibeam\1'),
|
|
]
|
|
|
|
@staticmethod
|
|
def is_keep_regular(basename):
|
|
"""This has been decided by the font designers, we need to mimic that (for comparison purposes)"""
|
|
KEEP_REGULAR = [
|
|
'Agave',
|
|
'Arimo',
|
|
'Aurulent',
|
|
'Cascadia',
|
|
'Cousine',
|
|
'Fantasque',
|
|
'Fira',
|
|
|
|
'Overpass',
|
|
'Lilex',
|
|
'Inconsolata$', # not InconsolataGo
|
|
'IAWriter',
|
|
'Meslo',
|
|
'Monoid',
|
|
'Mononoki',
|
|
'Hack',
|
|
'JetBrains Mono',
|
|
'Noto Sans',
|
|
'Noto Serif',
|
|
'Victor',
|
|
]
|
|
for kr in KEEP_REGULAR:
|
|
if (basename.rstrip() + '$').startswith(kr): return True
|
|
return False
|
|
|
|
@staticmethod
|
|
def _parse_simple_font_name(name):
|
|
"""Parse a filename that does not follow the 'FontFamilyName-FontStyle' pattern"""
|
|
# No dash in name, maybe we have blanc separated filename?
|
|
if ' ' in name:
|
|
return FontnameTools.parse_font_name(name.replace(' ', '-'))
|
|
# Do we have a number-name boundary?
|
|
p = re.split('(?<=[0-9])(?=[a-zA-Z])', name)
|
|
if len(p) > 1:
|
|
return FontnameTools.parse_font_name('-'.join(p))
|
|
# Or do we have CamelCase?
|
|
n = FontnameTools.camel_explode(name)
|
|
if n != name:
|
|
return FontnameTools.parse_font_name(n.replace(' ', '-'))
|
|
return (False, FontnameTools.camel_casify(name), [], [], [], '')
|
|
|
|
@staticmethod
|
|
def parse_font_name(name):
|
|
"""Expects a filename following the 'FontFamilyName-FontStyle' pattern and returns ... parts"""
|
|
name = re.sub(r'\bsemi-narrow\b', 'SemiNarrow', name, 1, re.IGNORECASE) # Just for "3270 Semi-Narrow" :-/
|
|
name = re.sub('[_\s]+', ' ', name)
|
|
matches = re.match(r'([^-]+)(?:-(.*))?', name)
|
|
familyname = FontnameTools.camel_casify(matches.group(1))
|
|
style = matches.group(2)
|
|
|
|
if not style:
|
|
return FontnameTools._parse_simple_font_name(name)
|
|
|
|
# These are the FontStyle keywords we know, in three categories
|
|
# Weights end up as Typographic Family parts ('after the dash')
|
|
# Styles end up as Family parts (for classic grouping of four)
|
|
# Others also end up in Typographic Family ('before the dash')
|
|
weights = [ 'Thin', 'Light', 'ExtraLight', 'SemiBold', 'Demi',
|
|
'SemiLight', 'Medium', 'Black', 'ExtraBold', 'Heavy',
|
|
'Oblique', 'Condensed', 'SemiCondensed', 'ExtraCondensed',
|
|
'Narrow', 'SemiNarrow', 'Retina', ]
|
|
styles = [ 'Bold', 'Italic', 'Regular', 'Normal', ]
|
|
# Some font specialities:
|
|
other = [
|
|
'-', 'Book', 'For', 'Powerline',
|
|
'Text', # Plex
|
|
'IIx', # Profont IIx
|
|
'LGC', # Inconsolata LGC
|
|
r'\(TTF\)', # Terminus (TTF)
|
|
r'\bCE\b', # ProggycleanTT CE
|
|
r'[12][cmp]n?', # MPlus
|
|
r'(?:uni-)?1[14]', # GohuFont uni
|
|
]
|
|
|
|
# Sometimes used abbreviations
|
|
weight_abbrevs = [ 'ob', 'c', 'm', 'l', ]
|
|
style_abbrevs = [ 'it', 'r', 'b', 'i', ]
|
|
|
|
( style, weight_token ) = FontnameTools.get_name_token(style, weights)
|
|
( style, style_token ) = FontnameTools.get_name_token(style, styles)
|
|
( style, other_token ) = FontnameTools.get_name_token(style, other, True)
|
|
if len(style) < 4:
|
|
( style, weight_token_abbrevs ) = FontnameTools.get_name_token(style, weight_abbrevs)
|
|
( style, style_token_abbrevs ) = FontnameTools.get_name_token(style, style_abbrevs)
|
|
weight_token += weight_token_abbrevs
|
|
style_token += style_token_abbrevs
|
|
while 'Regular' in style_token and len(style_token) > 1:
|
|
# Correct situation where "Regular" and something else is given
|
|
style_token.remove('Regular')
|
|
|
|
# Recurse to see if unmatched stuff between dashes can belong to familyname
|
|
matches2 = re.match(r'(\w+)-(.*)', style)
|
|
if matches2:
|
|
return FontnameTools.parse_font_name(familyname + matches2.group(1) + '-' + matches2.group(2))
|
|
|
|
style = re.sub(r'(^|\s)\d+(\.\d+)+(\s|$)', r'\1\3', style) # Remove (free standing) version numbers
|
|
style_parts = FontnameTools.drop_empty(style.split(' '))
|
|
style = ' '.join(map(FontnameTools.front_upper, style_parts))
|
|
familyname = FontnameTools.camel_explode(familyname)
|
|
return (True, familyname, weight_token, style_token, other_token, style)
|
|
|
|
############################################
|
|
# bin/scripts/name_parser/FontnameParser.py
|
|
#
|
|
|
|
class FontnameParser:
|
|
"""Parse a font name and generate all kinds of names"""
|
|
|
|
def __init__(self, filename):
|
|
"""Parse a font filename and store the results"""
|
|
self.parse_ok = False
|
|
self.for_windows = False
|
|
self.use_short_families = (False, False) # ( camelcase name, short styles )
|
|
self.keep_regular_in_family = None # None = auto, True, False
|
|
self.suppress_preferred_if_identical = True
|
|
self.fullname_suff = ''
|
|
self.fontname_suff = ''
|
|
self.family_suff = ''
|
|
self.name_subst = []
|
|
[ self.parse_ok, self._basename, self.weight_token, self.style_token, self.other_token, self._rest ] = FontnameTools.parse_font_name(filename)
|
|
self.basename = self._basename
|
|
self.rest = self._rest
|
|
self.add_name_substitution_table(FontnameTools.SIL_TABLE)
|
|
|
|
def _make_ps_mame(self, n):
|
|
"""Helper to limit font name length in PS names"""
|
|
if self.for_windows and len(n) > 31:
|
|
print('Shortening too long PS family name')
|
|
return n[:31]
|
|
return n
|
|
|
|
def _shortened_name(self):
|
|
"""Return a blank free basename-rest combination"""
|
|
if not self.use_short_families[0]:
|
|
return (self.basename, self.rest)
|
|
else:
|
|
return (FontnameTools.concat(self.basename, self.rest).replace(' ', ''), '')
|
|
|
|
def set_for_windows(self, for_windows):
|
|
"""Create slightly different names, suitable for Windows use"""
|
|
self.for_windows = for_windows
|
|
return self
|
|
|
|
def set_keep_regular_in_family(self, keep):
|
|
"""Familyname may contain 'Regular' where it should normally be suppressed"""
|
|
self.keep_regular_in_family = keep
|
|
|
|
def set_suppress_preferred(self, suppress):
|
|
"""Suppress ID16/17 if it is identical to ID1/2 (True is default)"""
|
|
self.suppress_preferred_if_identical = suppress
|
|
|
|
def inject_suffix(self, fullname, fontname, family):
|
|
"""Add a custom additonal string that shows up in the resulting names"""
|
|
self.fullname_suff = fullname.strip()
|
|
self.fontname_suff = fontname.replace(' ', '')
|
|
self.family_suff = family.strip()
|
|
return self
|
|
# font-patcher behavior:
|
|
# verboseSuff = "Nerd Font"
|
|
# shortSuff = win ? "NF" : "Nerd Font"
|
|
# verboseSuff += "Plus Font Awesome"
|
|
# shortSuff += "A"
|
|
# OR when complete:
|
|
# shortSuff = "Nerd Font Complete"
|
|
# verboseSuff = "Nerd Font Complete"
|
|
# AND
|
|
# shortSuff += "M"
|
|
# verboseSuff += "Mono"
|
|
#
|
|
# fullname += verboseSuff
|
|
# fontname += shortSuff
|
|
# if win familyname += "NF"
|
|
# else familyname += "Nerd Font"
|
|
# if win fullname += "Windows Compatible"
|
|
# if !win familyname += "Mono"
|
|
#
|
|
# THUS:
|
|
# fontname => shortSuff
|
|
# fullname => verboseSuff {{ we do the following already: }} + win ? "Windows Compatible" : ""
|
|
# family => win ? "NF" : "Nerd Font" + mono ? "Mono" : ""
|
|
|
|
def enable_short_families(self, camelcase_name, prefix):
|
|
"""Enable short styles in Family when (original) font name starts with prefix; enable CamelCase basename in (Typog.) Family"""
|
|
# camelcase_name is boolean
|
|
# prefix is either a string or False
|
|
if type(prefix) == str:
|
|
prefix = self._basename.startswith(prefix)
|
|
self.use_short_families = ( camelcase_name, prefix )
|
|
return self
|
|
|
|
def add_name_substitution_table(self, table):
|
|
"""Have some fonts renamed, takes list of tuples (regex, replacement)"""
|
|
# The regex will be anchored to name begin and used case insensitive
|
|
# Replacement can have regex matches, mind to catch the correct source case
|
|
self.name_subst = table
|
|
self.basename = self._basename
|
|
self.rest = self._rest
|
|
base_and_rest = self._basename + (' ' + self._rest if len(self._rest) else '')
|
|
for regex, replacement in self.name_subst:
|
|
m = re.match(regex, base_and_rest, re.IGNORECASE)
|
|
if not m:
|
|
continue
|
|
i = len(self._basename) - len(m.group(0))
|
|
if i < 0:
|
|
self.basename = m.expand(replacement)
|
|
self.rest = self._rest[-(i+1):].lstrip()
|
|
else:
|
|
self.basename = m.expand(replacement) + self._basename[len(m.group(0)):]
|
|
break
|
|
return self
|
|
|
|
def drop_for_powerline(self):
|
|
"""Remove 'for Powerline' from all names (can not be undone)"""
|
|
if 'Powerline' in self.other_token:
|
|
idx = self.other_token.index('Powerline')
|
|
self.other_token.pop(idx)
|
|
if idx > 0 and self.other_token[idx - 1] == 'For':
|
|
self.other_token.pop(idx - 1)
|
|
self._basename = re.sub(r'(\b|for\s?)?powerline\b', '', self._basename, 1, re.IGNORECASE).strip()
|
|
self.add_name_substitution_table(self.name_subst) # re-evaluate
|
|
return self
|
|
|
|
### Following the creation of the name parts:
|
|
#
|
|
# Relevant websites
|
|
# https://www.fonttutorials.com/how-to-name-font-family/
|
|
# https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
|
|
# https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fss
|
|
# https://docs.microsoft.com/en-us/typography/opentype/spec/head#macstyle
|
|
|
|
# Example (mind that they group 'semibold' as classic-group-of-4 Bold, while we will always only take bold as Bold):
|
|
# Adobe Caslon Pro Regular ID1: Adobe Caslon Pro ID2: Regular
|
|
# Adobe Caslon Pro Italic ID1: Adobe Caslon Pro ID2: Italic
|
|
# Adobe Caslon Pro Semibold ID1: Adobe Caslon Pro ID2: Bold ID16: Adobe Caslon Pro ID17: Semibold
|
|
# Adobe Caslon Pro Semibold Italic ID1: Adobe Caslon Pro ID2: Bold Italic ID16: Adobe Caslon Pro ID17: Semibold Italic
|
|
# Adobe Caslon Pro Bold ID1: Adobe Caslon Pro Bold ID2: Regular ID16: Adobe Caslon Pro ID17: Bold
|
|
# Adobe Caslon Pro Bold Italic ID1: Adobe Caslon Pro Bold ID2: Italic ID16: Adobe Caslon Pro ID17: Bold Italic
|
|
|
|
# fontname === preferred_family + preferred_styles
|
|
# fontname === family + subfamily
|
|
#
|
|
# familybase = basename + rest + other (+ suffix)
|
|
# ID 1/2 just have self.style in the subfamily, all the rest ends up in the family
|
|
# ID 16/17 have self.style and self.weight in the subfamily, the rest ends up in the family
|
|
|
|
def fullname(self):
|
|
"""Get the SFNT Fullname (ID 4)"""
|
|
if self.for_windows:
|
|
win = 'Windows Compatible'
|
|
else:
|
|
win = ''
|
|
styles = self.style_token
|
|
weights = self.weight_token
|
|
if self.keep_regular_in_family == None:
|
|
keep_regular = FontnameTools.is_keep_regular(self._basename + ' ' + self._rest)
|
|
else:
|
|
keep_regular = self.keep_regular_in_family
|
|
if ('Regular' in styles
|
|
and (not keep_regular
|
|
or len(self.weight_token) > 0)): # This is actually a malformed font name
|
|
styles = list(self.style_token)
|
|
styles.remove('Regular')
|
|
# For naming purposes we want Oblique to be part of the styles
|
|
(weights, styles) = FontnameTools.make_oblique_style(weights, styles)
|
|
return FontnameTools.concat(self.basename, self.rest, self.other_token, self.fullname_suff, win, weights, styles)
|
|
|
|
def psname(self):
|
|
"""Get the SFNT PostScriptName (ID 6)"""
|
|
# This is almost self.family() + '-' + self.subfamily() but without short styles
|
|
fam = FontnameTools.camel_casify(FontnameTools.concat(self.basename, self.rest, self.other_token, self.fontname_suff))
|
|
sub = FontnameTools.camel_casify(FontnameTools.concat(self.weight_token, self.style_token))
|
|
if len(sub) > 0:
|
|
sub = '-' + sub
|
|
out = FontnameTools.postscript_char_filter(fam + sub)
|
|
# The name string must be no longer than 63 characters
|
|
return out[:63]
|
|
|
|
def preferred_family(self):
|
|
"""Get the SFNT Preferred Familyname (ID 16)"""
|
|
if self.suppress_preferred_if_identical and len(self.weight_token) == 0:
|
|
# Do not set if identical to ID 1
|
|
return ''
|
|
(name, rest) = self._shortened_name()
|
|
return FontnameTools.concat(name, rest, self.other_token, self.family_suff)
|
|
|
|
def preferred_styles(self):
|
|
"""Get the SFNT Preferred Styles (ID 17)"""
|
|
styles = self.style_token
|
|
weights = self.weight_token
|
|
if self.suppress_preferred_if_identical and len(weights) == 0:
|
|
# Do not set if identical to ID 2
|
|
return ''
|
|
# For naming purposes we want Oblique to be part of the styles
|
|
(weights, styles) = FontnameTools.make_oblique_style(weights, styles)
|
|
return FontnameTools.concat(weights, styles)
|
|
|
|
def family(self):
|
|
"""Get the SFNT Familyname (ID 1)"""
|
|
# We use the short form of the styles to save on number of chars
|
|
(name, rest) = self._shortened_name()
|
|
other = self.other_token
|
|
weight = self.weight_token
|
|
if self.use_short_families[1]:
|
|
other = FontnameTools.short_styles(other)
|
|
weight = FontnameTools.short_styles(weight)
|
|
return FontnameTools.concat(name, rest, other, self.family_suff, weight)
|
|
|
|
def subfamily(self):
|
|
"""Get the SFNT SubFamily (ID 2)"""
|
|
if len(self.style_token) == 0:
|
|
if 'Oblique' in self.weight_token:
|
|
return FontnameTools.concat(self.style_token, 'Italic')
|
|
return 'Regular'
|
|
if 'Oblique' in self.weight_token and not 'Italic' in self.style_token:
|
|
return FontnameTools.concat(self.style_token, 'Italic')
|
|
return FontnameTools.concat(self.style_token)
|
|
|
|
def ps_familyname(self):
|
|
"""Get the PS Familyname"""
|
|
return self._make_ps_mame(self.family())
|
|
|
|
def ps_fontname(self):
|
|
"""Get the PS fontname"""
|
|
# This Adobe restriction is classically ignored
|
|
# if len(n) > 29:
|
|
# print('Shortening too long PS fontname')
|
|
# return n[:29]
|
|
return self._make_ps_mame(self.psname())
|
|
|
|
def macstyle(self, style):
|
|
"""Modify a given macStyle value for current name, just bits 0 and 1 touched"""
|
|
b = style & (~3)
|
|
b |= 1 if 'Bold' in self.style_token else 0
|
|
b |= 2 if 'Italic' in self.style_token else 0
|
|
return b
|
|
|
|
def fs_selection(self, fs):
|
|
"""Modify a given fsSelection value for current name, bits 0, 5, 6, 8, 9 touched"""
|
|
ITALIC = 1 << 0; BOLD = 1 << 5; REGULAR = 1 << 6; WWS = 1 << 8; OBLIQUE = 1 << 9
|
|
b = fs & (~(ITALIC | BOLD | REGULAR | WWS | OBLIQUE))
|
|
if 'Bold' in self.style_token:
|
|
b |= BOLD
|
|
# Ignore Italic if we have Oblique
|
|
if 'Oblique' in self.weight_token:
|
|
b |= OBLIQUE
|
|
elif 'Italic' in self.style_token:
|
|
b |= ITALIC
|
|
# Regular is just the basic weight
|
|
if len(self.weight_token) == 0:
|
|
b |= REGULAR
|
|
b |= WWS # We assert this by our naming process
|
|
return b
|
|
|
|
def rename_font(self, font):
|
|
"""Rename the font to include all information we found (font is fontforge font object)"""
|
|
font.fontname = self.ps_fontname()
|
|
font.fullname = self.fullname()
|
|
font.familyname = self.ps_familyname()
|
|
|
|
# We have to work around several issues in fontforge:
|
|
#
|
|
# a. Remove some entries from SFNT table; fontforge has no API function for that
|
|
#
|
|
# b. Fontforge does not allow to set SubFamily (and other) to any value:
|
|
#
|
|
# Fontforge lets you set any value, unless it is the default value. If it
|
|
# is the default value it does not set anything. It also does not remove
|
|
# a previously existing non-default value. Why it is done this way is
|
|
# unclear:
|
|
# fontforge/python.c SetSFNTName() line 11431
|
|
# return( 1 ); /* If they set it to the default, there's nothing to do */
|
|
#
|
|
# Then is the question: What is the default? It is taken from the
|
|
# currently set fontname (??!). The fontname is parsed and everything
|
|
# behind the dash is the default SubFamily:
|
|
# fontforge/tottf.c DefaultTTFEnglishNames()
|
|
# fontforge/splinefont.c _GetModifiers()
|
|
#
|
|
# To fix this without touching Fontforge we need to set the SubFamily
|
|
# directly in the SFNT table:
|
|
#
|
|
# c. Fontforge has the bug that it allows to write empty-string to a SFNT field
|
|
# and it is actually embedded as empty string, but empty strings are not
|
|
# shown if you query the sfnt_names *rolleyes*
|
|
|
|
sfnt_list = []
|
|
TO_DEL = ['Family', 'SubFamily', 'Fullname', 'Postscriptname', 'Preferred Family',
|
|
'Preferred Styles', 'Compatible Full', 'WWS Family', 'WWS Subfamily']
|
|
for l, k, v in list(font.sfnt_names):
|
|
if not k in TO_DEL:
|
|
sfnt_list += [( l, k, v )]
|
|
|
|
sfnt_list += [( 'English (US)', 'Family', self.family() )]
|
|
sfnt_list += [( 'English (US)', 'SubFamily', self.subfamily() )]
|
|
sfnt_list += [( 'English (US)', 'Fullname', self.fullname() )]
|
|
sfnt_list += [( 'English (US)', 'PostScriptName', self.psname() )]
|
|
|
|
p_fam = self.preferred_family()
|
|
if len(p_fam):
|
|
sfnt_list += [( 'English (US)', 'Preferred Family', p_fam )]
|
|
p_sty = self.preferred_styles()
|
|
if len(p_sty):
|
|
sfnt_list += [( 'English (US)', 'Preferred Styles', p_sty )]
|
|
|
|
font.sfnt_names = tuple(sfnt_list)
|
|
|
|
font.macstyle = self.macstyle(0)
|
|
font.os2_stylemap = self.fs_selection(0)
|
|
|
|
############################################
|
|
|
|
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):
|
|
if i + j <= end:
|
|
extra += ord(self.f.read(1))
|
|
extra = extra << 8
|
|
checksum = (checksum + extra) & 0xFFFFFFFF
|
|
return checksum
|
|
|
|
def find_head_table(self):
|
|
""" Search all tables for the HEAD table and store its metadata """
|
|
self.f.seek(4)
|
|
numtables = self.getshort()
|
|
self.f.seek(3*2, 1)
|
|
|
|
for i in range(numtables):
|
|
tab_name = self.f.read(4)
|
|
self.tab_check_offset = self.f.tell()
|
|
self.tab_check = self.getlong()
|
|
self.tab_offset = self.getlong()
|
|
self.tab_length = self.getlong()
|
|
if tab_name == b'head':
|
|
return
|
|
raise Exception('No HEAD table found')
|
|
|
|
def goto(self, where):
|
|
""" Go to a named location in the file or to the specified index """
|
|
if type(where) is str:
|
|
positions = {'checksumAdjustment': 2+2+4,
|
|
'flags': 2+2+4+4+4,
|
|
'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2,
|
|
}
|
|
where = self.tab_offset + positions[where]
|
|
self.f.seek(where)
|
|
|
|
|
|
def calc_full_checksum(self, check = False):
|
|
""" Calculate the whole file's checksum """
|
|
self.f.seek(0, 2)
|
|
self.end = self.f.tell()
|
|
full_check = self.calc_checksum(0, self.end, (-self.checksum_adj) & 0xFFFFFFFF)
|
|
if check and (0xB1B0AFBA - full_check) & 0xFFFFFFFF != self.checksum_adj:
|
|
sys.exit("Checksum of whole font is bad")
|
|
return full_check
|
|
|
|
def calc_table_checksum(self, check = False):
|
|
tab_check_new = self.calc_checksum(self.tab_offset, self.tab_offset + self.tab_length - 1, (-self.checksum_adj) & 0xFFFFFFFF)
|
|
if check and tab_check_new != self.tab_check:
|
|
sys.exit("Checksum of 'head' in font is bad")
|
|
return tab_check_new
|
|
|
|
def reset_table_checksum(self):
|
|
new_check = self.calc_table_checksum()
|
|
self.putlong(new_check, self.tab_check_offset)
|
|
|
|
def reset_full_checksum(self):
|
|
new_adj = (0xB1B0AFBA - self.calc_full_checksum()) & 0xFFFFFFFF
|
|
self.putlong(new_adj, 'checksumAdjustment')
|
|
|
|
def close(self):
|
|
self.f.close()
|
|
|
|
|
|
def __init__(self, filename):
|
|
self.modified = False
|
|
self.f = open(filename, 'r+b')
|
|
|
|
self.find_head_table()
|
|
|
|
self.flags = self.getshort('flags')
|
|
self.lowppem = self.getshort('lowestRecPPEM')
|
|
self.checksum_adj = self.getlong('checksumAdjustment')
|
|
|
|
|
|
class font_patcher:
|
|
def __init__(self):
|
|
self.args = None # class 'argparse.Namespace'
|
|
self.sym_font_args = []
|
|
self.config = None # class 'configparser.ConfigParser'
|
|
self.sourceFont = None # class 'fontforge.font'
|
|
self.octiconsExactEncodingPosition = True
|
|
self.patch_set = None # class 'list'
|
|
self.font_dim = None # class 'dict'
|
|
self.onlybitmaps = 0
|
|
self.extension = ""
|
|
self.setup_arguments()
|
|
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
|
|
if not os.path.isfile(self.args.font):
|
|
sys.exit("{}: Font file does not exist: {}".format(projectName, self.args.font))
|
|
if not os.access(self.args.font, os.R_OK):
|
|
sys.exit("{}: Can not open font file for reading: {}".format(projectName, self.args.font))
|
|
if len(fontforge.fontsInFile(self.args.font)) > 1:
|
|
sys.exit("{}: Font file contains {} fonts, can only handle single font files".format(projectName,
|
|
len(fontforge.fontsInFile(self.args.font))))
|
|
try:
|
|
self.sourceFont = fontforge.open(self.args.font, 1) # 1 = ("fstypepermitted",))
|
|
except Exception:
|
|
sys.exit(projectName + ": Can not open font, try to open with fontforge interactively to get more information")
|
|
self.setup_font_names()
|
|
self.remove_ligatures()
|
|
make_sure_path_exists(self.args.outputdir)
|
|
self.check_position_conflicts()
|
|
self.setup_patch_set()
|
|
self.setup_line_dimensions()
|
|
self.get_sourcefont_dimensions()
|
|
self.sourceFont.encoding = 'UnicodeFull' # Update the font encoding to ensure that the Unicode glyphs are available
|
|
self.onlybitmaps = self.sourceFont.onlybitmaps # Fetch this property before adding outlines. NOTE self.onlybitmaps initialized and never used
|
|
if self.args.extension == "":
|
|
self.extension = os.path.splitext(self.args.font)[1]
|
|
else:
|
|
self.extension = '.' + self.args.extension
|
|
if re.match("\.ttc$", self.extension, re.IGNORECASE):
|
|
sys.exit(projectName + ": Can not create True Type Collections")
|
|
|
|
|
|
def patch(self):
|
|
|
|
print("{} Patcher v{} executing\n".format(projectName, version))
|
|
|
|
if self.args.single:
|
|
# Force width to be equal on all glyphs to ensure the font is considered monospaced on Windows.
|
|
# This needs to be done on all characters, as some information seems to be lost from the original font file.
|
|
self.set_sourcefont_glyph_widths()
|
|
# For some Windows applications (e.g. 'cmd') that is not enough. But they seem to honour the Panose table
|
|
# https://forum.high-logic.com/postedfiles/Panose.pdf
|
|
panose = list(self.sourceFont.os2_panose)
|
|
if panose[0] == 0 or panose[0] == 2: # 0 (1st value) = family kind; 0 = any (default); 2 = latin text and display
|
|
panose[0] = 2 # Assert kind
|
|
panose[3] = 9 # 3 (4th value) = propotion; 9 = monospaced
|
|
self.sourceFont.os2_panose = tuple(panose)
|
|
|
|
# Prevent opening and closing the fontforge font. Makes things faster when patching
|
|
# multiple ranges using the same symbol font.
|
|
PreviousSymbolFilename = ""
|
|
symfont = None
|
|
|
|
for patch in self.patch_set:
|
|
if patch['Enabled']:
|
|
if PreviousSymbolFilename != patch['Filename']:
|
|
# We have a new symbol font, so close the previous one if it exists
|
|
if symfont:
|
|
symfont.close()
|
|
symfont = None
|
|
symfont = fontforge.open(self.args.glyphdir + patch['Filename'])
|
|
|
|
# Match the symbol font size to the source font size
|
|
symfont.em = self.sourceFont.em
|
|
PreviousSymbolFilename = patch['Filename']
|
|
|
|
# If patch table doesn't include a source start and end, re-use the symbol font values
|
|
SrcStart = patch['SrcStart']
|
|
SrcEnd = patch['SrcEnd']
|
|
if not SrcStart:
|
|
SrcStart = patch['SymStart']
|
|
if not SrcEnd:
|
|
SrcEnd = patch['SymEnd']
|
|
self.copy_glyphs(SrcStart, SrcEnd, symfont, patch['SymStart'], patch['SymEnd'], patch['Exact'], patch['ScaleGlyph'], patch['Name'], patch['Attributes'])
|
|
|
|
if symfont:
|
|
symfont.close()
|
|
print("\nDone with Patch Sets, generating font...")
|
|
|
|
# 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"
|
|
|
|
# the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'.
|
|
if self.sourceFont.fullname != None:
|
|
outfile = self.args.outputdir + "/" + self.sourceFont.fullname + self.extension
|
|
self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments')))
|
|
message = "\nGenerated: {} in '{}'".format(self.sourceFont.fontname, outfile)
|
|
else:
|
|
outfile = self.args.outputdir + "/" + self.sourceFont.cidfontname + self.extension
|
|
self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments')))
|
|
message = "\nGenerated: {} in '{}'".format(self.sourceFont.fullname, outfile)
|
|
|
|
# Adjust flags that can not be changed via fontforge
|
|
try:
|
|
source_font = TableHEADWriter(self.args.font)
|
|
dest_font = TableHEADWriter(outfile)
|
|
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
|
|
print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08))
|
|
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
|
|
if source_font.lowppem != dest_font.lowppem:
|
|
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
|
|
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
|
|
if dest_font.modified:
|
|
dest_font.reset_table_checksum()
|
|
dest_font.reset_full_checksum()
|
|
except Exception as error:
|
|
print("Can not handle font flags ({})".format(repr(error)))
|
|
finally:
|
|
try:
|
|
source_font.close()
|
|
dest_font.close()
|
|
except:
|
|
pass
|
|
print(message)
|
|
|
|
if self.args.postprocess:
|
|
subprocess.call([self.args.postprocess, outfile])
|
|
print("\nPost Processed: {}".format(outfile))
|
|
|
|
|
|
def setup_arguments(self):
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
'Nerd Fonts Font Patcher: patches a given font with programming and development related glyphs\n\n'
|
|
'* Website: https://www.nerdfonts.com\n'
|
|
'* Version: ' + version + '\n'
|
|
'* Development Website: https://github.com/ryanoasis/nerd-fonts\n'
|
|
'* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/master/changelog.md'),
|
|
formatter_class=RawTextHelpFormatter
|
|
)
|
|
|
|
# optional arguments
|
|
parser.add_argument('font', help='The path to the font to patch (e.g., Inconsolata.otf)')
|
|
parser.add_argument('-v', '--version', action='version', version=projectName + ": %(prog)s (" + version + ")")
|
|
parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='store_true', help='Whether to generate the glyphs as single-width not double-width (default is double-width)')
|
|
parser.add_argument('-l', '--adjust-line-height', dest='adjustLineHeight', default=False, action='store_true', help='Whether to adjust line heights (attempt to center powerline separators more evenly)')
|
|
parser.add_argument('-q', '--quiet', '--shutup', dest='quiet', default=False, action='store_true', help='Do not generate verbose output')
|
|
parser.add_argument('-w', '--windows', dest='windows', default=False, action='store_true', help='Limit the internal font name to 31 characters (for Windows compatibility)')
|
|
parser.add_argument('-c', '--complete', dest='complete', default=False, action='store_true', help='Add all available Glyphs')
|
|
parser.add_argument('--careful', dest='careful', default=False, action='store_true', help='Do not overwrite existing glyphs if detected')
|
|
parser.add_argument('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specificed in JSON configuration file')
|
|
parser.add_argument('--postprocess', dest='postprocess', default=False, type=str, nargs='?', help='Specify a Script for Post Processing')
|
|
parser.add_argument('--configfile', dest='configfile', default=False, type=str, nargs='?', help='Specify a file path for JSON configuration file (see sample: src/config.sample.json)')
|
|
parser.add_argument('--custom', dest='custom', default=False, type=str, nargs='?', help='Specify a custom symbol font. All new glyphs will be copied, with no scaling applied.')
|
|
parser.add_argument('-ext', '--extension', dest='extension', default="", type=str, nargs='?', help='Change font file type to create (e.g., ttf, otf)')
|
|
parser.add_argument('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to')
|
|
parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching')
|
|
parser.add_argument('--parser', dest='parser', default=False, action='store_true', help='Use alternative method to name patched fonts (experimental)')
|
|
|
|
# progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
|
|
progressbars_group_parser = parser.add_mutually_exclusive_group(required=False)
|
|
progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set')
|
|
progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set')
|
|
parser.set_defaults(progressbars=True)
|
|
|
|
# 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('--fontlinux', '--fontlogos', dest='fontlinux', default=False, action='store_true', help='Add Font Linux and other open source 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)')
|
|
|
|
self.args = parser.parse_args()
|
|
|
|
# if you add a new font, set it to True here inside the if condition
|
|
if self.args.complete:
|
|
self.args.fontawesome = True
|
|
self.args.fontawesomeextension = True
|
|
self.args.fontlinux = True
|
|
self.args.octicons = True
|
|
self.args.codicons = True
|
|
self.args.powersymbols = True
|
|
self.args.pomicons = True
|
|
self.args.powerline = True
|
|
self.args.powerlineextra = True
|
|
self.args.material = True
|
|
self.args.weather = True
|
|
|
|
if not self.args.complete:
|
|
# add the list of arguments for each symbol font to the list self.sym_font_args
|
|
for action in sym_font_group._group_actions:
|
|
self.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 self.sym_font_args:
|
|
found = False
|
|
for alias in sym_font_arg_aliases:
|
|
if alias in sys.argv:
|
|
found = True
|
|
if found is not True:
|
|
font_complete = False
|
|
self.args.complete = font_complete
|
|
|
|
# this one also works but it needs to be updated every time a font is added
|
|
# it was a conditional in self.setup_font_names() before, but it was missing
|
|
# a symbol font, so it would name the font complete without being so sometimes.
|
|
# that's why i did the above.
|
|
#
|
|
# if you add a new font, put it in here too, as the others are
|
|
# self.args.complete = all([
|
|
# self.args.fontawesome is True,
|
|
# self.args.fontawesomeextension is True,
|
|
# self.args.fontlinux is True,
|
|
# self.args.octicons is True,
|
|
# self.args.powersymbols is True,
|
|
# self.args.pomicons is True,
|
|
# self.args.powerline is True,
|
|
# self.args.powerlineextra is True,
|
|
# self.args.material is True,
|
|
# self.args.weather is True
|
|
# ])
|
|
|
|
|
|
def setup_font_names(self):
|
|
verboseAdditionalFontNameSuffix = " " + projectNameSingular
|
|
if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later
|
|
additionalFontNameSuffix = " " + projectNameAbbreviation
|
|
else:
|
|
additionalFontNameSuffix = verboseAdditionalFontNameSuffix
|
|
if not self.args.complete:
|
|
# NOTE not all symbol fonts have appended their suffix here
|
|
if self.args.fontawesome:
|
|
additionalFontNameSuffix += " A"
|
|
verboseAdditionalFontNameSuffix += " Plus Font Awesome"
|
|
if self.args.fontawesomeextension:
|
|
additionalFontNameSuffix += " AE"
|
|
verboseAdditionalFontNameSuffix += " Plus Font Awesome Extension"
|
|
if self.args.octicons:
|
|
additionalFontNameSuffix += " O"
|
|
verboseAdditionalFontNameSuffix += " Plus Octicons"
|
|
if self.args.powersymbols:
|
|
additionalFontNameSuffix += " PS"
|
|
verboseAdditionalFontNameSuffix += " Plus Power Symbols"
|
|
if self.args.codicons:
|
|
additionalFontNameSuffix += " C"
|
|
verboseAdditionalFontNameSuffix += " Plus Codicons"
|
|
if self.args.pomicons:
|
|
additionalFontNameSuffix += " P"
|
|
verboseAdditionalFontNameSuffix += " Plus Pomicons"
|
|
if self.args.fontlinux:
|
|
additionalFontNameSuffix += " L"
|
|
verboseAdditionalFontNameSuffix += " Plus Font Logos (Font Linux)"
|
|
if self.args.material:
|
|
additionalFontNameSuffix += " MDI"
|
|
verboseAdditionalFontNameSuffix += " Plus Material Design Icons"
|
|
if self.args.weather:
|
|
additionalFontNameSuffix += " WEA"
|
|
verboseAdditionalFontNameSuffix += " Plus Weather Icons"
|
|
|
|
# if all source glyphs included simplify the name
|
|
else:
|
|
additionalFontNameSuffix = " " + projectNameSingular + " Complete"
|
|
verboseAdditionalFontNameSuffix = " " + projectNameSingular + " Complete"
|
|
|
|
# add mono signifier to end of name
|
|
if self.args.single:
|
|
additionalFontNameSuffix += " M"
|
|
verboseAdditionalFontNameSuffix += " Mono"
|
|
|
|
if self.args.parser:
|
|
use_fullname = type(self.sourceFont.fullname) == str # Usually the fullname is better to parse
|
|
# Use fullname if it is 'equal' to the fontname
|
|
if self.sourceFont.fullname:
|
|
use_fullname |= self.sourceFont.fontname.lower() == FontnameTools.postscript_char_filter(self.sourceFont.fullname).lower()
|
|
# Use fullname for any of these source fonts (that are impossible to disentangle from the fontname, we need the blanks)
|
|
for hit in [ 'Meslo' ]:
|
|
use_fullname |= self.sourceFont.fontname.lower().startswith(hit.lower())
|
|
parser_name = self.sourceFont.fullname if use_fullname else self.sourceFont.fontname
|
|
# Gohu fontnames hide the weight, but the file names are ok...
|
|
if parser_name.startswith('Gohu'):
|
|
parser_name = os.path.splitext(os.path.basename(self.args.font))[0]
|
|
n = FontnameParser(parser_name)
|
|
if not n.parse_ok:
|
|
print("Have only minimal naming information, check resulting name. Maybe omit --parser option")
|
|
n.drop_for_powerline()
|
|
n.enable_short_families(True, "Noto")
|
|
n.set_for_windows(self.args.windows)
|
|
|
|
# All the following stuff is ignored in parser-mode
|
|
|
|
# basically split the font name around the dash "-" to get the fontname and the style (e.g. Bold)
|
|
# this does not seem very reliable so only use the style here as a fallback if the font does not
|
|
# have an internal style defined (in sfnt_names)
|
|
# using '([^-]*?)' to get the item before the first dash "-"
|
|
# using '([^-]*(?!.*-))' to get the item after the last dash "-"
|
|
fontname, fallbackStyle = re.match("^([^-]*).*?([^-]*(?!.*-))$", self.sourceFont.fontname).groups()
|
|
|
|
# dont trust 'sourceFont.familyname'
|
|
familyname = fontname
|
|
|
|
# fullname (filename) can always use long/verbose font name, even in windows
|
|
if self.sourceFont.fullname != None:
|
|
fullname = self.sourceFont.fullname + verboseAdditionalFontNameSuffix
|
|
else:
|
|
fullname = self.sourceFont.cidfontname + verboseAdditionalFontNameSuffix
|
|
|
|
fontname = fontname + additionalFontNameSuffix.replace(" ", "")
|
|
|
|
# let us try to get the 'style' from the font info in sfnt_names and fallback to the
|
|
# parse fontname if it fails:
|
|
try:
|
|
# search tuple:
|
|
subFamilyTupleIndex = [x[1] for x in self.sourceFont.sfnt_names].index("SubFamily")
|
|
|
|
# String ID is at the second index in the Tuple lists
|
|
sfntNamesStringIDIndex = 2
|
|
|
|
# now we have the correct item:
|
|
subFamily = self.sourceFont.sfnt_names[subFamilyTupleIndex][sfntNamesStringIDIndex]
|
|
except IndexError:
|
|
sys.stderr.write("{}: Could not find 'SubFamily' for given font, falling back to parsed fontname\n".format(projectName))
|
|
subFamily = fallbackStyle
|
|
|
|
# some fonts have inaccurate 'SubFamily', if it is Regular let us trust the filename more:
|
|
if subFamily == "Regular":
|
|
subFamily = fallbackStyle
|
|
|
|
# This is meant to cover the case where the SubFamily is "Italic" and the filename is *-BoldItalic.
|
|
if len(subFamily) < len(fallbackStyle):
|
|
subFamily = fallbackStyle
|
|
|
|
if self.args.windows:
|
|
maxFamilyLength = 31
|
|
maxFontLength = maxFamilyLength - len('-' + subFamily)
|
|
familyname += " " + projectNameAbbreviation
|
|
fullname += " Windows Compatible"
|
|
|
|
# now make sure less than 32 characters name length
|
|
if len(fontname) > maxFontLength:
|
|
fontname = fontname[:maxFontLength]
|
|
if len(familyname) > maxFamilyLength:
|
|
familyname = familyname[:maxFamilyLength]
|
|
else:
|
|
familyname += " " + projectNameSingular
|
|
if self.args.single:
|
|
familyname += " Mono"
|
|
|
|
# Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with
|
|
# the same name as the same font, even if subFamily is different.
|
|
fontname += '-' + subFamily
|
|
|
|
# rename font
|
|
#
|
|
# comply with SIL Open Font License (OFL)
|
|
reservedFontNameReplacements = {
|
|
'source' : 'sauce',
|
|
'Source' : 'Sauce',
|
|
'hermit' : 'hurmit',
|
|
'Hermit' : 'Hurmit',
|
|
'hasklig' : 'hasklug',
|
|
'Hasklig' : 'Hasklug',
|
|
'Share' : 'Shure',
|
|
'share' : 'shure',
|
|
'IBMPlex' : 'Blex',
|
|
'ibmplex' : 'blex',
|
|
'IBM-Plex' : 'Blex',
|
|
'IBM Plex' : 'Blex',
|
|
'terminus' : 'terminess',
|
|
'Terminus' : 'Terminess',
|
|
'liberation' : 'literation',
|
|
'Liberation' : 'Literation',
|
|
'iAWriter' : 'iMWriting',
|
|
'iA Writer' : 'iM Writing',
|
|
'iA-Writer' : 'iM-Writing',
|
|
'Anka/Coder' : 'AnaConder',
|
|
'anka/coder' : 'anaconder',
|
|
'Cascadia Code' : 'Caskaydia Cove',
|
|
'cascadia code' : 'caskaydia cove',
|
|
'CascadiaCode' : 'CaskaydiaCove',
|
|
'cascadiacode' : 'caskaydiacove',
|
|
'Cascadia Mono' : 'Caskaydia Mono',
|
|
'cascadia mono' : 'caskaydia mono',
|
|
'CascadiaMono' : 'CaskaydiaMono',
|
|
'cascadiamono' : 'caskaydiamono',
|
|
'Fira Mono' : 'Fura Mono',
|
|
'Fira Sans' : 'Fura Sans',
|
|
'FiraMono' : 'FuraMono',
|
|
'FiraSans' : 'FuraSans',
|
|
'fira mono' : 'fura mono',
|
|
'fira sans' : 'fura sans',
|
|
'firamono' : 'furamono',
|
|
'firasans' : 'furasans',
|
|
}
|
|
|
|
# remove overly verbose font names
|
|
# particularly regarding Powerline sourced Fonts (https://github.com/powerline/fonts)
|
|
additionalFontNameReplacements = {
|
|
'for Powerline': '',
|
|
'ForPowerline': ''
|
|
}
|
|
|
|
additionalFontNameReplacements2 = {
|
|
'Powerline': ''
|
|
}
|
|
|
|
projectInfo = (
|
|
"Patched with '" + projectName + " Patcher' (https://github.com/ryanoasis/nerd-fonts)\n\n"
|
|
"* Website: https://www.nerdfonts.com\n"
|
|
"* Version: " + version + "\n"
|
|
"* Development Website: https://github.com/ryanoasis/nerd-fonts\n"
|
|
"* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/master/changelog.md"
|
|
)
|
|
|
|
familyname = replace_font_name(familyname, reservedFontNameReplacements)
|
|
fullname = replace_font_name(fullname, reservedFontNameReplacements)
|
|
fontname = replace_font_name(fontname, reservedFontNameReplacements)
|
|
familyname = replace_font_name(familyname, additionalFontNameReplacements)
|
|
fullname = replace_font_name(fullname, additionalFontNameReplacements)
|
|
fontname = replace_font_name(fontname, additionalFontNameReplacements)
|
|
familyname = replace_font_name(familyname, additionalFontNameReplacements2)
|
|
fullname = replace_font_name(fullname, additionalFontNameReplacements2)
|
|
fontname = replace_font_name(fontname, additionalFontNameReplacements2)
|
|
|
|
if not self.args.parser:
|
|
# replace any extra whitespace characters:
|
|
self.sourceFont.familyname = " ".join(familyname.split())
|
|
self.sourceFont.fullname = " ".join(fullname.split())
|
|
self.sourceFont.fontname = " ".join(fontname.split())
|
|
|
|
self.sourceFont.appendSFNTName(str('English (US)'), str('Preferred Family'), self.sourceFont.familyname)
|
|
self.sourceFont.appendSFNTName(str('English (US)'), str('Family'), self.sourceFont.familyname)
|
|
self.sourceFont.appendSFNTName(str('English (US)'), str('Compatible Full'), self.sourceFont.fullname)
|
|
self.sourceFont.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily)
|
|
else:
|
|
fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation
|
|
fam_suffix += ' Mono' if self.args.single else ''
|
|
n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix)
|
|
n.rename_font(self.sourceFont)
|
|
|
|
self.sourceFont.comment = projectInfo
|
|
self.sourceFont.fontlog = projectInfo
|
|
|
|
# print("Version was {}".format(sourceFont.version))
|
|
if self.sourceFont.version != None:
|
|
self.sourceFont.version += ";" + projectName + " " + version
|
|
else:
|
|
self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version
|
|
self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge
|
|
self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version)
|
|
# print("Version now is {}".format(sourceFont.version))
|
|
|
|
|
|
def remove_ligatures(self):
|
|
# let's deal with ligatures (mostly for monospaced fonts)
|
|
if self.args.configfile and self.config.read(self.args.configfile):
|
|
if self.args.removeligatures:
|
|
print("Removing ligatures from configfile `Subtables` section")
|
|
ligature_subtables = json.loads(self.config.get("Subtables", "ligatures"))
|
|
for subtable in ligature_subtables:
|
|
print("Removing subtable:", subtable)
|
|
try:
|
|
self.sourceFont.removeLookupSubtable(subtable)
|
|
print("Successfully removed subtable:", subtable)
|
|
except Exception:
|
|
print("Failed to remove subtable:", subtable)
|
|
elif self.args.removeligatures:
|
|
print("Unable to read configfile, unable to remove ligatures")
|
|
else:
|
|
print("No configfile given, skipping configfile related actions")
|
|
|
|
|
|
def check_position_conflicts(self):
|
|
# Prevent glyph encoding position conflicts between glyph sets
|
|
if self.args.fontawesome and self.args.octicons:
|
|
self.octiconsExactEncodingPosition = False
|
|
|
|
|
|
def setup_patch_set(self):
|
|
""" Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """
|
|
# Supported params: overlap | careful
|
|
# Powerline dividers
|
|
SYM_ATTR_POWERLINE = {
|
|
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': ''},
|
|
|
|
# Arrow tips
|
|
0xe0b0: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0b1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0b2: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0b3: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
|
|
# Rounded arcs
|
|
0xe0b4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
|
|
0xe0b5: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
|
|
0xe0b6: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
|
|
0xe0b7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
|
|
|
|
# Bottom Triangles
|
|
0xe0b8: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0b9: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0ba: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0bb: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
|
|
# Top Triangles
|
|
0xe0bc: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0bd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0be: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0bf: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
|
|
# Flames
|
|
0xe0c0: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
|
|
0xe0c1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
|
|
0xe0c2: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
|
|
0xe0c3: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
|
|
|
|
# Small squares
|
|
0xe0c4: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': ''},
|
|
0xe0c5: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': ''},
|
|
|
|
# Bigger squares
|
|
0xe0c6: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': ''},
|
|
0xe0c7: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': ''},
|
|
|
|
# Waveform
|
|
0xe0c8: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.01}},
|
|
|
|
# Hexagons
|
|
0xe0cc: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': ''},
|
|
0xe0cd: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': ''},
|
|
|
|
# Legos
|
|
0xe0ce: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': ''},
|
|
0xe0cf: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': ''},
|
|
0xe0d1: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
|
|
# Top and bottom trapezoid
|
|
0xe0d2: {'align': 'l', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}},
|
|
0xe0d4: {'align': 'r', 'valign': 'c', 'stretch': 'xy', 'params': {'overlap': 0.02}}
|
|
}
|
|
|
|
SYM_ATTR_DEFAULT = {
|
|
# 'pa' == preserve aspect ratio
|
|
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': ''}
|
|
}
|
|
|
|
SYM_ATTR_FONTA = {
|
|
# 'pa' == preserve aspect ratio
|
|
'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': ''},
|
|
|
|
# Don't center these arrows vertically
|
|
0xf0dc: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': ''},
|
|
0xf0dd: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': ''},
|
|
0xf0de: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': ''}
|
|
}
|
|
|
|
CUSTOM_ATTR = {
|
|
# 'pa' == preserve aspect ratio
|
|
'default': {'align': 'c', 'valign': '', 'stretch': '', 'params': ''}
|
|
}
|
|
|
|
# Most glyphs we want to maximize during the scale. However, there are some
|
|
# that need to be small or stay relative in size to each other.
|
|
# The following list are those glyphs. A tuple represents a range.
|
|
DEVI_SCALE_LIST = {'ScaleGlyph': 0xE60E, # Android logo
|
|
'GlyphsToScale': [
|
|
(0xe6bd, 0xe6c3) # very small things
|
|
]}
|
|
FONTA_SCALE_LIST = {'GlyphsToScale': [
|
|
[0xf005, 0xf006, 0xf089], # star, star empty, half star
|
|
range(0xf026, 0xf028 + 1), # volume off, down, up
|
|
range(0xf02b, 0xf02c + 1), # tag, tags
|
|
range(0xf031, 0xf035 + 1), # font et al
|
|
range(0xf044, 0xf046 + 1), # edit, share, check (boxes)
|
|
range(0xf048, 0xf052 + 1), # multimedia buttons
|
|
range(0xf060, 0xf063 + 1), # arrows
|
|
[0xf053, 0xf054, 0xf077, 0xf078], # chevron all directions
|
|
range(0xf07d, 0xf07e + 1), # resize
|
|
[0xf0d7, 0xf0da, 0xf0dc, 0xf0fe], # caret all directions and same looking sort
|
|
range(0xf100, 0xf107 + 1), # angle
|
|
range(0xf141, 0xf142 + 1), # ellipsis
|
|
range(0xf153, 0xf15a + 1), # currencies
|
|
range(0xf175, 0xf178 + 1), # long arrows
|
|
range(0xf182, 0xf183 + 1), # male and female
|
|
range(0xf221, 0xf22d + 1), # gender or so
|
|
range(0xf255, 0xf25b + 1), # hand symbols
|
|
]}
|
|
OCTI_SCALE_LIST = {'ScaleGlyph': 0xF02E, # looking glass (probably biggest glyph?)
|
|
'GlyphsToScale': [
|
|
(0xf03d, 0xf040), # arrows
|
|
0xf044, 0xf05a, 0xf05b, 0xf0aa, # triangles
|
|
(0xf051, 0xf053), # small stuff
|
|
0xf071, 0xf09f, 0xf0a0, 0xf0a1, # small arrows
|
|
0xf078, 0xf0a2, 0xf0a3, 0xf0a4, # chevrons
|
|
0xf0ca, # dash
|
|
]}
|
|
|
|
# Define the character ranges
|
|
# Symbol font ranges
|
|
self.patch_set = [
|
|
{'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE531, 'SrcStart': 0xE5FA, 'SrcEnd': 0xE631, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
|
|
{'Enabled': True, 'Name': "Devicons", 'Filename': "devicons.ttf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE6C5, 'SrcStart': 0xE700, 'SrcEnd': 0xE7C5, 'ScaleGlyph': DEVI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
|
|
{'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0A0, 'SymEnd': 0xE0A2, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
|
|
{'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0B0, 'SymEnd': 0xE0B3, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
|
|
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0A3, 'SymEnd': 0xE0A3, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
|
|
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
|
|
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
|
|
{'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D4, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_POWERLINE},
|
|
{'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
|
|
{'Enabled': self.args.fontawesome, 'Name': "Font Awesome", 'Filename': "font-awesome/FontAwesome.otf", 'Exact': True, 'SymStart': 0xF000, 'SymEnd': 0xF2E0, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': FONTA_SCALE_LIST, 'Attributes': SYM_ATTR_FONTA},
|
|
{'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'SrcEnd': 0xE2A9, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize
|
|
{'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x23FB, 'SymEnd': 0x23FE, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, # Power, Power On/Off, Power On, Sleep
|
|
{'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off)
|
|
{'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'SrcEnd': 0xFD46, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
|
|
{'Enabled': self.args.weather, 'Name': "Weather Icons", 'Filename': "weather-icons/weathericons-regular-webfont.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF0EB, 'SrcStart': 0xE300, 'SrcEnd': 0xE3EB, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
|
|
{'Enabled': self.args.fontlinux, 'Name': "Font Logos (Font Linux)", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'SrcEnd': None , 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
|
|
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': self.octiconsExactEncodingPosition, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'SrcEnd': 0xF505, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass
|
|
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': self.octiconsExactEncodingPosition, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart
|
|
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': self.octiconsExactEncodingPosition, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap
|
|
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': self.octiconsExactEncodingPosition, 'SymStart': 0xF27C, 'SymEnd': 0xF27C, 'SrcStart': 0xF4A9, 'SrcEnd': 0xF4A9, 'ScaleGlyph': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Desktop
|
|
{'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'SrcEnd': None, 'ScaleGlyph': None, 'Attributes': SYM_ATTR_DEFAULT},
|
|
{'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': 0x0000, 'SrcEnd': 0x0000, 'ScaleGlyph': None, 'Attributes': CUSTOM_ATTR}
|
|
]
|
|
|
|
def setup_line_dimensions(self):
|
|
# win_ascent and win_descent are used to set the line height for windows fonts.
|
|
# hhead_ascent and hhead_descent are used to set the line height for mac fonts.
|
|
#
|
|
# Make the total line size even. This seems to make the powerline separators
|
|
# center more evenly.
|
|
if self.args.adjustLineHeight:
|
|
if (self.sourceFont.os2_winascent + self.sourceFont.os2_windescent) % 2 != 0:
|
|
self.sourceFont.os2_winascent += 1
|
|
|
|
# Make the line size identical for windows and mac
|
|
# ! This is broken because hhea* is changed but os2_typo* is not
|
|
# ! On the other hand we need intact (i.e. original) typo values
|
|
# ! in get_sourcefont_dimensions() @TODO FIXME
|
|
self.sourceFont.hhea_ascent = self.sourceFont.os2_winascent
|
|
self.sourceFont.hhea_descent = -self.sourceFont.os2_windescent
|
|
|
|
# Line gap add extra space on the bottom of the line which
|
|
# doesn't allow the powerline glyphs to fill the entire line.
|
|
self.sourceFont.hhea_linegap = 0
|
|
self.sourceFont.os2_typolinegap = 0
|
|
|
|
|
|
def get_sourcefont_dimensions(self):
|
|
# Initial font dimensions
|
|
self.font_dim = {
|
|
'xmin' : 0,
|
|
'ymin' : -self.sourceFont.os2_windescent,
|
|
'xmax' : 0,
|
|
'ymax' : self.sourceFont.os2_winascent,
|
|
'width' : 0,
|
|
'height': 0,
|
|
}
|
|
if self.sourceFont.os2_use_typo_metrics:
|
|
self.font_dim['ymin'] = self.sourceFont.os2_typodescent
|
|
self.font_dim['ymax'] = self.sourceFont.os2_typoascent
|
|
|
|
# Find the biggest char width
|
|
# Ignore the y-values, os2_winXXXXX values set above are used for line height
|
|
#
|
|
# 0x00-0x17f is the Latin Extended-A range
|
|
for glyph in range(0x00, 0x17f):
|
|
if glyph in range(0x7F, 0xBF):
|
|
continue # ignore special characters like '1/4' etc
|
|
try:
|
|
(_, _, xmax, _) = self.sourceFont[glyph].boundingBox()
|
|
except TypeError:
|
|
continue
|
|
if self.font_dim['width'] < self.sourceFont[glyph].width:
|
|
self.font_dim['width'] = self.sourceFont[glyph].width
|
|
if xmax > self.font_dim['xmax']:
|
|
self.font_dim['xmax'] = xmax
|
|
|
|
# Calculate font height
|
|
if self.font_dim['height'] == 0:
|
|
# This can only happen if the input font is empty
|
|
# Assume we are using our prepared templates
|
|
self.font_dim = {
|
|
'xmin' : 0,
|
|
'ymin' : -self.sourceFont.descent,
|
|
'xmax' : self.sourceFont.em,
|
|
'ymax' : self.sourceFont.ascent,
|
|
'width' : self.sourceFont.em,
|
|
'height': 0,
|
|
}
|
|
self.font_dim['height'] = abs(self.font_dim['ymin']) + self.font_dim['ymax']
|
|
|
|
|
|
def get_scale_factor(self, sym_dim):
|
|
scale_ratio = 1
|
|
|
|
# We want to preserve x/y aspect ratio, so find biggest scale factor that allows symbol to fit
|
|
scale_ratio_x = self.font_dim['width'] / sym_dim['width']
|
|
|
|
# font_dim['height'] represents total line height, keep our symbols sized based upon font's em
|
|
# NOTE: is this comment correct? font_dim['height'] isn't used here
|
|
scale_ratio_y = self.sourceFont.em / sym_dim['height']
|
|
if scale_ratio_x > scale_ratio_y:
|
|
scale_ratio = scale_ratio_y
|
|
else:
|
|
scale_ratio = scale_ratio_x
|
|
return scale_ratio
|
|
|
|
|
|
def copy_glyphs(self, sourceFontStart, sourceFontEnd, symbolFont, symbolFontStart, symbolFontEnd, exactEncoding, scaleGlyph, setName, attributes):
|
|
""" Copies symbol glyphs into self.sourceFont """
|
|
progressText = ''
|
|
careful = False
|
|
glyphSetLength = 0
|
|
|
|
if self.args.careful:
|
|
careful = True
|
|
|
|
if exactEncoding is False:
|
|
sourceFontList = list(range(sourceFontStart, sourceFontEnd + 1))
|
|
sourceFontCounter = 0
|
|
|
|
# Create glyphs from symbol font
|
|
#
|
|
# If we are going to copy all Glyphs, then assume we want to be careful
|
|
# and only copy those that are not already contained in the source font
|
|
if symbolFontStart == 0:
|
|
symbolFont.selection.all()
|
|
careful = True
|
|
else:
|
|
symbolFont.selection.select((str("ranges"), str("unicode")), symbolFontStart, symbolFontEnd)
|
|
|
|
# Get number of selected non-empty glyphs
|
|
symbolFontSelection = list(symbolFont.selection.byGlyphs)
|
|
glyphSetLength = len(symbolFontSelection)
|
|
|
|
if self.args.quiet is False:
|
|
sys.stdout.write("Adding " + str(max(1, glyphSetLength)) + " Glyphs from " + setName + " Set \n")
|
|
|
|
for index, sym_glyph in enumerate(symbolFontSelection):
|
|
index = max(1, index)
|
|
|
|
try:
|
|
sym_attr = attributes[sym_glyph.unicode]
|
|
except KeyError:
|
|
sym_attr = attributes['default']
|
|
|
|
if exactEncoding:
|
|
# use the exact same hex values for the source font as for the symbol font
|
|
currentSourceFontGlyph = sym_glyph.encoding
|
|
else:
|
|
# use source font defined hex values based on passed in start and end
|
|
currentSourceFontGlyph = sourceFontList[sourceFontCounter]
|
|
sourceFontCounter += 1
|
|
|
|
if self.args.quiet is False:
|
|
if self.args.progressbars:
|
|
update_progress(round(float(index + 1) / glyphSetLength, 2))
|
|
else:
|
|
progressText = "\nUpdating glyph: {} {} putting at: {:X}".format(sym_glyph, sym_glyph.glyphname, currentSourceFontGlyph)
|
|
sys.stdout.write(progressText)
|
|
sys.stdout.flush()
|
|
|
|
# check if a glyph already exists in this location
|
|
if careful or 'careful' in sym_attr['params']:
|
|
if currentSourceFontGlyph in self.sourceFont:
|
|
if self.args.quiet is False:
|
|
print(" Found existing Glyph at {:X}. Skipping...".format(currentSourceFontGlyph))
|
|
# We don't want to touch anything so move to next Glyph
|
|
continue
|
|
else:
|
|
# If we overwrite an existing glyph all subtable entries regarding it will be wrong
|
|
# (Probably; at least if we add a symbol and do not substitude a ligature or such)
|
|
if currentSourceFontGlyph in self.sourceFont:
|
|
self.sourceFont[currentSourceFontGlyph].removePosSub("*")
|
|
|
|
# Select and copy symbol from its encoding point
|
|
# We need to do this select after the careful check, this way we don't
|
|
# reset our selection before starting the next loop
|
|
symbolFont.selection.select(sym_glyph.encoding)
|
|
symbolFont.copy()
|
|
|
|
# Paste it
|
|
self.sourceFont.selection.select(currentSourceFontGlyph)
|
|
self.sourceFont.paste()
|
|
self.sourceFont[currentSourceFontGlyph].glyphname = sym_glyph.glyphname
|
|
scale_ratio_x = 1
|
|
scale_ratio_y = 1
|
|
|
|
# Prepare symbol glyph dimensions
|
|
sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph])
|
|
|
|
# Now that we have copy/pasted the glyph, if we are creating a monospace
|
|
# font we need to scale and move the glyphs. It is possible to have
|
|
# empty glyphs, so we need to skip those.
|
|
if self.args.single and sym_dim['width'] and sym_dim['height']:
|
|
# If we want to preserve that aspect ratio of the glyphs we need to
|
|
# find the largest possible scaling factor that will allow the glyph
|
|
# to fit in both the x and y directions
|
|
if sym_attr['stretch'] == 'pa':
|
|
scale_ratio_x = False
|
|
if scaleGlyph:
|
|
# We want to preserve the relative size of each glyph in a glyph group
|
|
scale_ratio_x = self.get_glyph_scale(sym_glyph.unicode, scaleGlyph, symbolFont)
|
|
if scale_ratio_x is False:
|
|
# In the remaining cases, each glyph is sized independently to each other
|
|
scale_ratio_x = self.get_scale_factor(sym_dim)
|
|
scale_ratio_y = scale_ratio_x
|
|
else:
|
|
if 'x' in sym_attr['stretch']:
|
|
# Stretch the glyph horizontally to fit the entire available width
|
|
scale_ratio_x = self.font_dim['width'] / sym_dim['width']
|
|
# end if single width
|
|
|
|
# non-monospace (double width glyphs)
|
|
# elif sym_dim['width'] and sym_dim['height']:
|
|
# any special logic we want to apply for double-width variation
|
|
# would go here
|
|
|
|
if 'y' in sym_attr['stretch']:
|
|
# Stretch the glyph vertically to total line height (good for powerline separators)
|
|
# Currently stretching vertically for both monospace and double-width
|
|
scale_ratio_y = self.font_dim['height'] / sym_dim['height']
|
|
|
|
if 'overlap' in sym_attr['params']:
|
|
overlap = sym_attr['params']['overlap']
|
|
else:
|
|
overlap = 0
|
|
|
|
if scale_ratio_x != 1 or scale_ratio_y != 1:
|
|
if overlap != 0:
|
|
scale_ratio_x *= 1 + overlap
|
|
scale_ratio_y *= 1 + overlap
|
|
self.sourceFont[currentSourceFontGlyph].transform(psMat.scale(scale_ratio_x, scale_ratio_y))
|
|
|
|
# Use the dimensions from the newly pasted and stretched glyph
|
|
sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph])
|
|
y_align_distance = 0
|
|
if sym_attr['valign'] == 'c':
|
|
# Center the symbol vertically by matching the center of the line height and center of symbol
|
|
sym_ycenter = sym_dim['ymax'] - (sym_dim['height'] / 2)
|
|
font_ycenter = self.font_dim['ymax'] - (self.font_dim['height'] / 2)
|
|
y_align_distance = font_ycenter - sym_ycenter
|
|
|
|
# Handle glyph l/r/c alignment
|
|
x_align_distance = 0
|
|
if sym_attr['align']:
|
|
# First find the baseline x-alignment (left alignment amount)
|
|
x_align_distance = self.font_dim['xmin'] - sym_dim['xmin']
|
|
if sym_attr['align'] == 'c':
|
|
# Center align
|
|
x_align_distance += (self.font_dim['width'] / 2) - (sym_dim['width'] / 2)
|
|
elif sym_attr['align'] == 'r':
|
|
# Right align
|
|
x_align_distance += self.font_dim['width'] - sym_dim['width']
|
|
|
|
if overlap != 0:
|
|
overlap_width = self.font_dim['width'] * overlap
|
|
if sym_attr['align'] == 'l':
|
|
x_align_distance -= overlap_width
|
|
if sym_attr['align'] == 'r':
|
|
x_align_distance += overlap_width
|
|
|
|
align_matrix = psMat.translate(x_align_distance, y_align_distance)
|
|
self.sourceFont[currentSourceFontGlyph].transform(align_matrix)
|
|
|
|
# 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.
|
|
self.set_glyph_width_mono(self.sourceFont[currentSourceFontGlyph])
|
|
|
|
# Ensure after horizontal adjustments and centering that the glyph
|
|
# does not overlap the bearings (edges)
|
|
self.remove_glyph_neg_bearings(self.sourceFont[currentSourceFontGlyph])
|
|
|
|
# Check if the inserted glyph is scaled correctly for monospace
|
|
if self.args.single:
|
|
(xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox()
|
|
if int(xmax - xmin) > self.font_dim['width'] * (1 + overlap):
|
|
print("\n Warning: Scaled glyph U+{:X} wider than one monospace width ({} / {} (overlap {}))".format(
|
|
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap))
|
|
|
|
# end for
|
|
|
|
if self.args.quiet is False or self.args.progressbars:
|
|
sys.stdout.write("\n")
|
|
|
|
|
|
def set_sourcefont_glyph_widths(self):
|
|
""" Makes self.sourceFont monospace compliant """
|
|
|
|
for glyph in self.sourceFont.glyphs():
|
|
if (glyph.width == self.font_dim['width']):
|
|
# Don't touch the (negative) bearings if the width is ok
|
|
# Ligartures will have these.
|
|
continue
|
|
|
|
if (glyph.width != 0):
|
|
# If the width is zero this glyph is intened to be printed on top of another one.
|
|
# In this case we need to keep the negative bearings to shift it 'left'.
|
|
# Things like Ä have these: composed of U+0041 'A' and U+0308 'double dot above'
|
|
#
|
|
# If width is not zero, correct the bearings such that they are within the width:
|
|
self.remove_glyph_neg_bearings(glyph)
|
|
|
|
self.set_glyph_width_mono(glyph)
|
|
|
|
|
|
def remove_glyph_neg_bearings(self, glyph):
|
|
""" Sets passed glyph's bearings 0 if they are negative. """
|
|
try:
|
|
if glyph.left_side_bearing < 0:
|
|
glyph.left_side_bearing = 0
|
|
if glyph.right_side_bearing < 0:
|
|
glyph.right_side_bearing = 0
|
|
except:
|
|
pass
|
|
|
|
|
|
def set_glyph_width_mono(self, glyph):
|
|
""" Sets passed glyph.width to self.font_dim.width.
|
|
|
|
self.font_dim.width is set with self.get_sourcefont_dimensions().
|
|
"""
|
|
try:
|
|
glyph.width = self.font_dim['width']
|
|
except:
|
|
pass
|
|
|
|
def prepareScaleGlyph(self, scaleGlyph, symbolFont):
|
|
""" Prepare raw ScaleGlyph data for use """
|
|
# The GlyphData is a dict with these (possible) entries:
|
|
# 'GlyphsToScale': List of ((lists of glyph codes) or (ranges of glyph codes)) that shall be scaled
|
|
# 'scales': List of associated scale factors, one for each entry in 'GlyphsToScale' (generated by this function)
|
|
# Example:
|
|
# { 'GlyphsToScale': [ range(1, 3), [ 7, 10 ], ],
|
|
# 'scales': [ 1.23, 1.33, ] }
|
|
#
|
|
# Each item in 'GlyphsToScale' (a range or an explicit list) forms a group of glyphs that shall be
|
|
# as rescaled all with the same and maximum possible (for the included glyphs) factor.
|
|
#
|
|
# Previously this structure has been used:
|
|
# 'ScaleGlyph' Lead glyph, which scaling factor is taken
|
|
# 'GlyphsToScale': List of (glyph code) or (list of two glyph codes that form a closed range)) that shall be scaled
|
|
# Note that this allows only one group for the whle symbol font, and that the scaling factor is defined by
|
|
# a specific character, which needs to be manually selected (on each symbol font update).
|
|
# Previous entries are automatically rewritten to the new style.
|
|
if 'scales' in scaleGlyph:
|
|
# Already prepared... must not happen, ignore call
|
|
return
|
|
if 'ScaleGlyph' in scaleGlyph:
|
|
# old method. Rewrite to new.
|
|
flat_list = []
|
|
for i in scaleGlyph['GlyphsToScale']:
|
|
if isinstance(i, tuple):
|
|
flat_list += list(range(i[0], i[1] + 1))
|
|
else:
|
|
flat_list.append(i)
|
|
scaleGlyph['GlyphsToScale'] = [ flat_list ]
|
|
sym_dim = get_glyph_dimensions(symbolFont[scaleGlyph['ScaleGlyph']])
|
|
scaleGlyph['scales'] = [ self.get_scale_factor(sym_dim) ]
|
|
else:
|
|
scaleGlyph['scales'] = []
|
|
for group in scaleGlyph['GlyphsToScale']:
|
|
sym_dim = get_multiglyph_boundingBox([ symbolFont[g] if g in symbolFont else None for g in group ])
|
|
scaleGlyph['scales'].append(self.get_scale_factor(sym_dim))
|
|
|
|
def get_glyph_scale(self, unicode_value, scaleGlyph, symbolFont):
|
|
""" Determines whether or not to use scaled glyphs for glyphs in passed glyph_list """
|
|
if not 'scales' in scaleGlyph:
|
|
self.prepareScaleGlyph(scaleGlyph, symbolFont)
|
|
for glyph_list, scale in zip(scaleGlyph['GlyphsToScale'], scaleGlyph['scales']):
|
|
if unicode_value in glyph_list:
|
|
return scale
|
|
return False
|
|
|
|
|
|
def replace_font_name(font_name, replacement_dict):
|
|
""" Replaces all keys with vals from replacement_dict in font_name. """
|
|
for key, val in replacement_dict.items():
|
|
font_name = font_name.replace(key, val)
|
|
return font_name
|
|
|
|
|
|
def make_sure_path_exists(path):
|
|
""" Verifies path passed to it exists. """
|
|
try:
|
|
os.makedirs(path)
|
|
except OSError as exception:
|
|
if exception.errno != errno.EEXIST:
|
|
raise
|
|
|
|
def get_multiglyph_boundingBox(glyphs):
|
|
""" Returns dict of the dimensions of multiple glyphs combined """
|
|
bbox = [ None, None, None, None ]
|
|
for glyph in glyphs:
|
|
if glyph is None:
|
|
# Glyph has been in defining range but is not in the actual font
|
|
continue
|
|
gbb = glyph.boundingBox()
|
|
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]
|
|
return {
|
|
'xmin' : bbox[0],
|
|
'ymin' : bbox[1],
|
|
'xmax' : bbox[2],
|
|
'ymax' : bbox[3],
|
|
'width' : bbox[2] + (-bbox[0]),
|
|
'height': bbox[3] + (-bbox[1]),
|
|
}
|
|
|
|
def get_glyph_dimensions(glyph):
|
|
""" Returns dict of the dimesions of the glyph passed to it. """
|
|
return get_multiglyph_boundingBox([ glyph ])
|
|
|
|
def update_progress(progress):
|
|
""" Updates progress bar length.
|
|
|
|
Accepts a float between 0.0 and 1.0. Any int will be converted to a float.
|
|
A value at 1 or bigger represents 100%
|
|
modified from: https://stackoverflow.com/questions/3160699/python-progress-bar
|
|
"""
|
|
barLength = 40 # Modify this to change the length of the progress bar
|
|
if isinstance(progress, int):
|
|
progress = float(progress)
|
|
if progress >= 1:
|
|
progress = 1
|
|
status = "Done...\r\n" # NOTE: status initialized and never used
|
|
block = int(round(barLength * progress))
|
|
text = "\r╢{0}╟ {1}%".format("█" * block + "░" * (barLength - block), int(progress * 100))
|
|
sys.stdout.write(text)
|
|
sys.stdout.flush()
|
|
|
|
|
|
def check_fontforge_min_version():
|
|
""" Verifies installed FontForge version meets minimum requirement. """
|
|
minimumVersion = 20141231
|
|
actualVersion = int(fontforge.version())
|
|
|
|
# un-comment following line for testing invalid version error handling
|
|
# actualVersion = 20120731
|
|
|
|
# versions tested: 20150612, 20150824
|
|
if actualVersion < minimumVersion:
|
|
sys.stderr.write("{}: You seem to be using an unsupported (old) version of fontforge: {}\n".format(projectName, actualVersion))
|
|
sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion))
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
check_fontforge_min_version()
|
|
patcher = font_patcher()
|
|
patcher.patch()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
__dir__ = os.path.dirname(os.path.abspath(__file__))
|
|
main()
|