# unpacker/binary-font-unpacker _(no submodules)_ ## Purpose `BinaryFontUnpacker` reverses the BTF (Binary TypeFace) wire format produced by `BinaryFontPacker` from `@suaveplan/font-compress`, returning a `TypefaceFont` object (from `@suaveplan/font-convert`) ready for Three.js's `FontLoader`, R3F text rendering, or any other typeface.js-compatible consumer. The unpacker is the runtime side of the BTF pipeline: build-time tooling packs once, then ships the compact `Uint8Array`; renderers unpack on demand. The class is intentionally self-contained — no helpers imported from `@suaveplan/font-compress`, no shared mutable state. Boundary validation runs at every read so malformed or truncated buffers raise `FontDecompressError` synchronously rather than corrupting the returned font object. ## Features - Round-trips every field a `BinaryFontPacker` writes: header strings, reversed flag, dictionary, metrics, glyphs, glyph outlines - Handles all five glyph commands (`m`/`l`/`b`/`q`/`z`) per the BTF coordinate-count table - Validates every buffer read for truncation; raises `FontDecompressError` with `FONT_DECOMPRESS.INVALID_DATA` - Wraps any underlying primitive exception (`RangeError`, `TypeError` from DataView/TextDecoder) as `FontDecompressError` with `FONT_DECOMPRESS.UNPACK_FAILED` while preserving `cause` - Stateless — a single instance is safe to reuse across many `unpack(...)` calls - Universal — no `node:`/`bun:` imports ## Usage: Basic ```ts import { BinaryFontUnpacker, createBinaryFontUnpacker, } from "@suaveplan/font-decompress"; // Load BTF bytes from anywhere — fetch, fs, memory, etc. const btfBuffer: Uint8Array = await fetch("/fonts/inter.btf") .then((r) => r.arrayBuffer()) .then((b) => new Uint8Array(b)); const font = createBinaryFontUnpacker().unpack(btfBuffer); // Feed `font` to Three.js's FontLoader.parse(font) or R3F's console.log(font.familyName, font.glyphs["A"]?.ha); ``` ## Usage: Advanced ```ts import { BinaryFontUnpacker, FONT_DECOMPRESS_CODES, FontDecompressError, } from "@suaveplan/font-decompress"; const unpacker = new BinaryFontUnpacker(); // safe to reuse async function loadFontWithFallback( url: string, fallback: Uint8Array, ): Promise> { try { const buf = new Uint8Array( await (await fetch(url)).arrayBuffer(), ); return unpacker.unpack(buf); } catch (err) { if ( err instanceof FontDecompressError && (err.code === FONT_DECOMPRESS_CODES.UNPACK_FAILED || err.code === FONT_DECOMPRESS_CODES.INVALID_DATA) ) { // BTF corrupted in transit; fall back to the bundled font return unpacker.unpack(fallback); } throw err; } } ``` ## API Reference ### `class BinaryFontUnpacker` | Method | Signature | | ----------------- | -------------------------------------------------- | | `unpack` | `(data: Uint8Array) => TypefaceFont` | ### `createBinaryFontUnpacker(): BinaryFontUnpacker` Convenience factory equivalent to `new BinaryFontUnpacker()`. Use it when you prefer functional composition over `new`. ### Error contract | Condition | Code | | ------------------------------------------------------ | --------------------------------- | | Truncated buffer at any header / dictionary / glyph | `FONT_DECOMPRESS.INVALID_DATA` | | Out-of-range command byte (not 0–4) | `FONT_DECOMPRESS.INVALID_DATA` | | Out-of-range dictionary index | `FONT_DECOMPRESS.INVALID_DATA` | | `indexCount < 3` for a glyph (missing ha/x_min/x_max) | `FONT_DECOMPRESS.INVALID_DATA` | | Underlying `DataView` / `TextDecoder` throw | `FONT_DECOMPRESS.UNPACK_FAILED` | All errors are `FontDecompressError` instances and carry `cause` when wrapping a primitive throw. ## Implementation Notes The unpacker walks `data` once, advancing a single `offset` cursor. Reads use a shared `DataView` constructed over `data.buffer + data.byteOffset` so subarray inputs work without an extra slice. Every read goes through one of three private helpers: - `readUint32(data, offset)` — little-endian uint32 read - `assertRemaining(data, offset, need, field)` — throws `INVALID_DATA` if `offset + need` exceeds `data.length`, naming the field for diagnostics - `dictAt(dict, index, field)` — throws `INVALID_DATA` if `index` is out of range; otherwise returns `dict[index]` The shared `decodeString` helper (in `utils/decode-string.ts`) handles the three header strings with the same boundary validation. The `BYTE_TO_CMD` table (`["m", "l", "b", "q", "z"]`) and `CMD_COORD_COUNTS` map (`m: 2, l: 2, b: 6, q: 4, z: 0`) come from the package-local `constants/index.ts` module — a verbatim duplicate of the constants in `@suaveplan/font-compress`. Duplicating 19 lines of format magic keeps each package self-contained at the cost of zero maintenance burden (the BTF wire format is locked). `original_font_information` is initialised to `{}` because the BTF format does not carry it — the field is preserved on the `TypefaceFont` shape for downstream compatibility but is always empty on the round-trip side.