Keep things up-to-date with NerdFonts 3.0.0 release
This commit is contained in:
parent
6322fa255b
commit
43b872a6ce
|
@ -0,0 +1,2 @@
|
|||
/font-patcher-log.txt
|
||||
__pycache__/
|
14
Makefile
14
Makefile
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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()
|
|
@ -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]))
|
||||
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
380
font-patcher
380
font-patcher
|
@ -1,14 +1,14 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf8
|
||||
# Nerd Fonts Version: 2.3.3
|
||||
# Nerd Fonts Version: 3.0.0
|
||||
# Script version is further down
|
||||
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
# Change the script version when you edit this script:
|
||||
script_version = "3.6.1"
|
||||
script_version = "4.1.1"
|
||||
|
||||
version = "2.3.3"
|
||||
version = "3.0.0"
|
||||
projectName = "Nerd Fonts"
|
||||
projectNameAbbreviation = "NF"
|
||||
projectNameSingular = projectName[:-1]
|
||||
|
@ -22,6 +22,7 @@ import errno
|
|||
import subprocess
|
||||
import json
|
||||
from enum import Enum
|
||||
import logging
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
|
@ -135,6 +136,7 @@ class TableHEADWriter:
|
|||
positions = {'checksumAdjustment': 2+2+4,
|
||||
'flags': 2+2+4+4+4,
|
||||
'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2,
|
||||
'avgWidth': 2,
|
||||
}
|
||||
where = self.tab_offset + positions[where]
|
||||
self.f.seek(where)
|
||||
|
@ -238,10 +240,10 @@ def force_panose_monospaced(font):
|
|||
panose = list(font.os2_panose)
|
||||
if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default)
|
||||
panose[0] = 2 # make kind latin text and display
|
||||
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)
|
||||
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
|
||||
font.os2_panose = tuple(panose)
|
||||
|
||||
|
@ -282,6 +284,35 @@ def get_btb_metrics(font):
|
|||
win_btb = win_height + win_gap
|
||||
return (hhea_btb, typo_btb, win_btb, win_gap)
|
||||
|
||||
def get_old_average_x_width(font):
|
||||
""" Determine xAvgCharWidth of the OS/2 table """
|
||||
# Fontforge can not create fonts with old (i.e. prior to OS/2 version 3)
|
||||
# table values, but some very old applications do need them sometimes
|
||||
# https://learn.microsoft.com/en-us/typography/opentype/spec/os2#xavgcharwidth
|
||||
s = 0
|
||||
weights = {
|
||||
'a': 64, 'b': 14, 'c': 27, 'd': 35, 'e': 100, 'f': 20, 'g': 14, 'h': 42, 'i': 63,
|
||||
'j': 3, 'k': 6, 'l': 35, 'm': 20, 'n': 56, 'o': 56, 'p': 17, 'q': 4, 'r': 49,
|
||||
's': 56, 't': 71, 'u': 31, 'v': 10, 'w': 18, 'x': 3, 'y': 18, 'z': 2, 32: 166,
|
||||
}
|
||||
for g in weights:
|
||||
if g not in font:
|
||||
logger.critical("Can not determine ancient style xAvgCharWidth")
|
||||
sys.exit(1)
|
||||
s += font[g].width * weights[g]
|
||||
return int(s / 1000)
|
||||
|
||||
def create_filename(fonts):
|
||||
""" Determine filename from font object(s) """
|
||||
sfnt = { k: v for l, k, v in fonts[0].sfnt_names }
|
||||
sfnt_pfam = sfnt.get('Preferred Family', sfnt['Family'])
|
||||
sfnt_psubfam = sfnt.get('Preferred Styles', sfnt['SubFamily'])
|
||||
if len(fonts) > 1:
|
||||
return sfnt_pfam
|
||||
if len(sfnt_psubfam) > 0:
|
||||
sfnt_psubfam = '-' + sfnt_psubfam
|
||||
return (sfnt_pfam + sfnt_psubfam).replace(' ', '')
|
||||
|
||||
|
||||
class font_patcher:
|
||||
def __init__(self, args):
|
||||
|
@ -293,9 +324,11 @@ class font_patcher:
|
|||
self.font_dim = None # class 'dict'
|
||||
self.font_extrawide = False
|
||||
self.source_monospaced = None # Later True or False
|
||||
self.symbolsonly = False
|
||||
self.onlybitmaps = 0
|
||||
self.essential = set()
|
||||
self.config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True)
|
||||
self.xavgwidth = [] # list of ints
|
||||
|
||||
def patch(self, font):
|
||||
self.sourceFont = font
|
||||
|
@ -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
|
||||
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
|
||||
|
||||
# Prevent opening and closing the fontforge font. Makes things faster when patching
|
||||
|
@ -326,8 +359,12 @@ class font_patcher:
|
|||
symfont = None
|
||||
|
||||
if not os.path.isdir(self.args.glyphdir):
|
||||
sys.exit("{}: Can not find symbol glyph directory {} "
|
||||
"(probably you need to download the src/glyphs/ directory?)".format(projectName, self.args.glyphdir))
|
||||
logger.critical("Can not find symbol glyph directory %s "
|
||||
"(probably you need to download the src/glyphs/ directory?)", self.args.glyphdir)
|
||||
sys.exit(1)
|
||||
|
||||
if self.args.dry_run:
|
||||
return
|
||||
|
||||
for patch in self.patch_set:
|
||||
if patch['Enabled']:
|
||||
|
@ -337,11 +374,13 @@ class font_patcher:
|
|||
symfont.close()
|
||||
symfont = None
|
||||
if not os.path.isfile(self.args.glyphdir + patch['Filename']):
|
||||
sys.exit("{}: Can not find symbol source for '{}'\n{:>{}} (i.e. {})".format(
|
||||
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename']))
|
||||
logger.critical("Can not find symbol source for '%s' (i.e. %s)",
|
||||
patch['Name'], self.args.glyphdir + patch['Filename'])
|
||||
sys.exit(1)
|
||||
if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK):
|
||||
sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format(
|
||||
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename']))
|
||||
logger.critical("Can not open symbol source for '%s' (i.e. %s)",
|
||||
patch['Name'], self.args.glyphdir + patch['Filename'])
|
||||
sys.exit(1)
|
||||
symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename']))
|
||||
symfont.encoding = 'UnicodeFull'
|
||||
|
||||
|
@ -383,11 +422,11 @@ class font_patcher:
|
|||
break
|
||||
outfile = os.path.normpath(os.path.join(
|
||||
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)
|
||||
message = " Generated {} fonts\n \===> '{}'".format(len(sourceFonts), outfile)
|
||||
else:
|
||||
fontname = sourceFont.fullname
|
||||
fontname = create_filename(sourceFonts)
|
||||
if not fontname:
|
||||
fontname = sourceFont.cidfontname
|
||||
outfile = os.path.normpath(os.path.join(
|
||||
|
@ -395,9 +434,11 @@ class font_patcher:
|
|||
sanitize_filename(fontname) + self.args.extension))
|
||||
bitmaps = str()
|
||||
if len(self.sourceFont.bitmapSizes):
|
||||
if not self.args.quiet:
|
||||
print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
|
||||
logger.debug("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
|
||||
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)
|
||||
message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile)
|
||||
|
||||
|
@ -407,23 +448,34 @@ class font_patcher:
|
|||
source_font = TableHEADWriter(self.args.font)
|
||||
dest_font = TableHEADWriter(outfile)
|
||||
for idx in range(source_font.num_fonts):
|
||||
if not self.args.quiet:
|
||||
print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts))
|
||||
logger.debug("Tweaking %d/%d", 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)
|
||||
dest_font.find_head_table(idx)
|
||||
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
|
||||
if not self.args.quiet:
|
||||
print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08))
|
||||
logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08)
|
||||
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
|
||||
if source_font.lowppem != dest_font.lowppem:
|
||||
if not self.args.quiet:
|
||||
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
|
||||
logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem)
|
||||
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
|
||||
if dest_font.modified:
|
||||
dest_font.reset_table_checksum()
|
||||
dest_font.reset_full_checksum()
|
||||
if dest_font.modified:
|
||||
dest_font.reset_full_checksum()
|
||||
except Exception as error:
|
||||
print("Can not handle font flags ({})".format(repr(error)))
|
||||
logger.error("Can not handle font flags (%s)", repr(error))
|
||||
finally:
|
||||
try:
|
||||
source_font.close()
|
||||
|
@ -431,12 +483,13 @@ class font_patcher:
|
|||
except:
|
||||
pass
|
||||
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)
|
||||
|
||||
if self.args.postprocess:
|
||||
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):
|
||||
|
@ -454,11 +507,8 @@ class font_patcher:
|
|||
font.fullname = font.persistent["fullname"]
|
||||
if isinstance(font.persistent["familyname"], str):
|
||||
font.familyname = font.persistent["familyname"]
|
||||
verboseAdditionalFontNameSuffix = " " + projectNameSingular
|
||||
if self.args.windows: # attempt to shorten here on the additional name BEFORE trimming later
|
||||
additionalFontNameSuffix = " " + projectNameAbbreviation
|
||||
else:
|
||||
additionalFontNameSuffix = verboseAdditionalFontNameSuffix
|
||||
verboseAdditionalFontNameSuffix = ""
|
||||
additionalFontNameSuffix = ""
|
||||
if not self.args.complete:
|
||||
# NOTE not all symbol fonts have appended their suffix here
|
||||
if self.args.fontawesome:
|
||||
|
@ -489,17 +539,24 @@ class font_patcher:
|
|||
additionalFontNameSuffix += " WEA"
|
||||
verboseAdditionalFontNameSuffix += " Plus Weather Icons"
|
||||
|
||||
# if all source glyphs included simplify the name
|
||||
else:
|
||||
additionalFontNameSuffix = " " + projectNameSingular + " Complete"
|
||||
verboseAdditionalFontNameSuffix = " " + projectNameSingular + " Complete"
|
||||
|
||||
# add mono signifier to end of name
|
||||
# add mono signifier to beginning of name suffix
|
||||
if self.args.single:
|
||||
additionalFontNameSuffix += " M"
|
||||
verboseAdditionalFontNameSuffix += " Mono"
|
||||
variant_abbrev = "M"
|
||||
variant_full = " Mono"
|
||||
elif self.args.nonmono and not self.symbolsonly:
|
||||
variant_abbrev = "P"
|
||||
variant_full = " Propo"
|
||||
else:
|
||||
variant_abbrev = ""
|
||||
variant_full = ""
|
||||
|
||||
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 if it is 'equal' to the fontname
|
||||
if font.fullname:
|
||||
|
@ -511,12 +568,14 @@ class font_patcher:
|
|||
# Gohu fontnames hide the weight, but the file names are ok...
|
||||
if parser_name.startswith('Gohu'):
|
||||
parser_name = os.path.splitext(os.path.basename(self.args.font))[0]
|
||||
n = FontnameParser(parser_name)
|
||||
n = FontnameParser(parser_name, logger)
|
||||
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.enable_short_families(True, "Noto")
|
||||
n.set_for_windows(self.args.windows)
|
||||
n.enable_short_families(True, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ])
|
||||
if not n.set_expect_no_italic(self.args.noitalic):
|
||||
logger.critical("Detected 'Italic' slant but --has-no-italic specified")
|
||||
sys.exit(1)
|
||||
|
||||
# All the following stuff is ignored in makegroups-mode
|
||||
|
||||
|
@ -564,23 +623,7 @@ class font_patcher:
|
|||
if len(subFamily) == 0:
|
||||
subFamily = "Regular"
|
||||
|
||||
if self.args.windows:
|
||||
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"
|
||||
familyname += " " + projectNameSingular + variant_full
|
||||
|
||||
# Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with
|
||||
# the same name as the same font, even if subFamily is different. Make sure to
|
||||
|
@ -593,6 +636,10 @@ class font_patcher:
|
|||
reservedFontNameReplacements = {
|
||||
'source' : 'sauce',
|
||||
'Source' : 'Sauce',
|
||||
'Bitstream Vera Sans Mono' : 'Bitstrom Wera',
|
||||
'BitstreamVeraSansMono' : 'BitstromWera',
|
||||
'bitstream vera sans mono' : 'bitstrom wera',
|
||||
'bitstreamverasansmono' : 'bitstromwera',
|
||||
'hermit' : 'hurmit',
|
||||
'Hermit' : 'Hurmit',
|
||||
'hasklig' : 'hasklug',
|
||||
|
@ -659,7 +706,7 @@ class font_patcher:
|
|||
fullname = replace_font_name(fullname, 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:
|
||||
font.familyname = " ".join(familyname.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('SubFamily'), subFamily)
|
||||
else:
|
||||
fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation
|
||||
if self.args.single:
|
||||
if self.args.windows:
|
||||
fam_suffix += 'M'
|
||||
else:
|
||||
fam_suffix += ' Mono'
|
||||
n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix)
|
||||
short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full
|
||||
# inject_suffix(family, ps_fontname, short_family)
|
||||
n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family)
|
||||
n.rename_font(font)
|
||||
|
||||
font.comment = projectInfo
|
||||
|
@ -692,6 +735,7 @@ class font_patcher:
|
|||
self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version
|
||||
self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge
|
||||
self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version)
|
||||
# The Version SFNT name is later reused by the NameParser for UniqueID
|
||||
# print("Version now is {}".format(sourceFont.version))
|
||||
|
||||
|
||||
|
@ -700,17 +744,17 @@ class font_patcher:
|
|||
# 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.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"))
|
||||
for subtable in ligature_subtables:
|
||||
print("Removing subtable:", subtable)
|
||||
logger.debug("Removing subtable: %s", subtable)
|
||||
try:
|
||||
self.sourceFont.removeLookupSubtable(subtable)
|
||||
print("Successfully removed subtable:", subtable)
|
||||
logger.debug("Successfully removed subtable: %s", subtable)
|
||||
except Exception:
|
||||
print("Failed to remove subtable:", subtable)
|
||||
logger.error("Failed to remove subtable: %s", subtable)
|
||||
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):
|
||||
|
@ -722,16 +766,17 @@ class font_patcher:
|
|||
panose_mono = check_panose_monospaced(self.sourceFont)
|
||||
# The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown'
|
||||
if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1):
|
||||
print(" Warning: Monospaced check: Panose assumed to be wrong")
|
||||
print(" {} and {}".format(
|
||||
logger.warning("Monospaced check: Panose assumed to be wrong")
|
||||
logger.warning(" %s and %s",
|
||||
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:
|
||||
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:
|
||||
print(" Offending char: 0x{:X}".format(offending_char))
|
||||
logger.warning(" Offending char: %X", offending_char)
|
||||
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:
|
||||
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 """
|
||||
|
||||
box_enabled = self.source_monospaced # Box glyph only for monospaced
|
||||
box_keep = False
|
||||
if box_enabled:
|
||||
self.sourceFont.selection.select(("ranges",), 0x2500, 0x259f)
|
||||
box_glyphs_target = len(list(self.sourceFont.selection))
|
||||
box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs))
|
||||
if box_glyphs_target > box_glyphs_current:
|
||||
# Sourcefont does not have all of these glyphs, do not mix sets
|
||||
if not self.args.quiet and box_glyphs_current > 0:
|
||||
print("INFO: {}/{} box drawing glyphs will be replaced".format(
|
||||
box_glyphs_current, box_glyphs_target))
|
||||
box_keep = False
|
||||
# Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing)
|
||||
if box_glyphs_current > 0:
|
||||
logger.debug("%d/%d box drawing glyphs will be replaced",
|
||||
box_glyphs_current, box_glyphs_target)
|
||||
box_enabled = True
|
||||
else:
|
||||
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
|
||||
|
||||
# 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.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.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.weather, 'Name': "Weather Icons", 'Filename': "weather-icons/weathericons-regular-webfont.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF0EB, 'SrcStart': 0xE300, 'ScaleRules': WEATH_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT},
|
||||
{'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF32F, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT},
|
||||
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons.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.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': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass
|
||||
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart
|
||||
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap
|
||||
{'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.ttf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 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.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
|
||||
if our_btb == hhea_btb:
|
||||
metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font
|
||||
elif abs(our_btb - hhea_btb) / our_btb < 0.03:
|
||||
logger.info("Font vertical metrics slightly off (%.1f%)", (our_btb - hhea_btb) / our_btb * 100.0)
|
||||
metrics = Metric.TYPO if use_typo else Metric.WIN
|
||||
else:
|
||||
# We trust the WIN metric more, see experiments in #1056
|
||||
print("{}: WARNING Font vertical metrics inconsistent (HHEA {} / TYPO {} / WIN {}), using WIN".format(projectName, hhea_btb, typo_btb, win_btb))
|
||||
our_btb = win_btb
|
||||
metrics = Metric.WIN
|
||||
# Try the other metric
|
||||
our_btb = typo_btb if not use_typo else win_btb
|
||||
if our_btb == hhea_btb:
|
||||
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))
|
||||
|
||||
|
@ -1094,6 +1151,7 @@ class font_patcher:
|
|||
if self.font_dim['height'] == 0:
|
||||
# This can only happen if the input font is empty
|
||||
# Assume we are using our prepared templates
|
||||
self.symbolsonly = True
|
||||
self.font_dim = {
|
||||
'xmin' : 0,
|
||||
'ymin' : -self.sourceFont.descent,
|
||||
|
@ -1104,7 +1162,8 @@ class font_patcher:
|
|||
}
|
||||
our_btb = self.sourceFont.descent + self.sourceFont.ascent
|
||||
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
|
||||
self.sourceFont.os2_typolinegap = 0
|
||||
|
@ -1118,12 +1177,13 @@ class font_patcher:
|
|||
self.sourceFont.os2_use_typo_metrics = 1
|
||||
(check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont)
|
||||
if check_hhea_btb != check_typo_btb or check_typo_btb != check_win_btb or check_win_btb != our_btb:
|
||||
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
|
||||
# Find the biggest char width and advance width
|
||||
# 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
|
||||
for glyph in range(0x21, 0x17f):
|
||||
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:
|
||||
self.font_dim['width'] = self.sourceFont[glyph].width
|
||||
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(
|
||||
report_advance_widths(self.sourceFont)))
|
||||
logger.debug("Extended glyphs wider than basic glyphs, results might be useless\n %s",
|
||||
report_advance_widths(self.sourceFont))
|
||||
warned1 = True
|
||||
# print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
|
||||
if xmax > self.font_dim['xmax']:
|
||||
self.font_dim['xmax'] = xmax
|
||||
if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
|
||||
print("Info: Extended glyphs wider bounding box than basic glyphs")
|
||||
logger.debug("Extended glyphs wider bounding box than basic glyphs")
|
||||
warned2 = True
|
||||
# print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
|
||||
if self.font_dim['width'] < self.font_dim['xmax']:
|
||||
if not self.args.quiet:
|
||||
print("Warning: Font has negative right side bearing in extended glyphs")
|
||||
logger.debug("Font has negative right side bearing in extended glyphs")
|
||||
self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used
|
||||
# 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):
|
||||
""" Get the target width (1 or 2 'cell') for a given stretch parameter """
|
||||
|
@ -1249,7 +1312,7 @@ class font_patcher:
|
|||
if sym_glyph.altuni:
|
||||
possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ]
|
||||
if len(possible_codes) == 0:
|
||||
print(" Can not determine codepoint of {:X}. Skipping...".format(sym_glyph.unicode))
|
||||
logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode)
|
||||
continue
|
||||
currentSourceFontGlyph = min(possible_codes)
|
||||
else:
|
||||
|
@ -1272,9 +1335,8 @@ class font_patcher:
|
|||
# check if a glyph already exists in this location
|
||||
if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential:
|
||||
if currentSourceFontGlyph in self.sourceFont:
|
||||
if not self.args.quiet:
|
||||
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
|
||||
print(" Found {} Glyph at {:X}. Skipping...".format(careful_type, currentSourceFontGlyph))
|
||||
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
|
||||
logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph)
|
||||
# We don't want to touch anything so move to next Glyph
|
||||
continue
|
||||
else:
|
||||
|
@ -1422,8 +1484,8 @@ class font_patcher:
|
|||
if self.args.single:
|
||||
(xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox()
|
||||
if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)):
|
||||
print("\n Warning: Scaled glyph U+{:X} wider than one monospace width ({} / {} (overlap {}))".format(
|
||||
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap))
|
||||
logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %f))",
|
||||
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)
|
||||
|
||||
# end for
|
||||
|
||||
|
@ -1564,7 +1626,7 @@ def half_gap(gap, top):
|
|||
gap_top = int(gap / 2)
|
||||
gap_bottom = gap - gap_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_bottom
|
||||
|
||||
|
@ -1689,8 +1751,8 @@ def check_fontforge_min_version():
|
|||
|
||||
# versions tested: 20150612, 20150824
|
||||
if actualVersion < minimumVersion:
|
||||
sys.stderr.write("{}: You seem to be using an unsupported (old) version of fontforge: {}\n".format(projectName, actualVersion))
|
||||
sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion))
|
||||
logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion)
|
||||
logger.critical("Please use at least version: %d", minimumVersion)
|
||||
sys.exit(1)
|
||||
|
||||
def check_version_with_git(version):
|
||||
|
@ -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('-l', '--adjust-line-height', dest='adjustLineHeight', default=False, action='store_true', help='Whether to adjust line heights (attempt to center powerline separators more evenly)')
|
||||
parser.add_argument('-q', '--quiet', '--shutup', dest='quiet', default=False, action='store_true', help='Do not generate verbose output')
|
||||
parser.add_argument('-w', '--windows', dest='windows', default=False, action='store_true', help='Limit the internal font name to 31 characters (for Windows compatibility)')
|
||||
parser.add_argument('-c', '--complete', dest='complete', default=False, action='store_true', help='Add all available Glyphs')
|
||||
parser.add_argument('--careful', dest='careful', default=False, action='store_true', help='Do not overwrite existing glyphs if detected')
|
||||
parser.add_argument('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specificed in JSON configuration file')
|
||||
|
@ -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('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to')
|
||||
parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching')
|
||||
parser.add_argument('--makegroups', dest='makegroups', default=False, action='store_true', help='Use alternative method to name patched fonts (experimental)')
|
||||
parser.add_argument('--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('--has-no-italic', dest='noitalic', default=False, action='store_true', help='Font family does not have Italic (but Oblique)')
|
||||
|
||||
# progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
|
||||
progressbars_group_parser = parser.add_mutually_exclusive_group(required=False)
|
||||
progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set')
|
||||
progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set (default)')
|
||||
progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set')
|
||||
parser.set_defaults(progressbars=True)
|
||||
parser.add_argument('--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
|
||||
sym_font_group = parser.add_argument_group('Symbol Fonts')
|
||||
|
@ -1774,8 +1854,9 @@ def setup_arguments():
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.makegroups and not FontnameParserOK:
|
||||
sys.exit("{}: FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName))
|
||||
if args.makegroups > 0 and not FontnameParserOK:
|
||||
logger.critical("FontnameParser module missing (bin/scripts/name_parser/Fontname*), specify --makegroups 0")
|
||||
sys.exit(1)
|
||||
|
||||
# if you add a new font, set it to True here inside the if condition
|
||||
if args.complete:
|
||||
|
@ -1808,24 +1889,23 @@ def setup_arguments():
|
|||
font_complete = False
|
||||
args.complete = font_complete
|
||||
|
||||
if args.alsowindows:
|
||||
args.windows = False
|
||||
|
||||
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
|
||||
|
||||
make_sure_path_exists(args.outputdir)
|
||||
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):
|
||||
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
|
||||
try:
|
||||
source_font_test = TableHEADWriter(args.font)
|
||||
args.is_variable = source_font_test.find_table([b'avar', b'cvar', b'fvar', b'gvarb', b'HVAR', b'MVAR', b'VVAR'], 0)
|
||||
if args.is_variable:
|
||||
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:
|
||||
args.is_variable = False
|
||||
finally:
|
||||
|
@ -1840,10 +1920,20 @@ def setup_arguments():
|
|||
args.extension = '.' + args.extension
|
||||
if re.match("\.ttc$", args.extension, re.IGNORECASE):
|
||||
if not is_ttc:
|
||||
sys.exit(projectName + ": Can not create True Type Collections from single font files")
|
||||
logging.critical("Can not create True Type Collections from single font files")
|
||||
sys.exit(1)
|
||||
else:
|
||||
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
|
||||
|
||||
|
@ -1851,24 +1941,43 @@ def setup_arguments():
|
|||
def main():
|
||||
global version
|
||||
git_version = check_version_with_git(version)
|
||||
print("{} Patcher v{} ({}) (ff {}) executing".format(
|
||||
projectName, git_version if git_version else version, script_version, fontforge.version()))
|
||||
allversions = "Patcher v{} ({}) (ff {})".format(
|
||||
git_version if git_version else version, script_version, fontforge.version())
|
||||
print("{} {}".format(projectName, allversions))
|
||||
if git_version:
|
||||
version = git_version
|
||||
check_fontforge_min_version()
|
||||
args = setup_arguments()
|
||||
|
||||
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)
|
||||
|
||||
sourceFonts = []
|
||||
all_fonts = fontforge.fontsInFile(args.font)
|
||||
for i, subfont in enumerate(all_fonts):
|
||||
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:
|
||||
sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",))
|
||||
except Exception:
|
||||
sys.exit("{}: Can not open font '{}', try to open with fontforge interactively to get more information".format(
|
||||
projectName, subfont))
|
||||
logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information",
|
||||
subfont)
|
||||
sys.exit(1)
|
||||
|
||||
patcher.patch(sourceFonts[-1])
|
||||
|
||||
|
@ -1877,13 +1986,6 @@ def main():
|
|||
patcher.setup_font_names(f)
|
||||
patcher.generate(sourceFonts)
|
||||
|
||||
# This mainly helps to improve CI runtime
|
||||
if patcher.args.alsowindows:
|
||||
patcher.args.windows = True
|
||||
for f in sourceFonts:
|
||||
patcher.setup_font_names(f)
|
||||
patcher.generate(sourceFonts)
|
||||
|
||||
for f in sourceFonts:
|
||||
f.close()
|
||||
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
*.ttf
|
||||
*.ttx
|
||||
*.zip
|
||||
|
|
|
@ -9,11 +9,6 @@ if ! command -v fontforge >/dev/null; then
|
|||
exit 1
|
||||
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
|
||||
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: ")
|
||||
|
@ -32,28 +27,16 @@ variants=(
|
|||
zipfile="original/ttf-iosevka-term-${version}.zip"
|
||||
if [ ! -f "${zipfile}" ]; then
|
||||
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
|
||||
|
||||
printf "\033[1;34mUnzipping the downloaded archive ...\033[0m\n"
|
||||
unzip ${zipfile} -d ./original
|
||||
unzip "${zipfile}" -d ./original
|
||||
|
||||
for variant in "${variants[@]}"; do
|
||||
printf "\033[1;34mPatching Iosevka term \033[1;31m%s\033[1;34m ...\033[0m\n" "${variant}"
|
||||
|
||||
# Run the font-patcher script
|
||||
fontforge -script ./font-patcher --quiet --no-progressbars --careful --complete ./original/iosevka-term-${variant}.ttf
|
||||
mv -f ./*Complete.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
|
||||
fontforge -script ./font-patcher --careful --complete ./original/"iosevka-term-${variant}.ttf"
|
||||
mv -fv ./IosevkaTermNerdFont-*.ttf ./patched/"iosevka-term-${variant}-nerd-font.ttf"
|
||||
done
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
*.ttf
|
||||
*.ttx
|
||||
|
|
Binary file not shown.
Binary file not shown.
28
update.sh
28
update.sh
|
@ -4,16 +4,17 @@ echo "Downloading 'font-patcher' script..."
|
|||
curl -fsSL https://github.com/ryanoasis/nerd-fonts/raw/master/font-patcher -o ./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"
|
||||
done
|
||||
mkdir -p ./bin/scripts/name_parser
|
||||
|
||||
echo "Downloading glyph fonts..."
|
||||
glyphs=(
|
||||
"codicons/codicon.ttf"
|
||||
"font-awesome/FontAwesome.otf"
|
||||
"materialdesign/MaterialDesignIconsDesktop.ttf"
|
||||
"materialdesign/MaterialDesignIconsDesktop_orig.ttf"
|
||||
"octicons/octicons.ttf"
|
||||
"powerline-symbols/PowerlineSymbols.otf"
|
||||
"weather-icons/weathericons-regular-webfont.ttf"
|
||||
"Pomicons.otf"
|
||||
|
@ -24,16 +25,33 @@ glyphs=(
|
|||
"font-awesome-extension.ttf"
|
||||
"font-logos.ttf"
|
||||
"materialdesignicons-webfont.ttf"
|
||||
"octicons.ttf"
|
||||
"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
|
||||
# replace all `whitespace` characters with `%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
|
||||
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 '{}' \;
|
||||
|
|
Loading…
Reference in New Issue