Draft: Introduce a file name parser

DO NOT MERGE

[why]
A lot of the fonts have incorrect naming after patching. A completely
different approach can help to come up with a consistent naming scheme.

[how]
See bin/scripts/name-parser/README.md

Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
This commit is contained in:
Fini Jastrow 2021-12-02 22:29:54 +01:00 committed by Fini
parent d939fd4f56
commit 6d86114a38
14 changed files with 3827 additions and 8 deletions

View file

@ -0,0 +1,313 @@
#!/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):
"""Parse a font filename and store the results"""
self.parse_ok = False
self.for_windows = False
self.use_short_families = (False, False) # ( camelcase name, short styles )
self.keep_regular_in_family = None # None = auto, True, False
self.suppress_preferred_if_identical = True
self.fullname_suff = ''
self.fontname_suff = ''
self.family_suff = ''
self.name_subst = []
[ self.parse_ok, self._basename, self.weight_token, self.style_token, self.other_token, self._rest ] = FontnameTools.parse_font_name(filename)
self.basename = self._basename
self.rest = self._rest
self.add_name_substitution_table(FontnameTools.SIL_TABLE)
def _make_ps_mame(self, n):
"""Helper to limit font name length in PS names"""
if self.for_windows and len(n) > 31:
print('Shortening too long PS family name')
return n[:31]
return n
def _shortened_name(self):
"""Return a blank free basename-rest combination"""
if not self.use_short_families[0]:
return (self.basename, self.rest)
else:
return (FontnameTools.concat(self.basename, self.rest).replace(' ', ''), '')
def set_for_windows(self, for_windows):
"""Create slightly different names, suitable for Windows use"""
self.for_windows = for_windows
return self
def set_keep_regular_in_family(self, keep):
"""Familyname may contain 'Regular' where it should normally be suppressed"""
self.keep_regular_in_family = keep
def set_suppress_preferred(self, suppress):
"""Suppress ID16/17 if it is identical to ID1/2 (True is default)"""
self.suppress_preferred_if_identical = suppress
def inject_suffix(self, fullname, fontname, family):
"""Add a custom additonal string that shows up in the resulting names"""
self.fullname_suff = fullname.strip()
self.fontname_suff = fontname.replace(' ', '')
self.family_suff = family.strip()
return self
# font-patcher behavior:
# verboseSuff = "Nerd Font"
# shortSuff = win ? "NF" : "Nerd Font"
# verboseSuff += "Plus Font Awesome"
# shortSuff += "A"
# OR when complete:
# shortSuff = "Nerd Font Complete"
# verboseSuff = "Nerd Font Complete"
# AND
# shortSuff += "M"
# verboseSuff += "Mono"
#
# fullname += verboseSuff
# fontname += shortSuff
# if win familyname += "NF"
# else familyname += "Nerd Font"
# if win fullname += "Windows Compatible"
# if !win familyname += "Mono"
#
# THUS:
# fontname => shortSuff
# fullname => verboseSuff {{ we do the following already: }} + win ? "Windows Compatible" : ""
# family => win ? "NF" : "Nerd Font" + mono ? "Mono" : ""
def enable_short_families(self, camelcase_name, prefix):
"""Enable short styles in Family when (original) font name starts with prefix; enable CamelCase basename in (Typog.) Family"""
# camelcase_name is boolean
# prefix is either a string or False
if type(prefix) == str:
prefix = self._basename.startswith(prefix)
self.use_short_families = ( camelcase_name, prefix )
return self
def add_name_substitution_table(self, table):
"""Have some fonts renamed, takes list of tuples (regex, replacement)"""
# The regex will be anchored to name begin and used case insensitive
# Replacement can have regex matches, mind to catch the correct source case
self.name_subst = table
self.basename = self._basename
self.rest = self._rest
base_and_rest = self._basename + (' ' + self._rest if len(self._rest) else '')
for regex, replacement in self.name_subst:
m = re.match(regex, base_and_rest, re.IGNORECASE)
if not m:
continue
i = len(self._basename) - len(m.group(0))
if i < 0:
self.basename = m.expand(replacement)
self.rest = self._rest[-(i+1):].lstrip()
else:
self.basename = m.expand(replacement) + self._basename[len(m.group(0)):]
break
return self
def drop_for_powerline(self):
"""Remove 'for Powerline' from all names (can not be undone)"""
if 'Powerline' in self.other_token:
idx = self.other_token.index('Powerline')
self.other_token.pop(idx)
if idx > 0 and self.other_token[idx - 1] == 'For':
self.other_token.pop(idx - 1)
self._basename = re.sub(r'(\b|for\s?)?powerline\b', '', self._basename, 1, re.IGNORECASE).strip()
self.add_name_substitution_table(self.name_subst) # re-evaluate
return self
### Following the creation of the name parts:
#
# Relevant websites
# https://www.fonttutorials.com/how-to-name-font-family/
# https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
# https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fss
# https://docs.microsoft.com/en-us/typography/opentype/spec/head#macstyle
# Example (mind that they group 'semibold' as classic-group-of-4 Bold, while we will always only take bold as Bold):
# Adobe Caslon Pro Regular ID1: Adobe Caslon Pro ID2: Regular
# Adobe Caslon Pro Italic ID1: Adobe Caslon Pro ID2: Italic
# Adobe Caslon Pro Semibold ID1: Adobe Caslon Pro ID2: Bold ID16: Adobe Caslon Pro ID17: Semibold
# Adobe Caslon Pro Semibold Italic ID1: Adobe Caslon Pro ID2: Bold Italic ID16: Adobe Caslon Pro ID17: Semibold Italic
# Adobe Caslon Pro Bold ID1: Adobe Caslon Pro Bold ID2: Regular ID16: Adobe Caslon Pro ID17: Bold
# Adobe Caslon Pro Bold Italic ID1: Adobe Caslon Pro Bold ID2: Italic ID16: Adobe Caslon Pro ID17: Bold Italic
# fontname === preferred_family + preferred_styles
# fontname === family + subfamily
#
# familybase = basename + rest + other (+ suffix)
# ID 1/2 just have self.style in the subfamily, all the rest ends up in the family
# ID 16/17 have self.style and self.weight in the subfamily, the rest ends up in the family
def fullname(self):
"""Get the SFNT Fullname (ID 4)"""
if self.for_windows:
win = 'Windows Compatible'
else:
win = ''
styles = self.style_token
weights = self.weight_token
if self.keep_regular_in_family == None:
keep_regular = FontnameTools.is_keep_regular(self._basename + ' ' + self._rest)
else:
keep_regular = self.keep_regular_in_family
if ('Regular' in styles
and (not keep_regular
or len(self.weight_token) > 0)): # This is actually a malformed font name
styles = list(self.style_token)
styles.remove('Regular')
# For naming purposes we want Oblique to be part of the styles
(weights, styles) = FontnameTools.make_oblique_style(weights, styles)
return FontnameTools.concat(self.basename, self.rest, self.other_token, self.fullname_suff, win, weights, styles)
def psname(self):
"""Get the SFNT PostScriptName (ID 6)"""
# This is almost self.family() + '-' + self.subfamily() but without short styles
fam = FontnameTools.camel_casify(FontnameTools.concat(self.basename, self.rest, self.other_token, self.fontname_suff))
sub = FontnameTools.camel_casify(FontnameTools.concat(self.weight_token, self.style_token))
if len(sub) > 0:
sub = '-' + sub
out = FontnameTools.postscript_char_filter(fam + sub)
# The name string must be no longer than 63 characters
return out[:63]
def preferred_family(self):
"""Get the SFNT Preferred Familyname (ID 16)"""
if self.suppress_preferred_if_identical and len(self.weight_token) == 0:
# Do not set if identical to ID 1
return ''
(name, rest) = self._shortened_name()
return FontnameTools.concat(name, rest, self.other_token, self.family_suff)
def preferred_styles(self):
"""Get the SFNT Preferred Styles (ID 17)"""
styles = self.style_token
weights = self.weight_token
if self.suppress_preferred_if_identical and len(weights) == 0:
# Do not set if identical to ID 2
return ''
# For naming purposes we want Oblique to be part of the styles
(weights, styles) = FontnameTools.make_oblique_style(weights, styles)
return FontnameTools.concat(weights, styles)
def family(self):
"""Get the SFNT Familyname (ID 1)"""
# We use the short form of the styles to save on number of chars
(name, rest) = self._shortened_name()
other = self.other_token
weight = self.weight_token
if self.use_short_families[1]:
other = FontnameTools.short_styles(other)
weight = FontnameTools.short_styles(weight)
return FontnameTools.concat(name, rest, other, self.family_suff, weight)
def subfamily(self):
"""Get the SFNT SubFamily (ID 2)"""
if len(self.style_token) == 0:
if 'Oblique' in self.weight_token:
return FontnameTools.concat(self.style_token, 'Italic')
return 'Regular'
if 'Oblique' in self.weight_token and not 'Italic' in self.style_token:
return FontnameTools.concat(self.style_token, 'Italic')
return FontnameTools.concat(self.style_token)
def ps_familyname(self):
"""Get the PS Familyname"""
return self._make_ps_mame(self.family())
def ps_fontname(self):
"""Get the PS fontname"""
# This Adobe restriction is classically ignored
# if len(n) > 29:
# print('Shortening too long PS fontname')
# return n[:29]
return self._make_ps_mame(self.psname())
def macstyle(self, style):
"""Modify a given macStyle value for current name, just bits 0 and 1 touched"""
b = style & (~3)
b |= 1 if 'Bold' in self.style_token else 0
b |= 2 if 'Italic' in self.style_token else 0
return b
def fs_selection(self, fs):
"""Modify a given fsSelection value for current name, bits 0, 5, 6, 8, 9 touched"""
ITALIC = 1 << 0; BOLD = 1 << 5; REGULAR = 1 << 6; WWS = 1 << 8; OBLIQUE = 1 << 9
b = fs & (~(ITALIC | BOLD | REGULAR | WWS | OBLIQUE))
if 'Bold' in self.style_token:
b |= BOLD
# Ignore Italic if we have Oblique
if 'Oblique' in self.weight_token:
b |= OBLIQUE
elif 'Italic' in self.style_token:
b |= ITALIC
# Regular is just the basic weight
if len(self.weight_token) == 0:
b |= REGULAR
b |= WWS # We assert this by our naming process
return b
def rename_font(self, font):
"""Rename the font to include all information we found (font is fontforge font object)"""
font.fontname = self.ps_fontname()
font.fullname = self.fullname()
font.familyname = self.ps_familyname()
# We have to work around several issues in fontforge:
#
# a. Remove some entries from SFNT table; fontforge has no API function for that
#
# b. Fontforge does not allow to set SubFamily (and other) to any value:
#
# Fontforge lets you set any value, unless it is the default value. If it
# is the default value it does not set anything. It also does not remove
# a previously existing non-default value. Why it is done this way is
# unclear:
# fontforge/python.c SetSFNTName() line 11431
# return( 1 ); /* If they set it to the default, there's nothing to do */
#
# Then is the question: What is the default? It is taken from the
# currently set fontname (??!). The fontname is parsed and everything
# behind the dash is the default SubFamily:
# fontforge/tottf.c DefaultTTFEnglishNames()
# fontforge/splinefont.c _GetModifiers()
#
# To fix this without touching Fontforge we need to set the SubFamily
# directly in the SFNT table:
#
# c. Fontforge has the bug that it allows to write empty-string to a SFNT field
# and it is actually embedded as empty string, but empty strings are not
# shown if you query the sfnt_names *rolleyes*
sfnt_list = []
TO_DEL = ['Family', 'SubFamily', 'Fullname', 'Postscriptname', 'Preferred Family', 'Preferred Styles', 'Compatible Full']
for l, k, v in list(font.sfnt_names):
if not k in TO_DEL:
sfnt_list += [( l, k, v )]
sfnt_list += [( 'English (US)', 'Family', self.family() )]
sfnt_list += [( 'English (US)', 'SubFamily', self.subfamily() )]
sfnt_list += [( 'English (US)', 'Fullname', self.fullname() )]
sfnt_list += [( 'English (US)', 'PostScriptName', self.psname() )]
p_fam = self.preferred_family()
if len(p_fam):
sfnt_list += [( 'English (US)', 'Preferred Family', p_fam )]
p_sty = self.preferred_styles()
if len(p_sty):
sfnt_list += [( 'English (US)', 'Preferred Styles', p_sty )]
font.sfnt_names = tuple(sfnt_list)
font.macstyle = self.macstyle(font.macstyle)
# TODO: fsSelection, unfortunately fontforge does not support that directly
# but has some automaton to deduce it from macstyle, which means loosing information
# https://github.com/fontforge/fontforge/issues/2131
# https://github.com/jsomedon/Fix-fsSelection-bits-for-SF-fonts/blob/main/fix_fsSelection.sh
# Well, lets ignore it for now, as we always did ;)

View file

@ -0,0 +1,285 @@
#!/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) == str:
all.append(thing)
else:
all += thing
return ' '.join(FontnameTools.drop_empty(all))
@staticmethod
def unify_style_names(style_name):
"""Substitude some known token with standard wording"""
known_names = {
# Source of the table is the current sourcefonts
# Left side needs to be lower case
'-': '',
'book': '',
'text': '',
'ce': 'CE',
'(ttf)': '(TTF)',
#'semibold': 'Demi',
'ob': 'Oblique',
'it': 'Italic',
'i': 'Italic',
'b': 'Bold',
'normal': 'Regular',
'c': 'Condensed',
'r': 'Regular',
'm': 'Medium',
'l': 'Light',
}
if style_name in known_names:
return known_names[style_name.lower()]
return style_name
@staticmethod
def shorten_style_name(name):
"""Substitude some known styles to short form"""
known_names = {
# Chiefly from Noto
'SemiCondensed': 'SemCond',
'Condensed': 'Cond',
'ExtraCondensed': 'ExtCond',
'SemiBold': 'SemBd',
'ExtraBold': 'ExtBd',
'Medium': 'Med',
'ExtraLight': 'ExtLt',
'Black': 'Blk',
}
if name in known_names:
return known_names[name]
return name
@staticmethod
def short_styles(styles):
"""Shorten all style names in a list"""
return list(map(FontnameTools.shorten_style_name, styles))
@staticmethod
def make_oblique_style(weights, styles):
"""Move "Oblique" from weights to styles for font naming purposes"""
if 'Oblique' in weights:
weights = list(weights)
weights.remove('Oblique')
styles = list(styles)
styles.append('Oblique')
return (weights, styles)
@staticmethod
def get_name_token(name, tokens, allow_regex_token = False):
"""Try to find any case insensitive token from tokens in the name, return tuple with found token-list and rest"""
# The default mode (allow_regex_token = False) will try to find any verbatim string in the
# tokens list (case insensitive matching) and give that tokens list item back with
# unchanged case (i.e. [ 'Bold' ] will match "bold" and return it as [ 'Bold', ]
# In the regex mode (allow_regex_token = True) it will use the tokens elements as
# regexes and return the original (i.e. from name) case.
#
# Token are always used in a regex and may not capture, use non capturing
# grouping if needed (?: ... )
lower_tokens = [ t.lower() for t in tokens ]
not_matched = ""
all_tokens = []
j = 1
regex = re.compile('(.*?)(' + '|'.join(tokens) + ')(.*)', re.IGNORECASE)
while j:
j = regex.match(name)
if not j:
break
if len(j.groups()) != 3:
sys.exit('Malformed regex in FontnameTools.get_name_token()')
not_matched += ' ' + j.groups()[0] # Blanc prevents unwanted concatenation of unmatched substrings
tok = j.groups()[1].lower()
if tok in lower_tokens:
tok = tokens[lower_tokens.index(tok)]
tok = FontnameTools.unify_style_names(tok)
if len(tok):
all_tokens.append(tok)
name = j.groups()[2] # Recurse rest
not_matched += ' ' + name
return ( not_matched.strip(), all_tokens )
@staticmethod
def postscript_char_filter(name):
"""Filter out characters that are not allowed in Postscript names"""
# The name string must be restricted to the printable ASCII subset, codes 33 to 126,
# except for the 10 characters '[', ']', '(', ')', '{', '}', '<', '>', '/', '%'
out = ""
for c in name:
if c in '[](){}<>/%' or ord(c) < 33 or ord(c) > 126:
continue
out += c
return out
SIL_TABLE = [
( '(s)ource', r'\1auce' ),
( '(h)ermit', r'\1urmit' ),
( '(h)asklig', r'\1asklug' ),
( '(s)hare', r'\1hure' ),
( 'IBM[- ]?plex', r'Blex' ), # We do not keep the case here
( '(t)erminus', r'\1erminess' ),
( '(l)iberation', r'\1iteration' ),
( 'iA([- ]?)writer', r'iM\1Writing' ),
( '(a)nka/(c)oder', r'\1na\2onder' ),
( '(c)ascadia( ?)(c)ode', r'\1askaydia\2\3ove' ),
( '(c)ascadia( ?)(m)ono', r'\1askaydia\2\3ono' ),
( '(m)plus', r'\1+'), # Added this, because they use a plus symbol :->
( 'Gohufont', r'GohuFont'), # Correct to CamelCase
# Noone cares that font names starting with a digit are forbidden:
# ( '(3270)', r'Ibeam\1'),
]
@staticmethod
def is_keep_regular(basename):
"""This has been decided by the font designers, we need to mimic that (for comparison purposes)"""
KEEP_REGULAR = [
'Agave',
'Arimo',
'Aurulent',
'Cascadia',
'Cousine',
'Fantasque',
'Fira',
'Overpass',
'Lilex',
'Inconsolata$', # not InconsolataGo
'IAWriter',
'Meslo',
'Monoid',
'Mononoki',
'Hack',
'JetBrains Mono',
'Noto Sans',
'Noto Serif',
'Victor',
]
for kr in KEEP_REGULAR:
if (basename.rstrip() + '$').startswith(kr): return True
return False
@staticmethod
def _parse_simple_font_name(name):
"""Parse a filename that does not follow the 'FontFamilyName-FontStyle' pattern"""
# No dash in name, maybe we have blanc separated filename?
if ' ' in name:
return FontnameTools.parse_font_name(name.replace(' ', '-'))
# Do we have a number-name boundary?
p = re.split('(?<=[0-9])(?=[a-zA-Z])', name)
if len(p) > 1:
return FontnameTools.parse_font_name('-'.join(p))
# Or do we have CamelCase?
n = FontnameTools.camel_explode(name)
if n != name:
return FontnameTools.parse_font_name(n.replace(' ', '-'))
return (False, FontnameTools.camel_casify(name), [], [], [], '')
@staticmethod
def parse_font_name(name):
"""Expects a filename following the 'FontFamilyName-FontStyle' pattern and returns ... parts"""
name = re.sub(r'\bsemi-narrow\b', 'SemiNarrow', name, 1, re.IGNORECASE) # Just for "3270 Semi-Narrow" :-/
name = re.sub('[_\s]+', ' ', name)
matches = re.match(r'([^-]+)(?:-(.*))?', name)
familyname = FontnameTools.camel_casify(matches.group(1))
style = matches.group(2)
if not style:
return FontnameTools._parse_simple_font_name(name)
# These are the FontStyle keywords we know, in three categories
# Weights end up as Typographic Family parts ('after the dash')
# Styles end up as Family parts (for classic grouping of four)
# Others also end up in Typographic Family ('before the dash')
weights = [ 'Thin', 'Light', 'ExtraLight', 'SemiBold', 'Demi',
'SemiLight', 'Medium', 'Black', 'ExtraBold', 'Heavy',
'Oblique', 'Condensed', 'SemiCondensed', 'ExtraCondensed',
'Narrow', 'SemiNarrow', 'Retina', ]
styles = [ 'Bold', 'Italic', 'Regular', 'Normal', ]
# Some font specialities:
other = [
'-', 'Book', 'For', 'Powerline',
'Text', # Plex
'IIx', # Profont IIx
'LGC', # Inconsolata LGC
r'\(TTF\)', # Terminus (TTF)
r'\bCE\b', # ProggycleanTT CE
r'[12][cmp]n?', # MPlus
r'(?:uni-)?1[14]', # GohuFont uni
]
# Sometimes used abbreviations
weight_abbrevs = [ 'ob', 'c', 'm', 'l', ]
style_abbrevs = [ 'it', 'r', 'b', 'i', ]
( style, weight_token ) = FontnameTools.get_name_token(style, weights)
( style, style_token ) = FontnameTools.get_name_token(style, styles)
( style, other_token ) = FontnameTools.get_name_token(style, other, True)
if len(style) < 4:
( style, weight_token_abbrevs ) = FontnameTools.get_name_token(style, weight_abbrevs)
( style, style_token_abbrevs ) = FontnameTools.get_name_token(style, style_abbrevs)
weight_token += weight_token_abbrevs
style_token += style_token_abbrevs
while 'Regular' in style_token and len(style_token) > 1:
# Correct situation where "Regular" and something else is given
style_token.remove('Regular')
# Recurse to see if unmatched stuff between dashes can belong to familyname
matches2 = re.match(r'(\w+)-(.*)', style)
if matches2:
return FontnameTools.parse_font_name(familyname + matches2.group(1) + '-' + matches2.group(2))
style = re.sub(r'(^|\s)\d+(\.\d+)+(\s|$)', r'\1\3', style) # Remove (free standing) version numbers
style_parts = FontnameTools.drop_empty(style.split(' '))
style = ' '.join(map(FontnameTools.front_upper, style_parts))
familyname = FontnameTools.camel_explode(familyname)
return (True, familyname, weight_token, style_token, other_token, style)

View file

@ -0,0 +1,194 @@
## 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 `--parser 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 there are two tests.
1. The first test checks the basics of the algorithm. It takes the filenames of all fonts in
`src/unpatched\_fonts`, then it calculates the naming and compares it to the original
naming in the font files. Ideally they would be equal.
2. The second test does a 'production run'. It patches each font in `src/unpatched_fonts/`
and patches it two times: Once without `--parser` and once with. Then it compares the
naming, and it also shows the original font naming (for comparison).
All tests base on these assumptions
* Fullname must be roughly equal
* Fontname must be roughly equal
* Familyname must roughly equal, order of all words does not matter
_(Order of words is ignored with test 2 only)_
* SubFamilyname must be equal, order of words does not matter
_(First word must be equal, order of other words is ignored with test 2 only)_
* Typographic names can be empty if the correct typographic name would be equal to the ordinary name
* Tests are done case insensitive
* Some special exemptions are made (see `lenient_cmp()` in test scripts)
#### Test 1
`fontforge name_parser_test1 ../../../src/unpatched-fonts/**/*.[ot]tf 2>/dev/null`
This test takes the filename of a font, parses it and generates names from it. Then the actual
font is opened and the generated names are compared with the stored names. This test is used
to test the algorithm itself. Of course no SIL table is active as we want to preserve the original
names.
The output shows all the names, always two lines: first the generated names, then the readout
names. If there are differences the generated names are tagged with `+` and the readout ones
with `-`. If there are differences the actually different name part is marked with an `X`.
The differences have reasons, and there is a file with textual explanations for them. So far
all differences are 'ok'. A new run of the script will compare all differences with the stored
ones and alert the user if a new difference is detected (or a difference vanished). In this
way changes of the algorithm can be tested with a wide base of inputs.
#### Test 2
`fontforge name_parser_test2 ../../../src/unpatched-fonts/**/*.[ot]tf 2>/dev/null`
This test compares actually patched fonts. Every font in `src/unpatched_fonts/` is patched two
times: First with the 'old/classic' `font-patcher` naming, and second with the new naming
algorithm in action (by specifying `--parser`). Again the name parts are compared with some
lenience and an output generated like test 1 does.
Also again a file with known differences (with explanations) is read, and any new or vanished
differences are reported. In the report an additional line is given, tagged with `>`, that
contains the names of the original font, for human interpretation (often the reason
for a difference is obvious, because the classic `font-patcher` dropped information.
_Note: Fonts `NotoColorEmoji` and `Lilex-VF` are not patchable, and thus ignored_
_Note: Fonts `iosevka-heavyoblique`, `iosevka-term-heavyoblique`, `iosevka-mediumoblique` crash my machine and are ignored_
### Differences
The naming of the patched fonts, if `--parse` is applied, will be different. Of course, that is the goal.
What are the differences in particular:
* `Nerd Font` is not added in the end, but after the extended base name before the style
* The SubFamily contains only 4 Styles max: Regular, Bold, Italic, Bold-Italic
* The Noto fonts retain their abbreviated style names in the Family information
* `Nerd Font Mono` fonts get a `M` in windows mode (I believe that has been left out accidentally before)
Apart from these general things, all changes are documented in detail in the `name_parser_test2` issues file.
Here is an overview over all the things that get renamed and why:
| Occurences | Description |
|------------|-------------|
| 511 | Add weight/style to family |
| 43 | The fonts name is M+ not Mplus |
| 36 | Drop unneeded Typogr.Family/Typogr.Style |
| 26 | 'Term' is missing from Family |
| 22 | Change regular-equivalent name to Regular |
| 19 | Put Oblique into own SubFamily (and mark it as italic) |
| 5 | Drop Regular from Style |
| 4 | We handle (TTF) as sub-name |
| 4 | Fullname has been missing 'Nerd Font' |
| 4 | Bold / Bold-Italic are just a styles of Regular |
| 2 | Original font broken (Light in Family) |
| 2 | Classify Medium as own weigt and not Bold |
| 2 | Bold and Italic are styles of a basefont |
| 1 | Weight Condensed does not belong to base name |
| 1 | Use only Regular/Bold/Italic in SubFamily |
| 1 | Handle Retina as Weight and not Style |
| 1 | Do not call Semibold Light-Bold |
From the count we see that almost all fonts are affected by incorrect Family naming.
### Further steps
One can examine all the (current) naming differences in the `name_parser_test2.known_issues`
file. The Explanation is followed by three lines of names: source-file, patched-with-parser,
and patched-classic.
The Explanation sorts most differences into common groups. This helps to weed out
explanations that might do not need much attention.
### 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_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`.
### Appendix: The `name_parser_test*.known_issues` files
All differences of 'old' to 'new' naming (if not one of the very general kind like resorting of
the words) are documented in the `known_issues` files. For each difference a reason is given.
The files consist of entries that spans 3 (for test 1) or 4 (for test 2) lines.
| Line starts with | Contents |
|------------------|----------|
| # | Reson for the difference (or `AUTOGENERATED`) |
| > | Naming fo the original/source font (only test 2) |
| + | Naming with `--parser` (new naming) |
| - | Naming classically generated by font-patcher |
After any test run a `known_issues.new` file is generated. It contains all the issues
from the `known_issues` file that were detected. Original issues that are not
existing anymore are at the bottom of the new file, clearly marked as such. If new
(previously unexplained) issues were detected they show up with the `AUTOGENERATED`
reason.
After adding new fonts or replacing font files the test can be rerun. If there are issues
in the `.new` file they should be documented there, and the `.new` file replace the
original `known_issues` file (after removing possible 'obsolete' issues that are listed in
the bottom of the new file).
In this way one can tweak the parser code and compare very easily what a change
means for all the fonts, which will break or be repaired.

View file

@ -0,0 +1,19 @@
cat name_parser_test2.known_issues | grep '####' | sed -E 's/#### //;s/\] /]\n/g;s/ \[[0-9]+\]//g' | sort | uniq -c | sort -nr
511 Add weight/style to family
43 The fonts name is M+ not Mplus
36 Drop unneeded Typogr.Family/Typogr.Style
26 'Term' is missing from Family
22 Change regular-equivalent name to Regular
19 Put Oblique into own SubFamily (and mark it as italic)
5 Drop Regular from Style
4 We handle (TTF) as sub-name
4 Fullname has been missing 'Nerd Font'
4 Bold / Bold-Italic are just a styles of Regular
2 Original font broken (Light in Family)
2 Classify Medium as own weigt and not Bold
2 Bold and Italic are styles of a basefont
1 Weight Condensed does not belong to base name
1 Use only Regular/Bold/Italic in SubFamily
1 Handle Retina as Weight and not Style
1 Do not call Semibold Light-Bold

View file

@ -0,0 +1,160 @@
#!/usr/bin/env python
# coding=utf8
import sys
import re
import os.path
import fontforge
from FontnameParser import FontnameParser
###### Some helpers
def get_sfnt_dict(font):
"""Extract SFNT table as nice dict"""
d = []
for i, el in enumerate(font.sfnt_names):
d += [(el[1], el[2])]
return dict(d)
def format_names(header, *stuff):
"""Unify outputs (with header)"""
f = '{:1.1}|{:50.50} |{:1.1}| {:50.50} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:.30}'
if header:
d = '------------------------------------------------------------'
return f.format(*stuff) + '\n' + f.format('', d, d, d, d, d, d, d, d, d, d, d)
return f.format(*stuff).rstrip()
def lenient_cmp(s1, s2):
"""Compare two font name (parts) but be a bit lenient ;->"""
# We do not care about:
# - Case
# - "Display" vs "Disp" (in Noto)
# Allow for "IBM 3278" name
s = [ s1, s2 ]
for i in range(2):
# Usually given transform from 'their' to 'our' style
s[i] = s[i].lower()
s[i] = re.sub(r'\bdisp\b', 'display', s[i]) # Noto
s[i] = s[i].replace('ibm 3270', '3270') # 3270
s[i] = s[i].replace('3270-', '3270 ') # 3270
s[i] = s[i].replace('lekton-', 'lekton ') # Lekton
s[i] = s[i].replace('semi-narrow', 'seminarrow') # 3270
s[i] = s[i].replace('bolditalic', 'bold italic')
s[i] = re.sub(r'\bfor\b', '', s[i]) # Meslo, Monofur
s[i] = re.sub(r'\bpowerline\b', '', s[i]) # Meslo, Monofur
s[i] = s[i].replace('fira mono', 'fura mono') # Obviously someone forgot to rename the fonts in Fira/
s[i] = s[i].replace('aurulentsansmono-', 'aurulent sans mono ') # Aurulent fullname oddity
s[i] = s[i].replace('mononoki-', 'mononoki ') # Mononoki has somtimes a dash
s[i] = re.sub(r'\br\b', 'regular', s[i]) # Nonstandard style in Agave
s[i] = re.sub(r'(bitstream vera sans mono.*) oblique', r'\1 italic', s[i]) # They call it Oblique but the filename says Italic
s[i] = re.sub(r'gohufont (uni-)?(11|14)', 'gohufont', s[i]) # They put the 'name' into the subfamily/weight
s[i] = s[i].replace('xltobl', 'extralight oblique') # Iosevka goes inventing names
s[i] = re.sub(r'proggyclean(?!TT)( ?)', 'proggycleantt\1', s[i]) # ProggyClean has no TT in filename
s[i] = re.sub(r' +', ' ', s[i]).strip()
return s[0] == s[1]
TEST_TABLE = [
( '(m)plus', '\\1+'),
( 'IAWriter', 'iA Writer'),
( 'IBMPlex', 'IBM Plex'),
( 'Vera', 'Bitstream Vera Sans'),
]
###### Let's go!
if len(sys.argv) < 2:
print('Usage: {} font_name [font_name ...]\n'.format(sys.argv[0]))
sys.exit(1)
try:
with open(sys.argv[0] + '.known_issues', 'r') as f:
known_issues = f.read().splitlines()
# known_issues = [line.rstrip() for line in known_issues]
print('Found {:.0f} known issues'.format(len(known_issues) / 3)) # approx ;)
except OSError:
print('Can not open known_issues file')
known_issues = []
new_issues = open(sys.argv[0] + '.known_issues.new', 'w')
print('Examining {} font files'.format(len(sys.argv) - 1))
all_files = 0
issue_files = 0
known_files = 0
print(format_names(True, '', 'Filename', '', 'Fullname', '', 'Family', '', 'Subfamily', '', 'Typogr. Family', '', 'Typogr. Subfamily'))
for filename in sys.argv[1:]:
fullfile = os.path.basename(filename)
fname = os.path.splitext(fullfile)[0]
if fname == 'NotoColorEmoji':
continue # font is not patchable
n = FontnameParser(fname).enable_short_families(False, 'Noto').add_name_substitution_table(TEST_TABLE)
# Example for name injection:
# n.inject_suffix("Nerd Font Complete Mono", "Nerd Font Complete M", "Nerd Font Mono")
font = fontforge.open(filename, 1)
sfnt = get_sfnt_dict(font)
font.close()
all_files += 1
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 ''
t1 = not lenient_cmp(sfnt_full, n.fullname())
t2 = not lenient_cmp(sfnt_fam, n.family())
t3 = not lenient_cmp(sfnt_subfam, n.subfamily())
t4 = not lenient_cmp(sfnt_pfam, n.preferred_family())
t5 = not lenient_cmp(sfnt_psubfam, n.preferred_styles())
# Lenience: Allow for dropping unneeded prefered stuff:
# New (sub)family is same as old preferred sub(family)
if t4 and n.preferred_family() == '' and sfnt_pfam.lower() == n.family().lower():
t4 = False
if t5 and n.preferred_styles() == '' and sfnt_psubfam.lower() == n.subfamily().lower():
t5 = False
if t1 or t2 or t3 or t4 or t5:
m1 = '+'; m2 = '-'
else:
m1 = ''; m2 = ''
if not n.parse_ok:
m1 = '!'
t1_ = 'X' if t1 else ''
t2_ = 'X' if t2 else ''
t3_ = 'X' if t3 else ''
t4_ = 'X' if t4 else ''
t5_ = 'X' if t5 else ''
o1 = format_names(False, m1, fullfile, t1_, n.fullname(), t2_, n.family(), t3_, n.subfamily(), t4_, n.preferred_family(), t5_, n.preferred_styles())
o2 = format_names(False, m2, fullfile, '', sfnt_full, '', sfnt_fam, '', sfnt_subfam, '', sfnt_pfam, '', sfnt_psubfam)
if len(m1):
issue_files += 1
if not o1 in known_issues or not o2 in known_issues:
new_issues.writelines(['#### AUTOGENERATED\n', o1 + '\n', o2 + '\n'])
else:
known_files += 1
idx = known_issues.index(o1) - 1 # should be the index of the explanation line
if known_issues[idx][0] != '#':
sys.exit('Problem with known issues file, line', known_issues.index(o1))
new_issues.writelines([known_issues[idx] + '\n', o1 + '\n', o2 + '\n'])
# remove known_issue triplet
known_issues.pop(idx)
known_issues.pop(idx)
known_issues.pop(idx)
print(o1, o2, sep='\n')
print('Fonts with different name rendering: {}/{} ({}/{} are in known_issues)'.format(issue_files, all_files, known_files, issue_files))
if len(known_issues) > 0:
print('There are {} lines not needed in known_issues, appending commented out in new known_issues'.format(len(known_issues)))
new_issues.write('\n#### The following lines are not needed anymore\n\n')
for l in known_issues:
new_issues.writelines([' ', l, '\n'])
new_issues.close()

View file

@ -0,0 +1,210 @@
#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead
+|3270Medium.otf | | 3270 Medium |X| 3270 Medium |X| Regular |X| 3270 |X| Medium
-|3270Medium.otf | | 3270-Medium | | IBM 3270 | | Medium | | | |
#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead
+|3270Medium.ttf | | 3270 Medium |X| 3270 Medium |X| Regular |X| 3270 |X| Medium
-|3270Medium.ttf | | 3270-Medium | | IBM 3270 | | Medium | | | |
#### Limit Subfamiliy to 4 standard styles, obviously for them Medium is Regular
+|3270Narrow.otf | | 3270 Narrow | | 3270 Narrow |X| Regular |X| 3270 |X| Narrow
-|3270Narrow.otf | | 3270 Narrow | | IBM 3270 Narrow | | Medium | | | |
#### Limit Subfamiliy to 4 standard styles, obviously for them Medium is Regular
+|3270Narrow.ttf | | 3270 Narrow | | 3270 Narrow |X| Regular |X| 3270 |X| Narrow
-|3270Narrow.ttf | | 3270 Narrow | | IBM 3270 Narrow | | Medium | | | |
#### Limit Subfamiliy to 4 standard styles, obviously for them Medium is Regular
+|3270SemiNarrow.otf | | 3270 SemiNarrow | | 3270 SemiNarrow |X| Regular |X| 3270 |X| SemiNarrow
-|3270SemiNarrow.otf | | 3270 Semi-Narrow | | IBM 3270 Semi-Narrow | | Medium | | | |
#### Limit Subfamiliy to 4 standard styles, obviously for them Medium is Regular
+|3270SemiNarrow.ttf | | 3270 SemiNarrow | | 3270 SemiNarrow |X| Regular |X| 3270 |X| SemiNarrow
-|3270SemiNarrow.ttf | | 3270 Semi-Narrow | | IBM 3270 Semi-Narrow | | Medium | | | |
#### Drop special/unexpected name in Typographic Family
+|Anonymice Powerline.ttf | | Anonymice Powerline | | Anonymice Powerline | | Regular |X| | |
-|Anonymice Powerline.ttf | | Anonymice Powerline | | Anonymice Powerline | | Regular | | Anonymous Pro for Powerline | | Regular
#### Font file says it is italic and not oblique
+|VeraMono-Bold-Italic.ttf | | Bitstream Vera Sans Mono Bold Italic | | Bitstream Vera Sans Mono |X| Bold Italic | | | |
-|VeraMono-Bold-Italic.ttf | | Bitstream Vera Sans Mono Bold Oblique | | Bitstream Vera Sans Mono | | Bold Oblique | | | |
#### Font file says it is italic and not oblique
+|VeraMono-Italic.ttf | | Bitstream Vera Sans Mono Italic | | Bitstream Vera Sans Mono |X| Italic | | | |
-|VeraMono-Italic.ttf | | Bitstream Vera Sans Mono Oblique | | Bitstream Vera Sans Mono | | Oblique | | | |
#### Limit Subfamiliy to 4 standard styles, Roman is usually equivalent to Regular
+|VeraMono.ttf | | Bitstream Vera Sans Mono | | Bitstream Vera Sans Mono |X| Regular | | | |
-|VeraMono.ttf | | Bitstream Vera Sans Mono | | Bitstream Vera Sans Mono | | Roman | | | |
#### Limit Subfamiliy to 4 standard styles, Book is usually equivalent to Regular
!|DaddyTimeMono.ttf | | DaddyTimeMono | | DaddyTimeMono |X| Regular | | | |
-|DaddyTimeMono.ttf | | DaddyTimeMono | | DaddyTimeMono | | Book | | DaddyTimeMono | |
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|DejaVuSansMono-BoldOblique.ttf | | DejaVu Sans Mono Bold Oblique |X| DejaVu Sans Mono Oblique |X| Bold Italic |X| DejaVu Sans Mono |X| Bold Oblique
-|DejaVuSansMono-BoldOblique.ttf | | DejaVu Sans Mono Bold Oblique | | DejaVu Sans Mono | | Bold Oblique | | | |
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|DejaVuSansMono-Oblique.ttf | | DejaVu Sans Mono Oblique |X| DejaVu Sans Mono Oblique |X| Italic |X| DejaVu Sans Mono |X| Oblique
-|DejaVuSansMono-Oblique.ttf | | DejaVu Sans Mono Oblique | | DejaVu Sans Mono | | Oblique | | | |
#### Limit Subfamiliy to 4 standard styles, Book is usually equivalent to Regular
+|DejaVuSansMono.ttf | | DejaVu Sans Mono | | DejaVu Sans Mono |X| Regular | | | |
-|DejaVuSansMono.ttf | | DejaVu Sans Mono | | DejaVu Sans Mono | | Book | | | |
#### No need to have Typographic Family/Subfamily if it is identical to normal Family/Subfamily
+|FuraMono-Bold Powerline.otf | | Fura Mono Powerline Bold | | Fura Mono Powerline | | Bold |X| | |
-|FuraMono-Bold Powerline.otf | | Fira Mono Bold for Powerline | | Fira Mono for Powerline | | Bold | | Fira Mono for Powerline | |
#### False positive, move Powerline from end to middle, Powerline will be dropped when patching anyhow
+|FuraMono-Medium Powerline.otf | | Fura Mono Powerline Medium | | Fura Mono Powerline Medium | | Regular |X| Fura Mono Powerline | | Medium
-|FuraMono-Medium Powerline.otf | | Fira Mono Medium for Powerline | | Fira Mono Medium for Powerline | | Regular | | Fira Mono Medium for Powerline | | Medium
#### No need to have Typographic Family/Subfamily if it is identical to normal Family/Subfamily
+|FuraMono-Regular Powerline.otf | | Fura Mono Powerline | | Fura Mono Powerline | | Regular |X| | |
-|FuraMono-Regular Powerline.otf | | Fira Mono | | Fira Mono for Powerline | | Regular | | Fira Mono for Powerline | |
#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead
+|gohufont-11.ttf | | Gohufont 11 | | Gohufont 11 |X| Regular | | | |
-|gohufont-11.ttf | | GohuFont | | GohuFont | | Medium | | | |
#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead
+|gohufont-14.ttf | | Gohufont 14 | | Gohufont 14 |X| Regular | | | |
-|gohufont-14.ttf | | GohuFont | | GohuFont | | 14 | | | |
#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead
+|gohufont-uni-11.ttf | | Gohufont uni-11 | | Gohufont uni-11 |X| Regular | | | |
-|gohufont-uni-11.ttf | | GohuFont | | GohuFont | | Medium | | | |
#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead
+|gohufont-uni-14.ttf | | Gohufont uni-14 | | Gohufont uni-14 |X| Regular | | | |
-|gohufont-uni-14.ttf | | GohuFont | | GohuFont | | uni-14 | | | |
#### Limit Subfamiliy to 4 standard styles, Normal is usually equivalent to Regular
+|heavy_data.ttf | | Heavy Data | | Heavy Data |X| Regular | | | |
-|heavy_data.ttf | | Heavy Data | | Heavy Data | | Normal | | | |
#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead
+|Hermit-light.otf | | Hermit Light |X| Hermit Light |X| Regular |X| Hermit |X| Light
-|Hermit-light.otf | | Hermit Light | | Hermit | | light | | | |
#### Limit Subfamiliy to 4 standard styles, put Subfamily name into Family instead
+|Hermit-medium.otf | | Hermit Medium |X| Hermit Medium |X| Regular |X| Hermit |X| Medium
-|Hermit-medium.otf | | Hermit Medium | | Hermit | | medium | | | |
#### Limit Subfamiliy to 4 standard styles, put Bold into Subfamily name
+|iAWriterDuospace-Bold.otf | | iA Writer Duospace Bold |X| iA Writer Duospace |X| Bold | | | |
-|iAWriterDuospace-Bold.otf | | iA Writer Duospace Bold | | iA Writer Duospace Bold | | Regular | | iA Writer Duospace | | Bold
#### Limit Subfamiliy to 4 standard styles, put Bold into Subfamily name
+|iAWriterDuospace-Bold.ttf | | iA Writer Duospace Bold |X| iA Writer Duospace |X| Bold | | | |
-|iAWriterDuospace-Bold.ttf | | iA Writer Duospace Bold | | iA Writer Duospace Bold | | Regular | | iA Writer Duospace | | Bold
#### Limit Subfamiliy to 4 standard styles, put Bold into Subfamily name
+|iAWriterDuospace-BoldItalic.otf | | iA Writer Duospace Bold Italic |X| iA Writer Duospace |X| Bold Italic | | |X|
-|iAWriterDuospace-BoldItalic.otf | | iA Writer Duospace BoldItalic | | iA Writer Duospace Bold | | Italic | | iA Writer Duospace | | BoldItalic
#### Limit Subfamiliy to 4 standard styles, put Bold into Subfamily name
+|iAWriterDuospace-BoldItalic.ttf | | iA Writer Duospace Bold Italic |X| iA Writer Duospace |X| Bold Italic | | |X|
-|iAWriterDuospace-BoldItalic.ttf | | iA Writer Duospace BoldItalic | | iA Writer Duospace Bold | | Italic | | iA Writer Duospace | | BoldItalic
#### Ignore naming part Text
+|IBMPlexMono-TextItalic.ttf | | IBM Plex Mono Text Italic | | IBM Plex Mono Text | | Italic |X| |X|
-|IBMPlexMono-TextItalic.ttf | | IBM Plex Mono Text Italic | | IBM Plex Mono Text | | Italic | | IBM Plex Mono | | Text Italic
#### Ignore naming part Text
+|IBMPlexMono-Text.ttf | | IBM Plex Mono Text | | IBM Plex Mono Text | | Regular |X| |X|
-|IBMPlexMono-Text.ttf | | IBM Plex Mono Text | | IBM Plex Mono Text | | Regular | | IBM Plex Mono | | Text
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-boldoblique.ttf | | Iosevka Bold Oblique | | Iosevka Oblique |X| Bold Italic | | Iosevka | | Bold Oblique
-|iosevka-boldoblique.ttf | | Iosevka Bold Oblique | | Iosevka Oblique | | Bold | | Iosevka | | Bold Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-term-boldoblique.ttf | | Iosevka Term Bold Oblique | | Iosevka Term Oblique |X| Bold Italic | | Iosevka Term | | Bold Oblique
-|iosevka-term-boldoblique.ttf | | Iosevka Term Bold Oblique | | Iosevka Term Oblique | | Bold | | Iosevka Term | | Bold Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-extraboldoblique.ttf | | Iosevka ExtraBold Oblique | | Iosevka ExtraBold Oblique |X| Italic | | Iosevka | | ExtraBold Oblique
-|iosevka-extraboldoblique.ttf | | Iosevka Extrabold Oblique | | Iosevka Extrabold Oblique | | Regular | | Iosevka | | Extrabold Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-term-extraboldoblique.ttf | | Iosevka Term ExtraBold Oblique | | Iosevka Term ExtraBold Oblique |X| Italic | | Iosevka Term | | ExtraBold Oblique
-|iosevka-term-extraboldoblique.ttf | | Iosevka Term Extrabold Oblique | | Iosevka Term Extrabold Oblique | | Regular | | Iosevka Term | | Extrabold Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-extralightoblique.ttf | | Iosevka ExtraLight Oblique | | Iosevka ExtraLight Oblique |X| Italic | | Iosevka | | ExtraLight Oblique
-|iosevka-extralightoblique.ttf | | Iosevka Extralight Oblique | | Iosevka Extralight Oblique | | Regular | | Iosevka | | Extralight Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-term-extralightoblique.ttf | | Iosevka Term ExtraLight Oblique | | Iosevka Term ExtraLight Obliqu |X| Italic | | Iosevka Term | | ExtraLight Oblique
-|iosevka-term-extralightoblique.ttf | | Iosevka Term Extralight Oblique | | Iosevka Term XLtObl | | Regular | | Iosevka Term | | Extralight Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-heavyoblique.ttf | | Iosevka Heavy Oblique | | Iosevka Heavy Oblique |X| Italic | | Iosevka | | Heavy Oblique
-|iosevka-heavyoblique.ttf | | Iosevka Heavy Oblique | | Iosevka Heavy Oblique | | Regular | | Iosevka | | Heavy Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-term-heavyoblique.ttf | | Iosevka Term Heavy Oblique | | Iosevka Term Heavy Oblique |X| Italic | | Iosevka Term | | Heavy Oblique
-|iosevka-term-heavyoblique.ttf | | Iosevka Term Heavy Oblique | | Iosevka Term Heavy Oblique | | Regular | | Iosevka Term | | Heavy Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-lightoblique.ttf | | Iosevka Light Oblique | | Iosevka Light Oblique |X| Italic | | Iosevka | | Light Oblique
-|iosevka-lightoblique.ttf | | Iosevka Light Oblique | | Iosevka Light Oblique | | Regular | | Iosevka | | Light Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-term-lightoblique.ttf | | Iosevka Term Light Oblique | | Iosevka Term Light Oblique |X| Italic | | Iosevka Term | | Light Oblique
-|iosevka-term-lightoblique.ttf | | Iosevka Term Light Oblique | | Iosevka Term Light Oblique | | Regular | | Iosevka Term | | Light Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-mediumoblique.ttf | | Iosevka Medium Oblique | | Iosevka Medium Oblique |X| Italic | | Iosevka | | Medium Oblique
-|iosevka-mediumoblique.ttf | | Iosevka Medium Oblique | | Iosevka Medium Oblique | | Regular | | Iosevka | | Medium Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-term-mediumoblique.ttf | | Iosevka Term Medium Oblique | | Iosevka Term Medium Oblique |X| Italic | | Iosevka Term | | Medium Oblique
-|iosevka-term-mediumoblique.ttf | | Iosevka Term Medium Oblique | | Iosevka Term Medium Oblique | | Regular | | Iosevka Term | | Medium Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-oblique.ttf | | Iosevka Oblique | | Iosevka Oblique |X| Italic | | Iosevka | | Oblique
-|iosevka-oblique.ttf | | Iosevka Oblique | | Iosevka Oblique | | Regular | | Iosevka | | Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-term-oblique.ttf | | Iosevka Term Oblique | | Iosevka Term Oblique |X| Italic | | Iosevka Term | | Oblique
-|iosevka-term-oblique.ttf | | Iosevka Term Oblique | | Iosevka Term Oblique | | Regular | | Iosevka Term | | Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-semiboldoblique.ttf | | Iosevka SemiBold Oblique | | Iosevka SemiBold Oblique |X| Italic | | Iosevka | | SemiBold Oblique
-|iosevka-semiboldoblique.ttf | | Iosevka Semibold Oblique | | Iosevka Semibold Oblique | | Regular | | Iosevka | | Semibold Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-term-semiboldoblique.ttf | | Iosevka Term SemiBold Oblique | | Iosevka Term SemiBold Oblique |X| Italic | | Iosevka Term | | SemiBold Oblique
-|iosevka-term-semiboldoblique.ttf | | Iosevka Term Semibold Oblique | | Iosevka Term Semibold Oblique | | Regular | | Iosevka Term | | Semibold Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-term-thinoblique.ttf | | Iosevka Term Thin Oblique | | Iosevka Term Thin Oblique |X| Italic | | Iosevka Term | | Thin Oblique
-|iosevka-term-thinoblique.ttf | | Iosevka Term Thin Oblique | | Iosevka Term Thin Oblique | | Regular | | Iosevka Term | | Thin Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead (and mark it Italic so applications know it's slanted) & Keep the grouping in Typographic Family
+|iosevka-thinoblique.ttf | | Iosevka Thin Oblique | | Iosevka Thin Oblique |X| Italic | | Iosevka | | Thin Oblique
-|iosevka-thinoblique.ttf | | Iosevka Thin Oblique | | Iosevka Thin Oblique | | Regular | | Iosevka | | Thin Oblique
#### Do we really base on the VF (variable font) We can not create VF. This makes no sense.
+|Lilex-VF.ttf |X| Lilex VF |X| Lilex VF | | Regular | | | |
-|Lilex-VF.ttf | | Lilex Regular | | Lilex | | Regular | | | |
#### Limit Subfamiliy to 4 standard styles, put Retina into Family instead
+|Monoid-Retina.ttf | | Monoid Retina |X| Monoid Retina |X| Regular |X| Monoid |X| Retina
-|Monoid-Retina.ttf | | Monoid Retina | | Monoid | | Retina | | | |
#### Remove nonstandard BoldItalic typographic style
+|mononoki-BoldItalic.ttf | | Mononoki Bold Italic | | Mononoki | | Bold Italic | | |X|
-|mononoki-BoldItalic.ttf | | mononoki Bold Italic | | mononoki | | Bold Italic | | | | BoldItalic
#### They say SemiBold is the same as Light Bold, we can not generalize this and make SemiBold self standing
+|overpass-mono-semibold.otf | | Overpass Mono SemiBold |X| Overpass Mono SemiBold |X| Regular | | Overpass Mono | | SemiBold
-|overpass-mono-semibold.otf | | Overpass Mono SemiBold | | Overpass Mono Light | | Bold | | Overpass Mono | | SemiBold
#### They say SemiBold is the same as Light Bold, we can not generalize this and make SemiBold self standing
+|overpass-semibold.otf | | Overpass SemiBold |X| Overpass SemiBold |X| Regular | | Overpass | | SemiBold
-|overpass-semibold.otf | | Overpass SemiBold | | Overpass Light | | Bold | | Overpass | | SemiBold
#### Nonstandard font naming: fullname shall be same as familyname plus more
+|ProFontIIx.ttf | | ProFont IIx |X| ProFont IIx | | Regular | | | |
-|ProFontIIx.ttf | | ProFont IIx | | ProFontIIx | | Regular | | | |
#### We are fine here (just list with exclamation mark because it is a potentially problematic case)
!|ProFontWindows.ttf | | ProFontWindows | | ProFontWindows | | Regular | | | |
|ProFontWindows.ttf | | ProFontWindows | | ProFontWindows | | Regular | | | |
#### No mention of TT in file name
+|ProggyCleanCE.ttf |X| ProggyClean CE |X| ProggyClean CE | | Regular | | | |
-|ProggyCleanCE.ttf | | ProggyCleanTT CE | | ProggyCleanTT CE | | Regular | | | |
#### No mention of TT in file name
!|ProggyClean.ttf |X| ProggyClean |X| ProggyClean | | Regular | | | |
-|ProggyClean.ttf | | ProggyCleanTT | | ProggyCleanTT | | Regular | | | |
#### No mention of TT in file name
+|ProggyCleanSZ.ttf |X| ProggyClean SZ |X| ProggyClean SZ | | Regular | | | |
-|ProggyCleanSZ.ttf | | ProggyCleanTTSZ | | ProggyCleanTTSZ | | Regular | | | |
#### They put one name part in parens
+|TerminusTTF-Bold Italic-4.40.1.ttf |X| Terminus TTF Bold Italic |X| Terminus TTF | | Bold Italic | | | |
-|TerminusTTF-Bold Italic-4.40.1.ttf | | Terminus (TTF) Bold Italic | | Terminus (TTF) | | Bold Italic | | | |
#### They put one name part in parens
+|TerminusTTF-Bold-4.40.1.ttf |X| Terminus TTF Bold |X| Terminus TTF | | Bold | | | |
-|TerminusTTF-Bold-4.40.1.ttf | | Terminus (TTF) Bold | | Terminus (TTF) | | Bold | | | |
#### They put one name part in parens
+|TerminusTTF-Italic-4.40.1.ttf |X| Terminus TTF Italic |X| Terminus TTF | | Italic | | | |
-|TerminusTTF-Italic-4.40.1.ttf | | Terminus (TTF) Italic | | Terminus (TTF) | | Italic | | | |
#### They put one name part in parens
+|TerminusTTF-4.40.1.ttf |X| Terminus TTF |X| Terminus TTF |X| Regular | | | |
-|TerminusTTF-4.40.1.ttf | | Terminus (TTF) | | Terminus (TTF) | | Medium | | | |
#### Ubuntu Condensed should be grouped with Ubuntu, that they didn't is an error?
+|Ubuntu-C.ttf | | Ubuntu Condensed | | Ubuntu Condensed | | Regular |X| Ubuntu |X| Condensed
-|Ubuntu-C.ttf | | Ubuntu Condensed | | Ubuntu Condensed | | Regular | | Ubuntu Condensed | | Regular
#### They say Medium is the same as Light Bold, we can not generalize this and make Medium self standing
+|Ubuntu-MI.ttf | | Ubuntu Medium Italic |X| Ubuntu Medium |X| Italic | | Ubuntu | | Medium Italic
-|Ubuntu-MI.ttf | | Ubuntu Medium Italic | | Ubuntu Light | | Bold Italic | | Ubuntu | | Medium Italic
#### They say Medium is the same as Light Bold, we can not generalize this and make Medium self standing
+|Ubuntu-M.ttf | | Ubuntu Medium |X| Ubuntu Medium |X| Regular | | Ubuntu | | Medium
-|Ubuntu-M.ttf | | Ubuntu Medium | | Ubuntu Light | | Bold | | Ubuntu | | Medium
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead
+|VictorMono-ExtraLightOblique.ttf | | Victor Mono ExtraLight Oblique |X| Victor Mono ExtraLight Oblique | | Italic | | Victor Mono | | ExtraLight Oblique
-|VictorMono-ExtraLightOblique.ttf | | Victor Mono ExtraLight Oblique | | Victor Mono ExtraLight | | Italic | | Victor Mono | | ExtraLight Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead
+|VictorMono-LightOblique.ttf | | Victor Mono Light Oblique |X| Victor Mono Light Oblique | | Italic | | Victor Mono | | Light Oblique
-|VictorMono-LightOblique.ttf | | Victor Mono Light Oblique | | Victor Mono Light | | Italic | | Victor Mono | | Light Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead
+|VictorMono-MediumOblique.ttf | | Victor Mono Medium Oblique |X| Victor Mono Medium Oblique | | Italic | | Victor Mono | | Medium Oblique
-|VictorMono-MediumOblique.ttf | | Victor Mono Medium Oblique | | Victor Mono Medium | | Italic | | Victor Mono | | Medium Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead
+|VictorMono-SemiBoldOblique.ttf | | Victor Mono SemiBold Oblique |X| Victor Mono SemiBold Oblique | | Italic | | Victor Mono | | SemiBold Oblique
-|VictorMono-SemiBoldOblique.ttf | | Victor Mono SemiBold Oblique | | Victor Mono SemiBold | | Italic | | Victor Mono | | SemiBold Oblique
#### Limit Subfamiliy to 4 standard styles, put Oblique into Family instead
+|VictorMono-ThinOblique.ttf | | Victor Mono Thin Oblique |X| Victor Mono Thin Oblique | | Italic | | Victor Mono | | Thin Oblique
-|VictorMono-ThinOblique.ttf | | Victor Mono Thin Oblique | | Victor Mono Thin | | Italic | | Victor Mono | | Thin Oblique

View file

@ -0,0 +1,195 @@
#!/usr/bin/env python
# coding=utf8
import sys
import re
import os.path
import glob
import subprocess
import fontforge
###### Some helpers
def get_sfnt_dict(font):
"""Extract SFNT table as nice dict"""
d = []
for i, el in enumerate(font.sfnt_names):
d += [(el[1], el[2])]
return dict(d)
def extract_sfnt_data(sfnt):
"""Get the usual names out of the SFNT table"""
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 ''
return (sfnt_full, sfnt_fam, sfnt_subfam, sfnt_pfam, sfnt_psubfam)
def format_names(header, *stuff):
"""Unify outputs (with header)"""
f = '{:1.1}|{:50.50} |{:1.1}| {:65.65} |{:1.1}| {:55.55} |{:1.1}| {:30.30} |{:1.1}| {:40.40} |{:1.1}| {:.40}'
if header:
d = '------------------------------------------------------------'
return f.format(*stuff) + '\n' + f.format('', d, d, d, d, d, d, d, d, d, d, d)
return f.format(*stuff).rstrip()
def lenient_cmp(s1, s2, allow_shuffle_all):
"""Compare two font name (parts) but be a bit lenient ;->"""
# We do not care about:
# - Case
# - "Display" vs "Disp" (in Noto)
# Allow for "IBM 3278" name
s = [ s1, s2 ]
for i in range(2):
# Usually given transform from 'their' to 'our' style
s[i] = s[i].lower()
s[i] = re.sub(r'\bdisp\b', 'display', s[i]) # Noto
s[i] = s[i].replace('ibm 3270', '3270') # 3270
s[i] = s[i].replace('3270-', '3270 ') # 3270
s[i] = s[i].replace('lekton-', 'lekton ') # Lekton
s[i] = s[i].replace('semi-narrow', 'seminarrow') # 3270
s[i] = s[i].replace('bolditalic', 'bold italic')
s[i] = re.sub(r'\bfor\b', '', s[i]) # Meslo, Monofur
s[i] = re.sub(r'\bpowerline\b', '', s[i]) # Meslo, Monofur
s[i] = s[i].replace('fira mono', 'fura mono') # Obviously someone forgot to rename the fonts in Fira/
s[i] = s[i].replace('aurulentsansmono-', 'aurulent sans mono ') # Aurulent fullname oddity
s[i] = s[i].replace('mononoki-', 'mononoki ') # Mononoki has somtimes a dash
s[i] = re.sub(r'\br\b', 'regular', s[i]) # Nonstandard style in Agave
s[i] = re.sub(r'(bitstream vera sans mono.*) oblique', r'\1 italic', s[i]) # They call it Oblique but the filename says Italic
s[i] = re.sub(r'gohufont (uni-)?(11|14)', 'gohufont', s[i]) # They put the 'name' into the subfamily/weight
s[i] = s[i].replace('xltobl', 'extralight oblique') # Iosevka goes inventing names
s[i] = re.sub(r'proggyclean(?!TT)( ?)', 'proggycleantt\1', s[i]) # ProggyClean has no TT in filename
s[i] = re.sub(r' +', ' ', s[i]).strip()
p = []
for e in s:
parts = e.split(' ')
if not allow_shuffle_all and len(parts) > 2:
tail = parts[1:]
tail.sort()
parts = [parts[0]] + tail
elif len(parts) > 1:
parts.sort()
p.append(' '.join(parts))
return p[0] == p[1]
###### Let's go!
if len(sys.argv) < 2:
print('Usage: {} font_name [font_name ...]\n'.format(sys.argv[0]))
sys.exit(1)
font_patcher = os.path.realpath(os.path.dirname(os.path.realpath(sys.argv[0]))+'/../../../font-patcher')
existing_font = glob.glob('*.[ot]tf')
if len(existing_font):
sys.exit('Would overwrite any existing *.ttf and *.otf, bailing out (remove them first)')
try:
with open(sys.argv[0] + '.known_issues', 'r') as f:
known_issues = f.read().splitlines()
# known_issues = [line.rstrip() for line in known_issues]
print('Found {:.0f} known issues'.format(len(known_issues) / 4)) # approx ;)
except OSError:
print('Can not open known_issues file')
known_issues = []
new_issues = open(sys.argv[0] + '.known_issues.new', 'w')
print('Examining {} font files'.format(len(sys.argv) - 1))
all_files = 0
issue_files = 0
known_files = 0
print(format_names(True, '', 'Filename', '', 'Fullname', '', 'Family', '', 'Subfamily', '', 'Typogr. Family', '', 'Typogr. Subfamily'))
for filename in sys.argv[1:]:
data = []
fullfile = os.path.basename(filename)
fname = os.path.splitext(fullfile)[0]
if fname == 'NotoColorEmoji':
continue # font is not patchable
if fname in [ 'iosevka-heavyoblique', 'iosevka-term-heavyoblique', 'iosevka-mediumoblique', 'Lilex-VF' ]:
continue # Patch resultant font not openable
log = open("log", "w")
log.write(filename)
log.close()
for option in ['--parser', '']:
cmd = ['fontforge', '--script', font_patcher, '--powerline', option, filename ]
cmd = [ c for c in cmd if len(c) ]
ff = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, encoding='utf8')
#ff = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf8')
if ff.returncode:
print("\nERROR running command:\n {}\n\n{}".format(' '.join(cmd), ''))#ff.stdout))
sys.exit(1)
new_font = glob.glob('*.[ot]tf')
font = fontforge.open(new_font[0], 1)
sfnt = get_sfnt_dict(font)
font.close()
os.system('rm -f *.[ot]tf')
(sfnt_full, sfnt_fam, sfnt_subfam, sfnt_pfam, sfnt_psubfam) = extract_sfnt_data(sfnt)
data.append(( os.path.basename(new_font[0]), sfnt_full, sfnt_fam, sfnt_subfam, sfnt_pfam, sfnt_psubfam ))
all_files += 1
t1 = not lenient_cmp(data[0][1], data[1][1], False)
t2 = not lenient_cmp(data[0][2], data[1][2], False)
t3 = not lenient_cmp(data[0][3], data[1][3], True)
t4 = not lenient_cmp(data[0][4], data[1][4], False)
t5 = not lenient_cmp(data[0][5], data[1][5], True)
# Lenience: Allow for dropping unneeded prefered stuff:
# New (sub)family is same as old preferred sub(family)
if t4 and data[0][4] == '' and data[1][4].lower() == data[0][2].lower():
t4 = False
if t5 and data[0][5] == '' and data[1][5].lower() == data[0][3].lower():
t5 = False
if t1 or t2 or t3 or t4 or t5:
m1 = '+'; m2 = '-'
else:
m1 = ''; m2 = ''
t1_ = 'X' if t1 else ''
t2_ = 'X' if t2 else ''
t3_ = 'X' if t3 else ''
t4_ = 'X' if t4 else ''
t5_ = 'X' if t5 else ''
o1 = format_names(False, m1, data[0][0], t1_, data[0][1], t2_, data[0][2], t3_, data[0][3], t4_, data[0][4], t5_, data[0][5])
o2 = format_names(False, m2, data[1][0], '', data[1][1], '', data[1][2], '', data[1][3], '', data[1][4], '', data[1][5])
if len(m1):
issue_files += 1
font = fontforge.open(filename, 1)
sfnt = get_sfnt_dict(font)
font.close()
(sfnt_full, sfnt_fam, sfnt_subfam, sfnt_pfam, sfnt_psubfam) = extract_sfnt_data(sfnt)
o3 = format_names(False, '>', os.path.basename(filename), '', sfnt_full, '', sfnt_fam, '', sfnt_subfam, '', sfnt_pfam, '', sfnt_psubfam)
if not o1 in known_issues or not o2 in known_issues:
new_issues.writelines(['#### AUTOGENERATED\n', o3 + '\n', o1 + '\n', o2 + '\n'])
else:
known_files += 1
idx = known_issues.index(o1) - 2 # should be the index of the explanation line
if known_issues[idx][0] != '#':
sys.exit('Problem with known issues file, line', known_issues.index(o1))
new_issues.writelines([known_issues[idx] + '\n', o3 + '\n', o1 + '\n', o2 + '\n'])
# remove known_issue triplet
known_issues.pop(idx)
known_issues.pop(idx)
known_issues.pop(idx)
known_issues.pop(idx)
print(o1, o2, sep='\n')
print('Fonts with different name rendering: {}/{} ({}/{} are in known_issues)'.format(issue_files, all_files, known_files, issue_files))
if len(known_issues) > 0:
print('There are {} lines not needed in known_issues, appending commented out in new known_issues'.format(len(known_issues)))
new_issues.write('\n#### The following lines are not needed anymore\n\n')
for l in known_issues:
new_issues.writelines([' ', l, '\n'])
new_issues.close()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
#!/usr/bin/env python
# coding=utf8
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 = '{:1.1}|{:50.50} |{:1.1}| {:50.50} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:30.30} |{:1.1}| {:.30}'
if header:
d = '------------------------------------------------------------'
return f.format(*stuff) + '\n' + f.format('', d, d, d, d, d, d, d, d, d, d, d)
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', '', '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)
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, '', sfnt_full, '', sfnt_fam, '', sfnt_subfam, '', sfnt_pfam, '', sfnt_psubfam)
print(o2)

View file

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

View file

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

View file

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

View file

@ -0,0 +1,14 @@
Add weight/style to family [1]
Use only Regular/Bold/Italic in SubFamily [2]
Classify Medium as own weight and not Bold [3]
Change regular-equivalent name to Regular [4]
Drop unneeded Typogr.Family/Typogr.Style [5]
Do not call Semibold Light-Bold [6]
Fullname has been missing 'Nerd Font' [7]
The fonts name is M+ not Mplus [8]
Handle Retina as Weight and not Style [9]
Bold and Italic are styles of a basefont [10]
Put Oblique into own SubFamily (and mark it as italic) [11]
'Term' is missing from Family [12]
Bold / Bold-Italic are just a styles of Regular [13]
Drop Regular from Style [14]

View file

@ -36,6 +36,10 @@ except ImportError:
)
)
# This is (for experimenting) far far away...
sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0])) + '/bin/scripts/name_parser/')
from FontnameParser import FontnameParser
from FontnameTools import FontnameTools
class TableHEADWriter:
""" Access to the HEAD table without external dependencies """
@ -306,6 +310,7 @@ class font_patcher:
parser.add_argument('-ext', '--extension', dest='extension', default="", type=str, nargs='?', help='Change font file type to create (e.g., ttf, otf)')
parser.add_argument('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to')
parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching')
parser.add_argument('--parser', dest='parser', default=False, action='store_true', help='Use alternative method to name patched fonts (experimental)')
# progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
progressbars_group_parser = parser.add_mutually_exclusive_group(required=False)
@ -425,6 +430,26 @@ class font_patcher:
additionalFontNameSuffix += " M"
verboseAdditionalFontNameSuffix += " Mono"
if self.args.parser:
use_fullname = True # Usually the fullname is better to parse
# Use fullname if it is 'equal' to the fontname
use_fullname |= self.sourceFont.fontname.lower() == FontnameTools.postscript_char_filter(self.sourceFont.fullname).lower()
# Use fullname for any of these source fonts (that are impossible to disentangle from the fontname, we need the blanks)
for hit in [ 'Meslo' ]:
use_fullname |= self.sourceFont.fontname.lower().startswith(hit.lower())
parser_name = self.sourceFont.fullname if use_fullname else self.sourceFont.fontname
# Gohu fontnames hide the weight, but the file names are ok...
if parser_name.startswith('Gohu'):
parser_name = os.path.splitext(os.path.basename(self.args.font))[0]
n = FontnameParser(parser_name)
if not n.parse_ok:
print("Have only minimal naming information, check resulting name. Maybe omit --parser option")
n.drop_for_powerline()
n.enable_short_families(True, "Noto")
n.set_for_windows(self.args.windows)
# All the following stuff is ignored in parser-mode
# basically split the font name around the dash "-" to get the fontname and the style (e.g. Bold)
# this does not seem very reliable so only use the style here as a fallback if the font does not
# have an internal style defined (in sfnt_names)
@ -558,6 +583,7 @@ class font_patcher:
fullname = replace_font_name(fullname, additionalFontNameReplacements2)
fontname = replace_font_name(fontname, additionalFontNameReplacements2)
if not self.args.parser:
# replace any extra whitespace characters:
self.sourceFont.familyname = " ".join(familyname.split())
self.sourceFont.fullname = " ".join(fullname.split())
@ -567,6 +593,12 @@ class font_patcher:
self.sourceFont.appendSFNTName(str('English (US)'), str('Family'), self.sourceFont.familyname)
self.sourceFont.appendSFNTName(str('English (US)'), str('Compatible Full'), self.sourceFont.fullname)
self.sourceFont.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily)
else:
fam_suffix = projectNameSingular if not self.args.windows else projectNameAbbreviation
fam_suffix += ' Mono' if self.args.single else ''
n.inject_suffix(verboseAdditionalFontNameSuffix, additionalFontNameSuffix, fam_suffix)
n.rename_font(self.sourceFont)
self.sourceFont.comment = projectInfo
self.sourceFont.fontlog = projectInfo