#!/usr/bin/env python3 """Decode a TeX PK font into one PBM per glyph plus a JSON metrics file. Usage: pk2pbm.py FONT.pk OUTDIR """ import json import os import struct import sys class Nybbles: def __init__(self, data, pos): self.data, self.pos, self.high = data, pos, True def get(self): b = self.data[self.pos] if self.high: self.high = False return b >> 4 self.high = True self.pos += 1 return b & 15 def decode_packed(data, pos, w, h, dyn_f, black): nyb = Nybbles(data, pos) repeat = 0 def packed_num(): nonlocal repeat i = nyb.get() if i == 0: j = 0 while i == 0: i = nyb.get() j += 1 while j > 0: i = i * 16 + nyb.get() j -= 1 return i - 15 + (13 - dyn_f) * 16 + dyn_f if i <= dyn_f: return i if i < 14: return (i - dyn_f - 1) * 16 + nyb.get() + dyn_f + 1 repeat = packed_num() if i == 14 else 1 return packed_num() rows, row = [], [] while len(rows) < h: count = packed_num() while count > 0: take = min(count, w - len(row)) row.extend([black] * take) count -= take if len(row) == w: rows.append(row) for _ in range(repeat): rows.append(list(row)) repeat = 0 row = [] black = 1 - black return rows[:h] def decode_bitmap(data, pos, w, h): rows, bit = [], 0 for _ in range(h): row = [] for _ in range(w): row.append((data[pos + bit // 8] >> (7 - bit % 8)) & 1) bit += 1 rows.append(row) return rows def read_pk(path): data = open(path, "rb").read() assert data[0] == 247 and data[1] == 89, "not a PK file" k = data[2] pos = 3 + k ds, cs, hppp, vppp = struct.unpack(">iiii", data[pos:pos + 16]) pos += 16 glyphs = {} while pos < len(data): f = data[pos] if f >= 240: pos += 1 if f in (240, 241, 242, 243): n = f - 239 length = int.from_bytes(data[pos:pos + n], "big") pos += n + length elif f == 244: pos += 4 elif f == 245: break continue dyn_f, black = f >> 4, (f >> 3) & 1 low = f & 7 if low < 4: pl = ((f & 3) << 8) + data[pos + 1] cc = data[pos + 2] tfm = int.from_bytes(data[pos + 3:pos + 6], "big") w, h = data[pos + 7], data[pos + 8] hoff, voff = struct.unpack(">bb", data[pos + 9:pos + 11]) start, end = pos + 11, pos + 3 + pl elif low < 7: pl = ((f & 3) << 16) + int.from_bytes(data[pos + 1:pos + 3], "big") cc = data[pos + 3] tfm = int.from_bytes(data[pos + 4:pos + 7], "big") w, h = struct.unpack(">HH", data[pos + 9:pos + 13]) hoff, voff = struct.unpack(">hh", data[pos + 13:pos + 17]) start, end = pos + 17, pos + 4 + pl else: pl, cc, tfm = struct.unpack(">iii", data[pos + 1:pos + 13]) w, h, hoff, voff = struct.unpack(">iiii", data[pos + 21:pos + 37]) start, end = pos + 37, pos + 9 + pl if dyn_f == 14: rows = decode_bitmap(data, start, w, h) else: rows = decode_packed(data, start, w, h, dyn_f, black) glyphs[cc] = dict(tfm=tfm / 2**20, w=w, h=h, hoff=hoff, voff=voff, rows=rows) pos = end dpi = hppp / 2**16 * 72.27 return ds / 2**20, dpi, glyphs def main(): pk, outdir = sys.argv[1], sys.argv[2] os.makedirs(outdir, exist_ok=True) ds, dpi, glyphs = read_pk(pk) meta = dict(design_size=ds, dpi=dpi, glyphs={}) for cc, g in glyphs.items(): # 1px white margin so potrace sees closed outlines. w, h = g["w"] + 2, g["h"] + 2 with open(os.path.join(outdir, f"{cc}.pbm"), "w") as fh: fh.write(f"P1\n{w} {h}\n") fh.write(" ".join(["0"] * w) + "\n") for r in g["rows"]: fh.write(" ".join(map(str, [0] + r + [0])) + "\n") fh.write(" ".join(["0"] * w) + "\n") meta["glyphs"][cc] = {k: g[k] for k in ("tfm", "w", "h", "hoff", "voff")} json.dump(meta, open(os.path.join(outdir, "meta.json"), "w"), indent=1) print(f"{pk}: {len(glyphs)} glyphs, ds={ds}pt, {dpi:.0f}dpi") if __name__ == "__main__": main()