Keep things up-to-date with NerdFonts 3.0.0 release

This commit is contained in:
Hoang Nguyen 2023-05-02 13:04:31 +07:00
parent 6322fa255b
commit 43b872a6ce
Signed by: folliehiyuki
GPG Key ID: B0567C20730E9B11
18 changed files with 1217 additions and 291 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/font-patcher-log.txt
__pycache__/

View File

@ -1,14 +0,0 @@
all: update iosevka
update:
./update.sh
# Keep the patched font files
clean:
rm -rf original/*.ttf
rm -rf original/*.ttx
rm -rf patched/*.ttx
rm -rf patched/*.original.ttf
iosevka: clean
./patch_Iosevka.sh $(IOSEVKA_VERSION)

View File

@ -0,0 +1,334 @@
#!/usr/bin/env python
# coding=utf8
import re
from FontnameTools import FontnameTools
class FontnameParser:
"""Parse a font name and generate all kinds of names"""
def __init__(self, filename, logger):
"""Parse a font filename and store the results"""
self.parse_ok = False
self.use_short_families = (False, False, False) # ( camelcase name, short styles, aggressive )
self.keep_regular_in_family = None # None = auto, True, False
self.suppress_preferred_if_identical = True
self.family_suff = ''
self.ps_fontname_suff = ''
self.short_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)
self.rename_oblique = True
self.logger = logger
def _make_ps_name(self, n, is_family):
"""Helper to limit font name length in PS names"""
fam = 'family ' if is_family else ''
limit = 31 if is_family else 63
if len(n) <= limit:
return n
r = re.search('(.*)(-.*)', n)
if not r:
new_n = n[:limit]
else:
q = limit - len(r.groups()[1])
if q < 1:
q = 1
self.logger.error('====-< Shortening too long PS {}name: Garbage warning'. format(fam))
new_n = r.groups()[0][:q] + r.groups()[1]
if new_n != n:
self.logger.error('====-< Shortening too long PS {}name: {} -> {}'.format(fam, n, new_n))
return new_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_keep_regular_in_family(self, keep):
"""Familyname may contain 'Regular' where it should normally be suppressed"""
self.keep_regular_in_family = keep
def set_expect_no_italic(self, noitalic):
"""Prevents rewriting Oblique as family name part"""
# To prevent naming clashes usually Oblique is moved out in the family name
# because some fonts have Italic and Oblique, and we want to generate pure
# RIBBI families in ID1/2.
# But some fonts have Oblique instead of Italic, here the prevential movement
# is not needed, or rather contraproductive. This can not be detected on a
# font file level but needs to be specified per family from the outside.
# Returns true if setting was successful.
if 'Italic' in self.style_token:
self.rename_oblique = True
return not noitalic
self.rename_oblique = not noitalic
return True
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, family, ps_fontname, short_family):
"""Add a custom additonal string that shows up in the resulting names"""
self.family_suff = family.strip()
self.ps_fontname_suff = ps_fontname.replace(' ', '')
self.short_family_suff = short_family.strip()
return self
def enable_short_families(self, camelcase_name, prefix, aggressive):
"""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/True
if isinstance(prefix, str):
prefix = self._basename.startswith(prefix)
self.use_short_families = ( camelcase_name, prefix, aggressive )
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
for regex, replacement in self.name_subst:
base_and_rest = self.basename + (' ' + self.rest if len(self.rest) else '')
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).rstrip()
self.rest = self.rest[-(i+1):].lstrip()
else:
self.basename = m.expand(replacement) + self.basename[len(m.group(0)):]
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)"""
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)
(name, rest) = self._shortened_name()
if self.use_short_families[1]:
[ weights, styles ] = FontnameTools.short_styles([ weights, styles ], self.use_short_families[2])
return FontnameTools.concat(name, rest, self.other_token, self.short_family_suff, weights, styles)
def psname(self):
"""Get the SFNT PostScriptName (ID 6)"""
# This is almost self.family() + '-' + self.subfamily()
(name, rest) = self._shortened_name()
styles = self.style_token
weights = self.weight_token
if self.use_short_families[1]:
styles = FontnameTools.short_styles(styles, self.use_short_families[2])
weights = FontnameTools.short_styles(weights, self.use_short_families[2])
fam = FontnameTools.camel_casify(FontnameTools.concat(name, rest, self.other_token, self.ps_fontname_suff))
sub = FontnameTools.camel_casify(FontnameTools.concat(weights, styles))
if len(sub) > 0:
sub = '-' + sub
fam = FontnameTools.postscript_char_filter(fam)
sub = FontnameTools.postscript_char_filter(sub)
return self._make_ps_name(fam + sub, False)
def preferred_family(self):
"""Get the SFNT Preferred Familyname (ID 16)"""
(name, rest) = self._shortened_name()
pfn = FontnameTools.concat(name, rest, self.other_token, self.family_suff)
if self.suppress_preferred_if_identical and pfn == self.family():
# Do not set if identical to ID 1
return ''
return pfn
def preferred_styles(self):
"""Get the SFNT Preferred Styles (ID 17)"""
styles = self.style_token
weights = self.weight_token
# For naming purposes we want Oblique to be part of the styles
(weights, styles) = FontnameTools.make_oblique_style(weights, styles)
pfs = FontnameTools.concat(weights, styles)
if self.suppress_preferred_if_identical and pfs == self.subfamily():
# Do not set if identical to ID 2
return ''
return pfs
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
weights = self.weight_token
aggressive = self.use_short_families[2]
if not self.rename_oblique:
(weights, styles) = FontnameTools.make_oblique_style(weights, [])
if self.use_short_families[1]:
[ other, weights ] = FontnameTools.short_styles([ other, weights ], aggressive)
weights = [ w if w != 'Oblique' else 'Obl' for w in weights ]
return FontnameTools.concat(name, rest, other, self.short_family_suff, weights)
def subfamily(self):
"""Get the SFNT SubFamily (ID 2)"""
styles = self.style_token
weights = self.weight_token
if not self.rename_oblique:
(weights, styles) = FontnameTools.make_oblique_style(weights, styles)
if len(styles) == 0:
if 'Oblique' in weights:
return FontnameTools.concat(styles, 'Italic')
return 'Regular'
if 'Oblique' in weights and not 'Italic' in styles:
return FontnameTools.concat(styles, 'Italic')
return FontnameTools.concat(styles)
def ps_familyname(self):
"""Get the PS Familyname"""
fam = self.preferred_family()
if len(fam) < 1:
fam = self.family()
return self._make_ps_name(fam, True)
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 checklen(self, max_len, entry_id, name):
"""Check the length of a name string and report violations"""
if len(name) <= max_len:
self.logger.debug('=====> {:18} ok ({:2} <={:2}): {}'.format(entry_id, len(name), max_len, name))
else:
self.logger.error('====-< {:18} too long ({:2} > {:2}): {}'.format(entry_id, len(name), max_len, name))
return name
def rename_font(self, font):
"""Rename the font to include all information we found (font is fontforge font object)"""
font.fondname = None
font.fontname = self.psname()
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*
version_tag = ''
sfnt_list = []
TO_DEL = ['Family', 'SubFamily', 'Fullname', 'PostScriptName', 'Preferred Family',
'Preferred Styles', 'Compatible Full', 'WWS Family', 'WWS Subfamily',
'UniqueID', 'CID findfont Name']
# Remove these entries in all languages and add (at least the vital ones) some
# back, but only as 'English (US)'. This makes sure we do not leave contradicting
# names over different languages.
for l, k, v in list(font.sfnt_names):
if not k in TO_DEL:
sfnt_list += [( l, k, v )]
if k == 'Version' and l == 'English (US)':
version_tag = ' ' + v.split()[-1]
sfnt_list += [( 'English (US)', 'Family', self.checklen(31, 'Family (ID 1)', self.family()) )] # 1
sfnt_list += [( 'English (US)', 'SubFamily', self.checklen(31, 'SubFamily (ID 2)', self.subfamily()) )] # 2
sfnt_list += [( 'English (US)', 'UniqueID', self.fullname() + version_tag )] # 3
sfnt_list += [( 'English (US)', 'Fullname', self.checklen(63, 'Fullname (ID 4)', self.fullname()) )] # 4
sfnt_list += [( 'English (US)', 'PostScriptName', self.checklen(63, 'PSN (ID 6)', self.psname()) )] # 6
p_fam = self.preferred_family()
if len(p_fam):
sfnt_list += [( 'English (US)', 'Preferred Family', self.checklen(31, 'PrefFamily (ID 16)', p_fam) )] # 16
p_sty = self.preferred_styles()
if len(p_sty):
sfnt_list += [( 'English (US)', 'Preferred Styles', self.checklen(31, 'PrefStyles (ID 17)', p_sty) )] # 17
font.sfnt_names = tuple(sfnt_list)
font.macstyle = self.macstyle(0)
font.os2_stylemap = self.fs_selection(0)

View File

@ -0,0 +1,382 @@
#!/usr/bin/env python
# coding=utf8
import re
import sys
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) is not list:
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',
#'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 find_in_dicts(key, dicts):
"""Find an entry in a list of dicts, return entry and in which list it was"""
for i, d in enumerate(dicts):
if key in d:
return ( d[key], i )
return (None, 0)
@staticmethod
def get_shorten_form_idx(aggressive, prefix, form_if_prefixed):
"""Get the tuple index of known_* data tables"""
if aggressive:
return 0
if len(prefix):
return form_if_prefixed
return 1
@staticmethod
def shorten_style_name(name, aggressive):
"""Substitude some known styles to short form"""
# If aggressive is False create the mild short form
# aggressive == True: Always use first form of everything
# aggressive == False:
# - has no modifier: use the second form
# - has modifier: use second form of mod plus first form of weights2
# - has modifier: use second form of mod plus second form of widths
name_rest = name
name_pre = ''
form = FontnameTools.get_shorten_form_idx(aggressive, '', 0)
for mod in FontnameTools.known_modifiers:
if name.startswith(mod) and len(name) > len(mod): # Second condition specifically for 'Demi'
name_pre = FontnameTools.known_modifiers[mod][form]
name_rest = name[len(mod):]
break
subst, i = FontnameTools.find_in_dicts(name_rest, [ FontnameTools.known_weights2, FontnameTools.known_widths ])
form = FontnameTools.get_shorten_form_idx(aggressive, name_pre, i)
if isinstance(subst, tuple):
return name_pre + subst[form]
if not len(name_pre):
# The following sets do not allow modifiers
subst, _ = FontnameTools.find_in_dicts(name_rest, [ FontnameTools.known_weights1, FontnameTools.known_slopes ])
if isinstance(subst, tuple):
return subst[form]
return name
@staticmethod
def short_styles(lists, aggressive):
"""Shorten all style names in a list or a list of lists"""
if not len(lists) or not isinstance(lists[0], list):
return list(map(lambda x: FontnameTools.shorten_style_name(x, aggressive), lists))
return [ list(map(lambda x: FontnameTools.shorten_style_name(x, aggressive), styles)) for styles in lists ]
@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 = [
( '(a)nonymous', r'\1nonymice' ),
( '(b)itstream( ?)(v)era( ?sans ?mono)?', r'\1itstrom\2Wera' ),
( '(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:
( 'IBM 3270', r'3270'), # for historical reasons and 'IBM' is a TM or something
# Some name parts that are too long for us
( '(.*sans ?m)ono', r'\1'), # Various SomenameSansMono fonts
( '(.*code ?lat)in Expanded', r'\1X'), # for 'M PLUS Code Latin Expanded'
( '(.*code ?lat)in', r'\1'), # for 'M PLUS Code Latin'
( '(b)ig( ?)(b)lue( ?)(t)erminal', r'\1ig\3lue\5erm'), # Shorten BigBlueTerminal
( '(.*)437TT', r'\g<1>437'), # Shorten BigBlueTerminal 437 TT even further
( '(.*dyslexic ?alt)a', r'\1'), # Open Dyslexic Alta -> Open Dyslexic Alt
( '(.*dyslexic ?m)ono', r'\1'), # Open Dyslexic Mono -> Open Dyslexic M
( '(overpass ?m)ono', r'\1'), # Overpass Mono -> Overpass M
( '(proggyclean) ?tt', r'\1'), # Remove TT from ProggyClean
( '(terminess) ?\(ttf\)', r'\1'), # Remove TTF from Terminus (after renamed to Terminess)
( '(im ?writing ?q)uattro', r'\1uat'), # Rename iM Writing Quattro to Quat
( '(im ?writing ?(mono|duo|quat)) ?s', r'\1'), # Remove S from all iM Writing styles
]
# From https://adobe-type-tools.github.io/font-tech-notes/pdfs/5088.FontNames.pdf
# The first short variant is from the linked table.
# The second (longer) short variant is from diverse fonts like Noto.
# We can
# - use the long form
# - use the very short form (first)
# - use mild short form:
# - has no modifier: use the second form
# - has modifier: use second form of mod plus first form of weights2
# - has modifier: use second form of mod plus second form of widths
# This is encoded in get_shorten_form_idx()
known_weights1 = { # can not take modifiers
'Medium': ('Md', 'Med'),
'Nord': ('Nd', 'Nord'),
'Book': ('Bk', 'Book'),
'Poster': ('Po', 'Poster'),
'Demi': ('Dm', 'Demi'), # Demi is sometimes used as a weight, sometimes as a modifier
'Regular': ('Rg', 'Reg'),
'Display': ('DS', 'Disp'),
'Super': ('Su', 'Sup'),
'Retina': ('Rt', 'Ret'),
}
known_weights2 = { # can take modifiers
'Black': ('Blk', 'Black'),
'Bold': ('Bd', 'Bold'),
'Heavy': ('Hv', 'Heavy'),
'Thin': ('Th', 'Thin'),
'Light': ('Lt', 'Light'),
' ': (), # Just for CodeClimate :-/
}
known_widths = { # can take modifiers
'Compressed': ('Cm', 'Comp'),
'Extended': ('Ex', 'Extd'),
'Condensed': ('Cn', 'Cond'),
'Narrow': ('Nr', 'Narrow'),
'Compact': ('Ct', 'Compact'),
}
known_slopes = { # can not take modifiers
'Inclined': ('Ic', 'Incl'),
'Oblique': ('Obl', 'Obl'),
'Italic': ('It', 'Italic'),
'Upright': ('Up', 'Uprght'),
'Kursiv': ('Ks', 'Kurs'),
'Sloped': ('Sl', 'Slop'),
}
known_modifiers = {
'Demi': ('Dm', 'Dem'),
'Ultra': ('Ult', 'Ult'),
'Semi': ('Sm', 'Sem'),
'Extra': ('X', 'Ext'),
}
@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-condensed\b', 'SemiCondensed', name, 1, re.IGNORECASE) # Just for "3270 Semi-Condensed" :-/
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 = [ m + s
for s in list(FontnameTools.known_weights2) + list(FontnameTools.known_widths)
for m in list(FontnameTools.known_modifiers) + [''] if m != s
] + list(FontnameTools.known_weights1) + list(FontnameTools.known_slopes)
styles = [ 'Bold', 'Italic', 'Regular', 'Normal', ]
weights = [ w for w in weights if w not in styles ]
# Some font specialities:
other = [
'-', 'Book', 'For', 'Powerline',
'Text', # Plex
'IIx', # Profont IIx
'LGC', # Inconsolata LGC
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
and style.lower() != 'pro'): # Prevent 'r' of Pro to be detected as style_abbrev
( 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)

View File

@ -0,0 +1,94 @@
#!/usr/bin/env python3
# coding=utf8
import sys
import os.path
import fontforge
###### Some helpers (code from font-patcher)
def check_panose_monospaced(font):
""" Check if the font's Panose flags say it is monospaced """
# https://forum.high-logic.com/postedfiles/Panose.pdf
panose = list(font.os2_panose)
if panose[0] < 2 or panose[0] > 5:
return -1 # invalid Panose info
panose_mono = ((panose[0] == 2 and panose[3] == 9) or
(panose[0] == 3 and panose[3] == 3))
return 1 if panose_mono else 0
def is_monospaced(font):
""" Check if a font is probably monospaced """
# Some fonts lie (or have not any Panose flag set), spot check monospaced:
width = -1
width_mono = True
for glyph in [ 0x49, 0x4D, 0x57, 0x61, 0x69, 0x2E ]: # wide and slim glyphs 'I', 'M', 'W', 'a', 'i', '.'
if not glyph in font:
# A 'strange' font, believe Panose
return check_panose_monospaced(font) == 1
# print(" -> {} {}".format(glyph, font[glyph].width))
if width < 0:
width = font[glyph].width
continue
if font[glyph].width != width:
# Exception for fonts like Code New Roman Regular or Hermit Light/Bold:
# Allow small 'i' and dot to be smaller than normal
# I believe the source fonts are buggy
if glyph in [ 0x69, 0x2E ]:
if width > font[glyph].width:
continue
(xmin, _, xmax, _) = font[glyph].boundingBox()
if width > xmax - xmin:
continue
width_mono = False
break
# We believe our own check more then Panose ;-D
return width_mono
def get_advance_width(font, extended, minimum):
""" Get the maximum/minimum advance width in the extended(?) range """
width = 0
if extended:
end = 0x17f
else:
end = 0x07e
for glyph in range(0x21, end):
if not glyph in font:
continue
if glyph in range(0x7F, 0xBF):
continue # ignore special characters like '1/4' etc
if width == 0:
width = font[glyph].width
continue
if not minimum and width < font[glyph].width:
width = font[glyph].width
elif minimum and width > font[glyph].width:
width = font[glyph].width
return width
###### Let's go!
if len(sys.argv) < 2:
print('Usage: {} font_name [font_name ...]\n'.format(sys.argv[0]))
sys.exit(1)
print('Examining {} font files'.format(len(sys.argv) - 1))
for filename in sys.argv[1:]:
fullfile = os.path.basename(filename)
fname = os.path.splitext(fullfile)[0]
font = fontforge.open(filename, 1)
width_mono = is_monospaced(font)
panose_mono = check_panose_monospaced(font)
if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1):
print('[{:50.50}] Warning: Monospaced check: Panose assumed to be wrong; Glyph widths {} / {} - {} and Panose says "monospace {}" ({})'.format(fullfile, get_advance_width(font, False, True),
get_advance_width(font, False, False), get_advance_width(font, True, False), panose_mono, list(font.os2_panose)))
if not width_mono:
print('[{:50.50}] Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless; Glyph widths {} / {} - {}'.format(fullfile, get_advance_width(font, False, True),
get_advance_width(font, False, False), get_advance_width(font, True, False), panose_mono, list(font.os2_panose)))
else:
print('[{:50.50}] OK'.format(fullfile))
font.close()

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
# coding=utf8
#
# Usually called via
# $ fontforge query_names fontfile.tff 2>/dev/null
import sys
import os.path
import fontforge
###### Some helpers
def get_sfnt_dict(font):
"""Extract SFNT table as nice dict"""
return { k: v for l, k, v in font.sfnt_names }
def format_names(header, *stuff):
"""Unify outputs (with header)"""
f = '| {:50.50}|{:>2.2}| {:64.64}|{:>2.2}| {:64.64}|{:>2.2}| {:55.55}|{:>2.2}| {:30.30}|{:>2.2}| {:40.40}|{:>2.2}| {:40.40}|{:>2.2}|'
if header:
d = ''
return f.format(*stuff) + '\n' + f.format(d, d, d, d, d, d, d, d, d, d, d, d, d, d).replace(' ', '-')
return f.format(*stuff).rstrip()
###### Let's go!
if len(sys.argv) < 2:
print('Usage: {} font_name [font_name ...]\n'.format(sys.argv[0]))
sys.exit(1)
print('Examining {} font files'.format(len(sys.argv) - 1))
print(format_names(True, 'Filename', '', 'PS Name', '', 'Fullname', '', 'Family', '', 'Subfamily', '', 'Typogr. Family', '', 'Typogr. Subfamily', ''))
for filename in sys.argv[1:]:
fullfile = os.path.basename(filename)
fname = os.path.splitext(fullfile)[0]
font = fontforge.open(filename, 1)
sfnt = get_sfnt_dict(font)
psname = font.fontname
font.close()
sfnt_full = sfnt['Fullname']
sfnt_fam = sfnt['Family']
sfnt_subfam = sfnt['SubFamily']
sfnt_pfam = sfnt['Preferred Family'] if 'Preferred Family' in sfnt else ''
sfnt_psubfam = sfnt['Preferred Styles'] if 'Preferred Styles' in sfnt else ''
o2 = format_names(False,
fullfile, str(len(fullfile)),
psname, str(len(psname)),
sfnt_full, str(len(sfnt_full)),
sfnt_fam, str(len(sfnt_fam)),
sfnt_subfam, str(len(sfnt_subfam)),
# show length zero if a zero length string is stored, show nothing if nothing is stored:
sfnt_pfam, str(len(sfnt_pfam)) if 'Preferred Family' in sfnt else '',
sfnt_psubfam, str(len(sfnt_psubfam)) if 'Preferred Family' in sfnt else '')
print(o2)

View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
# coding=utf8
import fontforge
import sys
if len(sys.argv) != 2:
print("Usage: {} font_name\n".format(sys.argv[0]))
sys.exit(1)
font = fontforge.open(sys.argv[1])
panose = list(font.os2_panose)
print("Panose 4 = {} in {}".format(panose[3], font.fullname))
font.close()

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
# coding=utf8
import fontforge
import sys
def get_sfnt_dict(font):
"""Extract SFNT table as nice dict"""
return { k: v for l, k, v in font.sfnt_names }
if len(sys.argv) < 2 or len(sys.argv) > 3:
print("Usage: {} [<sfnt-name>] font_name\n".format(sys.argv[0]))
sys.exit(1)
if len(sys.argv) == 2:
fname = sys.argv[1]
sname = None
else:
fname = sys.argv[2]
sname = sys.argv[1]
font = fontforge.open(fname)
sfnt = get_sfnt_dict(font)
font.close()
if sname:
for key in sname.split(','):
if key in sfnt:
print("SFNT {:20.20} is {:80.80}".format(key, '\'' + sfnt[key] + '\''));
else:
print("SFNT {:20.20} is not set".format(key));
else:
for k in sfnt:
print("{:20.20} {:80.80}".format(k, sfnt[k]))

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# coding=utf8
import fontforge
import sys
def get_sfnt_dict(font):
"""Extract SFNT table as nice dict"""
return { k: v for l, k, v in font.sfnt_names }
if len(sys.argv) != 2:
print("Usage: {} font_name\n".format(sys.argv[0]))
sys.exit(1)
font = fontforge.open(sys.argv[1])
sfnt = get_sfnt_dict(font)
print("Version is '{}'".format(font.version));
print("CID Version is '{}'".format(font.cidversion));
print("SFNT Revision is '{}'".format(font.sfntRevision));
if "Version" in sfnt:
print("SFNT ['Version'] is '{}'".format(sfnt["Version"]));
else:
print("SFNT ['Version'] is not set".format(sys.argv[1]));
font.close()

View File

@ -1,49 +0,0 @@
#! /usr/bin/python
# credit: https://github.com/be5invis/Sarasa-Gothic/issues/108#issuecomment-517240248
# usage:
# python build-hdmx-for-sarasa.py your-sarasa-font.ttf
import sys
import math
from fontTools.ttLib import TTFont, newTable
def main():
headFlagInstructionsMayAlterAdvanceWidth = 0x0010
sarasaHintPpemMin = 11
sarasaHintPpemMax = 48
filename = sys.argv[1]
font = TTFont(filename, recalcBBoxes=False)
originalFontHead = font["head"]
originalFontHmtx = font["hmtx"]
originalFontHead.flags |= headFlagInstructionsMayAlterAdvanceWidth
hdmxTable = newTable("hdmx")
hdmxTable.hdmx = {}
# build hdmx table for odd and hinted ppems only.
for ppem in range(
math.floor(sarasaHintPpemMin / 2) * 2 + 1, sarasaHintPpemMax + 1, 2
):
halfUpm = originalFontHead.unitsPerEm / 2
halfPpem = math.ceil(ppem / 2)
hdmxTable.hdmx[ppem] = {
name: math.ceil(width / halfUpm) * halfPpem
for name, (width, _) in originalFontHmtx.metrics.items()
}
font["hdmx"] = hdmxTable
font.save(filename)
font.close()
if __name__ == "__main__":
main()

View File

@ -1,61 +0,0 @@
#! /usr/bin/python
# usage:
# python correct-ttf-font-family-name.py filename.ttf
import sys
from fontTools.ttLib import TTFont
def main():
filename = sys.argv[1]
font = TTFont(filename, recalcBBoxes=False)
fontName = font["name"]
originalFontUniqueID = fontName.getName(3, 1, 0, 0).toUnicode()
originalFontFullname = fontName.getName(4, 1, 0, 0).toUnicode()
originalFontPreferredStyle = fontName.getName(17, 1, 0, 0).toUnicode()
for entry in fontName.names:
nameID = entry.nameID
platformID = entry.platformID
platEncID = entry.platEncID
langID = entry.langID
if langID in [1028, 1041, 2052, 3076]:
string = (
entry.toUnicode()
.replace(" CL", " CL Nerd Font")
.replace(" TC", " TC Nerd Font")
.replace(" J", " J Nerd Font")
.replace(" SC", " SC Nerd Font")
.replace(" HC", " HC Nerd Font")
)
fontName.setName(string, nameID, platformID, platEncID, langID)
elif nameID in [1, 16]:
string = originalFontUniqueID.replace(
f" {originalFontPreferredStyle}", " Nerd Font"
)
fontName.setName(string, nameID, platformID, platEncID, langID)
elif nameID == 3:
string = originalFontUniqueID.replace(
f" {originalFontPreferredStyle}",
f" Nerd Font {originalFontPreferredStyle}",
)
fontName.setName(string, nameID, platformID, platEncID, langID)
elif nameID == 6:
fontName.setName(
originalFontFullname, nameID, platformID, platEncID, langID
)
font.save(filename)
font.close()
if __name__ == "__main__":
main()

View File

@ -1,14 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding=utf8 # coding=utf8
# Nerd Fonts Version: 2.3.3 # Nerd Fonts Version: 3.0.0
# Script version is further down # Script version is further down
from __future__ import absolute_import, print_function, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
# Change the script version when you edit this script: # Change the script version when you edit this script:
script_version = "3.6.1" script_version = "4.1.1"
version = "2.3.3" version = "3.0.0"
projectName = "Nerd Fonts" projectName = "Nerd Fonts"
projectNameAbbreviation = "NF" projectNameAbbreviation = "NF"
projectNameSingular = projectName[:-1] projectNameSingular = projectName[:-1]
@ -22,6 +22,7 @@ import errno
import subprocess import subprocess
import json import json
from enum import Enum from enum import Enum
import logging
try: try:
import configparser import configparser
except ImportError: except ImportError:
@ -135,6 +136,7 @@ class TableHEADWriter:
positions = {'checksumAdjustment': 2+2+4, positions = {'checksumAdjustment': 2+2+4,
'flags': 2+2+4+4+4, 'flags': 2+2+4+4+4,
'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2, 'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2,
'avgWidth': 2,
} }
where = self.tab_offset + positions[where] where = self.tab_offset + positions[where]
self.f.seek(where) self.f.seek(where)
@ -238,10 +240,10 @@ def force_panose_monospaced(font):
panose = list(font.os2_panose) panose = list(font.os2_panose)
if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default) if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default)
panose[0] = 2 # make kind latin text and display panose[0] = 2 # make kind latin text and display
print(" Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')") logger.info("Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')")
font.os2_panose = tuple(panose) font.os2_panose = tuple(panose)
if panose[0] == 2 and panose[3] != 9: if panose[0] == 2 and panose[3] != 9:
print(" Setting Panose 'Proportion' to 'Monospaced' (was '{}')".format(panose_proportion_to_text(panose[3]))) logger.info("Setting Panose 'Proportion' to 'Monospaced' (was '%s')", panose_proportion_to_text(panose[3]))
panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced
font.os2_panose = tuple(panose) font.os2_panose = tuple(panose)
@ -282,6 +284,35 @@ def get_btb_metrics(font):
win_btb = win_height + win_gap win_btb = win_height + win_gap
return (hhea_btb, typo_btb, win_btb, win_gap) return (hhea_btb, typo_btb, win_btb, win_gap)
def get_old_average_x_width(font):
""" Determine xAvgCharWidth of the OS/2 table """
# Fontforge can not create fonts with old (i.e. prior to OS/2 version 3)
# table values, but some very old applications do need them sometimes
# https://learn.microsoft.com/en-us/typography/opentype/spec/os2#xavgcharwidth
s = 0
weights = {
'a': 64, 'b': 14, 'c': 27, 'd': 35, 'e': 100, 'f': 20, 'g': 14, 'h': 42, 'i': 63,
'j': 3, 'k': 6, 'l': 35, 'm': 20, 'n': 56, 'o': 56, 'p': 17, 'q': 4, 'r': 49,
's': 56, 't': 71, 'u': 31, 'v': 10, 'w': 18, 'x': 3, 'y': 18, 'z': 2, 32: 166,
}
for g in weights:
if g not in font:
logger.critical("Can not determine ancient style xAvgCharWidth")
sys.exit(1)
s += font[g].width * weights[g]
return int(s / 1000)
def create_filename(fonts):
""" Determine filename from font object(s) """
sfnt = { k: v for l, k, v in fonts[0].sfnt_names }
sfnt_pfam = sfnt.get('Preferred Family', sfnt['Family'])
sfnt_psubfam = sfnt.get('Preferred Styles', sfnt['SubFamily'])
if len(fonts) > 1:
return sfnt_pfam
if len(sfnt_psubfam) > 0:
sfnt_psubfam = '-' + sfnt_psubfam
return (sfnt_pfam + sfnt_psubfam).replace(' ', '')
class font_patcher: class font_patcher:
def __init__(self, args): def __init__(self, args):
@ -293,9 +324,11 @@ class font_patcher:
self.font_dim = None # class 'dict' self.font_dim = None # class 'dict'
self.font_extrawide = False self.font_extrawide = False
self.source_monospaced = None # Later True or False self.source_monospaced = None # Later True or False
self.symbolsonly = False
self.onlybitmaps = 0 self.onlybitmaps = 0
self.essential = set() self.essential = set()
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
self.xavgwidth = [] # list of ints
def patch(self, font): def patch(self, font):
self.sourceFont = font self.sourceFont = font
@ -317,7 +350,7 @@ class font_patcher:
# For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs # 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: if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2:
print("Very wide and short font, disabling 2 cell Powerline glyphs") logger.warning("Very wide and short font, disabling 2 cell Powerline glyphs")
self.font_extrawide = True self.font_extrawide = True
# Prevent opening and closing the fontforge font. Makes things faster when patching # Prevent opening and closing the fontforge font. Makes things faster when patching
@ -326,8 +359,12 @@ class font_patcher:
symfont = None symfont = None
if not os.path.isdir(self.args.glyphdir): if not os.path.isdir(self.args.glyphdir):
sys.exit("{}: Can not find symbol glyph directory {} " logger.critical("Can not find symbol glyph directory %s "
"(probably you need to download the src/glyphs/ directory?)".format(projectName, self.args.glyphdir)) "(probably you need to download the src/glyphs/ directory?)", self.args.glyphdir)
sys.exit(1)
if self.args.dry_run:
return
for patch in self.patch_set: for patch in self.patch_set:
if patch['Enabled']: if patch['Enabled']:
@ -337,11 +374,13 @@ class font_patcher:
symfont.close() symfont.close()
symfont = None symfont = None
if not os.path.isfile(self.args.glyphdir + patch['Filename']): if not os.path.isfile(self.args.glyphdir + patch['Filename']):
sys.exit("{}: Can not find symbol source for '{}'\n{:>{}} (i.e. {})".format( logger.critical("Can not find symbol source for '%s' (i.e. %s)",
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename'])) patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK): if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK):
sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format( logger.critical("Can not open symbol source for '%s' (i.e. %s)",
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename'])) patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename'])) symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename']))
symfont.encoding = 'UnicodeFull' symfont.encoding = 'UnicodeFull'
@ -383,11 +422,11 @@ class font_patcher:
break break
outfile = os.path.normpath(os.path.join( outfile = os.path.normpath(os.path.join(
sanitize_filename(self.args.outputdir, True), sanitize_filename(self.args.outputdir, True),
sanitize_filename(sourceFont.familyname) + ".ttc")) sanitize_filename(create_filename(sourceFonts)) + ".ttc"))
sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer) sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer)
message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile) message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile)
else: else:
fontname = sourceFont.fullname fontname = create_filename(sourceFonts)
if not fontname: if not fontname:
fontname = sourceFont.cidfontname fontname = sourceFont.cidfontname
outfile = os.path.normpath(os.path.join( outfile = os.path.normpath(os.path.join(
@ -395,9 +434,11 @@ class font_patcher:
sanitize_filename(fontname) + self.args.extension)) sanitize_filename(fontname) + self.args.extension))
bitmaps = str() bitmaps = str()
if len(self.sourceFont.bitmapSizes): if len(self.sourceFont.bitmapSizes):
if not self.args.quiet: logger.debug("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
bitmaps = str('otf') # otf/ttf, both is bf_ttf bitmaps = str('otf') # otf/ttf, both is bf_ttf
if self.args.dry_run:
logger.debug("=====> Filename '{}'".format(outfile))
return
sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags) sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags)
message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile) message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile)
@ -407,23 +448,34 @@ class font_patcher:
source_font = TableHEADWriter(self.args.font) source_font = TableHEADWriter(self.args.font)
dest_font = TableHEADWriter(outfile) dest_font = TableHEADWriter(outfile)
for idx in range(source_font.num_fonts): for idx in range(source_font.num_fonts):
if not self.args.quiet: logger.debug("Tweaking %d/%d", idx + 1, source_font.num_fonts)
print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts)) xwidth_s = ''
xwidth = self.xavgwidth[idx]
if isinstance(xwidth, int):
if isinstance(xwidth, bool) and xwidth:
source_font.find_table([b'OS/2'], idx)
xwidth = source_font.getshort('avgWidth')
xwidth_s = ' (copied from source)'
dest_font.find_table([b'OS/2'], idx)
d_xwidth = dest_font.getshort('avgWidth')
if d_xwidth != xwidth:
logger.debug("Changing xAvgCharWidth from %d to %d%s", d_xwidth, xwidth, xwidth_s)
dest_font.putshort(xwidth, 'avgWidth')
dest_font.reset_table_checksum()
source_font.find_head_table(idx) source_font.find_head_table(idx)
dest_font.find_head_table(idx) dest_font.find_head_table(idx)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0: if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
if not self.args.quiet: logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08)
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' dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
if source_font.lowppem != dest_font.lowppem: if source_font.lowppem != dest_font.lowppem:
if not self.args.quiet: logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem)
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM') dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified: if dest_font.modified:
dest_font.reset_table_checksum() dest_font.reset_table_checksum()
dest_font.reset_full_checksum() if dest_font.modified:
dest_font.reset_full_checksum()
except Exception as error: except Exception as error:
print("Can not handle font flags ({})".format(repr(error))) logger.error("Can not handle font flags (%s)", repr(error))
finally: finally:
try: try:
source_font.close() source_font.close()
@ -431,12 +483,13 @@ class font_patcher:
except: except:
pass pass
if self.args.is_variable: if self.args.is_variable:
print("Warning: Source font is a variable open type font (VF) and the patch results will most likely not be what you want") logger.critical("Source font is a variable open type font (VF) and the patch results will most likely not be what you want")
print(message) print(message)
if self.args.postprocess: if self.args.postprocess:
subprocess.call([self.args.postprocess, outfile]) subprocess.call([self.args.postprocess, outfile])
print("\nPost Processed: {}".format(outfile)) print("\n")
logger.info("Post Processed: %s", outfile)
def setup_name_backup(self, font): def setup_name_backup(self, font):
@ -454,11 +507,8 @@ class font_patcher:
font.fullname = font.persistent["fullname"] font.fullname = font.persistent["fullname"]
if isinstance(font.persistent["familyname"], str): if isinstance(font.persistent["familyname"], str):
font.familyname = font.persistent["familyname"] font.familyname = font.persistent["familyname"]
verboseAdditionalFontNameSuffix = " " + projectNameSingular verboseAdditionalFontNameSuffix = ""
if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later additionalFontNameSuffix = ""
additionalFontNameSuffix = " " + projectNameAbbreviation
else:
additionalFontNameSuffix = verboseAdditionalFontNameSuffix
if not self.args.complete: if not self.args.complete:
# NOTE not all symbol fonts have appended their suffix here # NOTE not all symbol fonts have appended their suffix here
if self.args.fontawesome: if self.args.fontawesome:
@ -489,17 +539,24 @@ class font_patcher:
additionalFontNameSuffix += " WEA" additionalFontNameSuffix += " WEA"
verboseAdditionalFontNameSuffix += " Plus Weather Icons" verboseAdditionalFontNameSuffix += " Plus Weather Icons"
# if all source glyphs included simplify the name # add mono signifier to beginning of name suffix
else:
additionalFontNameSuffix = " " + projectNameSingular + " Complete"
verboseAdditionalFontNameSuffix = " " + projectNameSingular + " Complete"
# add mono signifier to end of name
if self.args.single: if self.args.single:
additionalFontNameSuffix += " M" variant_abbrev = "M"
verboseAdditionalFontNameSuffix += " Mono" variant_full = " Mono"
elif self.args.nonmono and not self.symbolsonly:
variant_abbrev = "P"
variant_full = " Propo"
else:
variant_abbrev = ""
variant_full = ""
if FontnameParserOK and self.args.makegroups: ps_suffix = projectNameAbbreviation + variant_abbrev + additionalFontNameSuffix
# add 'Nerd Font' to beginning of name suffix
verboseAdditionalFontNameSuffix = " " + projectNameSingular + variant_full + verboseAdditionalFontNameSuffix
additionalFontNameSuffix = " " + projectNameSingular + variant_full + additionalFontNameSuffix
if FontnameParserOK and self.args.makegroups > 0:
use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse
# Use fullname if it is 'equal' to the fontname # Use fullname if it is 'equal' to the fontname
if font.fullname: if font.fullname:
@ -511,12 +568,14 @@ class font_patcher:
# Gohu fontnames hide the weight, but the file names are ok... # Gohu fontnames hide the weight, but the file names are ok...
if parser_name.startswith('Gohu'): if parser_name.startswith('Gohu'):
parser_name = os.path.splitext(os.path.basename(self.args.font))[0] parser_name = os.path.splitext(os.path.basename(self.args.font))[0]
n = FontnameParser(parser_name) n = FontnameParser(parser_name, logger)
if not n.parse_ok: if not n.parse_ok:
print("Have only minimal naming information, check resulting name. Maybe omit --makegroups option") logger.warning("Have only minimal naming information, check resulting name. Maybe specify --makegroups 0")
n.drop_for_powerline() n.drop_for_powerline()
n.enable_short_families(True, "Noto") n.enable_short_families(True, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ])
n.set_for_windows(self.args.windows) if not n.set_expect_no_italic(self.args.noitalic):
logger.critical("Detected 'Italic' slant but --has-no-italic specified")
sys.exit(1)
# All the following stuff is ignored in makegroups-mode # All the following stuff is ignored in makegroups-mode
@ -564,23 +623,7 @@ class font_patcher:
if len(subFamily) == 0: if len(subFamily) == 0:
subFamily = "Regular" subFamily = "Regular"
if self.args.windows: familyname += " " + projectNameSingular + variant_full
maxFamilyLength = 31
maxFontLength = maxFamilyLength - len('-' + subFamily)
familyname += " " + projectNameAbbreviation
if self.args.single:
familyname += "M"
fullname += " Windows Compatible"
# now make sure less than 32 characters name length
if len(fontname) > maxFontLength:
fontname = fontname[:maxFontLength]
if len(familyname) > maxFamilyLength:
familyname = familyname[:maxFamilyLength]
else:
familyname += " " + projectNameSingular
if self.args.single:
familyname += " Mono"
# Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with # Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with
# the same name as the same font, even if subFamily is different. Make sure to # the same name as the same font, even if subFamily is different. Make sure to
@ -593,6 +636,10 @@ class font_patcher:
reservedFontNameReplacements = { reservedFontNameReplacements = {
'source' : 'sauce', 'source' : 'sauce',
'Source' : 'Sauce', 'Source' : 'Sauce',
'Bitstream Vera Sans Mono' : 'Bitstrom Wera',
'BitstreamVeraSansMono' : 'BitstromWera',
'bitstream vera sans mono' : 'bitstrom wera',
'bitstreamverasansmono' : 'bitstromwera',
'hermit' : 'hurmit', 'hermit' : 'hurmit',
'Hermit' : 'Hurmit', 'Hermit' : 'Hurmit',
'hasklig' : 'hasklug', 'hasklig' : 'hasklug',
@ -659,7 +706,7 @@ class font_patcher:
fullname = replace_font_name(fullname, additionalFontNameReplacements2) fullname = replace_font_name(fullname, additionalFontNameReplacements2)
fontname = replace_font_name(fontname, additionalFontNameReplacements2) fontname = replace_font_name(fontname, additionalFontNameReplacements2)
if not (FontnameParserOK and self.args.makegroups): if not (FontnameParserOK and self.args.makegroups > 0):
# replace any extra whitespace characters: # replace any extra whitespace characters:
font.familyname = " ".join(familyname.split()) font.familyname = " ".join(familyname.split())
font.fullname = " ".join(fullname.split()) font.fullname = " ".join(fullname.split())
@ -670,13 +717,9 @@ class font_patcher:
font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname) font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname)
font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily)
else: else:
fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full
if self.args.single: # inject_suffix(family, ps_fontname, short_family)
if self.args.windows: n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family)
fam_suffix += 'M'
else:
fam_suffix += ' Mono'
n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix)
n.rename_font(font) n.rename_font(font)
font.comment = projectInfo font.comment = projectInfo
@ -692,6 +735,7 @@ class font_patcher:
self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version
self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge
self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version) self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version)
# The Version SFNT name is later reused by the NameParser for UniqueID
# print("Version now is {}".format(sourceFont.version)) # print("Version now is {}".format(sourceFont.version))
@ -700,17 +744,17 @@ class font_patcher:
# the tables have been removed from the repo with >this< commit # the tables have been removed from the repo with >this< commit
if self.args.configfile and self.config.read(self.args.configfile): if self.args.configfile and self.config.read(self.args.configfile):
if self.args.removeligatures: if self.args.removeligatures:
print("Removing ligatures from configfile `Subtables` section") logger.info("Removing ligatures from configfile `Subtables` section")
ligature_subtables = json.loads(self.config.get("Subtables", "ligatures")) ligature_subtables = json.loads(self.config.get("Subtables", "ligatures"))
for subtable in ligature_subtables: for subtable in ligature_subtables:
print("Removing subtable:", subtable) logger.debug("Removing subtable: %s", subtable)
try: try:
self.sourceFont.removeLookupSubtable(subtable) self.sourceFont.removeLookupSubtable(subtable)
print("Successfully removed subtable:", subtable) logger.debug("Successfully removed subtable: %s", subtable)
except Exception: except Exception:
print("Failed to remove subtable:", subtable) logger.error("Failed to remove subtable: %s", subtable)
elif self.args.removeligatures: elif self.args.removeligatures:
print("Unable to read configfile, unable to remove ligatures") logger.error("Unable to read configfile, unable to remove ligatures")
def assert_monospace(self): def assert_monospace(self):
@ -722,16 +766,17 @@ class font_patcher:
panose_mono = check_panose_monospaced(self.sourceFont) panose_mono = check_panose_monospaced(self.sourceFont)
# The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown' # 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): if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1):
print(" Warning: Monospaced check: Panose assumed to be wrong") logger.warning("Monospaced check: Panose assumed to be wrong")
print(" {} and {}".format( logger.warning(" %s and %s",
report_advance_widths(self.sourceFont), report_advance_widths(self.sourceFont),
panose_check_to_text(panose_mono, self.sourceFont.os2_panose))) panose_check_to_text(panose_mono, self.sourceFont.os2_panose))
if self.args.single and not width_mono: if self.args.single and not width_mono:
print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless") logger.warning("Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless")
if offending_char is not None: if offending_char is not None:
print(" Offending char: 0x{:X}".format(offending_char)) logger.warning(" Offending char: %X", offending_char)
if self.args.single <= 1: if self.args.single <= 1:
sys.exit(projectName + ": Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching") logger.critical("Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching")
sys.exit(1)
if width_mono: if width_mono:
force_panose_monospaced(self.sourceFont) force_panose_monospaced(self.sourceFont)
@ -740,19 +785,20 @@ class font_patcher:
""" Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """ """ Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """
box_enabled = self.source_monospaced # Box glyph only for monospaced box_enabled = self.source_monospaced # Box glyph only for monospaced
box_keep = False
if box_enabled: if box_enabled:
self.sourceFont.selection.select(("ranges",), 0x2500, 0x259f) self.sourceFont.selection.select(("ranges",), 0x2500, 0x259f)
box_glyphs_target = len(list(self.sourceFont.selection)) box_glyphs_target = len(list(self.sourceFont.selection))
box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs)) box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs))
if box_glyphs_target > box_glyphs_current: if box_glyphs_target > box_glyphs_current:
# Sourcefont does not have all of these glyphs, do not mix sets # Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing)
if not self.args.quiet and box_glyphs_current > 0: if box_glyphs_current > 0:
print("INFO: {}/{} box drawing glyphs will be replaced".format( logger.debug("%d/%d box drawing glyphs will be replaced",
box_glyphs_current, box_glyphs_target)) box_glyphs_current, box_glyphs_target)
box_keep = False
box_enabled = True box_enabled = True
else: else:
box_keep = True # just scale do not copy # Sourcefont does have all of these glyphs
# box_keep = True # just scale do not copy (need to scale to fit new cell size)
box_enabled = False # Cowardly not scaling existing glyphs, although the code would allow this box_enabled = False # Cowardly not scaling existing glyphs, although the code would allow this
# Stretch 'xz' or 'pa' (preserve aspect ratio) # Stretch 'xz' or 'pa' (preserve aspect ratio)
@ -985,14 +1031,14 @@ class font_patcher:
{'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.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': 0x23FB, 'SymEnd': 0x23FE, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Power, Power On/Off, Power On, Sleep
{'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off) {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off)
{'Enabled': self.args.material, 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': False , 'Name': "Material legacy", 'Filename': "materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesign/MaterialDesignIconsDesktop.ttf", 'Exact': True, 'SymStart': 0xF0001,'SymEnd': 0xF1AF0,'SrcStart': None, 'ScaleRules': MDI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesign/MaterialDesignIconsDesktop.ttf", 'Exact': True, 'SymStart': 0xF0001,'SymEnd': 0xF1AF0,'SrcStart': None, 'ScaleRules': MDI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.weather, 'Name': "Weather Icons", 'Filename': "weather-icons/weathericons-regular-webfont.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF0EB, 'SrcStart': 0xE300, 'ScaleRules': WEATH_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.weather, 'Name': "Weather Icons", 'Filename': "weather-icons/weathericons-regular-webfont.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF0EB, 'SrcStart': 0xE300, 'ScaleRules': WEATH_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF27C, 'SrcStart': 0xF4A9, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Desktop {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF305, 'SrcStart': 0xF4A9, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleRules': CODI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, {'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEBEB, 'SrcStart': None, 'ScaleRules': CODI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
{'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR} {'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR}
] ]
@ -1067,11 +1113,22 @@ class font_patcher:
our_btb = typo_btb if use_typo else win_btb our_btb = typo_btb if use_typo else win_btb
if our_btb == hhea_btb: if our_btb == hhea_btb:
metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font
elif abs(our_btb - hhea_btb) / our_btb < 0.03:
logger.info("Font vertical metrics slightly off (%.1f%)", (our_btb - hhea_btb) / our_btb * 100.0)
metrics = Metric.TYPO if use_typo else Metric.WIN
else: else:
# We trust the WIN metric more, see experiments in #1056 # Try the other metric
print("{}: WARNING Font vertical metrics inconsistent (HHEA {} / TYPO {} / WIN {}), using WIN".format(projectName, hhea_btb, typo_btb, win_btb)) our_btb = typo_btb if not use_typo else win_btb
our_btb = win_btb if our_btb == hhea_btb:
metrics = Metric.WIN logger.warning("Font vertical metrics probably wrong USE TYPO METRICS, assume opposite (i.e. %s)", 'True' if not use_typo else 'False')
use_typo = not use_typo
self.sourceFont.os2_use_typo_metrics = 1 if use_typo else 0
metrics = Metric.TYPO if use_typo else Metric.WIN
else:
# We trust the WIN metric more, see experiments in #1056
logger.warning("Font vertical metrics inconsistent (HHEA %d / TYPO %d / WIN %d), using WIN", hhea_btb, typo_btb, win_btb)
our_btb = win_btb
metrics = Metric.WIN
# print("FINI hhea {} typo {} win {} use {} {} {}".format(hhea_btb, typo_btb, win_btb, use_typo, our_btb != hhea_btb, self.sourceFont.fontname)) # print("FINI hhea {} typo {} win {} use {} {} {}".format(hhea_btb, typo_btb, win_btb, use_typo, our_btb != hhea_btb, self.sourceFont.fontname))
@ -1094,6 +1151,7 @@ class font_patcher:
if self.font_dim['height'] == 0: if self.font_dim['height'] == 0:
# This can only happen if the input font is empty # This can only happen if the input font is empty
# Assume we are using our prepared templates # Assume we are using our prepared templates
self.symbolsonly = True
self.font_dim = { self.font_dim = {
'xmin' : 0, 'xmin' : 0,
'ymin' : -self.sourceFont.descent, 'ymin' : -self.sourceFont.descent,
@ -1104,7 +1162,8 @@ class font_patcher:
} }
our_btb = self.sourceFont.descent + self.sourceFont.ascent our_btb = self.sourceFont.descent + self.sourceFont.ascent
elif self.font_dim['height'] < 0: elif self.font_dim['height'] < 0:
sys.exit("{}: Can not detect sane font height".format(projectName)) logger.critical("Can not detect sane font height")
sys.exit(1)
# Make all metrics equal # Make all metrics equal
self.sourceFont.os2_typolinegap = 0 self.sourceFont.os2_typolinegap = 0
@ -1118,12 +1177,13 @@ class font_patcher:
self.sourceFont.os2_use_typo_metrics = 1 self.sourceFont.os2_use_typo_metrics = 1
(check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont) (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: if check_hhea_btb != check_typo_btb or check_typo_btb != check_win_btb or check_win_btb != our_btb:
sys.exit("{}: Error in baseline to baseline code detected".format(projectName)) logger.critical("Error in baseline to baseline code detected")
sys.exit(1)
# Step 2 # Step 2
# Find the biggest char width and advance width # Find the biggest char width and advance width
# 0x00-0x17f is the Latin Extended-A range # 0x00-0x17f is the Latin Extended-A range
warned1 = self.args.quiet or self.args.nonmono # Do not warn if quiet or proportional target warned1 = self.args.nonmono # Do not warn if proportional target
warned2 = warned1 warned2 = warned1
for glyph in range(0x21, 0x17f): for glyph in range(0x21, 0x17f):
if glyph in range(0x7F, 0xBF) or glyph in [ if glyph in range(0x7F, 0xBF) or glyph in [
@ -1141,22 +1201,25 @@ class font_patcher:
if self.font_dim['width'] < self.sourceFont[glyph].width: if self.font_dim['width'] < self.sourceFont[glyph].width:
self.font_dim['width'] = self.sourceFont[glyph].width self.font_dim['width'] = self.sourceFont[glyph].width
if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Warning: Extended glyphs wider than basic glyphs, results might be useless\n {}".format( logger.debug("Extended glyphs wider than basic glyphs, results might be useless\n %s",
report_advance_widths(self.sourceFont))) report_advance_widths(self.sourceFont))
warned1 = True warned1 = True
# print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) # print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if xmax > self.font_dim['xmax']: if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax self.font_dim['xmax'] = xmax
if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Info: Extended glyphs wider bounding box than basic glyphs") logger.debug("Extended glyphs wider bounding box than basic glyphs")
warned2 = True warned2 = True
# print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) # print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if self.font_dim['width'] < self.font_dim['xmax']: if self.font_dim['width'] < self.font_dim['xmax']:
if not self.args.quiet: logger.debug("Font has negative right side bearing in extended glyphs")
print("Warning: Font has negative right side bearing in extended glyphs")
self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used
# print("FINAL", self.font_dim) # print("FINAL", self.font_dim)
self.xavgwidth.append(self.args.xavgwidth)
if isinstance(self.xavgwidth[-1], int) and self.xavgwidth[-1] == 0:
self.xavgwidth[-1] = get_old_average_x_width(self.sourceFont)
def get_target_width(self, stretch): def get_target_width(self, stretch):
""" Get the target width (1 or 2 'cell') for a given stretch parameter """ """ Get the target width (1 or 2 'cell') for a given stretch parameter """
@ -1249,7 +1312,7 @@ class font_patcher:
if sym_glyph.altuni: if sym_glyph.altuni:
possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ] possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ]
if len(possible_codes) == 0: if len(possible_codes) == 0:
print(" Can not determine codepoint of {:X}. Skipping...".format(sym_glyph.unicode)) logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode)
continue continue
currentSourceFontGlyph = min(possible_codes) currentSourceFontGlyph = min(possible_codes)
else: else:
@ -1272,9 +1335,8 @@ class font_patcher:
# check if a glyph already exists in this location # check if a glyph already exists in this location
if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential: if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential:
if currentSourceFontGlyph in self.sourceFont: if currentSourceFontGlyph in self.sourceFont:
if not self.args.quiet: careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing' logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph)
print(" Found {} Glyph at {:X}. Skipping...".format(careful_type, currentSourceFontGlyph))
# We don't want to touch anything so move to next Glyph # We don't want to touch anything so move to next Glyph
continue continue
else: else:
@ -1422,8 +1484,8 @@ class font_patcher:
if self.args.single: if self.args.single:
(xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox() (xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox()
if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)): if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)):
print("\n Warning: Scaled glyph U+{:X} wider than one monospace width ({} / {} (overlap {}))".format( logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %f))",
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)) currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)
# end for # end for
@ -1564,7 +1626,7 @@ def half_gap(gap, top):
gap_top = int(gap / 2) gap_top = int(gap / 2)
gap_bottom = gap - gap_top gap_bottom = gap - gap_top
if top: if top:
print("Redistributing line gap of {} ({} top and {} bottom)".format(gap, gap_top, gap_bottom)) logger.info("Redistributing line gap of %d (%d top and %d bottom)", gap, gap_top, gap_bottom)
return gap_top return gap_top
return gap_bottom return gap_bottom
@ -1689,8 +1751,8 @@ def check_fontforge_min_version():
# versions tested: 20150612, 20150824 # versions tested: 20150612, 20150824
if actualVersion < minimumVersion: if actualVersion < minimumVersion:
sys.stderr.write("{}: You seem to be using an unsupported (old) version of fontforge: {}\n".format(projectName, actualVersion)) logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion)
sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion)) logger.critical("Please use at least version: %d", minimumVersion)
sys.exit(1) sys.exit(1)
def check_version_with_git(version): def check_version_with_git(version):
@ -1738,7 +1800,6 @@ def setup_arguments():
parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='count', help='Whether to generate the glyphs as single-width not double-width (default is double-width)') parser.add_argument('-s', '--mono', '--use-single-width-glyphs', dest='single', default=False, action='count', help='Whether to generate the glyphs as single-width not double-width (default is double-width)')
parser.add_argument('-l', '--adjust-line-height', dest='adjustLineHeight', default=False, action='store_true', help='Whether to adjust line heights (attempt to center powerline separators more evenly)') parser.add_argument('-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('-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('-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('--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('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specificed in JSON configuration file')
@ -1748,15 +1809,34 @@ def setup_arguments():
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('-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('-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('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching')
parser.add_argument('--makegroups', dest='makegroups', default=False, action='store_true', help='Use alternative method to name patched fonts (experimental)') parser.add_argument('--makegroups', dest='makegroups', default=1, type=int, nargs='?', help='Use alternative method to name patched fonts (recommended)', const=1, choices=range(0, 6 + 1))
# --makegroup has an additional undocumented numeric specifier. '--makegroup' is in fact '--makegroup 1'.
# Original font name: Hugo Sans Mono ExtraCondensed Light Italic
# NF Fam agg.
# 0 turned off, use old naming scheme [-] [-] [-]
# 1 HugoSansMono Nerd Font ExtraCondensed Light Italic [ ] [ ] [ ]
# 2 HugoSansMono Nerd Font ExtCn Light Italic [ ] [X] [ ]
# 3 HugoSansMono Nerd Font XCn Lt It [ ] [X] [X]
# 4 HugoSansMono NF ExtraCondensed Light Italic [X] [ ] [ ]
# 5 HugoSansMono NF ExtCn Light Italic [X] [X] [ ]
# 6 HugoSansMono NF XCn Lt It [X] [X] [X]
parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")') parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")')
parser.add_argument('--has-no-italic', dest='noitalic', default=False, action='store_true', help='Font family does not have Italic (but Oblique)')
# progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse # 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 = 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('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set (default)')
progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set') progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set')
parser.set_defaults(progressbars=True) parser.set_defaults(progressbars=True)
parser.add_argument('--also-windows', dest='alsowindows', default=False, action='store_true', help='Create two fonts, the normal and the --windows version') parser.add_argument('--debug', dest='debugmode', default=False, action='store_true', help='Verbose mode')
parser.add_argument('--dry', dest='dry_run', default=False, action='store_true', help='Do neither patch nor store the font, to check naming')
parser.add_argument('--xavgcharwidth', dest='xavgwidth', default=None, type=int, nargs='?', help='Adjust xAvgCharWidth (optional: concrete value)', const=True)
# --xavgcharwidth for compatibility with old applications like notepad and non-latin fonts
# Possible values with examples:
# <none> - copy from sourcefont (default)
# 0 - calculate from font according to OS/2-version-2
# 500 - set to 500
# symbol fonts to include arguments # symbol fonts to include arguments
sym_font_group = parser.add_argument_group('Symbol Fonts') sym_font_group = parser.add_argument_group('Symbol Fonts')
@ -1774,8 +1854,9 @@ def setup_arguments():
args = parser.parse_args() args = parser.parse_args()
if args.makegroups and not FontnameParserOK: if args.makegroups > 0 and not FontnameParserOK:
sys.exit("{}: FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName)) logger.critical("FontnameParser module missing (bin/scripts/name_parser/Fontname*), specify --makegroups 0")
sys.exit(1)
# if you add a new font, set it to True here inside the if condition # if you add a new font, set it to True here inside the if condition
if args.complete: if args.complete:
@ -1808,24 +1889,23 @@ def setup_arguments():
font_complete = False font_complete = False
args.complete = font_complete args.complete = font_complete
if args.alsowindows:
args.windows = False
if args.nonmono and args.single: if args.nonmono and args.single:
print("Warning: Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.") logging.warning("Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.")
args.nonmono = False args.nonmono = False
make_sure_path_exists(args.outputdir) make_sure_path_exists(args.outputdir)
if not os.path.isfile(args.font): if not os.path.isfile(args.font):
sys.exit("{}: Font file does not exist: {}".format(projectName, args.font)) logging.critical("Font file does not exist: %s", args.font)
sys.exit(1)
if not os.access(args.font, os.R_OK): if not os.access(args.font, os.R_OK):
sys.exit("{}: Can not open font file for reading: {}".format(projectName, args.font)) logging.critical("Can not open font file for reading: %s", args.font)
sys.exit(1)
is_ttc = len(fontforge.fontsInFile(args.font)) > 1 is_ttc = len(fontforge.fontsInFile(args.font)) > 1
try: try:
source_font_test = TableHEADWriter(args.font) 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) 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: if args.is_variable:
print(" Warning: Source font is a variable open type font (VF), opening might fail...") logging.warning("Source font is a variable open type font (VF), opening might fail...")
except: except:
args.is_variable = False args.is_variable = False
finally: finally:
@ -1840,10 +1920,20 @@ def setup_arguments():
args.extension = '.' + args.extension args.extension = '.' + args.extension
if re.match("\.ttc$", args.extension, re.IGNORECASE): if re.match("\.ttc$", args.extension, re.IGNORECASE):
if not is_ttc: if not is_ttc:
sys.exit(projectName + ": Can not create True Type Collections from single font files") logging.critical("Can not create True Type Collections from single font files")
sys.exit(1)
else: else:
if is_ttc: if is_ttc:
sys.exit(projectName + ": Can not create single font files from True Type Collections") logging.critical("Can not create single font files from True Type Collections")
sys.exit(1)
if isinstance(args.xavgwidth, int) and not isinstance(args.xavgwidth, bool):
if args.xavgwidth < 0:
logging.critical("--xavgcharwidth takes no negative numbers")
sys.exit(2)
if args.xavgwidth > 16384:
logging.critical("--xavgcharwidth takes only numbers up to 16384")
sys.exit(2)
return args return args
@ -1851,24 +1941,43 @@ def setup_arguments():
def main(): def main():
global version global version
git_version = check_version_with_git(version) git_version = check_version_with_git(version)
print("{} Patcher v{} ({}) (ff {}) executing".format( allversions = "Patcher v{} ({}) (ff {})".format(
projectName, git_version if git_version else version, script_version, fontforge.version())) git_version if git_version else version, script_version, fontforge.version())
print("{} {}".format(projectName, allversions))
if git_version: if git_version:
version = git_version version = git_version
check_fontforge_min_version() check_fontforge_min_version()
args = setup_arguments() args = setup_arguments()
global logger
logger = logging.getLogger(os.path.basename(args.font))
logger.setLevel(logging.DEBUG)
f_handler = logging.FileHandler('font-patcher-log.txt')
f_handler.setFormatter(logging.Formatter('%(levelname)s: %(name)s %(message)s'))
logger.addHandler(f_handler)
logger.debug(allversions)
logger.debug("Options %s", repr(sys.argv[1:]))
c_handler = logging.StreamHandler(stream=sys.stdout)
c_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
if not args.debugmode:
c_handler.setLevel(logging.INFO)
logger.addHandler(c_handler)
logger.debug("Naming mode %d", args.makegroups)
patcher = font_patcher(args) patcher = font_patcher(args)
sourceFonts = [] sourceFonts = []
all_fonts = fontforge.fontsInFile(args.font) all_fonts = fontforge.fontsInFile(args.font)
for i, subfont in enumerate(all_fonts): for i, subfont in enumerate(all_fonts):
if len(all_fonts) > 1: if len(all_fonts) > 1:
print("\n{}: Processing {} ({}/{})".format(projectName, subfont, i + 1, len(all_fonts))) print("\n")
logger.info("Processing %s (%d/%d)", subfont, i + 1, len(all_fonts))
try: try:
sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",)) sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",))
except Exception: except Exception:
sys.exit("{}: Can not open font '{}', try to open with fontforge interactively to get more information".format( logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information",
projectName, subfont)) subfont)
sys.exit(1)
patcher.patch(sourceFonts[-1]) patcher.patch(sourceFonts[-1])
@ -1877,13 +1986,6 @@ def main():
patcher.setup_font_names(f) patcher.setup_font_names(f)
patcher.generate(sourceFonts) patcher.generate(sourceFonts)
# This mainly helps to improve CI runtime
if patcher.args.alsowindows:
patcher.args.windows = True
for f in sourceFonts:
patcher.setup_font_names(f)
patcher.generate(sourceFonts)
for f in sourceFonts: for f in sourceFonts:
f.close() f.close()

1
original/.gitignore vendored
View File

@ -1,3 +1,2 @@
*.ttf *.ttf
*.ttx
*.zip *.zip

View File

@ -9,11 +9,6 @@ if ! command -v fontforge >/dev/null; then
exit 1 exit 1
fi fi
# if ! command -v ttx >/dev/null; then
# printf "\033[1;31mfonttools\033[0m is not installed.\n"
# exit 1
# fi
if [[ $# -eq 0 ]]; then if [[ $# -eq 0 ]]; then
versions=$(curl -H "Accept: application/vnd.github.v3+json" -s https://api.github.com/repos/be5invis/Iosevka/releases | jq -r '.[] | .tag_name' | sed -e 's/^v//g') versions=$(curl -H "Accept: application/vnd.github.v3+json" -s https://api.github.com/repos/be5invis/Iosevka/releases | jq -r '.[] | .tag_name' | sed -e 's/^v//g')
version=$(echo "${versions}" | fzf --no-multi --prompt "Release: ") version=$(echo "${versions}" | fzf --no-multi --prompt "Release: ")
@ -32,28 +27,16 @@ variants=(
zipfile="original/ttf-iosevka-term-${version}.zip" zipfile="original/ttf-iosevka-term-${version}.zip"
if [ ! -f "${zipfile}" ]; then if [ ! -f "${zipfile}" ]; then
printf "\033[1;34mDownloading Iosevka Term version \033[1;31m%s\033[1;34m zip file ...\033[0m\n" "${version}" printf "\033[1;34mDownloading Iosevka Term version \033[1;31m%s\033[1;34m zip file ...\033[0m\n" "${version}"
curl -fSL https://github.com/be5invis/Iosevka/releases/download/v${version}/ttf-iosevka-term-${version}.zip -o ${zipfile} curl -fSL "https://github.com/be5invis/Iosevka/releases/download/v${version}/ttf-iosevka-term-${version}.zip" -o "${zipfile}"
fi fi
printf "\033[1;34mUnzipping the downloaded archive ...\033[0m\n" printf "\033[1;34mUnzipping the downloaded archive ...\033[0m\n"
unzip ${zipfile} -d ./original unzip "${zipfile}" -d ./original
for variant in "${variants[@]}"; do for variant in "${variants[@]}"; do
printf "\033[1;34mPatching Iosevka term \033[1;31m%s\033[1;34m ...\033[0m\n" "${variant}" printf "\033[1;34mPatching Iosevka term \033[1;31m%s\033[1;34m ...\033[0m\n" "${variant}"
# Run the font-patcher script # Run the font-patcher script
fontforge -script ./font-patcher --quiet --no-progressbars --careful --complete ./original/iosevka-term-${variant}.ttf fontforge -script ./font-patcher --careful --complete ./original/"iosevka-term-${variant}.ttf"
mv -f ./*Complete.ttf ./patched/iosevka-term-${variant}-nerd-font.ttf mv -fv ./IosevkaTermNerdFont-*.ttf ./patched/"iosevka-term-${variant}-nerd-font.ttf"
# Correct xAvgCharWidth
# ttx -t "OS/2" ./original/iosevka-term-${variant}.ttf
# ttx -t "OS/2" ./patched/iosevka-term-${variant}-nerd-font.ttf
# original_x_avg_char_width=$(grep xAvgCharWidth ./original/iosevka-term-${variant}.ttx | cut -d '"' -f 2)
# sed -i "s/xAvgCharWidth value=\"[0-9]\+\"/xAvgCharWidth value=\"${original_x_avg_char_width}\"/g" ./patched/iosevka-term-${variant}-nerd-font.ttx
# mv -f ./patched/iosevka-term-${variant}-nerd-font.ttf ./patched/iosevka-term-${variant}-nerd-font.original.ttf
# ttx -o ./patched/iosevka-term-${variant}-nerd-font.ttf -m ./patched/iosevka-term-${variant}-nerd-font.original.ttf ./patched/iosevka-term-${variant}-nerd-font.ttx
# Build hdmx table and correct TTF font family name
#python3 ./build-hdmx-for-sarasa.py ./patched/iosevka-term-${variant}-nerd-font.ttf
#python3 ./correct-ttf-font-family-name.py ./patched/iosevka-term-${variant}-nerd-font.ttf
done done

1
patched/.gitignore vendored
View File

@ -1,2 +1 @@
*.ttf *.ttf
*.ttx

Binary file not shown.

Binary file not shown.

View File

@ -4,16 +4,17 @@ echo "Downloading 'font-patcher' script..."
curl -fsSL https://github.com/ryanoasis/nerd-fonts/raw/master/font-patcher -o ./font-patcher curl -fsSL https://github.com/ryanoasis/nerd-fonts/raw/master/font-patcher -o ./font-patcher
chmod 755 ./font-patcher chmod 755 ./font-patcher
for path in codicons font-awesome powerline-symbols weather-icons; do for path in codicons font-awesome materialdesign octicons powerline-symbols weather-icons; do
mkdir -p ./src/glyphs/"$path" mkdir -p ./src/glyphs/"$path"
done done
mkdir -p ./bin/scripts/name_parser
echo "Downloading glyph fonts..."
glyphs=( glyphs=(
"codicons/codicon.ttf" "codicons/codicon.ttf"
"font-awesome/FontAwesome.otf" "font-awesome/FontAwesome.otf"
"materialdesign/MaterialDesignIconsDesktop.ttf" "materialdesign/MaterialDesignIconsDesktop.ttf"
"materialdesign/MaterialDesignIconsDesktop_orig.ttf" "materialdesign/MaterialDesignIconsDesktop_orig.ttf"
"octicons/octicons.ttf"
"powerline-symbols/PowerlineSymbols.otf" "powerline-symbols/PowerlineSymbols.otf"
"weather-icons/weathericons-regular-webfont.ttf" "weather-icons/weathericons-regular-webfont.ttf"
"Pomicons.otf" "Pomicons.otf"
@ -24,16 +25,33 @@ glyphs=(
"font-awesome-extension.ttf" "font-awesome-extension.ttf"
"font-logos.ttf" "font-logos.ttf"
"materialdesignicons-webfont.ttf" "materialdesignicons-webfont.ttf"
"octicons.ttf"
"original-source.otf" "original-source.otf"
) )
upstream_src_glyphs_url="https://github.com/ryanoasis/nerd-fonts/raw/master/src/glyphs" name_parser=(
"FontnameParser.py"
"FontnameTools.py"
"query_monospace"
"query_names"
"query_panose"
"query_sftn"
"query_version"
)
upstream_src_glyphs_url="https://github.com/ryanoasis/nerd-fonts/raw/master/src/glyphs"
upstream_name_parser_url="https://github.com/ryanoasis/nerd-fonts/raw/master/bin/scripts/name_parser"
echo "Downloading glyph fonts..."
for glyph in "${glyphs[@]}"; do for glyph in "${glyphs[@]}"; do
# replace all `whitespace` characters with `%20` # replace all `whitespace` characters with `%20`
percent_encoded_uri="${upstream_src_glyphs_url}/${glyph//\ /%20}" percent_encoded_uri="${upstream_src_glyphs_url}/${glyph//\ /%20}"
curl -fSL ${percent_encoded_uri} -o "src/glyphs/${glyph}" curl -fSL "${percent_encoded_uri}" -o "src/glyphs/${glyph}"
done done
find ./src/glyphs/ -type f -exec chmod 644 '{}' \; find ./src/glyphs/ -type f -exec chmod 644 '{}' \;
echo "Downloading helper scripts for font-patcher ..."
for file in "${name_parser[@]}"; do
curl -fSL "${upstream_name_parser_url}/${file}" -o "bin/scripts/name_parser/${file}"
done
find ./bin/scripts/name_parser/ -type f -exec chmod 644 '{}' \;