#!/usr/bin/env python3 """Independent TrueType reader used only to cross-check nox-text. Deliberately written from the spec with nothing but `struct`, so that agreement with the Rust parser is real evidence and not a shared bug. Emits a JSON fixture consumed by tests/parser.rs. """ import json import struct import sys U16 = lambda b, o: struct.unpack_from(">H", b, o)[0] S16 = lambda b, o: struct.unpack_from(">h", b, o)[0] U32 = lambda b, o: struct.unpack_from(">I", b, o)[0] PROBE = ("A", "V", "T", "W", "L", "M", "Q", "a", "o", "g", "f", "i", "z", "1", " ", ".", ",", "@", "~", "\u00e9", "\u00fc", "\u00f1", "\u03a9", "\u2192", "\u5b57") def tables(b): off = 0 if b[:4] == b"ttcf": off = U32(b, 12) n = U16(b, off + 4) out = {} for i in range(n): e = off + 12 + i * 16 tag = b[e:e + 4].decode("latin1") out[tag] = (U32(b, e + 8), U32(b, e + 12)) return out def cmap_lookup(b, base, ch): """Walk every subtable, score them, then resolve through the winner.""" n = U16(b, base + 2) best, best_score = None, -1 for i in range(n): rec = base + 4 + i * 8 plat, enc, off = U16(b, rec), U16(b, rec + 2), U32(b, rec + 4) sub = base + off if sub + 4 > len(b): continue fmt = U16(b, sub) score = {(3, 10, 12): 100, (0, 3, 12): 95, (0, 4, 12): 95, (0, 6, 12): 95, (3, 1, 4): 90, (0, 3, 4): 85, (0, 4, 4): 85, (0, 6, 4): 85, (0, 0, 4): 85, (0, 1, 4): 85, (0, 2, 4): 85, (3, 0, 4): 70, (1, 0, 6): 40, (1, 0, 0): 30}.get((plat, enc, fmt)) if score is None: score = 20 if fmt in (0, 4, 6, 12) else None if score is None: continue if score > best_score: best, best_score = (sub, fmt), score if best is None: return None sub, fmt = best cp = ord(ch) if fmt == 0: return b[sub + 6 + cp] if cp < 256 else 0 if fmt == 6: first, count = U16(b, sub + 6), U16(b, sub + 8) if cp < first or cp >= first + count: return 0 return U16(b, sub + 10 + (cp - first) * 2) if fmt == 4: if cp > 0xFFFF: return 0 seg2 = U16(b, sub + 6) segs = seg2 // 2 ends = sub + 14 starts = ends + seg2 + 2 deltas = starts + seg2 ranges = deltas + seg2 for s in range(segs): if U16(b, ends + s * 2) >= cp: start = U16(b, starts + s * 2) if cp < start: return 0 delta = S16(b, deltas + s * 2) ro = U16(b, ranges + s * 2) if ro == 0: return (cp + delta) & 0xFFFF idx = ranges + s * 2 + ro + (cp - start) * 2 if idx + 2 > len(b): return 0 g = U16(b, idx) return 0 if g == 0 else (g + delta) & 0xFFFF return 0 if fmt == 12: ngroups = U32(b, sub + 12) for g in range(ngroups): e = sub + 16 + g * 12 lo, hi, sg = U32(b, e), U32(b, e + 4), U32(b, e + 8) if lo <= cp <= hi: return min(sg + cp - lo, 0xFFFF) return 0 return None def kern_pairs(b, tb): """Every (left, right) -> value in the font's own kern table.""" if tb is None: return {} base, _ = tb if U16(b, base) != 0: return {} n = U16(b, base + 2) off = base + 4 out = {} for _ in range(n): length = U16(b, off + 2) cov = U16(b, off + 4) if cov & 1 and (cov >> 8) == 0: npairs = U16(b, off + 6) for p in range(npairs): e = off + 14 + p * 6 if e + 6 > len(b): break out.setdefault((U16(b, e), U16(b, e + 2)), S16(b, e + 4)) if length < 6: break off += length return out def contour_stats(b, tabs, gid, upem, long_loca, num_glyphs, depth=0): """(contours, points, on_curve_points, simple, bbox) with composites resolved. bbox is over decoded points and is only reported for simple glyphs, where it exercises the flag-repeat and x/y delta decoding end to end.""" if depth > 6 or gid >= num_glyphs: return None lo, ll = tabs["loca"] go, gl = tabs["glyf"] if long_loca: a, z = U32(b, lo + gid * 4), U32(b, lo + gid * 4 + 4) else: a, z = U16(b, lo + gid * 2) * 2, U16(b, lo + gid * 2 + 2) * 2 if z <= a: return (0, 0, 0, True, None) p = go + a nc = S16(b, p) if nc < 0: off = p + 10 tot = [0, 0, 0] for _ in range(32): flags, sub = U16(b, off), U16(b, off + 2) off += 4 off += 4 if flags & 1 else 2 if flags & 8: off += 2 elif flags & 0x40: off += 4 elif flags & 0x80: off += 8 r = contour_stats(b, tabs, sub, upem, long_loca, num_glyphs, depth + 1) if r: tot = [tot[i] + r[i] for i in range(3)] if not flags & 0x20: break return (tot[0], tot[1], tot[2], False, None) p += 10 ends = [U16(b, p + i * 2) for i in range(nc)] p += nc * 2 npts = ends[-1] + 1 if ends else 0 ins = U16(b, p) p += 2 + ins flags = [] while len(flags) < npts: f = b[p] p += 1 flags.append(f) if f & 8: rep = b[p] p += 1 flags.extend([f] * min(rep, npts - len(flags))) on = sum(1 for f in flags if f & 1) xs, v = [], 0 for f in flags: if f & 2: d = b[p]; p += 1 v += d if f & 16 else -d elif not f & 16: v += S16(b, p); p += 2 xs.append(v) ys, v = [], 0 for f in flags: if f & 4: d = b[p]; p += 1 v += d if f & 32 else -d elif not f & 32: v += S16(b, p); p += 2 ys.append(v) box = [min(xs), min(ys), max(xs), max(ys)] if xs else None return (nc, npts, on, True, box) def describe(path): b = open(path, "rb").read() tabs = tables(b) for need in ("head", "hhea", "hmtx", "maxp", "loca", "glyf"): if need not in tabs: return None head = tabs["head"][0] if U32(b, head + 12) != 0x5F0F3CF5: return None upem = U16(b, head + 18) long_loca = S16(b, head + 50) == 1 num_glyphs = U16(b, tabs["maxp"][0] + 4) hhea = tabs["hhea"][0] nhm = U16(b, hhea + 34) hmtx = tabs["hmtx"][0] def adv(g): return U16(b, hmtx + min(g, nhm - 1) * 4) if nhm else 0 cmap = tabs.get("cmap") glyphs = {} if cmap: for ch in PROBE: g = cmap_lookup(b, cmap[0], ch) if g is None: continue glyphs[ch] = g out = { "file": path, "units_per_em": upem, "num_glyphs": num_glyphs, "ascent": S16(b, hhea + 4), "descent": S16(b, hhea + 6), "line_gap": S16(b, hhea + 8), "bbox": [S16(b, head + 36), S16(b, head + 38), S16(b, head + 40), S16(b, head + 42)], "has_cmap": bool(cmap), "glyph_ids": glyphs, "advances": {str(g): adv(g) for g in sorted(set(glyphs.values()))}, "contours": {}, "kerning": {}, } for ch, g in sorted(glyphs.items()): st = contour_stats(b, tabs, g, upem, long_loca, num_glyphs) if st: out["contours"][str(g)] = st # Sample real pairs out of the font's own kern table, spread across it so the # binary search is exercised at both ends, plus pairs known to be absent. pairs = kern_pairs(b, tabs.get("kern")) if pairs: keys = sorted(pairs) step = max(1, len(keys) // 40) for k in keys[::step][:40]: out["kerning"][f"{k[0]},{k[1]}"] = pairs[k] out["kerning"][f"{keys[0][0]},{keys[0][1]}"] = pairs[keys[0]] out["kerning"][f"{keys[-1][0]},{keys[-1][1]}"] = pairs[keys[-1]] for miss in ((0, 0), (65535, 65534), (7, 65533)): if miss not in pairs: out["kerning"][f"{miss[0]},{miss[1]}"] = 0 return out def emit(d): w = sys.stdout.write w(f"FONT {d['file']}\n") w(f"UPEM {d['units_per_em']}\n") w(f"NGLYPHS {d['num_glyphs']}\n") w(f"VMETRICS {d['ascent']} {d['descent']} {d['line_gap']}\n") w("BBOX %d %d %d %d\n" % tuple(d["bbox"])) w(f"CMAP {int(d['has_cmap'])}\n") for ch, g in sorted(d["glyph_ids"].items()): w(f"GID {ord(ch)} {g}\n") for g, a in sorted(d["advances"].items(), key=lambda kv: int(kv[0])): w(f"ADV {g} {a}\n") for g, st in sorted(d["contours"].items(), key=lambda kv: int(kv[0])): nc, npts, non, simple, box = st if simple and box: w(f"GLYPH {g} {nc} {npts} {non} 1 {box[0]} {box[1]} {box[2]} {box[3]}\n") else: w(f"GLYPH {g} {nc} {npts} {non} 0 0 0 0 0\n") for k, v in sorted(d["kerning"].items()): l, r = k.split(",") w(f"KERN {l} {r} {v}\n") w("END\n") def main(): n = 0 for path in sys.argv[1:]: try: d = describe(path) except Exception as exc: print(f"skip {path}: {exc}", file=sys.stderr) continue if d: emit(d) n += 1 print(f"{n} fonts described", file=sys.stderr) main()