Skip to content
Merged
103 changes: 75 additions & 28 deletions font-patcher
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from __future__ import absolute_import, print_function, unicode_literals

# Change the script version when you edit this script:
script_version = "4.25.1"
script_version = "4.26.0"

version = "3.4.0"
projectName = "Nerd Fonts"
Expand Down Expand Up @@ -912,15 +912,19 @@ class font_patcher:

# Stretch 'xz' or 'pa' (preserve aspect ratio)
# Supported params: overlap | careful | xy-ratio | dont_copy | ypadding
# Overlap value is used horizontally but vertically limited to 0.01
# Overlap value is used horizontally but vertically limited to 0.01 (then params usually should have '1' when used with 'pa')
# Careful does not overwrite/modify existing glyphs
# The xy-ratio limits the x-scale for a given y-scale to make the ratio <= this value (to prevent over-wide glyphs)
# '1' means occupu 1 cell (default for 'xy')
# '2' means occupy 2 cells (default for 'pa')
# '!' means do the 'pa' scaling even with non mono fonts (else it just scales down, never up)
# '!' means do the 'pa' scaling even with non mono fonts into a 2 cell wide space (else it just scales down, never up (*))
# '^' means that scaling shall fill the whole cell and not only the icon-cap-height (for mono fonts, other always use the whole cell)
# Dont_copy does not overwrite existing glyphs but rescales the preexisting ones
#
# (*) 'pa' scaling without '!' of a non mono font scales:
# - [normal mode] only down, never up (keeps original icon sizes unless they are too big)
# - [upscale mode] down; or up into a 1 cell wide space (at least same size as mono font icons)
#
# Be careful, stretch may not change within a ScaleRule!

SYM_ATTR_DEFAULT = {
Expand Down Expand Up @@ -1332,7 +1336,7 @@ class font_patcher:

# print("FINI hhea {} typo {} win {} use {} {} {}".format(hhea_btb, typo_btb, win_btb, use_typo, our_btb != hhea_btb, self.sourceFont.fontname))

self.font_dim = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0, 'width' : 0, 'height': 0, 'iconheight': 0, 'ypadding': 0}
self.font_dim = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0, 'width' : 0, 'height': 0, 'ypadding': 0}

if metrics == Metric.HHEA:
self.font_dim['ymin'] = self.sourceFont.hhea_descent - half_gap(self.sourceFont.hhea_linegap, False)
Expand Down Expand Up @@ -1368,22 +1372,13 @@ class font_patcher:
'ymax' : self.sourceFont.ascent,
'width' : self.sourceFont.em,
'height' : self.sourceFont.descent + self.sourceFont.ascent,
'iconheight': self.sourceFont.descent + self.sourceFont.ascent,
'ypadding' : 0,
}
our_btb = self.sourceFont.descent + self.sourceFont.ascent
if self.font_dim['height'] <= 0:
logger.critical("Can not detect sane font height")
sys.exit(1)

self.font_dim['iconheight'] = self.font_dim['height']
if self.args.single and self.sourceFont.capHeight > 0 and not isinstance(self.args.cellopt, list):
# Limit the icon height on monospaced fonts because very slender and tall icons render
# excessively tall otherwise. We ignore that effect for the other variants because it
# does not look so much out of place there.
# Icons can be bigger than the letter capitals, but not the whole cell:
self.font_dim['iconheight'] = (self.sourceFont.capHeight * 2 + self.font_dim['height']) / 3

# Make all metrics equal
self.sourceFont.os2_typolinegap = 0
self.sourceFont.os2_typoascent = self.font_dim['ymax']
Expand Down Expand Up @@ -1451,7 +1446,7 @@ class font_patcher:
self.font_dim['ymax'] - self.font_dim['height'], self.font_dim['ymax'])
logger.debug("Final font cell dimensions %d w x %d h%s",
self.font_dim['width'], self.font_dim['height'],
' (with icon cell {} h)'.format(int(self.font_dim['iconheight'])) if self.font_dim['iconheight'] != self.font_dim['height'] else '')
' (with icon cell {} h)'.format(int(self.get_iconheight())) if self.get_iconheight() != self.font_dim['height'] else '')
try:
middle = lambda x, y: abs(x - y) / 2 + min(x, y)
x_bb = self.sourceFont['x'].boundingBox();
Expand All @@ -1467,6 +1462,18 @@ class font_patcher:
if isinstance(self.xavgwidth[-1], int) and self.xavgwidth[-1] == 0:
self.xavgwidth[-1] = get_old_average_x_width(self.sourceFont)

def get_iconheight(self, force_single=False):
""" Get the target height for a given target variant """
single_width_font = self.args.single or force_single

iconheight = self.font_dim['height']
if single_width_font and self.sourceFont.capHeight > 0 and not isinstance(self.args.cellopt, list):
# Limit the icon height on monospaced fonts because very slender and tall icons render
# excessively tall otherwise. We ignore that effect for the other variants because it
# does not look so much out of place there.
# Icons can be bigger than the letter capitals, but not the whole cell:
iconheight = (self.sourceFont.capHeight * 2 + self.font_dim['height']) / 3
return iconheight

def get_target_width(self, stretch):
""" Get the target width (1 or 2 'cell') for a given stretch parameter """
Expand All @@ -1476,31 +1483,46 @@ class font_patcher:
return 1
return 2

def get_scale_factors(self, sym_dim, stretch, overlap=None):
""" Get scale in x and y as tuple """
# It is possible to have empty glyphs, so we need to skip those.
if not sym_dim['width'] or not sym_dim['height']:
return (1.0, 1.0)

target_width = self.font_dim['width'] * self.get_target_width(stretch)
def get_scale_pair_raw(self, sym_dim, stretch, overlap, force_mono_metrix=False):
""" Get scale in x and y as tuple ignoring 'pa' and 'xy' flags """
if force_mono_metrix:
stretch = '1' + stretch
cell_width_factor = self.get_target_width(stretch)
if overlap:
target_width += self.font_dim['width'] * overlap
scale_ratio_x = target_width / sym_dim['width']
cell_width_factor += overlap
target_width = self.font_dim['width'] * cell_width_factor

# font_dim['height'] represents total line height, keep our symbols sized based upon font's em
# Use the font_dim['height'] only for explicit 'y' scaling (not 'pa')
target_height = self.font_dim['height'] if '^' in stretch else self.font_dim['iconheight']
target_height = self.font_dim['height'] if '^' in stretch else self.get_iconheight(force_mono_metrix)
target_height *= 1.0 - self.font_dim['ypadding']
if overlap:
target_height *= 1.0 + min(0.01, overlap) # never aggressive vertical overlap
scale_ratio_y = target_height / sym_dim['height']

return (target_width / sym_dim['width'], target_height / sym_dim['height'])


def get_scale_factors(self, sym_dim, stretch, overlap=None):
""" Get scale in x and y as tuple """
# It is possible to have empty glyphs, so we need to skip those.
if not sym_dim['width'] or not sym_dim['height']:
return (1.0, 1.0)

(scale_ratio_x, scale_ratio_y) = self.get_scale_pair_raw(sym_dim, stretch, overlap)

if 'pa' in stretch:
# We want to preserve x/y aspect ratio, so find biggest scale factor that allows symbol to fit
scale_ratio_x = min(scale_ratio_x, scale_ratio_y)
if not self.args.single and not '!' in stretch and not overlap:
# non monospaced fonts just scale down on 'pa', not up
scale_ratio_x = min(scale_ratio_x, 1.0)
if not self.args.single and not '!' in stretch and scale_ratio_x > 1.0:
if not self.args.upscale:
# non monospaced fonts just scale down on 'pa', not up
if not overlap:
scale_ratio_x = 1.0
else:
# on 'pa', non monospaced fonts only scale up as much as they would if monospaced
(single_ratio_x, single_ratio_y) = self.get_scale_pair_raw(sym_dim, stretch, overlap, force_mono_metrix=True)
single_ratio_x = min(single_ratio_x, single_ratio_y)
scale_ratio_x = max(single_ratio_x, 1.0)
scale_ratio_y = scale_ratio_x
else:
# Keep the not-stretched direction
Expand Down Expand Up @@ -2208,6 +2230,7 @@ def setup_arguments():
expert_group.add_argument('--configfile', dest='configfile', default=False, type=str, help='Specify a file path for configuration file (see sample: src/config.sample.cfg)')
expert_group.add_argument('--custom', dest='custom', default=False, type=str, help='Specify a custom symbol font, all glyphs will be copied; absolute path suggested')
expert_group.add_argument('--dry', dest='dry_run', default=False, action='store_true', help='Do neither patch nor store the font, to check naming')
expert_group.add_argument('--experimental', dest='experimental', default=[], action='extend', nargs=1, help='Enable experimental/unstable feature, get hints with "?" or "help"')
expert_group.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, help='Path to glyphs to be used for patching')
expert_group.add_argument('--has-no-italic', dest='noitalic', default=False, action='store_true', help='Font family does not have Italic (but Oblique/Slanted), to help create correct RIBBI set')
expert_group.add_argument('--metrics', dest='metrics', default=None, choices=get_metrics_names(), help='Select vertical metrics source (for problematic cases)')
Expand Down Expand Up @@ -2246,6 +2269,30 @@ def setup_arguments():
extraflags += ' ' + args.font # Need to re-add the mandatory argument
args = parser.parse_args(extraflags.split(), args)

experimental_features={
# user-switch-name: ( PR number, description, variable in args )
'nonmono-upscale': (1926, 'Scale non-mono icons to be at least as large as mono', 'upscale'),
}
args.experimental = list(set(args.experimental))
if '?' in args.experimental or 'help' in args.experimental:
print('\nThe following experimental features can be enabled.');
if not len(experimental_features):
print(' No experimental feature available today')
else:
for k, v in experimental_features.items():
print(' \'{}\': {} (PR {})'.format(k, v[1], v[0]))
print('Note that a feature might be removed without notice in the future.');
sys.exit(0)
for k,v in experimental_features.items():
if k in args.experimental:
setattr(args, v[2], True)
args.experimental.remove(k)
logger.warning('Enabling experimental feature %s', k)
else:
setattr(args, v[2], False)
if len(args.experimental):
logger.error('Unknown experimental flag(s): %s', ', '.join(args.experimental))

if args.makegroups > 0 and not FontnameParserOK:
logger.critical("FontnameParser module missing (bin/scripts/name_parser/Fontname*), specify --makegroups 0")
sys.exit(1)
Expand Down