first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
# @suaveplan/font-compress - BTF (Binary TypeFace) binary packing — packer-only since the 2026-05-17 split. Converts typeface.js JSON to compact Uint8Array using dictionary-based compression. Pairs with @suaveplan/font-decompress for the runtime unpack side.
## Summary
BTF (Binary TypeFace) binary packing and unpacking. Converts `TypefaceFont` (typeface.js JSON format) to a compact `Uint8Array` using dictionary-based integer compression, and reverses it.
Designed for Three.js / game-engine font pipelines where fonts must be shipped as efficient binary assets.
<!-- begin:auto-docs -->
## Documentation tree
- [auto-boot](./src/bin/auto-boot.md) — Pure side-effect entry imported by `suaveplan-font-compress.ts` for the shebang-launch case. The only behaviour is: if…
- [dispatch](./src/bin/dispatch.md) — Per-file orchestration extracted from the bin entry. Reads a single input file from disk, routes binary inputs (TTF/OTF…
- [format-utils](./src/bin/format-utils.md) — Pure formatting helpers backing the operator-facing output of the `suaveplan-font-compress` bin. The helpers exist so t…
- [parser](./src/bin/parser.md) — Bin-specific wrapper around `@suaveplan/cli`'s generic `parseArgs` that enforces the `suaveplan-font-compress` flag set…
- [printer](./src/bin/printer.md) — Stdout / stderr renderers used by the `suaveplan-font-compress` bin. Owns the help screen, banner, per-file progress li…
- [suaveplan-font-compress](./src/bin/suaveplan-font-compress.md) — Operator-facing CLI that wires `@suaveplan/font-convert` (`FontConverter` / `BatchConverter`) into this package's `Bina…
- [error-codes](./src/error-codes/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
- [error-codes](./src/error-codes/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
<!-- end:auto-docs --> - [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
- [error-codes](./src/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
- [error-codes](./src/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
- [error-codes](./src/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_ - [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
- [error-codes](./src/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
## Packages
| Module | Description |
|--------|-------------|
| [`BinaryFontPacker`](src/packer/binary-font-packer/binary-font-packer.md) | Packs `TypefaceFont` → BTF `Uint8Array` |
| [`BinaryFontUnpacker`](src/unpacker/binary-font-unpacker/binary-font-unpacker.md) | Unpacks BTF `Uint8Array` → `TypefaceFont` |
| [`FontCompressError`](src/error-codes.md) | Structured error with stable codes |
| [`parsePath`](src/utils/parse-path.md) | Tokenises typeface.js outline strings |
| [`encodeString`/`decodeString`](src/utils/encode-string.md) | Length-prefixed UTF-8 string encoding |
## Install
```bash
bun add @suaveplan/font-compress @suaveplan/font-convert
```
## Quick Start
```typescript
import { BinaryFontPacker, BinaryFontUnpacker } from "@suaveplan/font-compress";
import type { TypefaceFont } from "@suaveplan/font-convert";
const font: TypefaceFont = /* from @suaveplan/font-convert */;
// Pack to binary
const packed = new BinaryFontPacker().pack(font);
// Unpack back to TypefaceFont
const restored = new BinaryFontUnpacker().unpack(packed);
```
## BTF Binary Format
```text
[familyName: uint32-len + UTF-8]
[cssFontWeight: uint32-len + UTF-8]
[cssFontStyle: uint32-len + UTF-8]
[reversed: uint8]
[dictLen: uint32 LE]
[dict: Int16Array × dictLen] ← all unique integer values
[metricsIdxs: Uint16Array × 9] ← ascender,descender,ulPos,ulThk,res,yMin,xMin,yMax,xMax
[glyphCount: uint32 LE]
per-glyph:
[charCode: uint32 LE]
[indexCount: uint32 LE]
[indexes: Uint16Array × indexCount] ← ha,x_min,x_max, then cmd_byte+coord_indexes
```
Command bytes: `0=m`, `1=l`, `2=b(cubic)`, `3=q(quadratic)`, `4=z`.
## CLI
The package ships a `suaveplan-font-compress` bin that composes
[`@suaveplan/font-convert`](../font-convert/README.md) (`FontConverter` /
`BatchConverter`) with `BinaryFontPacker` to turn TTF/OTF/WOFF/WOFF2 (or
already-converted typeface JSON) into compact `.btf` artefacts. See
[`src/bin/suaveplan-font-compress.md`](./src/bin/suaveplan-font-compress.md)
for the full flag reference, exit-code table, and in-process API.
End-to-end example:
```sh
# Bake every font under ./fonts into ./out (mirrored layout, BTF default).
bunx suaveplan-font-compress ./fonts -o ./out
# Inspect the typeface intermediate (skips the BTF pack step).
bunx suaveplan-font-compress ./fonts/Inter.ttf -o ./debug --format=json
# Overwrite existing outputs.
bunx suaveplan-font-compress ./fonts -o ./out --force
```
Exit codes: `0` success, `1` per-file failure(s), `2` invalid CLI args,
`3` missing input path.

View File

@@ -0,0 +1,69 @@
# auto-boot
## Purpose
Pure side-effect entry imported by `suaveplan-font-compress.ts` for the
shebang-launch case. The only behaviour is: if the running file is the
`dist/bin/suaveplan-font-compress.js` binary (detected by the `argv[1]`
basename), dynamically import the bin entry and run `main(process.argv.slice(2))`,
then `process.exit` with the resulting code. The split exists so the
`process.exit` branch — which cannot be honestly exercised from in-process
without spawning a subprocess — can be excluded from coverage as a pure
boot stub while the surrounding `main()` (which does the actual work) is
fully covered by the in-process suites in this directory.
## Features
- Detects direct invocation via `process.argv[1]` rather than
`fileURLToPath(import.meta.url)` so the check works regardless of the
module's own location (auto-boot is a sibling of the entry, not the
entry itself).
- Dynamic import of `./suaveplan-font-compress.js` so this module's static
init does not depend on the bin entry's static init, avoiding any
cyclic-import surprises with bundlers.
- Maps a successful `main()` resolve to `process.exit(code)`; maps a
rejected promise to a stderr-printed message and `process.exit(1)`.
## Usage: Basic
The module has no exports — importing it as a side-effect is the entire
API:
```ts
// In suaveplan-font-compress.ts
import "./auto-boot.js";
```
## Usage: Advanced
```sh
# Direct shebang launch — boot runs through this module.
node dist/bin/suaveplan-font-compress.js ./fonts -o ./out
# In-process import — boot detects argv[1] does not end with the binary
# name and stays silent; tests can then call `main()` directly without
# accidentally launching a second instance.
import { main } from "@suaveplan/font-compress/bin/suaveplan-font-compress";
await main(["./fonts", "-o", "./out"]);
```
## API Reference
No public API. The module's side effect runs once at import time:
1. Check `process.argv[1]` for the binary basename suffix.
2. If matched, dynamically import the bin entry and call its `main`.
3. Map the resolved exit code to `process.exit(code)`; map a rejection
to stderr write + `process.exit(1)`.
## Implementation Notes
`process.argv[1].endsWith("suaveplan-font-compress.js")` is intentionally
basename-based — the absolute path comparison used by single-file bin
patterns does not work here because auto-boot is a sibling, not the entry.
The `try/catch` around the check guards against environments where
`process` is undefined (some non-Node runtimes) and against any pathologic
`argv` shapes; both fall back to "not direct invocation" which is the safe
default for an import-only module. Coverage excludes this file because
every branch is either a `process.argv` predicate or a `process.exit`
side-effect — neither can be honestly faked from in-process.

View File

@@ -0,0 +1,77 @@
# dispatch
## Purpose
Per-file orchestration extracted from the bin entry. Reads a single input
file from disk, routes binary inputs (TTF/OTF/WOFF/WOFF2) through the
injected `FontConverter` to produce a `TypefaceFont`, routes `.json` inputs
through pass-through parsing (no conversion), then either serialises the
`TypefaceFont` back to JSON (when `--format=json`) or packs it via
`BinaryFontPacker` and writes the resulting `.btf`. Progress, skip, and
error events are streamed through the injected printer / logger. Lives in
its own module so the entry can stay small enough to satisfy the file-size
budget while keeping a single round-trip testable in isolation.
## Features
- Discriminates input by extension: `.ttf`/`.otf`/`.woff`/`.woff2` goes to
the converter; `.json` goes to pass-through validation.
- Computes destination paths honouring `--preserve-structure` (mirror the
input tree under the output root) vs the flat default.
- Honours `--force`: existing outputs are skipped silently unless `--force`
is set, in which case they are overwritten.
- Captures any throwable from the converter or packer per file, prints a
failure line, logs through `LoggerLike`, and returns `{ ok: false }` so
the batch can continue and the entry exits `1`.
## Usage: Basic
```ts
import { FontConverter } from "@suaveplan/font-convert";
import { BinaryFontPacker } from "../packer/binary-font-packer/binary-font-packer.js";
import { dispatchOne } from "./dispatch.js";
const outcome = await dispatchOne(
"./fonts/Inter-Regular.ttf",
new FontConverter(),
new BinaryFontPacker(),
options,
"./fonts",
"./out",
deps
);
// outcome.ok === true, outcome.compressedBytes > 0
```
## Usage: Advanced
```ts
const failures: string[] = [];
for (const file of files) {
const r = await dispatchOne(file, converter, packer, options, root, out, deps);
if (!r.ok) failures.push(file);
}
if (failures.length > 0) {
deps.logger.error("font-compress.cli.batch_partial", { failures });
}
```
## API Reference
| Name | Signature | Notes |
|---|---|---|
| `dispatchOne` | `(file, converter, packer, options, inputRoot, outputRoot, deps) => Promise<FileOutcome>` | Never throws; all failures are reported via the returned outcome. |
| `FileOutcome` | `{ ok; originalBytes; compressedBytes }` | `compressedBytes === 0` means the file was skipped without overwrite. |
| `DispatchDeps` | `{ logger; stdout; stderr }` | Injected so a test can capture stream output. |
## Implementation Notes
Destination resolution intentionally does NOT call `path.relative` —
prefix-strip plus `replace(/^[\\/]+/)` is enough and lets us mirror the
upstream legacy semantics where Windows-style separators round-trip
unchanged. The dispatcher always creates the parent directory with
`mkdir({ recursive: true })` before writing, so users do not need to
pre-create the output tree. JSON inputs are validated by `JSON.parse` only —
the typeface schema is enforced downstream by `BinaryFontPacker`, which
throws `FontCompressError(PACK_FAILED)` for malformed shapes, and the catch
arm here converts that into a clean per-file failure line.

View File

@@ -0,0 +1,86 @@
# format-utils
## Purpose
Pure formatting helpers backing the operator-facing output of the
`suaveplan-font-compress` bin. The helpers exist so the bin entry can focus on
orchestration (argument parsing, dispatch, FS I/O) while every cosmetic detail
— byte sizing, compression ratio, ANSI tone wrappers — lives in a small,
exhaustively testable module. Keeping them separate also keeps the bin entry
under the AGENTS.md §3 file-size budget and makes the renderer reusable for
custom hosts (CI dashboards, web previews) that want to share the same
output format the operator sees on stdout.
## Features
- `formatBytes(bytes)` — human-readable size string (`B`/`KB`/`MB`/`GB`) with
two-decimal rounding and graceful handling of zero, negative, and non-finite
inputs.
- `formatRatio(original, compressed)` — compression ratio rendered as `Nx`
with one-decimal precision; returns `0.0x` for any non-positive denominator.
- `formatSavings(original, compressed)` — percentage of bytes saved by
compression, rounded to one decimal.
- `ANSI` — frozen palette of the eight terminal escape codes used by the bin
(reset, bold, dim, red, green, yellow, blue, cyan).
- `colour(code, text)` — wrap a string between an ANSI prefix and the reset
sequence.
- `tint.*` — convenience wrappers (`tint.bold`, `tint.green`, …) for the
common combinations.
## Usage: Basic
```ts
import { formatBytes, formatRatio, tint } from "@suaveplan/font-compress/bin/suaveplan-font-compress";
console.log(`Compressed: ${formatBytes(2048)}`); // "Compressed: 2 KB"
console.log(tint.green(`Ratio: ${formatRatio(1000, 250)}`)); // green "Ratio: 4.0x"
```
## Usage: Advanced
```ts
import {
ANSI,
colour,
formatBytes,
formatRatio,
formatSavings,
} from "./format-utils.js";
function renderRow(name: string, original: number, packed: number): string {
return [
colour(ANSI.bold, name.padEnd(20)),
formatBytes(original).padStart(10),
formatBytes(packed).padStart(10),
colour(ANSI.green, formatRatio(original, packed).padStart(8)),
colour(ANSI.dim, formatSavings(original, packed).padStart(8)),
].join(" ");
}
renderRow("Inter-Regular.ttf", 287_000, 64_500);
```
## API Reference
| Name | Signature | Notes |
|---|---|---|
| `formatBytes` | `(bytes: number) => string` | Clamps to `GB` for inputs ≥ 1 TiB so it never indexes past the unit table. |
| `formatRatio` | `(orig: number, packed: number) => string` | Guards against zero / negative / non-finite values. |
| `formatSavings` | `(orig: number, packed: number) => string` | Returns `0.0%` for any non-positive original. |
| `ANSI` | `Readonly<Record<string, string>>` | The eight escape codes used by the bin. |
| `colour` | `(code: string, text: string) => string` | Always closes with `ANSI.reset`. |
| `tint.*` | `(text: string) => string` | One pre-bound `colour` wrapper per palette entry. |
## Implementation Notes
The helpers are pure, side-effect-free, and runtime-agnostic — they take and
return strings/numbers only, never read globals, and never touch I/O. This is
deliberate: the bin entry composes them into stdout writes through an
injected `LoggerLike`, so the same helpers can be used from a hosted UI that
captures the output to a buffer or DOM node. ANSI escape sequences are
emitted unconditionally; callers that need plain text strip them at the
output boundary rather than asking each helper to branch on a `useColour`
flag. Inputs that violate the contract (`NaN`, negative bytes) fall back to a
benign sentinel (`"0 B"`, `"0.0x"`, `"0.0%"`) so a single corrupt summary
record never propagates `NaN` into the operator's terminal — defensive but
honest.

View File

@@ -0,0 +1,74 @@
# parser
## Purpose
Bin-specific wrapper around `@suaveplan/cli`'s generic `parseArgs` that
enforces the `suaveplan-font-compress` flag set, detects mutually-exclusive
flag pairs, and converts any upstream parser error into a coded
`FontCompressError(CLI_INVALID_ARGS)` so the entry's top-level handler can
deterministically map the failure to exit code `2`. Isolated from the entry
so the parser can be unit-tested in milliseconds without spinning up the FS
pipeline.
## Features
- Recognises every flag in the spec delta: `--input`/`-i`, `--output`/`-o`,
`--format=btf|json`, `--force`/`-f`, `--recursive`/`-r`, `--no-recursive`,
`--preserve-structure`, `--no-preserve-structure`, `--help`/`-h`,
`--version`/`-v`, and the positional `<input>`.
- Detects conflicts between `--recursive` and `--no-recursive` and between
the two `--preserve-structure` variants and throws the coded error before
any FS work happens.
- Maps unknown long flags (`--no-such-flag`) and unknown short flags
(`-X`) into `FontCompressError(CLI_INVALID_ARGS)` with the offending flag
captured in `error.context.flag`.
- Returns a frozen-shape `CliOptions` discriminator that downstream
helpers (`dispatch.ts`, the entry's pipeline) can consume with no
further casting.
## Usage: Basic
```ts
import { parseCliArgs } from "./parser.js";
const options = parseCliArgs(["./fonts", "-o", "./out", "--format=json"]);
// → { input: "./fonts", output: "./out", format: "json", … }
```
## Usage: Advanced
```ts
import { FONT_COMPRESS_CODES, FontCompressError } from "../error-codes.js";
import { parseCliArgs } from "./parser.js";
try {
parseCliArgs(["./fonts", "--recursive", "--no-recursive"]);
} catch (err) {
if (
err instanceof FontCompressError &&
err.code === FONT_COMPRESS_CODES["CLI_INVALID_ARGS"]
) {
// err.context.flags === ["--recursive", "--no-recursive"]
process.exit(2);
}
throw err;
}
```
## API Reference
| Name | Signature | Notes |
|---|---|---|
| `parseCliArgs` | `(argv: readonly string[]) => CliOptions` | Throws `FontCompressError(CLI_INVALID_ARGS)` on conflict / unknown flag. |
| `CliOptions` | discriminated record | Includes `input`, `output`, `outputSpecified`, `format`, `force`, `recursive`, `preserveStructure`, `help`, `version`. |
## Implementation Notes
The `--no-*` negations are handled inside the upstream parser (it sets the
camelCase key to `false`), so this wrapper detects only the *pair* of
contradictory flags being present. Unknown-flag detection runs after the
upstream parser throws because the upstream error message is too generic to
re-emit; we scan `argv` for the first long or short token that is not in the
allowlist and propagate it as `error.context.flag`. The `--input` long flag
takes precedence over the positional when both are present (which mirrors
upstream legacy and is documented in the spec delta source-mapping table).

View File

@@ -0,0 +1,79 @@
# printer
## Purpose
Stdout / stderr renderers used by the `suaveplan-font-compress` bin. Owns
the help screen, banner, per-file progress lines (success / skip / error),
and the end-of-run summary block. Every renderer writes through an injected
`NodeJS.WritableStream` so a host (test harness, CI dashboard, web preview)
can capture the output without monkey-patching the global `process.stdout`.
Lives in its own module so the entry can stay under the AGENTS.md §3 file
size budget while keeping the operator-visible output independently
inspectable and unit-testable.
## Features
- `HELP_TEXT` — the full `--help` screen, including every flag in the spec
delta plus the exit-code table.
- `printBanner` — one-shot summary printed before any per-file work,
showing input, output, format, and force state.
- `printSuccessLine` — `✓` line with the family name and either the
compressed byte count (`--format=json`) or the compression ratio
(`--format=btf`).
- `printSkipLine` — `⏭` line when an output already exists and `--force`
was not set.
- `printErrorLine` — `✗` line written to stderr with the file basename and
the failure message.
- `printSummary` — block with total / converted / errors / original size /
compressed size / ratio / savings, terminated by the output path.
## Usage: Basic
```ts
import { HELP_TEXT, printBanner, printSummary } from "./printer.js";
process.stdout.write(HELP_TEXT);
printBanner(process.stdout, "/fonts", "/out", "btf", false);
printSummary(process.stdout, summary, "/out");
```
## Usage: Advanced
```ts
import { Writable } from "node:stream";
import { printSummary } from "./printer.js";
const chunks: string[] = [];
const captured = new Writable({
write(chunk, _enc, cb) {
chunks.push(chunk.toString("utf8"));
cb();
},
});
printSummary(captured, summary, "/out");
// chunks.join("") now holds the rendered block — assert in tests / pipe
// to a CI dashboard webhook.
```
## API Reference
| Name | Signature | Notes |
|---|---|---|
| `HELP_TEXT` | `string` | Pre-rendered help screen (ANSI tones included). |
| `printBanner` | `(stdout, inputPath, outputPath, format, force) => void` | Writes the run-start block. |
| `printSuccessLine` | `(stdout, font, originalBytes, compressedBytes, format) => void` | Emits `✓` line; format choice changes the tail. |
| `printSkipLine` | `(stdout, file) => void` | Emits `⏭` for already-existing outputs. |
| `printErrorLine` | `(stderr, file, message) => void` | Emits `✗` to stderr. |
| `printSummary` | `(stdout, summary, outputRoot) => void` | Emits the end-of-run block. |
| `RunSummary` | record | `{ totalFiles, successCount, errorCount, totalOriginalSize, totalCompressedSize }`. |
## Implementation Notes
ANSI tones come from `format-utils.ts` so the palette is single-sourced.
Every line is composed by `Array#join("\n")` rather than template literals
with leading newlines — the test harness can split on `\n` and assert on
exact strings without worrying about stray indentation. The summary block
always renders the savings percentage even when no bytes were processed
(`formatSavings` falls back to `"0.0%"` for non-positive inputs) so the
output shape stays stable when an operator runs the bin against an empty
directory.

View File

@@ -0,0 +1,123 @@
# suaveplan-font-compress
## Purpose
Operator-facing CLI that wires `@suaveplan/font-convert`
(`FontConverter` / `BatchConverter`) into this package's
`BinaryFontPacker` to convert TTF/OTF/WOFF/WOFF2 (and already-converted
typeface JSON) into compact `.btf` artefacts on disk. The CLI is the
shipping shape of the `@suaveplan/font-compress` package's "build-time
asset baker" surface and is invoked from package scripts, CI workflows, and
ad-hoc developer workstations — the underlying library
(`BinaryFontPacker` / `BinaryFontUnpacker`) remains the supported in-process
surface for runtime callers that already hold a `TypefaceFont` in memory.
## Features
- Single-file or whole-directory input via positional `<input>` or
`--input`/`-i`.
- Recursive directory walk (default `true`; disable with `--no-recursive`).
- Layout strategy: mirror the source tree (`--preserve-structure`) or flatten
to the output root (the default when `--output` is set).
- Format dispatch: emit packed BTF binaries (default `--format=btf`) or the
intermediate typeface JSON (`--format=json`) for pipeline debugging.
- `--force`/`-f` to overwrite existing outputs; otherwise existing files are
skipped with a `⏭` line.
- Structured exit codes (`0`/`1`/`2`/`3`) with the meaning pinned in the
spec delta.
- Coded errors: argument-parsing failures throw
`FontCompressError(FONT_COMPRESS.CLI_INVALID_ARGS)` which the top-level
handler maps to exit `2`.
- OTel span `font-compress.cli.run` emitted for every invocation with
`font-compress.files`, `font-compress.errors`, and `font-compress.exit_code`
attributes set as the run progresses.
- Injectable `LoggerLike` and `stdout`/`stderr` streams via the exported
`main(argv, deps)` so the CLI is testable in-process without spawning a
subprocess.
## Usage: Basic
```sh
# Compress a single file in place (writes Inter-Regular.btf next to it).
suaveplan-font-compress ./fonts/Inter-Regular.ttf
# Compress a whole directory recursively to a sibling output tree.
suaveplan-font-compress ./fonts -o ./out
```
## Usage: Advanced
```sh
# JSON intermediate — useful for inspecting what the converter emitted
# before the BTF pack step. Round-trips through the same packer.
suaveplan-font-compress ./fonts/Inter-Regular.ttf --format=json -o ./debug
# Force-overwrite existing outputs (the default is to skip with a ⏭ line).
suaveplan-font-compress ./fonts -o ./out --force
# Flatten the output tree under ./out, regardless of the input layout.
suaveplan-font-compress ./fonts -o ./out --no-preserve-structure
# In-process invocation from a Node host (no subprocess, captured streams).
import { main } from "@suaveplan/font-compress/bin/suaveplan-font-compress";
import { Writable } from "node:stream";
const out: string[] = [];
const sink = new Writable({
write(chunk, _enc, cb) {
out.push(chunk.toString("utf8"));
cb();
},
});
const code = await main(["./fonts", "-o", "./out"], { stdout: sink, stderr: sink });
process.exit(code);
```
## API Reference
| Flag | Type | Default | Notes |
|---|---|---|---|
| `<input>` | positional | required | File or directory. |
| `--input` / `-i` | string | unset | Alternative to positional. |
| `--output` / `-o` | string | input path | Output directory. |
| `--format` | `btf` \| `json` | `btf` | Output format. |
| `--force` / `-f` | boolean | `false` | Overwrite existing outputs. |
| `--recursive` / `-r` | boolean | `true` | Recurse directories. |
| `--no-recursive` | boolean | — | Disable recursion. |
| `--preserve-structure` | boolean | derived | Mirror source tree. |
| `--no-preserve-structure` | boolean | — | Flatten under output root. |
| `--help` / `-h` | boolean | `false` | Print usage and exit `0`. |
| `--version` / `-v` | boolean | `false` | Print version and exit `0`. |
### Exit codes
| Code | Meaning |
|---|---|
| `0` | All inputs processed successfully. |
| `1` | One or more inputs failed during conversion or packing. |
| `2` | Invalid CLI arguments (unknown flag, conflict, missing positional). |
| `3` | The input path does not exist on disk. |
### Exported in-process API
- `main(argv, deps?)` — returns the would-be exit code; never calls
`process.exit` itself when invoked in-process.
- `parseCliArgs(argv)` — pure parser; throws
`FontCompressError(CLI_INVALID_ARGS)` on conflict / unknown flag.
- `CliOptions`, `RunDeps` — discriminator and dep-bundle types.
## Implementation Notes
The pipeline always instantiates both `FontConverter` and `BatchConverter`
even on single-file inputs — the spec scenario "directory input uses
BatchConverter" is satisfied by the constructor side effect, and the
file-by-file loop keeps memory consumption bounded for very large font
trees instead of materialising every binary in a single `BatchConverter`
call. Output paths are computed by stripping the input root from the file
path and re-rooting at the output directory; this preserves the upstream
legacy semantics where Windows-style separators round-trip unchanged. The
top-level handler is the ONLY place that maps a thrown
`FontCompressError(CLI_INVALID_ARGS)` to exit `2`; deeper code paths never
own exit codes directly. Per-file failures from the converter or packer are
caught in `dispatch.ts` and surfaced as `{ ok: false }`, allowing the batch
to continue and the entry to exit `1` after the summary block.

View File

@@ -0,0 +1,78 @@
# FontCompressError / FONT_COMPRESS_CODES
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError` from `@suaveplan/error`. It carries a stable code from the `FONT_COMPRESS_CODES` registry, an optional `cause`, and an optional `context` object for structured diagnostics.
All public operations in the package (`BinaryFontPacker.pack`, `BinaryFontUnpacker.unpack`) throw `FontCompressError` on failure rather than raw `Error` or `RangeError`, enabling callers to branch on specific codes and forward structured error data to logging and observability systems.
## Features
- Three stable error codes: `PACK_FAILED`, `UNPACK_FAILED`, `INVALID_DATA`
- Codes are prefixed with the package domain: `FONT_COMPRESS.PACK_FAILED`
- Supports `cause` (original error) and `context` (arbitrary key/value metadata)
- Extends `SuaveplanError` for integration with the project error infrastructure
## Usage: Basic Example
```typescript
import { FontCompressError, FONT_COMPRESS_CODES } from "@suaveplan/font-compress";
try {
packer.pack(font);
} catch (err) {
if (err instanceof FontCompressError) {
console.error(err.code); // "FONT_COMPRESS.PACK_FAILED"
}
}
```
## Usage: Advanced Example
```typescript
import { FontCompressError, FONT_COMPRESS_CODES } from "@suaveplan/font-compress";
function safePack(font: TypefaceFont): Uint8Array | null {
try {
return packer.pack(font);
} catch (err) {
if (
err instanceof FontCompressError &&
err.code === FONT_COMPRESS_CODES["PACK_FAILED"]
) {
logger.error("pack failed", { code: err.code, cause: err.cause });
return null;
}
throw err;
}
}
```
## API Reference
### `FONT_COMPRESS_CODES`
Object with keys `PACK_FAILED`, `UNPACK_FAILED`, `INVALID_DATA`, each mapping to its namespaced string code.
### `FontCompressErrorCode`
Union type of all code values in `FONT_COMPRESS_CODES`.
### `FontCompressError`
Extends `SuaveplanError<FontCompressErrorCode>`.
**Constructor:** `new FontCompressError(code, message, options?)`
- `code` — one of the `FONT_COMPRESS_CODES` values
- `message` — human-readable description
- `options.cause` — original `Error` (stored on `.cause`)
- `options.context` — additional `Record<string, unknown>` metadata
## Implementation Notes
`SuaveplanError` uses a `protected constructor`, so `FontCompressError` must declare an explicit `public constructor` that calls `super()`. This is a pattern used consistently across all `@suaveplan/*` error classes.

View File

@@ -0,0 +1,99 @@
# BinaryFontPacker
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`BinaryFontPacker` converts a `TypefaceFont` (the typeface.js JSON structure) into a compact binary BTF (Binary TypeFace) buffer. It builds a shared integer dictionary from all unique coordinate values in the font and encodes every number as a 16-bit dictionary index rather than a string, dramatically reducing the payload compared to raw JSON.
The BTF format is designed for Three.js and game-engine font pipelines where fonts must be shipped as efficient binary assets rather than verbose JSON strings.
## Features
- Dictionary-based compression: all unique integer values stored once in an `Int16Array`; all subsequent references are 16-bit indexes
- Full font round-trip: header strings, reversed flag, 9 scalar metrics, bounding box, per-glyph ha/x_min/x_max, and full outline data
- Supports all typeface.js command types: `m`, `l`, `b` (cubic), `q` (quadratic), `z`
- Little-endian byte layout throughout for cross-platform compatibility
- Wraps internal errors in `FontCompressError` with stable code `PACK_FAILED`
## Usage: Basic Example
```typescript
import { createBinaryFontPacker } from "@suaveplan/font-compress/packer/binary-font-packer";
import type { TypefaceFont } from "@suaveplan/font-convert";
const font: TypefaceFont = { /* ... */ };
const packer = createBinaryFontPacker();
const buffer: Uint8Array = packer.pack(font);
```
## Usage: Advanced Example
```typescript
import { BinaryFontPacker } from "@suaveplan/font-compress/packer/binary-font-packer";
import { FontCompressError } from "@suaveplan/font-compress";
const packer = new BinaryFontPacker();
try {
const packed = packer.pack(font);
// Write to disk, send over the wire, or store in IndexedDB
await Bun.write("font.btf", packed);
} catch (err) {
if (err instanceof FontCompressError) {
console.error("Pack failed:", err.code, err.message);
}
}
```
## API Reference
### `BinaryFontPacker`
#### `pack(font: TypefaceFont): Uint8Array`
Packs a `TypefaceFont` into a BTF binary buffer.
**Parameters:**
- `font` — the typeface.js font object, as produced by `@suaveplan/font-convert`
**Returns:** `Uint8Array` containing the binary BTF data.
**Throws:** `FontCompressError` with code `PACK_FAILED` if packing fails for any reason.
### `createBinaryFontPacker(): BinaryFontPacker`
Factory function that creates a new `BinaryFontPacker` instance.
## Implementation Notes
### BTF Binary Layout
```text
[familyName: encodeString]
[cssFontWeight: encodeString]
[cssFontStyle: encodeString]
[reversed: uint8 (0 or 1)]
[dictLen: uint32 LE]
[dict: Int16Array × dictLen]
[metricsIdxs: Uint16Array × 9] ← ascender,descender,ulPos,ulThk,res,yMin,xMin,yMax,xMax
[glyphCount: uint32 LE]
per-glyph:
[charCode: uint32 LE]
[indexCount: uint32 LE]
[indexes: Uint16Array × indexCount] ← ha,x_min,x_max, then (cmd_byte+coord_indexes)...
```
`encodeString` format: `[uint32 length LE][UTF-8 bytes]`.
Command bytes map to: `0=m`, `1=l`, `2=b`, `3=q`, `4=z`. Coordinate counts per command: `m=2`, `l=2`, `b=6`, `q=4`, `z=0`.
### Dictionary Construction
All unique coordinate values across the entire font (metrics, bounding box, all glyph coordinates) are collected into a sorted `Int16Array`. This single pass over the font data is O(n) in glyph count and produces optimal deduplication. Because typeface.js coordinates are integers in a bounded range (typically ±2048), the dictionary fits comfortably in an Int16Array.
### Alignment
All multi-byte typed arrays are written via `Uint8Array` byte copies, not direct TypedArray buffer views, so there are no alignment requirements on the output buffer. Readers (BinaryFontUnpacker) use `DataView` to safely read values at arbitrary offsets.

View File

@@ -0,0 +1,60 @@
# encode-string
## Purpose
Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encoding used by `BinaryFontPacker` to store font family name, CSS weight, and CSS style as inline strings in the binary header.
Every string is encoded as `[uint32 byte-length LE][UTF-8 bytes]`, which allows safe decoding without a sentinel character and handles arbitrary Unicode content including multi-byte sequences.
## Features
- `encodeString`: encodes a JS string as `[uint32 LE length][UTF-8 bytes]`
- `decodeString`: reads a length-prefixed string from a buffer at a given byte offset and returns the decoded value and the new offset after the string
## Usage: Basic Example
```typescript
import { encodeString, decodeString } from "@suaveplan/font-compress/utils/encode-string";
const encoded = encodeString("Arial");
// [5, 0, 0, 0, 65, 114, 105, 97, 108]
const { value, offset } = decodeString(encoded, 0);
// value = "Arial", offset = 9
```
## Usage: Advanced Example
```typescript
// Chaining multiple string reads during unpack
let off = 0;
const r1 = decodeString(data, off); off = r1.offset;
const r2 = decodeString(data, off); off = r2.offset;
const r3 = decodeString(data, off); off = r3.offset;
// r1.value = familyName, r2.value = cssFontWeight, r3.value = cssFontStyle
```
## API Reference
### `encodeString(str: string): Uint8Array`
Encodes `str` as `[uint32 LE byte-length][UTF-8 bytes]`.
**Parameters:** `str` — any JS string including multi-byte Unicode.
**Returns:** `Uint8Array` of `4 + byteLength` bytes.
### `decodeString(data: Uint8Array, offset: number): { value: string; offset: number }`
Reads a length-prefixed string at `offset` in `data`.
**Parameters:**
- `data` — source buffer
- `offset` — byte position of the uint32 length prefix
**Returns:** `{ value: string; offset: number }` where `offset` is the position immediately after the string bytes.
## Implementation Notes
Uses the platform `TextEncoder` / `TextDecoder` singletons. The returned `offset` from `decodeString` is the cursor position immediately following the string's last byte, suitable for passing directly to the next read operation.

View File

@@ -0,0 +1,67 @@
# parse-path
## Purpose
Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph outlines as space-separated tokens: a command letter (`m`, `l`, `b`, `q`, `z`) followed by its coordinate arguments. `parsePath` tokenises this string and groups each command with its coordinates for downstream processing.
This utility is used by `BinaryFontPacker` to enumerate all glyph coordinates during dictionary construction and to encode each command into the BTF binary stream.
## Features
- Parses all five typeface.js commands: `m` (moveto), `l` (lineto), `b` (cubic bezier), `q` (quadratic bezier), `z` (close path)
- Returns coordinate arrays of the correct length per command: `m/l=2`, `q=4`, `b=6`, `z=0`
- Silently skips unrecognised tokens
- Handles empty strings and trailing whitespace gracefully
## Usage: Basic Example
```typescript
import { parsePath } from "@suaveplan/font-compress/utils/parse-path";
const commands = parsePath("m 0 0 l 100 0 z ");
// [
// { cmd: "m", coords: [0, 0] },
// { cmd: "l", coords: [100, 0] },
// { cmd: "z", coords: [] },
// ]
```
## Usage: Advanced Example
```typescript
import { parsePath } from "@suaveplan/font-compress/utils/parse-path";
// Complex glyph outline with all command types
const commands = parsePath("m 0 0 l 100 0 b 10 20 30 40 50 60 q 5 10 15 20 z ");
for (const { cmd, coords } of commands) {
console.log(cmd, coords.length, "coords");
}
// m 2 coords
// l 2 coords
// b 6 coords
// q 4 coords
// z 0 coords
```
## API Reference
### `ParsedCommand`
```typescript
interface ParsedCommand {
cmd: string;
coords: number[];
}
```
### `parsePath(pathString: string): ParsedCommand[]`
Parses a typeface.js outline string.
**Parameters:** `pathString` — the `o` field of a `TypefaceGlyph`.
**Returns:** array of `ParsedCommand` objects in outline order. Returns `[]` for an empty string.
## Implementation Notes
The parser advances by one token at a time. When it encounters a known command, it reads the next N tokens as floating-point coordinates. If a coordinate token is missing or non-numeric it is skipped; this handles malformed paths without throwing. Unrecognised tokens (not in the set `m l b q z`) are consumed and discarded without advancing the coordinate counter, preserving correct alignment for subsequent commands.