#!/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()
