first commit
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
# unpacker/binary-font-unpacker
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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 <Text3D font={font} />
|
||||
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<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 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.
|
||||
Reference in New Issue
Block a user