5.4 KiB
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
BinaryFontPackerwrites: 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
FontDecompressErrorwithFONT_DECOMPRESS.INVALID_DATA - Wraps any underlying primitive exception (
RangeError,TypeErrorfrom DataView/TextDecoder) asFontDecompressErrorwithFONT_DECOMPRESS.UNPACK_FAILEDwhile preservingcause - Stateless — a single instance is safe to reuse across many
unpack(...)calls - Universal — no
node:/bun:imports
Usage: Basic
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 <Text3D font={font} />
console.log(font.familyName, font.glyphs["A"]?.ha);
Usage: Advanced
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<ReturnType<BinaryFontUnpacker["unpack"]>> {
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 readassertRemaining(data, offset, need, field)— throwsINVALID_DATAifoffset + needexceedsdata.length, naming the field for diagnosticsdictAt(dict, index, field)— throwsINVALID_DATAifindexis out of range; otherwise returnsdict[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.