font-patcher: Use Python logging module

[why]
The outputs while patching are very ad-hoc and not grouped
systematically.
If run via gotta-patch-em the output is hard to process afterwards
automatically.

[how]
Use the logging module which outputs the messages and also writes a
run-log with all vital information.

Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
This commit is contained in:
Fini Jastrow 2023-04-13 12:11:13 +02:00
parent 0558b14525
commit 40f2f74e87
2 changed files with 103 additions and 75 deletions

View file

@ -7,7 +7,7 @@ from FontnameTools import FontnameTools
class FontnameParser:
"""Parse a font name and generate all kinds of names"""
def __init__(self, filename):
def __init__(self, filename, logger):
"""Parse a font filename and store the results"""
self.parse_ok = False
self.use_short_families = (False, False) # ( camelcase name, short styles )
@ -21,6 +21,7 @@ class FontnameParser:
self.basename = self._basename
self.rest = self._rest
self.add_name_substitution_table(FontnameTools.SIL_TABLE)
self.logger = logger
def _make_ps_name(self, n, is_family):
"""Helper to limit font name length in PS names"""
@ -35,10 +36,10 @@ class FontnameParser:
q = limit - len(r.groups()[1])
if q < 1:
q = 1
print('Shortening too long PS {}name: Garbage warning'. format(fam))
self.logger.error('Shortening too long PS {}name: Garbage warning'. format(fam))
new_n = r.groups()[0][:q] + r.groups()[1]
if new_n != n:
print('Shortening too long PS {}name: {} -> {}'.format(fam, n, new_n))
self.logger.error('Shortening too long PS {}name: {} -> {}'.format(fam, n, new_n))
return new_n
def _shortened_name(self):

View file

@ -22,6 +22,7 @@ import errno
import subprocess
import json
from enum import Enum
import logging
try:
import configparser
except ImportError:
@ -239,10 +240,10 @@ def force_panose_monospaced(font):
panose = list(font.os2_panose)
if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default)
panose[0] = 2 # make kind latin text and display
print(" Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')")
logger.info("Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')")
font.os2_panose = tuple(panose)
if panose[0] == 2 and panose[3] != 9:
print(" Setting Panose 'Proportion' to 'Monospaced' (was '{}')".format(panose_proportion_to_text(panose[3])))
logger.info("Setting Panose 'Proportion' to 'Monospaced' (was '%s')", panose_proportion_to_text(panose[3]))
panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced
font.os2_panose = tuple(panose)
@ -296,7 +297,8 @@ def get_old_average_x_width(font):
}
for g in weights:
if g not in font:
sys.exit("{}: Can not determine ancient style xAvgCharWidth".format(projectName))
logger.critical("Can not determine ancient style xAvgCharWidth")
sys.exit(1)
s += font[g].width * weights[g]
return int(s / 1000)
@ -348,7 +350,7 @@ class font_patcher:
# For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs
if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2:
print("Very wide and short font, disabling 2 cell Powerline glyphs")
logger.warning("Very wide and short font, disabling 2 cell Powerline glyphs")
self.font_extrawide = True
# Prevent opening and closing the fontforge font. Makes things faster when patching
@ -357,8 +359,9 @@ class font_patcher:
symfont = None
if not os.path.isdir(self.args.glyphdir):
sys.exit("{}: Can not find symbol glyph directory {} "
"(probably you need to download the src/glyphs/ directory?)".format(projectName, self.args.glyphdir))
logger.critical("Can not find symbol glyph directory %s "
"(probably you need to download the src/glyphs/ directory?)", self.args.glyphdir)
sys.exit(1)
for patch in self.patch_set:
if patch['Enabled']:
@ -368,11 +371,13 @@ class font_patcher:
symfont.close()
symfont = None
if not os.path.isfile(self.args.glyphdir + patch['Filename']):
sys.exit("{}: Can not find symbol source for '{}'\n{:>{}} (i.e. {})".format(
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename']))
logger.critical("Can not find symbol source for '%s' (i.e. %s)",
patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
if not os.access(self.args.glyphdir + patch['Filename'], os.R_OK):
sys.exit("{}: Can not open symbol source for '{}'\n{:>{}} (i.e. {})".format(
projectName, patch['Name'], '', len(projectName), self.args.glyphdir + patch['Filename']))
logger.critical("Can not open symbol source for '%s' (i.e. %s)",
patch['Name'], self.args.glyphdir + patch['Filename'])
sys.exit(1)
symfont = fontforge.open(os.path.join(self.args.glyphdir, patch['Filename']))
symfont.encoding = 'UnicodeFull'
@ -426,8 +431,7 @@ class font_patcher:
sanitize_filename(fontname) + self.args.extension))
bitmaps = str()
if len(self.sourceFont.bitmapSizes):
if not self.args.quiet:
print("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
logger.debug("Preserving bitmaps {}".format(self.sourceFont.bitmapSizes))
bitmaps = str('otf') # otf/ttf, both is bf_ttf
sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags)
message = " {}\n \===> '{}'".format(self.sourceFont.fullname, outfile)
@ -438,8 +442,7 @@ class font_patcher:
source_font = TableHEADWriter(self.args.font)
dest_font = TableHEADWriter(outfile)
for idx in range(source_font.num_fonts):
if not self.args.quiet:
print("{}: Tweaking {}/{}".format(projectName, idx + 1, source_font.num_fonts))
logger.debug("Tweaking %d/%d", idx + 1, source_font.num_fonts)
xwidth_s = ''
xwidth = self.xavgwidth[idx]
if isinstance(xwidth, int):
@ -450,26 +453,23 @@ class font_patcher:
dest_font.find_table([b'OS/2'], idx)
d_xwidth = dest_font.getshort('avgWidth')
if d_xwidth != xwidth:
if not self.args.quiet:
print("Changing xAvgCharWidth from {} to {}{}".format(d_xwidth, xwidth, xwidth_s))
logger.debug("Changing xAvgCharWidth from %d to %d%s", d_xwidth, xwidth, xwidth_s)
dest_font.putshort(xwidth, 'avgWidth')
dest_font.reset_table_checksum()
source_font.find_head_table(idx)
dest_font.find_head_table(idx)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
if not self.args.quiet:
print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08))
logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08)
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
if source_font.lowppem != dest_font.lowppem:
if not self.args.quiet:
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem)
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified:
dest_font.reset_table_checksum()
if dest_font.modified:
dest_font.reset_full_checksum()
except Exception as error:
print("Can not handle font flags ({})".format(repr(error)))
logger.error("Can not handle font flags (%s)", repr(error))
finally:
try:
source_font.close()
@ -477,12 +477,13 @@ class font_patcher:
except:
pass
if self.args.is_variable:
print("Warning: Source font is a variable open type font (VF) and the patch results will most likely not be what you want")
logger.error("Source font is a variable open type font (VF) and the patch results will most likely not be what you want")
print(message)
if self.args.postprocess:
subprocess.call([self.args.postprocess, outfile])
print("\nPost Processed: {}".format(outfile))
print("\n")
logger.info("Post Processed: %s", outfile)
def setup_name_backup(self, font):
@ -564,9 +565,9 @@ class font_patcher:
# 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)
n = FontnameParser(parser_name, logger)
if not n.parse_ok:
print("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.enable_short_families(True, self.args.makegroups in [ 1, 3])
@ -749,17 +750,17 @@ class font_patcher:
# the tables have been removed from the repo with >this< commit
if self.args.configfile and self.config.read(self.args.configfile):
if self.args.removeligatures:
print("Removing ligatures from configfile `Subtables` section")
logger.info("Removing ligatures from configfile `Subtables` section")
ligature_subtables = json.loads(self.config.get("Subtables", "ligatures"))
for subtable in ligature_subtables:
print("Removing subtable:", subtable)
logger.debug("Removing subtable: %s", subtable)
try:
self.sourceFont.removeLookupSubtable(subtable)
print("Successfully removed subtable:", subtable)
logger.debug("Successfully removed subtable: %s", subtable)
except Exception:
print("Failed to remove subtable:", subtable)
logger.error("Failed to remove subtable: %s", subtable)
elif self.args.removeligatures:
print("Unable to read configfile, unable to remove ligatures")
logger.error("Unable to read configfile, unable to remove ligatures")
def assert_monospace(self):
@ -771,16 +772,17 @@ class font_patcher:
panose_mono = check_panose_monospaced(self.sourceFont)
# The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown'
if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1):
print(" Warning: Monospaced check: Panose assumed to be wrong")
print(" {} and {}".format(
logger.warning("Monospaced check: Panose assumed to be wrong")
logger.warning(" %s and %s",
report_advance_widths(self.sourceFont),
panose_check_to_text(panose_mono, self.sourceFont.os2_panose)))
panose_check_to_text(panose_mono, self.sourceFont.os2_panose))
if self.args.single and not width_mono:
print(" Warning: Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless")
logger.warning("Sourcefont is not monospaced - forcing to monospace not advisable, results might be useless")
if offending_char is not None:
print(" Offending char: 0x{:X}".format(offending_char))
logger.warning(" Offending char: %X", offending_char)
if self.args.single <= 1:
sys.exit(projectName + ": Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching")
logger.critical("Font will not be patched! Give --mono (or -s, or --use-single-width-glyphs) twice to force patching")
sys.exit(1)
if width_mono:
force_panose_monospaced(self.sourceFont)
@ -796,9 +798,9 @@ class font_patcher:
box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs))
if box_glyphs_target > box_glyphs_current:
# Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing)
if not self.args.quiet and box_glyphs_current > 0:
print("INFO: {}/{} box drawing glyphs will be replaced".format(
box_glyphs_current, box_glyphs_target))
if box_glyphs_current > 0:
logger.debug("%d/%d box drawing glyphs will be replaced",
box_glyphs_current, box_glyphs_target)
box_enabled = True
else:
# Sourcefont does have all of these glyphs
@ -1119,7 +1121,7 @@ class font_patcher:
metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font
else:
# We trust the WIN metric more, see experiments in #1056
print("{}: WARNING Font vertical metrics inconsistent (HHEA {} / TYPO {} / WIN {}), using WIN".format(projectName, hhea_btb, typo_btb, win_btb))
logger.warning("Font vertical metrics inconsistent (HHEA %d / TYPO %d / WIN %d), using WIN", hhea_btb, typo_btb, win_btb)
our_btb = win_btb
metrics = Metric.WIN
@ -1155,7 +1157,8 @@ class font_patcher:
}
our_btb = self.sourceFont.descent + self.sourceFont.ascent
elif self.font_dim['height'] < 0:
sys.exit("{}: Can not detect sane font height".format(projectName))
logger.critical("Can not detect sane font height")
sys.exit(1)
# Make all metrics equal
self.sourceFont.os2_typolinegap = 0
@ -1169,12 +1172,13 @@ class font_patcher:
self.sourceFont.os2_use_typo_metrics = 1
(check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont)
if check_hhea_btb != check_typo_btb or check_typo_btb != check_win_btb or check_win_btb != our_btb:
sys.exit("{}: Error in baseline to baseline code detected".format(projectName))
logger.critical("Error in baseline to baseline code detected")
sys.exit(1)
# Step 2
# Find the biggest char width and advance width
# 0x00-0x17f is the Latin Extended-A range
warned1 = self.args.quiet or self.args.nonmono # Do not warn if quiet or proportional target
warned1 = self.args.nonmono # Do not warn if proportional target
warned2 = warned1
for glyph in range(0x21, 0x17f):
if glyph in range(0x7F, 0xBF) or glyph in [
@ -1192,19 +1196,18 @@ class font_patcher:
if self.font_dim['width'] < self.sourceFont[glyph].width:
self.font_dim['width'] = self.sourceFont[glyph].width
if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Warning: Extended glyphs wider than basic glyphs, results might be useless\n {}".format(
report_advance_widths(self.sourceFont)))
logger.debug("Extended glyphs wider than basic glyphs, results might be useless\n %s",
report_advance_widths(self.sourceFont))
warned1 = True
# print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if xmax > self.font_dim['xmax']:
self.font_dim['xmax'] = xmax
if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z
print("Info: Extended glyphs wider bounding box than basic glyphs")
logger.debug("Extended glyphs wider bounding box than basic glyphs")
warned2 = True
# print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax))
if self.font_dim['width'] < self.font_dim['xmax']:
if not self.args.quiet:
print("Warning: Font has negative right side bearing in extended glyphs")
logger.debug("Font has negative right side bearing in extended glyphs")
self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used
# print("FINAL", self.font_dim)
@ -1304,7 +1307,7 @@ class font_patcher:
if sym_glyph.altuni:
possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ]
if len(possible_codes) == 0:
print(" Can not determine codepoint of {:X}. Skipping...".format(sym_glyph.unicode))
logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode)
continue
currentSourceFontGlyph = min(possible_codes)
else:
@ -1327,9 +1330,8 @@ class font_patcher:
# check if a glyph already exists in this location
if careful or 'careful' in sym_attr['params'] or currentSourceFontGlyph in self.essential:
if currentSourceFontGlyph in self.sourceFont:
if not self.args.quiet:
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
print(" Found {} Glyph at {:X}. Skipping...".format(careful_type, currentSourceFontGlyph))
careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing'
logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph)
# We don't want to touch anything so move to next Glyph
continue
else:
@ -1477,8 +1479,8 @@ class font_patcher:
if self.args.single:
(xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox()
if int(xmax - xmin) > self.font_dim['width'] * (1 + (overlap or 0)):
print("\n Warning: Scaled glyph U+{:X} wider than one monospace width ({} / {} (overlap {}))".format(
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap))
logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %f))",
currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], overlap)
# end for
@ -1619,7 +1621,7 @@ def half_gap(gap, top):
gap_top = int(gap / 2)
gap_bottom = gap - gap_top
if top:
print("Redistributing line gap of {} ({} top and {} bottom)".format(gap, gap_top, gap_bottom))
logger.info("Redistributing line gap of %d (%d top and %d bottom)", gap, gap_top, gap_bottom)
return gap_top
return gap_bottom
@ -1744,8 +1746,8 @@ def check_fontforge_min_version():
# versions tested: 20150612, 20150824
if actualVersion < minimumVersion:
sys.stderr.write("{}: You seem to be using an unsupported (old) version of fontforge: {}\n".format(projectName, actualVersion))
sys.stderr.write("{}: Please use at least version: {}\n".format(projectName, minimumVersion))
logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion)
logger.critical("Please use at least version: %d", minimumVersion)
sys.exit(1)
def check_version_with_git(version):
@ -1843,7 +1845,8 @@ def setup_arguments():
args = parser.parse_args()
if args.makegroups > 0 and not FontnameParserOK:
sys.exit("{}: FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups".format(projectName))
logger.critical("FontnameParser module missing (bin/scripts/name_parser/Fontname*), can not --makegroups")
sys.exit(1)
# if you add a new font, set it to True here inside the if condition
if args.complete:
@ -1880,25 +1883,27 @@ def setup_arguments():
args.windows = False
if args.makegroups > 0 and (args.windows or args.alsowindows):
printf("Warning: --windows and --also-windows are ignored when --makegroups is specified.")
logger.warning("--windows and --also-windows are ignored when --makegroups is specified.")
args.windows = False
args.alsowindows = False
if args.nonmono and args.single:
print("Warning: Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.")
logger.warning("Specified contradicting --variable-width-glyphs and --use-single-width-glyph. Ignoring --variable-width-glyphs.")
args.nonmono = False
make_sure_path_exists(args.outputdir)
if not os.path.isfile(args.font):
sys.exit("{}: Font file does not exist: {}".format(projectName, args.font))
logging.critical("Font file does not exist: %s", args.font)
sys.exit(1)
if not os.access(args.font, os.R_OK):
sys.exit("{}: Can not open font file for reading: {}".format(projectName, args.font))
logging.critical("Can not open font file for reading: %s", args.font)
sys.exit(1)
is_ttc = len(fontforge.fontsInFile(args.font)) > 1
try:
source_font_test = TableHEADWriter(args.font)
args.is_variable = source_font_test.find_table([b'avar', b'cvar', b'fvar', b'gvarb', b'HVAR', b'MVAR', b'VVAR'], 0)
if args.is_variable:
print(" Warning: Source font is a variable open type font (VF), opening might fail...")
logger.warning("Source font is a variable open type font (VF), opening might fail...")
except:
args.is_variable = False
finally:
@ -1913,16 +1918,20 @@ def setup_arguments():
args.extension = '.' + args.extension
if re.match("\.ttc$", args.extension, re.IGNORECASE):
if not is_ttc:
sys.exit(projectName + ": Can not create True Type Collections from single font files")
logger.critical("Can not create True Type Collections from single font files")
sys.exit(1)
else:
if is_ttc:
sys.exit(projectName + ": Can not create single font files from True Type Collections")
logger.critical("Can not create single font files from True Type Collections")
sys.exit(1)
if isinstance(args.xavgwidth, int) and not isinstance(args.xavgwidth, bool):
if args.xavgwidth < 0:
sys.exit(projectName + ": --xavgcharwidth takes no negative numbers")
logger.critical("--xavgcharwidth takes no negative numbers")
sys.exit(2)
if args.xavgwidth > 16384:
sys.exit(projectName + ": --xavgcharwidth takes only numbers up to 16384")
logger.critical("--xavgcharwidth takes only numbers up to 16384")
sys.exit(2)
return args
@ -1930,24 +1939,42 @@ def setup_arguments():
def main():
global version
git_version = check_version_with_git(version)
print("{} Patcher v{} ({}) (ff {}) executing".format(
projectName, git_version if git_version else version, script_version, fontforge.version()))
allversions = "Patcher v{} ({}) (ff {})".format(
git_version if git_version else version, script_version, fontforge.version())
print("{} {}".format(projectName, allversions))
if git_version:
version = git_version
check_fontforge_min_version()
args = setup_arguments()
global logger
logger = logging.getLogger(os.path.basename(args.font))
logger.setLevel(logging.DEBUG)
f_handler = logging.FileHandler('font-patcher-log.txt')
f_handler.setFormatter(logging.Formatter('%(levelname)s: %(name)s %(message)s'))
logger.addHandler(f_handler)
logger.debug(allversions)
logger.debug("Options %s", repr(sys.argv[1:]))
c_handler = logging.StreamHandler(stream=sys.stdout)
c_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
if args.quiet:
c_handler.setLevel(logging.INFO)
logger.addHandler(c_handler)
patcher = font_patcher(args)
sourceFonts = []
all_fonts = fontforge.fontsInFile(args.font)
for i, subfont in enumerate(all_fonts):
if len(all_fonts) > 1:
print("\n{}: Processing {} ({}/{})".format(projectName, subfont, i + 1, len(all_fonts)))
print("\n")
logger.info("Processing %s (%d/%d)", subfont, i + 1, len(all_fonts))
try:
sourceFonts.append(fontforge.open("{}({})".format(args.font, subfont), 1)) # 1 = ("fstypepermitted",))
except Exception:
sys.exit("{}: Can not open font '{}', try to open with fontforge interactively to get more information".format(
projectName, subfont))
logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information",
subfont)
sys.exit(1)
patcher.patch(sourceFonts[-1])