2023-05-02 14:57:12 -05:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# coding=utf8
|
|
|
|
|
|
|
|
import re
|
|
|
|
from FontnameTools import FontnameTools
|
|
|
|
|
|
|
|
class FontnameParser:
|
|
|
|
"""Parse a font name and generate all kinds of names"""
|
|
|
|
|
2023-05-31 19:41:02 -05:00
|
|
|
def __init__(self, fontname, logger):
|
|
|
|
"""Parse a fontname and store the results"""
|
2023-05-02 14:57:12 -05:00
|
|
|
self.parse_ok = False
|
2023-05-14 19:29:20 -05:00
|
|
|
self.use_short_families = (False, False, False) # ( camelcase name, short styles, aggressive )
|
|
|
|
self.keep_regular_in_family = None # None = auto, True, False
|
2023-05-02 14:57:12 -05:00
|
|
|
self.suppress_preferred_if_identical = True
|
2023-05-14 19:29:20 -05:00
|
|
|
self.family_suff = ''
|
|
|
|
self.ps_fontname_suff = ''
|
|
|
|
self.short_family_suff = ''
|
2023-05-02 14:57:12 -05:00
|
|
|
self.name_subst = []
|
2023-05-31 19:41:02 -05:00
|
|
|
[ self.parse_ok, self._basename, self.weight_token, self.style_token, self.other_token, self._rest ] = FontnameTools.parse_font_name(fontname)
|
2023-05-02 14:57:12 -05:00
|
|
|
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"""
|
2023-05-14 19:29:20 -05:00
|
|
|
fam = 'family ' if is_family else ''
|
2023-05-02 14:57:12 -05:00
|
|
|
limit = 31 if is_family else 63
|
|
|
|
if len(n) <= limit:
|
|
|
|
return n
|
2023-05-14 19:29:20 -05:00
|
|
|
r = re.search('(.*)(-.*)', n)
|
2023-05-02 14:57:12 -05:00
|
|
|
if not r:
|
|
|
|
new_n = n[:limit]
|
|
|
|
else:
|
|
|
|
q = limit - len(r.groups()[1])
|
|
|
|
if q < 1:
|
|
|
|
q = 1
|
2023-05-14 19:29:20 -05:00
|
|
|
self.logger.error('====-< Shortening too long PS {}name: Garbage warning'. format(fam))
|
2023-05-02 14:57:12 -05:00
|
|
|
new_n = r.groups()[0][:q] + r.groups()[1]
|
|
|
|
if new_n != n:
|
2023-05-14 19:29:20 -05:00
|
|
|
self.logger.error('====-< Shortening too long PS {}name: {} -> {}'.format(fam, n, new_n))
|
2023-05-02 14:57:12 -05:00
|
|
|
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:
|
2023-05-14 19:29:20 -05:00
|
|
|
return (FontnameTools.concat(self.basename, self.rest).replace(' ', ''), '')
|
2023-05-02 14:57:12 -05:00
|
|
|
|
|
|
|
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.
|
2023-05-14 19:29:20 -05:00
|
|
|
if 'Italic' in self.style_token:
|
2023-05-02 14:57:12 -05:00
|
|
|
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()
|
2023-05-14 19:29:20 -05:00
|
|
|
self.ps_fontname_suff = ps_fontname.replace(' ', '')
|
2023-05-02 14:57:12 -05:00
|
|
|
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)
|
2023-05-14 19:29:20 -05:00
|
|
|
self.use_short_families = ( camelcase_name, prefix, aggressive )
|
2023-05-02 14:57:12 -05:00
|
|
|
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:
|
2023-05-14 19:29:20 -05:00
|
|
|
base_and_rest = self.basename + (' ' + self.rest if len(self.rest) else '')
|
2023-05-02 14:57:12 -05:00
|
|
|
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()
|
2023-05-14 19:29:20 -05:00
|
|
|
self.rest = self.rest[-(i+1):].lstrip()
|
2023-05-02 14:57:12 -05:00
|
|
|
else:
|
2023-05-14 19:29:20 -05:00
|
|
|
self.basename = m.expand(replacement) + self.basename[len(m.group(0)):]
|
2023-05-02 14:57:12 -05:00
|
|
|
return self
|
|
|
|
|
|
|
|
def drop_for_powerline(self):
|
|
|
|
"""Remove 'for Powerline' from all names (can not be undone)"""
|
2023-05-14 19:29:20 -05:00
|
|
|
if 'Powerline' in self.other_token:
|
|
|
|
idx = self.other_token.index('Powerline')
|
2023-05-02 14:57:12 -05:00
|
|
|
self.other_token.pop(idx)
|
2023-05-14 19:29:20 -05:00
|
|
|
if idx > 0 and self.other_token[idx - 1] == 'For':
|
2023-05-02 14:57:12 -05:00
|
|
|
self.other_token.pop(idx - 1)
|
2023-05-14 19:29:20 -05:00
|
|
|
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
|
2023-05-02 14:57:12 -05:00
|
|
|
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:
|
2023-05-14 19:29:20 -05:00
|
|
|
keep_regular = FontnameTools.is_keep_regular(self._basename + ' ' + self._rest)
|
2023-05-02 14:57:12 -05:00
|
|
|
else:
|
|
|
|
keep_regular = self.keep_regular_in_family
|
2023-05-14 19:29:20 -05:00
|
|
|
if ('Regular' in styles
|
|
|
|
and (not keep_regular
|
|
|
|
or len(self.weight_token) > 0)): # This is actually a malformed font name
|
2023-05-02 14:57:12 -05:00
|
|
|
styles = list(self.style_token)
|
2023-05-14 19:29:20 -05:00
|
|
|
styles.remove('Regular')
|
2023-05-02 14:57:12 -05:00
|
|
|
# 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]:
|
2023-05-14 19:29:20 -05:00
|
|
|
[ 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)
|
2023-05-02 14:57:12 -05:00
|
|
|
|
|
|
|
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])
|
2023-05-14 19:29:20 -05:00
|
|
|
fam = FontnameTools.camel_casify(FontnameTools.concat(name, rest, self.other_token, self.ps_fontname_suff))
|
2023-05-02 14:57:12 -05:00
|
|
|
sub = FontnameTools.camel_casify(FontnameTools.concat(weights, styles))
|
|
|
|
if len(sub) > 0:
|
2023-05-14 19:29:20 -05:00
|
|
|
sub = '-' + sub
|
2023-05-02 14:57:12 -05:00
|
|
|
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
|
2023-05-14 19:29:20 -05:00
|
|
|
return ''
|
2023-05-02 14:57:12 -05:00
|
|
|
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
|
2023-05-14 19:29:20 -05:00
|
|
|
return ''
|
2023-05-02 14:57:12 -05:00
|
|
|
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]:
|
2023-05-14 19:29:20 -05:00
|
|
|
[ other, weights ] = FontnameTools.short_styles([ other, weights ], aggressive)
|
|
|
|
weights = [ w if w != 'Oblique' else 'Obl' for w in weights ]
|
2023-05-02 14:57:12 -05:00
|
|
|
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:
|
2023-05-14 19:29:20 -05:00
|
|
|
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')
|
2023-05-02 14:57:12 -05:00
|
|
|
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)
|
2023-05-14 19:29:20 -05:00
|
|
|
b |= 1 if 'Bold' in self.style_token else 0
|
|
|
|
b |= 2 if 'Italic' in self.style_token else 0
|
2023-05-02 14:57:12 -05:00
|
|
|
return b
|
|
|
|
|
|
|
|
def fs_selection(self, fs):
|
|
|
|
"""Modify a given fsSelection value for current name, bits 0, 5, 6, 8, 9 touched"""
|
2023-05-14 19:29:20 -05:00
|
|
|
ITALIC = 1 << 0; BOLD = 1 << 5; REGULAR = 1 << 6; WWS = 1 << 8; OBLIQUE = 1 << 9
|
2023-05-02 14:57:12 -05:00
|
|
|
b = fs & (~(ITALIC | BOLD | REGULAR | WWS | OBLIQUE))
|
2023-05-14 19:29:20 -05:00
|
|
|
if 'Bold' in self.style_token:
|
2023-05-02 14:57:12 -05:00
|
|
|
b |= BOLD
|
|
|
|
# Ignore Italic if we have Oblique
|
2023-05-14 19:29:20 -05:00
|
|
|
if 'Oblique' in self.weight_token:
|
2023-05-02 14:57:12 -05:00
|
|
|
b |= OBLIQUE
|
2023-05-31 19:41:02 -05:00
|
|
|
if not self.rename_oblique:
|
|
|
|
# If we have no dedicated italic, than oblique = italic
|
|
|
|
b |= ITALIC
|
2023-05-14 19:29:20 -05:00
|
|
|
elif 'Italic' in self.style_token:
|
2023-05-02 14:57:12 -05:00
|
|
|
b |= ITALIC
|
|
|
|
# Regular is just the basic weight
|
2023-05-14 19:29:20 -05:00
|
|
|
if len(self.weight_token) == 0 and not b & (ITALIC | BOLD | OBLIQUE):
|
2023-05-02 14:57:12 -05:00
|
|
|
b |= REGULAR
|
2023-05-14 19:29:20 -05:00
|
|
|
b |= WWS # We assert this by our naming process
|
2023-05-02 14:57:12 -05:00
|
|
|
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:
|
2023-05-14 19:29:20 -05:00
|
|
|
self.logger.debug('=====> {:18} ok ({:2} <={:2}): {}'.format(entry_id, len(name), max_len, name))
|
2023-05-02 14:57:12 -05:00
|
|
|
else:
|
2023-05-14 19:29:20 -05:00
|
|
|
self.logger.error('====-< {:18} too long ({:2} > {:2}): {}'.format(entry_id, len(name), max_len, name))
|
2023-05-02 14:57:12 -05:00
|
|
|
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*
|
|
|
|
|
2023-05-14 19:29:20 -05:00
|
|
|
version_tag = ''
|
2023-05-02 14:57:12 -05:00
|
|
|
sfnt_list = []
|
2023-05-14 19:29:20 -05:00
|
|
|
TO_DEL = ['Family', 'SubFamily', 'Fullname', 'PostScriptName', 'Preferred Family',
|
|
|
|
'Preferred Styles', 'Compatible Full', 'WWS Family', 'WWS Subfamily',
|
|
|
|
'UniqueID', 'CID findfont Name']
|
2023-05-02 14:57:12 -05:00
|
|
|
# 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:
|
2023-05-14 19:29:20 -05:00
|
|
|
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
|
2023-05-02 14:57:12 -05:00
|
|
|
|
|
|
|
p_fam = self.preferred_family()
|
|
|
|
if len(p_fam):
|
2023-05-14 19:29:20 -05:00
|
|
|
sfnt_list += [( 'English (US)', 'Preferred Family', self.checklen(31, 'PrefFamily (ID 16)', p_fam) )] # 16
|
2023-05-02 14:57:12 -05:00
|
|
|
p_sty = self.preferred_styles()
|
|
|
|
if len(p_sty):
|
2023-05-14 19:29:20 -05:00
|
|
|
sfnt_list += [( 'English (US)', 'Preferred Styles', self.checklen(31, 'PrefStyles (ID 17)', p_sty) )] # 17
|
2023-05-02 14:57:12 -05:00
|
|
|
|
|
|
|
font.sfnt_names = tuple(sfnt_list)
|
|
|
|
|
|
|
|
font.macstyle = self.macstyle(0)
|
|
|
|
font.os2_stylemap = self.fs_selection(0)
|