parent
a3dae75e4e
commit
fd02872152
|
@ -1,5 +1,6 @@
|
|||
# https://github.com/ryanoasis/nerd-fonts
|
||||
vendor/font-patcher linguist-vendored
|
||||
vendor/bin/** linguist-vendored
|
||||
vendor/glyphs/** filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
private-build-plans.toml linguist-detectable
|
||||
|
|
2
Iosevka
2
Iosevka
|
@ -1 +1 @@
|
|||
Subproject commit b474ea733417fa68b9436560ae86e8540f4236c7
|
||||
Subproject commit 319931ccecace2f08b538aaf9ddc2f55fd9f0736
|
|
@ -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,71 @@
|
|||
## Creating Consistently Grouped Patched Fonts
|
||||
|
||||
This is a small sub-project to font-patcher that uses a little bit more knowledge
|
||||
to come up with font names and name parts. In applications multiple fonts are grouped
|
||||
under a 'Family'. Each member of the Family has a different 'SubFamily' or 'Style'.
|
||||
|
||||
Consider a font named 'Times' that has two variants: normal and bold. For this font the
|
||||
Family would be 'Times' and the 'Style' would be 'Regular' (i.e normal) in one file and
|
||||
'Bold' in the other file.
|
||||
|
||||
With this information applications are able to group all 'Times' together and additionally choose the
|
||||
'Bold' font if the user pushes the 'B' button on the font style dialog in that application.
|
||||
|
||||
### Motivation
|
||||
|
||||
Quite a number of patched fonts have inconsistent or simply wrong font grouping. The naming in
|
||||
general is sometimes surprising and not following naming conventions. This is in part due to
|
||||
the font-patcher, but in part the source fonts are already strange.
|
||||
This results in invisible (but installed) fonts in some applications, inconsistent naming
|
||||
(Familyname differs from Fullname) and not correctly working bold/italic selectors in some applications.
|
||||
|
||||
And we would like to have the information within the names sorted in a consistent way.
|
||||
usually a font name consists of these parts (in this order):
|
||||
|
||||
1. Name base (e.g. `Noto`)
|
||||
2. Variant (e.g. `Sans`)
|
||||
3. Subvariant (e.g. `Display`)
|
||||
4. Weight (e.g. `Black`)
|
||||
5. Style (e.g. `Italic`)
|
||||
|
||||
This is important because we want to add subvariant information, namely the `Nerd Font` part.
|
||||
|
||||
Example:
|
||||
|
||||
* (old) `Iosevka Term Light Italic Nerd Font`
|
||||
* (new) `Iosevka Term Nerd Font Light Italic`
|
||||
|
||||
### The Plan
|
||||
|
||||
To solve these issues the font name parts have to be analyzed more thoroughly and then categorized.
|
||||
These categories are then used to assemble the names in correct order. The simple (not
|
||||
typographically aware) applications shall always get groups of at most four styles, and these
|
||||
are Regular, Bold, Italic, and Bold-Italic. Other styles turn up as Families, because this is the
|
||||
only way they would work in these more simple applications.
|
||||
|
||||
Typographically aware applications, on the other hand, get all styles grouped under one Family name.
|
||||
|
||||
First experiments showed that the full information can usually be restored already from the file
|
||||
names that our source fonts have.
|
||||
|
||||
This new naming is complete optional (but recommended). Give the option `--makegroups` to `font-patcher`
|
||||
and it will try to come up with reasonable grouping and naming. Leave the option out and it will
|
||||
work as it always did.
|
||||
|
||||
### The Tests
|
||||
|
||||
In this directory were two tests. If interested you need to go back in the git history.
|
||||
They are not needed anymore.
|
||||
|
||||
### Helper scripts
|
||||
|
||||
There are some helper scripts that help examining the font files. Of course there are other,
|
||||
more professional tools to dump font information, but here we get all we need in a concise
|
||||
way:
|
||||
* `query_mono` `font_name [font_name ...]`
|
||||
* `query_names` `font_name [font_name ...]`
|
||||
* `query_panose` `font_name`
|
||||
* `query_sftn` `[<sfnt-name>] font_name`
|
||||
* `query_version` `font_name`
|
||||
|
||||
They can be invoked like this `$ fontforge query_sfnt foo.ttf`.
|
|
@ -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()
|
File diff suppressed because it is too large
Load Diff
BIN
vendor/glyphs/NerdFontsSymbols 1000 EM Nerd Font Complete Blank.sfd (Stored with Git LFS)
vendored
BIN
vendor/glyphs/NerdFontsSymbols 1000 EM Nerd Font Complete Blank.sfd (Stored with Git LFS)
vendored
Binary file not shown.
BIN
vendor/glyphs/NerdFontsSymbols 2048 EM Nerd Font Complete Blank.sfd (Stored with Git LFS)
vendored
BIN
vendor/glyphs/NerdFontsSymbols 2048 EM Nerd Font Complete Blank.sfd (Stored with Git LFS)
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
vendor/glyphs/materialdesign/MaterialDesignIconsDesktop.ttf (Stored with Git LFS)
vendored
Normal file
BIN
vendor/glyphs/materialdesign/MaterialDesignIconsDesktop.ttf (Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
vendor/glyphs/materialdesign/MaterialDesignIconsDesktop_orig.ttf (Stored with Git LFS)
vendored
Normal file
BIN
vendor/glyphs/materialdesign/MaterialDesignIconsDesktop_orig.ttf (Stored with Git LFS)
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue