# 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.