"""Parse tftopl property lists into TFM metrics and recipes.""" import re def _tok(s): # tokenize parens and atoms return re.findall(r'\(|\)|[^\s()]+', s) def _parse(tokens): it = iter(tokens) def node(): items = [] for t in it: if t == '(': items.append(node()) elif t == ')': return items else: items.append(t) return items top = [] for t in it: if t == '(': top.append(node()) return top def _charcode(t, v): if t == 'O': return int(v, 8) if t == 'D': return int(v) if t == 'C': return ord(v) if t == 'H': return int(v, 16) raise ValueError((t, v)) def _num(items): # ['R', '0.5'] or ['D','8'] ... t, v = items[0], items[1] if t == 'R': return float(v) if t == 'D': return int(v) if t == 'O': return int(v, 8) raise ValueError(items) FD_NAMES = {1: 'slant', 2: 'space', 3: 'stretch', 4: 'shrink', 5: 'xheight', 6: 'quad', 7: 'extraspace', 8: 'num1', 9: 'num2', 10: 'num3', 11: 'denom1', 12: 'denom2', 13: 'sup1', 14: 'sup2', 15: 'sup3', 16: 'sub1', 17: 'sub2', 18: 'supdrop', 19: 'subdrop', 20: 'delim1', 21: 'delim2', 22: 'axisheight'} FD_NAMES_EX = {8: 'defaultrulethickness', 9: 'bigopspacing1', 10: 'bigopspacing2', 11: 'bigopspacing3', 12: 'bigopspacing4', 13: 'bigopspacing5'} class TFM: def __init__(self, plpath): src = open(plpath).read() top = _parse(_tok(src)) self.fontdimen = {} self.design_size = None # TeX points; read from PL DESIGNSIZE self.chars = {} # code -> dict(wd,ht,dp,ic,next,varchar) self.kerns = {} # (left,right) -> amount for node in top: head = node[0] if head == 'DESIGNSIZE': self.design_size = float(_num(node[1:3])) elif head == 'FONTDIMEN': for sub in node[1:]: if not isinstance(sub, list): continue key = sub[0] if key == 'PARAMETER': idx = _charcode(sub[1], sub[2]) self.fontdimen[idx] = _num(sub[3:5]) else: names = {v.upper(): k for k, v in FD_NAMES.items()} names.update({v.upper(): k for k, v in FD_NAMES_EX.items()}) idx = names.get(key) if idx: self.fontdimen[idx] = _num(sub[1:3]) elif head == 'LIGTABLE': if any(isinstance(sub, list) and sub and sub[0] == 'SKIP' for sub in node[1:]): raise ValueError( f'{plpath}: unsupported PL LIGTABLE SKIP; kerning not imported') labels = [] for sub in node[1:]: if not isinstance(sub, list): continue if sub[0] == 'LABEL': labels.append(_charcode(sub[1], sub[2])) elif sub[0] == 'KRN': rc = _charcode(sub[1], sub[2]) amt = _num(sub[3:5]) for l in labels: self.kerns[(l, rc)] = amt elif sub[0] == 'LIG': pass elif sub[0] == 'STOP': labels = [] elif head == 'CHARACTER': code = _charcode(node[1], node[2]) d = {'wd': 0.0, 'ht': 0.0, 'dp': 0.0, 'ic': 0.0, 'next': None, 'varchar': None} for sub in node[3:]: if not isinstance(sub, list): continue k = sub[0] if k == 'CHARWD': d['wd'] = _num(sub[1:3]) elif k == 'CHARHT': d['ht'] = _num(sub[1:3]) elif k == 'CHARDP': d['dp'] = _num(sub[1:3]) elif k == 'CHARIC': d['ic'] = _num(sub[1:3]) elif k == 'NEXTLARGER': d['next'] = _charcode(sub[1], sub[2]) elif k == 'VARCHAR': vc = {} for p in sub[1:]: if isinstance(p, list) and p[0] in ('TOP', 'MID', 'BOT', 'REP'): vc[p[0]] = _charcode(p[1], p[2]) d['varchar'] = vc self.chars[code] = d if __name__ == '__main__': import sys, json t = TFM(sys.argv[1]) print(json.dumps({'fd': t.fontdimen, 'nchars': len(t.chars), 'chains': {c: d['next'] for c, d in t.chars.items() if d['next'] is not None}, 'varchars': {c: d['varchar'] for c, d in t.chars.items() if d['varchar']}}, indent=1))