summaryrefslogtreecommitdiff
path: root/misc
diff options
context:
space:
mode:
authorRasmus Andersson <rasmus@notion.se>2023-06-15 23:17:45 +0300
committerRasmus Andersson <rasmus@notion.se>2023-06-15 23:17:45 +0300
commit1bf738cec6594e83efcae50aba49eda53102e68b (patch)
treee519e00e84ac61ef5f3ecc5dd972e53139f6f0c1 /misc
parent1920f75082a5919cedd309efde710edbe6e92d83 (diff)
downloadinter-1bf738cec6594e83efcae50aba49eda53102e68b.tar.xz
adds two debugging tools: lsfvar.py & lsstat.py
Diffstat (limited to 'misc')
-rw-r--r--misc/tools/lsfvar.py184
-rw-r--r--misc/tools/lsstat.py194
2 files changed, 378 insertions, 0 deletions
diff --git a/misc/tools/lsfvar.py b/misc/tools/lsfvar.py
new file mode 100644
index 000000000..6244f35f4
--- /dev/null
+++ b/misc/tools/lsfvar.py
@@ -0,0 +1,184 @@
+import sys, os, os.path, re, argparse, pprint, shutil
+import yaml
+try:
+ from yaml import CLoader as YamlLoader, CDumper as YamlDumper
+except ImportError:
+ from yaml import Loader as YamlLoader, Dumper as YamlDumper
+from collections import OrderedDict
+from multiprocessing import Pool
+from fontTools.ttLib import TTFont
+
+
+def read_fvar_table(filename: str) -> {str:dict}:
+ font = TTFont(filename, recalcBBoxes=False, recalcTimestamp=False)
+ nametab = font["name"]
+
+ names = {}
+ for rec in nametab.names:
+ names[rec.nameID] = rec.toUnicode()
+
+ fvartab = font["fvar"]
+ axes = []
+ for a in fvartab.axes:
+ # axisTag
+ # axisNameID
+ # flags
+ # minValue
+ # defaultValue
+ # maxValue
+ d = a.__dict__
+ for k in list(d.keys()):
+ # replace name-table IDs with their values
+ if k.endswith('NameID'):
+ d[k[:-2]] = names[d[k]]
+ del d[k]
+ axes.append(d)
+
+ instances = []
+ for inst in fvartab.instances:
+ d = inst.__dict__
+ for k in list(d.keys()):
+ # replace name-table IDs with their values
+ if k.endswith('NameID'):
+ d[k[:-2]] = names[d[k]]
+ del d[k]
+ instances.append(d)
+
+ return OrderedDict(axes=axes, instances=instances)
+
+
+def fmtstructured(tables: [dict], files: [str], cl: dict) -> str:
+ doc = {}
+ for i, tab in enumerate(tables):
+ file = files[i]
+ doc[file] = dict(tab)
+ cols = shutil.get_terminal_size((80, 20)).columns
+ if cl.format == 'yaml':
+ return yaml.dump(doc, Dumper=YamlDumper, width=cols)
+ return pprint.PrettyPrinter(indent=2, width=cols).pformat(doc)
+
+
+def fmtplaintext(out: [str], header: [str], rows: [[str]]) -> str:
+ # calculate width needed for columns
+ colw = [0] * len(header)
+ for i, s in enumerate(header):
+ colw[i] = max(colw[i], len(s))
+ for row in rows:
+ for i, s in enumerate(row):
+ colw[i] = max(colw[i], len(s))
+
+ colglue, divglue, divchar = ' │ ', '─┼─', '─'
+ row_prefix, row_suffix, div_prefix = '', '\n', ''
+ if cl.format == 'md':
+ colglue, divglue, divchar = ' | ', ' | ', '-'
+ row_prefix, row_suffix, div_prefix = '| ', ' |\n', ':'
+
+ def format_row(row):
+ out.append(row_prefix)
+ out.append(colglue.join(['%-*s' % (colw[i], s) for i, s in enumerate(row)]))
+ out.append(row_suffix)
+
+ def format_divider():
+ out.append(row_prefix)
+ xs = [div_prefix + (divchar * (colw[i] - len(div_prefix))) for i in range(len(row))]
+ out.append(divglue.join(xs))
+ out.append(row_suffix)
+
+ format_row(header)
+ format_divider()
+ for row in rows:
+ format_row(row)
+
+
+def build_axes(tables: [dict], files: [str]) -> ([str], [[str]]):
+ uniqueAxes1 = OrderedDict()
+ for i, tab in enumerate(tables):
+ file = files[i]
+ for a in tab["axes"]:
+ vals = OrderedDict(sorted(a.items())).values()
+ key = ' '.join([str(v) for v in vals])
+ d = uniqueAxes1.get(key, {})
+ filev = d.get('files', [])
+ d.update(a)
+ filev.append(file)
+ d['files'] = filev
+ uniqueAxes1[key] = d
+
+ uniqueAxes = dict() # {tag:[{axis, ...}]}
+ for a in uniqueAxes1.values():
+ v = uniqueAxes.get(a['axisTag'], [])
+ v.append(a)
+ uniqueAxes[a['axisTag']] = v
+
+ header = ['tag', 'name', 'flags', 'min', 'default', 'max', 'files']
+ rows = []
+ for axesForTag in uniqueAxes.values():
+ for a in axesForTag:
+ rows.append([
+ a["axisTag"],
+ a["axisName"],
+ str(a["flags"]),
+ str(a["minValue"]),
+ str(a["defaultValue"]),
+ str(a["maxValue"]),
+ ', '.join(a["files"]),
+ ])
+
+ return header, rows
+
+
+def build_instances(tables: [dict], files: [str]) -> ([str], [[str]]):
+ axisTags = []
+ for axis in tables[0]["axes"]:
+ axisTags.append(axis["axisTag"])
+ header = axisTags + ['flags', 'postscriptName', 'subfamilyName', 'file']
+ rows = []
+ for i, tab in enumerate(tables):
+ file = files[i]
+ axes = tab['axes']
+ for inst in tab['instances']:
+ row = []
+ for axisTag in axisTags:
+ row.append(str(inst['coordinates'][axisTag]))
+ row.append(str(inst['flags']))
+ row.append(inst['postscriptName'])
+ row.append(inst['subfamilyName'])
+ row.append(file)
+ rows.append(row)
+
+ return header, rows
+
+
+def format_fvar(tables: [dict], files: [str], cl: dict) -> str:
+ if cl.format in ('yaml', 'py'):
+ return fmtstructured(tables, files, cl)
+
+ out = []
+
+ # axes
+ header, rows = build_axes(tables, files)
+ fmtplaintext(out, header, rows)
+ out.append('\n')
+
+ # instances
+ header, rows = build_instances(tables, files)
+ fmtplaintext(out, header, rows)
+
+ return ''.join(out)
+
+
+if __name__ == '__main__':
+ argparser = argparse.ArgumentParser(description='Print fvar table entries')
+ a = lambda *args, **kwargs: argparser.add_argument(*args, **kwargs)
+ a('-f', '--format', metavar='<format>', help='One of: plain, yaml, py, md')
+ a('inputs', metavar='<file>', nargs='+', help='Input fonts (ttf or otf)')
+ cl = argparser.parse_args()
+
+ if cl.format not in (None, 'plain', 'yaml', 'py', 'md'):
+ raise Exception(f'unknown format: "{cl.format}"')
+
+ with Pool() as p:
+ tables = p.map(read_fvar_table, cl.inputs)
+
+ filenames = [os.path.basename(fn) for fn in cl.inputs]
+ print(format_fvar(tables, filenames, cl))
diff --git a/misc/tools/lsstat.py b/misc/tools/lsstat.py
new file mode 100644
index 000000000..7fa13776d
--- /dev/null
+++ b/misc/tools/lsstat.py
@@ -0,0 +1,194 @@
+import sys, os, os.path, re, argparse, pprint, shutil
+import yaml
+try:
+ from yaml import CLoader as YamlLoader, CDumper as YamlDumper
+except ImportError:
+ from yaml import Loader as YamlLoader, Dumper as YamlDumper
+from collections import OrderedDict
+from multiprocessing import Pool
+from fontTools.ttLib import TTFont
+
+
+TODO_PRINTS = dict()
+
+
+def getName(nametab: object, nameID: int) -> str:
+ return nametab.getName(nameID, 3, 1).toUnicode()
+
+
+def read_stat_table(filename: str) -> {str:dict}:
+ font = TTFont(filename, recalcBBoxes=False, recalcTimestamp=False)
+ nametab = font["name"]
+ stattab = font["STAT"].table.__dict__
+
+ designAxisRecord = stattab['DesignAxisRecord'].Axis
+ axisValueArray = stattab['AxisValueArray'].AxisValue
+
+ axes = OrderedDict()
+ axesArray = []
+
+ for r in designAxisRecord:
+ tag = str(r.AxisTag)
+ axis = dict(
+ tag = tag,
+ name = getName(nametab, r.AxisNameID),
+ values = [],
+ )
+ axesArray.append(axis)
+ axes[tag] = axis
+
+ for r in axisValueArray:
+ axis = axesArray[r.AxisIndex]
+ format = r.Format
+ value = dict(
+ axisIndex = r.AxisIndex,
+ format = format,
+ flags = r.Flags,
+ name = getName(nametab, r.ValueNameID),
+ )
+ if format == 1:
+ value['value'] = r.Value
+ elif format == 2:
+ value['value'] = r.NominalValue
+ value['rangeMaxValue'] = r.RangeMaxValue
+ value['rangeMinValue'] = r.RangeMinValue
+ elif format == 3:
+ value['value'] = r.Value
+ value['linkedValue'] = r.LinkedValue
+ elif format == 4:
+ if not TODO_PRINTS.get('format4'):
+ TODO_PRINTS['format4'] = True
+ print('TODO: implement support for format 4 STAT records', file=sys.stderr)
+ axis['values'].append(value)
+
+ version = stattab['Version'] # e.g. 0x00010002 for 1.2
+
+ return dict(
+ version = '%d.%d' % (version >> 16, version & 0xffff),
+ axes = axes,
+ elidedFallbackName = getName(nametab, stattab['ElidedFallbackNameID']),
+ )
+
+
+def fmtstructured(tables: [dict], files: [str], cl: dict) -> str:
+ doc = {}
+ for i, tab in enumerate(tables):
+ file = files[i]
+ doc[file] = dict(tab)
+ cols = shutil.get_terminal_size((80, 20)).columns
+ if cl.format == 'yaml':
+ return yaml.dump(doc, Dumper=YamlDumper, width=cols)
+ return pprint.PrettyPrinter(indent=2, width=cols).pformat(doc)
+
+
+def fmtplaintext(out: [str], header: [str], rows: [[str]], cl: dict) -> str:
+ # calculate width needed for columns
+ colw = [0] * len(header)
+ for i, s in enumerate(header):
+ colw[i] = max(colw[i], len(s))
+ colw2 = [0] * len(header)
+ for row in rows:
+ for i, s in enumerate(row):
+ colw[i] = max(colw[i], len(s))
+ colw2[i] = max(colw2[i], len(s))
+
+ # elide empty columns
+ if 0 in colw2:
+ emptyColsIndices = set([i for i, w in enumerate(colw2) if w == 0])
+ header = [s for i, s in enumerate(header) if colw2[i] > 0]
+ rows2 = []
+ for row in rows:
+ row2 = []
+ for i, val in enumerate(row):
+ if i not in emptyColsIndices:
+ row2.append(val)
+ rows2.append(row2)
+ rows = rows2
+ colw = [w for i, w in enumerate(colw) if colw2[i] > 0]
+
+ colglue, divglue, divchar = ' │ ', '─┼─', '─'
+ row_prefix, row_suffix, div_prefix = '', '\n', ''
+ if cl.format == 'md':
+ colglue, divglue, divchar = ' | ', ' | ', '-'
+ row_prefix, row_suffix, div_prefix = '| ', ' |\n', ':'
+
+ def format_row(row):
+ out.append(row_prefix)
+ out.append(colglue.join(['%-*s' % (colw[i], s) for i, s in enumerate(row)]))
+ out.append(row_suffix)
+
+ def format_divider():
+ out.append(row_prefix)
+ xs = [div_prefix + (divchar * (colw[i] - len(div_prefix))) for i in range(len(colw))]
+ out.append(divglue.join(xs))
+ out.append(row_suffix)
+
+ format_row(header)
+ format_divider()
+ for row in rows:
+ format_row(row)
+
+
+def format_stat(tables: [dict], files: [str], cl: dict) -> str:
+ if cl.format in ('yaml', 'py'):
+ return fmtstructured(tables, files, cl)
+
+ out = []
+
+ uniqueAxes = OrderedDict()
+ for tab in tables:
+ for a in tab["axes"].values():
+ uniqueAxes[a['tag']] = a['name']
+
+ rubric_prefix, rubric_suffix = '', '\n'
+ if cl.format == 'md':
+ rubric_prefix, rubric_suffix = '### ', '\n\n'
+
+ header = ['name', 'value', 'minval', 'maxval', 'linkval', 'format', 'flags', 'file']
+ for axisTag, axisName in uniqueAxes.items():
+ out.append(f"{rubric_prefix}{axisTag} ({axisName}){rubric_suffix}")
+ rows = []
+ for i, tab in enumerate(tables):
+ file = files[i]
+ axis = tab["axes"].get(axisTag)
+ if not axis:
+ continue
+ for v in axis['values']:
+ row = [
+ v['name'],
+ str(v['value']),
+ str(v.get('rangeMinValue', '')),
+ str(v.get('rangeMaxValue', '')),
+ str(v.get('linkedValue', '')),
+ str(v['format']),
+ str(v['flags']),
+ file,
+ ]
+ rows.append(row)
+
+ fmtplaintext(out, header, rows, cl)
+ out.append('\n')
+
+ if len(uniqueAxes) > 0:
+ out.pop() # undo last \n
+
+ return ''.join(out)
+
+
+if __name__ == '__main__':
+ argparser = argparse.ArgumentParser(description='Print STAT table entries')
+ a = lambda *args, **kwargs: argparser.add_argument(*args, **kwargs)
+ a('-f', '--format', metavar='<format>', help='One of: plain, yaml, py, md')
+ a('inputs', metavar='<file>', nargs='+', help='Input fonts (ttf or otf)')
+ cl = argparser.parse_args()
+
+ if cl.format not in (None, 'plain', 'yaml', 'py', 'md'):
+ raise Exception(f'unknown format: "{cl.format}"')
+
+ with Pool() as p:
+ tables = p.map(read_stat_table, cl.inputs)
+
+ print(pprint.PrettyPrinter(indent=2).pformat(tables[0]))
+
+ filenames = [os.path.basename(fn) for fn in cl.inputs]
+ print(format_stat(tables, filenames, cl))