From f64f0618a3885273f73bcbba24643e1c34eb2c2b Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Sun, 11 Jun 2023 15:22:57 -0700 Subject: [PATCH] adds tool lsname.py for listing name table entries of fonts --- misc/tools/lsname.py | 258 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 misc/tools/lsname.py diff --git a/misc/tools/lsname.py b/misc/tools/lsname.py new file mode 100644 index 000000000..5edf0e2d3 --- /dev/null +++ b/misc/tools/lsname.py @@ -0,0 +1,258 @@ +import sys, os, os.path, re, argparse +from collections import OrderedDict +from multiprocessing import Pool +from fontTools.ttLib import TTFont + + +# NAME_LABELS maps name table ID to symbolic names. +# See https://learn.microsoft.com/en-us/typography/opentype/spec/name#name-ids +NAME_LABELS = { + # TrueType & OpenType + 0: 'Copyright', + 1: 'Font Family', + 2: 'Font Subfamily', + 3: 'Unique identifier', + 4: 'Full font name', + 5: 'Version', + 6: 'PostScript name', + 7: 'Trademark', + 8: 'Manufacturer', + 9: 'Designer', + 10: 'Description', + 11: 'Vendor URL', + 12: 'Designer URL', + 13: 'License Description', + 14: 'License URL', + # 15 RESERVED + 16: 'Typo Family', + 17: 'Typo Subfamily', + 18: 'Mac Compatible Full Name', + 19: 'Sample text', + + # OpenType + 20: 'PostScript CID', + 21: 'WWS Family', + 22: 'WWS Subfamily', + 23: 'Light Background Palette', + 24: 'Dark Background Palette', + 25: 'Variations PostScript Name Prefix', + + # 26-255: Reserved for future expansion + # 256-32767: Font-specific names (layout features and settings, variations, track names, etc.) +} + + +class AnySet: + def __contains__(self, k): + return True + + +def read_name_table(filename: str) -> {int:str}: + font = TTFont(filename, recalcBBoxes=False, recalcTimestamp=False) + nametab = font["name"] + names = {} + for rec in nametab.names: + names[rec.nameID] = rec.toUnicode() + return OrderedDict(sorted(names.items())) + + +def build_table(name_tables: [{int:str}], filenames: [str], name_ids_filter: set[str]): + columns = {} + for names in name_tables: + for name_id, value in names.items(): + if name_id not in name_ids_filter: + continue + label = None + if name_id in NAME_LABELS: + label = f'{name_id} {NAME_LABELS[name_id]}' + else: + label = f'{name_id}' + columns[name_id] = label + + columns = OrderedDict([(-1, "Filename")] + sorted(columns.items())) + + nameid_to_colidx = {} + i = 0 + for id in columns: + nameid_to_colidx[id] = i + i += 1 + + rows = [ list(columns.values()) ] + + for i in range(len(name_tables)): + names = name_tables[i] + row = [''] * len(rows[0]) + row[0] = filenames[i] + for name_id, value in names.items(): + if name_id not in name_ids_filter: + continue + row[nameid_to_colidx[name_id]] = value + rows.append(row) + + return rows + + +def format_table_plain(header: [(int,str)], rows: [[str]], colw: [int]) -> str: + ncols = len(rows[0]) + out = [] + + # print header labels + for i in range(ncols): + if i > 0: + out.append(' │ ') + out.append('%-*s' % (colw[i], header[i][1])) + out.append('\n') + for i in range(ncols): + id = header[i][0] + if i > 0: + out.append(' │ ') + if id < 0: + out.append('%-*s' % (colw[i], 'name ID →')) + else: + out.append('%-*d' % (colw[i], id)) + out.append('\n') + + # print header divider + for i in range(ncols): + if i > 0: + out.append('─┼─') + out.append('─' * colw[i]) + out.append('\n') + + # print data rows + for row_idx in range(1, len(rows)): + row = rows[row_idx] + for i in range(ncols): + if i > 0: + out.append(' │ ') + out.append('%-*s' % (colw[i], row[i])) + out.append('\n') + + out.pop() + return ''.join(out) + + +def format_table_md(header: [str], rows: [[str]], colw: [int]) -> str: + ncols = len(rows[0]) + out = [] + + # print header labels + out.append('|') + for i in range(ncols): + out.append(' %-*s |' % (colw[i], header[i])) + out.append('\n') + + # print header divider + out.append('|') + for i in range(ncols): + out.append(' :' + ('-' * max(0, colw[i] - 1)) + ' |') + out.append('\n') + + # print data rows + for row_idx in range(1, len(rows)): + row = rows[row_idx] + out.append('|') + for i in range(ncols): + out.append(' %-*s |' % (colw[i], row[i])) + out.append('\n') + + out.pop() + return ''.join(out) + + +def csv_quote(s: str) -> str: + return s.replace(",", "\\,") + + +def format_table_csv(header: [str], rows: [[str]]) -> str: + ncols = len(rows[0]) + out = [] + + # print header + for i in range(ncols): + out.append(csv_quote(header[i])) + out.append(',') + out[len(out) - 1] = '\n' + + # print data + for row_idx in range(1, len(rows)): + row = rows[row_idx] + for i in range(ncols): + out.append(csv_quote(row[i])) + out.append(',') + out[len(out) - 1] = '\n' + + out.pop() + return ''.join(out) + + +def format_table(rows: [[str]], format: str) -> str: + # calculate column widths and column header labels + ncols = len(rows[0]) + header = [None] * ncols + colw = [0] * ncols + i = 0 + unified_header = format == 'md' or format == 'csv' + + row = rows[0] + if unified_header: + while i < ncols: + colw[i] = max(colw[i], len(row[i])) + header[i] = row[i] + i += 1 + else: + while i < ncols: + id_label = row[i].split(' ', 1) + if len(id_label) == 1: + id_label = [-1, id_label[0]] + colw[i] = max(colw[i], len(id_label[1])) + header[i] = ( int(id_label[0]), id_label[1] ) + i += 1 + + for row_idx in range(1, len(rows)): + row = rows[row_idx] + i = 0 + while i < ncols: + colw[i] = max(colw[i], len(row[i])) + i += 1 + + if format == 'csv': + return format_table_csv(header, rows[1:]) + elif format == 'md': + return format_table_md(header, rows[1:], colw) + elif format == '': + return format_table_plain(header, rows[1:], colw) + else: + raise Exception(f'unknown format "{format}"') + + +if __name__ == '__main__': + argparser = argparse.ArgumentParser(description='Print name table entries') + a = lambda *args, **kwargs: argparser.add_argument(*args, **kwargs) + a('-i', '--id', metavar='', + help='Only print . Separate multiple IDs with comma.') + a('-a', '--all', help='Print all name entries') + a('--csv', help='CSV output format') + a('--md', help='Markdown output format') + a('inputs', metavar='', nargs='+', help='Input fonts (ttf or otf)') + args = argparser.parse_args() + + name_ids_filter = set((1, 2, 4, 6, 16, 17, 18, 21, 22)) + if args.id and len(args.id) > 0: + name_ids_filter = set() + for id in args.id.split(','): + name_ids_filter.add(int(id.strip())) + elif args.all: + name_ids_filter = AnySet() + + with Pool() as p: + name_tables = p.map(read_name_table, args.inputs) + + filenames = [os.path.basename(fn) for fn in args.inputs] + rows = build_table(name_tables, filenames, name_ids_filter) + + format = '' + if args.csv: format = 'csv' + if args.md: format = 'md' + + print(format_table(rows, format))