font-patcher: Introduce less severe family name shortening

[why]
We want to keep "Nerd Font" in the font name if possible and instead
shorten the weight part with accepted abbreviations. But these abbrevs
are hard to read and sometimes a more mild abbreviating might be
sufficient to get the desired name length.

[how]
Introduce a new shortening method for the weight parts of a family name.
It takes a longer word (often un-shortened) when a weight stands on its
own, but when a modifier is used together with the weight the more
aggressive two-letter abbreviations are used.

That new shortening method becomes the default and all the functions get
a new parameter to enforce completely aggressive shortening, i.e. always
use the shortest possible form.

The new way to shorten is exposed all the way out to the font-patcher
user who can select the shortening method as parameter to the
--makegroups option. That option is undocumented because I expect some
changes later on, still.

Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
This commit is contained in:
Fini Jastrow 2023-04-19 15:57:50 +02:00
parent ff2be6af81
commit 9be4835c29
3 changed files with 74 additions and 55 deletions

View file

@ -10,7 +10,7 @@ class FontnameParser:
def __init__(self, filename, logger): def __init__(self, filename, logger):
"""Parse a font filename and store the results""" """Parse a font filename and store the results"""
self.parse_ok = False self.parse_ok = False
self.use_short_families = (False, False) # ( camelcase name, short styles ) self.use_short_families = (False, False, False) # ( camelcase name, short styles, aggressive )
self.keep_regular_in_family = None # None = auto, True, False self.keep_regular_in_family = None # None = auto, True, False
self.suppress_preferred_if_identical = True self.suppress_preferred_if_identical = True
self.family_suff = '' self.family_suff = ''
@ -65,13 +65,13 @@ class FontnameParser:
self.short_family_suff = short_family.strip() self.short_family_suff = short_family.strip()
return self return self
def enable_short_families(self, camelcase_name, prefix): 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""" """Enable short styles in Family when (original) font name starts with prefix; enable CamelCase basename in (Typog.) Family"""
# camelcase_name is boolean # camelcase_name is boolean
# prefix is either a string or False/True # prefix is either a string or False/True
if isinstance(prefix, str): if isinstance(prefix, str):
prefix = self._basename.startswith(prefix) prefix = self._basename.startswith(prefix)
self.use_short_families = ( camelcase_name, prefix ) self.use_short_families = ( camelcase_name, prefix, aggressive )
return self return self
def add_name_substitution_table(self, table): def add_name_substitution_table(self, table):
@ -145,15 +145,15 @@ class FontnameParser:
(weights, styles) = FontnameTools.make_oblique_style(weights, styles) (weights, styles) = FontnameTools.make_oblique_style(weights, styles)
(name, rest) = self._shortened_name() (name, rest) = self._shortened_name()
if self.use_short_families[1]: if self.use_short_families[1]:
[ weights, styles ] = FontnameTools.short_styles([ weights, styles ]) [ 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) return FontnameTools.concat(name, rest, self.other_token, self.short_family_suff, weights, styles)
def psname(self): def psname(self):
"""Get the SFNT PostScriptName (ID 6)""" """Get the SFNT PostScriptName (ID 6)"""
# This is almost self.family() + '-' + self.subfamily() # This is almost self.family() + '-' + self.subfamily()
(name, rest) = self._shortened_name() (name, rest) = self._shortened_name()
styles = FontnameTools.short_styles(self.style_token) styles = FontnameTools.short_styles(self.style_token, self.use_short_families[2])
weights = FontnameTools.short_styles(self.weight_token) weights = FontnameTools.short_styles(self.weight_token, self.use_short_families[2])
fam = FontnameTools.camel_casify(FontnameTools.concat(name, rest, self.other_token, self.ps_fontname_suff)) fam = FontnameTools.camel_casify(FontnameTools.concat(name, rest, self.other_token, self.ps_fontname_suff))
sub = FontnameTools.camel_casify(FontnameTools.concat(weights, styles)) sub = FontnameTools.camel_casify(FontnameTools.concat(weights, styles))
if len(sub) > 0: if len(sub) > 0:
@ -190,7 +190,7 @@ class FontnameParser:
other = self.other_token other = self.other_token
weight = self.weight_token weight = self.weight_token
if self.use_short_families[1]: if self.use_short_families[1]:
[ other, weight ] = FontnameTools.short_styles([ other, weight ]) [ other, weight ] = FontnameTools.short_styles([ other, weight ], self.use_short_families[2])
return FontnameTools.concat(name, rest, other, self.short_family_suff, weight) return FontnameTools.concat(name, rest, other, self.short_family_suff, weight)
def subfamily(self): def subfamily(self):

View file

@ -93,31 +93,38 @@ class FontnameTools:
return None return None
@staticmethod @staticmethod
def shorten_style_name(name): def shorten_style_name(name, aggressive):
"""Substitude some known styles to short form""" """Substitude some known styles to short form"""
# If aggressive is False create the mild short form
# aggressive == True: Always use first form of everything
# aggressive == False:
# - has no modifier: use the second form
# - has modifier: use second form of mod plus first form of main
name_rest = name name_rest = name
name_pre = '' name_pre = ''
form = 0 if aggressive else 1
for mod in FontnameTools.known_modifiers: for mod in FontnameTools.known_modifiers:
if name.startswith(mod) and len(name) > len(mod): # Second condition specifically for 'Demi' if name.startswith(mod) and len(name) > len(mod): # Second condition specifically for 'Demi'
name_pre = FontnameTools.known_modifiers[mod] name_pre = FontnameTools.known_modifiers[mod][form]
name_rest = name[len(mod):] name_rest = name[len(mod):]
break break
form = 0 if aggressive or len(name_pre) else 1
subst = FontnameTools.find_in_dicts(name_rest, [ FontnameTools.known_weights2, FontnameTools.known_widths ]) subst = FontnameTools.find_in_dicts(name_rest, [ FontnameTools.known_weights2, FontnameTools.known_widths ])
if isinstance(subst, str): if isinstance(subst, tuple):
return name_pre + subst return name_pre + subst[form]
if not len(name_pre): if not len(name_pre):
# The following sets do not allow modifiers # The following sets do not allow modifiers
subst = FontnameTools.find_in_dicts(name_rest, [ FontnameTools.known_weights1, FontnameTools.known_slopes ]) subst = FontnameTools.find_in_dicts(name_rest, [ FontnameTools.known_weights1, FontnameTools.known_slopes ])
if isinstance(subst, str): if isinstance(subst, tuple):
return subst return subst[form]
return name return name
@staticmethod @staticmethod
def short_styles(lists): def short_styles(lists, aggressive):
"""Shorten all style names in a list or a list of lists""" """Shorten all style names in a list or a list of lists"""
if not len(lists) or not isinstance(lists[0], list): if not len(lists) or not isinstance(lists[0], list):
return list(map(FontnameTools.shorten_style_name, lists)) return list(map(lambda x: FontnameTools.shorten_style_name(x, aggressive), lists))
return [ list(map(FontnameTools.shorten_style_name, styles)) for styles in lists ] return [ list(map(lambda x: FontnameTools.shorten_style_name(x, aggressive), styles)) for styles in lists ]
@staticmethod @staticmethod
def make_oblique_style(weights, styles): def make_oblique_style(weights, styles):
@ -198,44 +205,52 @@ class FontnameTools:
] ]
# From https://adobe-type-tools.github.io/font-tech-notes/pdfs/5088.FontNames.pdf # From https://adobe-type-tools.github.io/font-tech-notes/pdfs/5088.FontNames.pdf
# The first short variant is from the linked table.
# The second (longer) short variant is from diverse fonts like Noto.
# We can
# - use the long form
# - use the very short form (first)
# - use mild short form:
# - has no modifier: use the second form
# - has modifier: use second form of mod plus first form of main
known_weights1 = { # can not take modifiers known_weights1 = { # can not take modifiers
'Medium': 'Md', 'Medium': ('Md', 'Med'),
'Nord': 'Nd', 'Nord': ('Nd', 'Nord'),
'Book': 'Bk', 'Book': ('Bk', 'Book'),
'Poster': 'Po', 'Poster': ('Po', 'Poster'),
'Demi': 'Dm', # Demi is sometimes used as a weight, sometimes as a modifier 'Demi': ('Dm', 'Demi'), # Demi is sometimes used as a weight, sometimes as a modifier
'Regular': 'Rg', 'Regular': ('Rg', 'Reg'),
'Display': 'DS', 'Display': ('DS', 'Disp'),
'Super': 'Su', 'Super': ('Su', 'Sup'),
'Retina': 'Rt', 'Retina': ('Rt', 'Ret'),
} }
known_weights2 = { # can take modifiers known_weights2 = { # can take modifiers
'Black': 'Blk', 'Black': ('Blk', 'Black'),
'Bold': 'Bd', 'Bold': ('Bd', 'Bold'),
'Heavy': 'Hv', 'Heavy': ('Hv', 'Heavy'),
'Thin': 'Th', 'Thin': ('Th', 'Thin'),
'Light': 'Lt', 'Light': ('Lt', 'Light'),
} }
known_widths = { # can take modifiers known_widths = { # can take modifiers
'Compressed': 'Cm', 'Compressed': ('Cm', 'Comp'),
'Extended': 'Ex', 'Extended': ('Ex', 'Extd'),
'Condensed': 'Cn', 'Condensed': ('Cn', 'Cond'),
'Narrow': 'Nr', 'Narrow': ('Nr', 'Narrow'),
'Compact': 'Ct', 'Compact': ('Ct', 'Compact'),
} }
known_slopes = { known_slopes = {
'Inclined': 'Ic', 'Inclined': ('Ic', 'Incl'),
'Oblique': 'Obl', 'Oblique': ('Obl', 'Obl'),
'Italic': 'It', 'Italic': ('It', 'Italic'),
'Upright': 'Up', 'Upright': ('Up', 'Uprght'),
'Kursiv': 'Ks', 'Kursiv': ('Ks', 'Kurs'),
'Sloped': 'Sl', 'Sloped': ('Sl', 'Slop'),
} }
known_modifiers = { known_modifiers = {
'Demi': 'Dm', 'Demi': ('Dm', 'Dem'),
'Ultra': 'Ult', 'Ultra': ('Ult', 'Ult'),
'Semi': 'Sm', 'Semi': ('Sm', 'Sem'),
'Extra': 'X', 'Extra': ('X', 'Ext'),
} }
@staticmethod @staticmethod

View file

@ -569,7 +569,7 @@ class font_patcher:
if not n.parse_ok: if not n.parse_ok:
logger.warning("Have only minimal naming information, check resulting name. Maybe omit --makegroups option") logger.warning("Have only minimal naming information, check resulting name. Maybe omit --makegroups option")
n.drop_for_powerline() n.drop_for_powerline()
n.enable_short_families(True, self.args.makegroups in [ 1, 3]) n.enable_short_families(True, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ])
# All the following stuff is ignored in makegroups-mode # All the following stuff is ignored in makegroups-mode
@ -723,7 +723,7 @@ class font_patcher:
font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname) font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname)
font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily)
else: else:
short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups in [ 2, 3] else projectNameSingular + variant_full short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full
# inject_suffix(family, ps_fontname, short_family) # inject_suffix(family, ps_fontname, short_family)
n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family) n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family)
n.rename_font(font) n.rename_font(font)
@ -1805,14 +1805,18 @@ def setup_arguments():
parser.add_argument('-ext', '--extension', dest='extension', default="", type=str, nargs='?', help='Change font file type to create (e.g., ttf, otf)') parser.add_argument('-ext', '--extension', dest='extension', default="", type=str, nargs='?', help='Change font file type to create (e.g., ttf, otf)')
parser.add_argument('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to') parser.add_argument('-out', '--outputdir', dest='outputdir', default=".", type=str, nargs='?', help='The directory to output the patched font file to')
parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching') parser.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, nargs='?', help='Path to glyphs to be used for patching')
parser.add_argument('--makegroups', dest='makegroups', default=0, type=int, nargs='?', help='Use alternative method to name patched fonts (recommended)', const=3, choices=range(0, 4 + 1)) parser.add_argument('--makegroups', dest='makegroups', default=0, type=int, nargs='?', help='Use alternative method to name patched fonts (recommended)', const=1, choices=range(0, 6 + 1))
# --makegroup has an additional undocumented numeric specifier. '--makegroup' is in fact '--makegroup 3'. # --makegroup has an additional undocumented numeric specifier. '--makegroup' is in fact '--makegroup 1'.
# Possible values with examples: # Original font name: Hugo Sans Mono ExtraCondensed Light Italic
# 0 - turned off, use old naming scheme # NF Fam agg.
# 1 - turned on, shortening 'Bold' to 'Bd' # 0 turned off, use old naming scheme [-] [-] [-]
# 2 - turned on, shortening 'Nerd Font' to 'NF' # 1 HugoSansMono Nerd Font ExtraCondensed Light Italic [ ] [ ] [ ]
# 3 - turned on, shortening 'Nerd Font' to 'NF' and 'Bold' to 'Bd' # 2 HugoSansMono Nerd Font ExtCn Light Italic [ ] [X] [ ]
# 4 - turned on, no shortening # 3 HugoSansMono Nerd Font XCn Lt It [ ] [X] [X]
# 4 HugoSansMono NF ExtraCondensed Light Italic [X] [ ] [ ]
# 5 HugoSansMono NF ExtCn Light Italic [X] [X] [ ]
# 6 HugoSansMono NF XCn Lt It [X] [X] [X]
parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")') parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang")')
# progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse # progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse