first commit
This commit is contained in:
27
docs/media/image/README.md
Normal file
27
docs/media/image/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# `image`
|
||||
|
||||
Image compression, colour conversion, EXIF handling, and optimisation.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Packages in `image`
|
||||
|
||||
15 packages.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/caddy-imgproxy`](./caddy-imgproxy/README.md) | Universal TypeScript port of the Caddy imgproxy URL rewriter. Parses the compact `/u<op><gravity><dims><modifiers>/<path>.<format>` grammar, resolves aspect-ra… |
|
||||
| [`@suaveplan/colour-convert-adapt`](./colour-convert-adapt/README.md) | Bradford chromatic adaptation for XYZ colour coordinates. Chromatic adaptation adjusts a colour's appearance so that it looks the same under different light so… |
|
||||
| [`@suaveplan/colour-convert-cieluv`](./colour-convert-cieluv/README.md) | CIELUV, xyY, LMS, and Bradford chromatic adaptation conversions. This plugin provides colour-space transforms between RGB, CIELUV, xyY, and LMS cone-response s… |
|
||||
| [`@suaveplan/colour-convert-cmyk`](./colour-convert-cmyk/README.md) | Bidirectional conversion between RGB and CMYK colour spaces using the standard Euclidean algorithm with black generation. CMYK is the subtractive model used in… |
|
||||
| [`@suaveplan/colour-convert-hsluv`](./colour-convert-hsluv/README.md) | HSLuv perceptually-uniform colour space with gamut-mapped ray intersection for sRGB. This plugin provides bidirectional RGB↔HSLuv/HPLuv conversions and uniform… |
|
||||
| [`@suaveplan/colour-filter-cct`](./colour-filter-cct/README.md) | Correlated Colour Temperature (CCT) conversions using the McCamy formula and black-body RGB approximation. Convert any RGB colour to its approximate colour tem… |
|
||||
| [`@suaveplan/colour-filter-cmc`](./colour-filter-cmc/README.md) | CMC l:c Delta-E colour difference metric with hue-dependent chroma scaling and low-luminance branch handling. This plugin computes perceptual distance between… |
|
||||
| [`@suaveplan/colour-filter-greyscale`](./colour-filter-greyscale/README.md) | Converts any RGB colour to its perceptual greyscale equivalent using the ITU-R BT.709 luminance coefficients. Unlike simple channel averaging, this formula wei… |
|
||||
| [`@suaveplan/colour-filter-sepia`](./colour-filter-sepia/README.md) | Applies a photographic sepia tone filter to RGB colours using the standard W3C CSS Filters 3×3 matrix. This plugin converts any colour into a warm brownish ton… |
|
||||
| [`@suaveplan/colours`](./colours/README.md) | Universal colour manipulation library: parsing, conversion, analysis, mixing, blending, and palette generation. Parses hex (3/4/6/8 digit), CSS `rgb()`, `hsl()… |
|
||||
| [`@suaveplan/compress-image`](./compress-image/README.md) | Runtime-agnostic image compression utilities with pluggable adapters for Node.js (Sharp) and testing (MemoryAdapter). Supports resize, quality reduction, forma… |
|
||||
| [`@suaveplan/emblem-solver`](./emblem-solver/README.md) | Minimal-rectangle SVG converter for pixelated sprite emblems. |
|
||||
| [`@suaveplan/exif`](./exif/README.md) | Dependency-free, runtime-agnostic EXIF/XMP/IPTC metadata reader, writer, and stripper for JPEG and PNG images. Parses JPEG segment chains to locate and decode… |
|
||||
| [`@suaveplan/image`](./image/README.md) | Universal image metadata extraction, format detection, resize math, crop validation, and transform pipeline planning. Zero browser or Node dependencies — runs… |
|
||||
| [`@suaveplan/image-quantisation`](./image-quantisation/README.md) | Image complexity analysis and adaptive colour quantisation with pool detection, spatial-aware anti-aliasing cleanup, and multiple quantisation algorithms. Desi… |
|
||||
<!-- end:auto-docs -->
|
||||
1
docs/media/image/_intro.md
Normal file
1
docs/media/image/_intro.md
Normal file
@@ -0,0 +1 @@
|
||||
Image compression, colour conversion, EXIF handling, and optimisation.
|
||||
101
docs/media/image/caddy-imgproxy/README.md
Normal file
101
docs/media/image/caddy-imgproxy/README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# @suaveplan/caddy-imgproxy - Universal TypeScript port of the Caddy imgproxy URL rewriter — compact-grammar parser, aspect / pixel dimension resolver, gravity + operation translator, HMAC-SHA256 URL signer, full handler with Zod-validated config and OTel telemetry
|
||||
|
||||
Universal TypeScript port of the Caddy imgproxy URL rewriter.
|
||||
Parses the compact `/u<op><gravity><dims><modifiers>/<path>.<format>`
|
||||
grammar, resolves aspect-ratio + hint dimensions, translates gravity +
|
||||
operations into imgproxy's URL DSL, signs the result with
|
||||
HMAC-SHA256, and exposes a high-level `ImgproxyHandler` orchestrator
|
||||
with injectable `Clock`, `LoggerLike`, and signing hash, plus
|
||||
Zod-validated configuration and OTel telemetry.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [config](./src/config/config.md) — The `config` module exposes the Zod schema and the validating `parseConfig` function for the package's public `Imgproxy…
|
||||
- [config](./src/config/config.md) — The `config` module exposes the Zod schema and the validating `parseConfig` function for the package's public `Imgproxy…
|
||||
- [dimensions](./src/dimensions/calculate-dimensions.md) — `calculateDimensions` resolves a `ParsedImgproxyUrl` plus the configured `defaultWidth` into a concrete `{ width, heigh…
|
||||
- [calculate-dimensions](./src/dimensions/calculate-dimensions.md) — `calculateDimensions` resolves a `ParsedImgproxyUrl` plus the configured `defaultWidth` into a concrete `{ width, heigh…
|
||||
- [domain](./src/domain/extract-domain.md) — `extractDomain` translates an incoming HTTP `Host` header into the S3 sub-path segment that the URL builder interpolate…
|
||||
- [extract-domain](./src/domain/extract-domain.md) — `extractDomain` translates an incoming HTTP `Host` header into the S3 sub-path segment that the URL builder interpolate…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module is the canonical home of the package's stable error-code registry and the typed `CaddyImgproxy…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module is the canonical home of the package's stable error-code registry and the typed `CaddyImgproxy…
|
||||
- [gravity](./src/gravity/gravity.md) — The `gravity` module exposes the bidirectional translation between the compact one- or two-character grammar codes (`t`…
|
||||
- [gravity](./src/gravity/gravity.md) — The `gravity` module exposes the bidirectional translation between the compact one- or two-character grammar codes (`t`…
|
||||
- [handler](./src/handler/handler.md) — `ImgproxyHandler` is the high-level orchestrator that wraps `parseImgproxyUrl`, `extractDomain`, and `buildImgproxyUrl`…
|
||||
- [handler](./src/handler/handler.md) — `ImgproxyHandler` is the high-level orchestrator that wraps `parseImgproxyUrl`, `extractDomain`, and `buildImgproxyUrl`…
|
||||
- [operation](./src/operation/operation.md) — `operationToImgproxy` converts a resolved operation + concrete pixel dimensions + gravity into the imgproxy processing-…
|
||||
- [operation](./src/operation/operation.md) — `operationToImgproxy` converts a resolved operation + concrete pixel dimensions + gravity into the imgproxy processing-…
|
||||
- [parse](./src/parse/parse-url.md) — `parseImgproxyUrl` parses the compact imgproxy URL grammar used by the Caddy module — `/[s|u]<op><gravity><dims><modifi…
|
||||
- [parse-url](./src/parse/parse-url.md) — `parseImgproxyUrl` parses the compact imgproxy URL grammar used by the Caddy module — `/[s|u]<op><gravity><dims><modifi…
|
||||
- [signing](./src/signing/sign-path.md) — `signImgproxyPath` computes the HMAC-SHA256 URL signature that imgproxy requires when it is configured with a key + sal…
|
||||
- [sign-path](./src/signing/sign-path.md) — `signImgproxyPath` computes the HMAC-SHA256 URL signature that imgproxy requires when it is configured with a key + sal…
|
||||
- [telemetry](./src/telemetry/telemetry.md) — The `telemetry` module owns the OTel instruments emitted by every public entry point in `@suaveplan/caddy-imgproxy`. It…
|
||||
- [telemetry](./src/telemetry/telemetry.md) — The `telemetry` module owns the OTel instruments emitted by every public entry point in `@suaveplan/caddy-imgproxy`. It…
|
||||
- [types](./src/types/types.md) — The `types` module declares every cross-feature type that flows between the package's runtime modules: `ParsedImgproxyU…
|
||||
- [types](./src/types/types.md) — The `types` module declares every cross-feature type that flows between the package's runtime modules: `ParsedImgproxyU…
|
||||
- [url-builder](./src/url-builder/url-builder.md) — `buildImgproxyUrl` composes the final imgproxy URL path from a parsed grammar representation. It is the orchestrator th…
|
||||
- [url-builder](./src/url-builder/url-builder.md) — `buildImgproxyUrl` composes the final imgproxy URL path from a parsed grammar representation. It is the orchestrator th…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/caddy-imgproxy
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { ImgproxyHandler, parseConfig } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const handler = new ImgproxyHandler({
|
||||
config: parseConfig({
|
||||
upstream: "imgproxy:8080",
|
||||
s3Bucket: "media",
|
||||
defaultWidth: 1200,
|
||||
key: "aa11bbcc",
|
||||
salt: "deadbeef",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = handler.rewrite({
|
||||
path: "/uvt9:16h1080/photos/hero.webp",
|
||||
host: "images.example.com",
|
||||
});
|
||||
|
||||
if (result.rewritten) {
|
||||
forwardToImgproxy(result.targetPath);
|
||||
} else if (result.reason === "passthrough") {
|
||||
forwardToImgproxy(originalPath);
|
||||
}
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [parse](./src/parse/parse-url.md) — compact-grammar parser.
|
||||
- [dimensions](./src/dimensions/calculate-dimensions.md) — aspect /
|
||||
hint → pixel resolution.
|
||||
- [gravity](./src/gravity/gravity.md) — grammar gravity → imgproxy
|
||||
compass tokens.
|
||||
- [operation](./src/operation/operation.md) — operation +
|
||||
dimensions → imgproxy processing string.
|
||||
- [signing](./src/signing/sign-path.md) — HMAC-SHA256 URL signing.
|
||||
- [url-builder](./src/url-builder/url-builder.md) — composes the
|
||||
final imgproxy path.
|
||||
- [domain](./src/domain/extract-domain.md) — Host header → S3
|
||||
sub-path.
|
||||
- [handler](./src/handler/handler.md) — `ImgproxyHandler`
|
||||
orchestrator.
|
||||
- [config](./src/config/config.md) — Zod-validated `ImgproxyConfig`.
|
||||
- [error-codes](./src/error-codes/error-codes.md) —
|
||||
`CADDY_IMGPROXY_CODES` + `CaddyImgproxyError`.
|
||||
- [telemetry](./src/telemetry/telemetry.md) — OTel counters +
|
||||
histogram.
|
||||
|
||||
## Tier & dependencies
|
||||
|
||||
Universal — no DOM or Node-only APIs at the package boundary.
|
||||
`sideEffects: false`. Internal deps: `@suaveplan/crypto-hash`,
|
||||
`@suaveplan/error`, `@suaveplan/types`. External deps:
|
||||
`@opentelemetry/api`, `tslib`, `zod`.
|
||||
|
||||
86
docs/media/image/caddy-imgproxy/src/config/config.md
Normal file
86
docs/media/image/caddy-imgproxy/src/config/config.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# config
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `config` module exposes the Zod schema and the validating
|
||||
`parseConfig` function for the package's public `ImgproxyConfig`. It
|
||||
is the single boundary where a raw object — typically loaded from
|
||||
environment variables, JSON, or a Caddyfile shim — is converted into
|
||||
the immutable, defaulted `ImgproxyConfig` that the URL builder and
|
||||
the handler consume.
|
||||
|
||||
`parseConfig` raises stable, typed `CaddyImgproxyError` instances on
|
||||
every failure path: `INVALID_OPTIONS` for general schema failures,
|
||||
`INVALID_KEY` for a malformed `key`, and `INVALID_SALT` for a
|
||||
malformed `salt`. The schema applies the same defaults the upstream
|
||||
Go module ships with (`localhost:8080`, `images`, `800`) so a bare
|
||||
`parseConfig({})` returns a usable production configuration.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod schema is exported so callers can compose it into larger
|
||||
configs.
|
||||
- Defaults match the Go reference exactly.
|
||||
- Hex validation built into the schema — even-length, lower-case
|
||||
only.
|
||||
- Distinct error codes for key vs salt failures so consumers can
|
||||
surface targeted remediation in their UI.
|
||||
- The returned `ImgproxyConfig` is frozen for safe sharing.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseConfig } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const cfg = parseConfig({});
|
||||
cfg.upstream; // "localhost:8080"
|
||||
cfg.s3Bucket; // "images"
|
||||
cfg.defaultWidth; // 800
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { parseConfig, ImgproxyConfigSchema } from "@suaveplan/caddy-imgproxy";
|
||||
import z from "zod";
|
||||
|
||||
// Compose into a larger application schema:
|
||||
const AppEnvSchema = z.object({
|
||||
NODE_ENV: z.enum(["development", "production"]),
|
||||
IMGPROXY: ImgproxyConfigSchema,
|
||||
});
|
||||
|
||||
const env = AppEnvSchema.parse(process.env);
|
||||
const cfg = parseConfig(env.IMGPROXY);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ImgproxyConfigSchema`
|
||||
|
||||
A Zod object schema with the following fields:
|
||||
|
||||
- `upstream: string` — default `"localhost:8080"`, non-empty.
|
||||
- `s3Bucket: string` — default `"images"`, non-empty.
|
||||
- `defaultWidth: number` — positive integer, default `800`.
|
||||
- `key?: string` — optional even-length lower-hex string.
|
||||
- `salt?: string` — optional even-length lower-hex string.
|
||||
|
||||
### `parseConfig(raw: unknown): ImgproxyConfig`
|
||||
|
||||
- **Returns:** a frozen `ImgproxyConfig`.
|
||||
- **Throws:** `CaddyImgproxyError` with one of `INVALID_OPTIONS`,
|
||||
`INVALID_KEY`, `INVALID_SALT`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`safeParse` is used instead of `parse` so the function can branch on
|
||||
the first issue's `path` to choose the appropriate error code. The
|
||||
schema lifts the hex check into a `refine` so the Zod issue path is
|
||||
exactly `["key"]` or `["salt"]` when the hex format fails — that
|
||||
makes the `parseConfig` dispatch one-liner reliable. The returned
|
||||
config is frozen so downstream code cannot mutate it accidentally.
|
||||
@@ -0,0 +1,77 @@
|
||||
# calculateDimensions
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`calculateDimensions` resolves a `ParsedImgproxyUrl` plus the
|
||||
configured `defaultWidth` into a concrete `{ width, height }` pair in
|
||||
integer pixels. It is the single source of truth for how the compact
|
||||
grammar's aspect-ratio + hint syntax becomes pixel values; every
|
||||
processing string emitted by `operationToImgproxy` is derived from
|
||||
this result.
|
||||
|
||||
Semantics mirror the upstream Go `calculateDimensions` byte-for-byte:
|
||||
no dimensions → `{0, 0}` (passthrough); pixel dimensions return
|
||||
verbatim; aspect-ratio with an `h` hint drives height first; otherwise
|
||||
width drives (from the `w` hint or the configured default). All
|
||||
divisions truncate toward zero via `Math.trunc` so the TypeScript and
|
||||
Go implementations agree on every fixture.
|
||||
|
||||
## Features
|
||||
|
||||
- Total function — never throws, always returns concrete numbers.
|
||||
- Honors the same precedence as the Go reference: hint axis wins over
|
||||
default width.
|
||||
- Integer truncation matches Go integer division exactly.
|
||||
- Zero runtime cost beyond two multiplications and one divide on the
|
||||
aspect path.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { calculateDimensions, parseImgproxyUrl } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const parsed = parseImgproxyUrl("/uv16:9/avatar.png");
|
||||
const dims = calculateDimensions(parsed, 800);
|
||||
dims; // { width: 800, height: 450 }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { calculateDimensions, parseImgproxyUrl } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
// Height hint with portrait aspect ratio
|
||||
const portrait = parseImgproxyUrl("/uv9:16h1080/photo.jpg");
|
||||
calculateDimensions(portrait, 800);
|
||||
// → { width: 607, height: 1080 }
|
||||
|
||||
// Pixel passthrough — the default-width parameter is ignored
|
||||
const pixel = parseImgproxyUrl("/uf800x600/photo.jpg");
|
||||
calculateDimensions(pixel, 9999);
|
||||
// → { width: 800, height: 600 }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `calculateDimensions(parsed: ParsedImgproxyUrl, defaultWidth: number): ImgproxyDimensions`
|
||||
|
||||
- **Parameters:**
|
||||
- `parsed` — output of `parseImgproxyUrl`.
|
||||
- `defaultWidth` — width used when the URL is in aspect-ratio mode
|
||||
without a `w`/`h` hint.
|
||||
- **Returns:** `{ width, height }`, both non-negative integers. Either
|
||||
axis may be `0` (meaning "auto / unspecified").
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is intentionally a pure expression — it never touches
|
||||
the clock, the meter, or the logger so it composes cleanly under
|
||||
property-based tests. The default-width branch is reached when the
|
||||
parsed URL specifies an aspect ratio but neither a `w` nor an `h`
|
||||
hint, or when `hintType === "h"` but `hintValue === 0` (the
|
||||
zero-value clause is unreachable from the public regex but defended
|
||||
for callers who construct `ParsedImgproxyUrl` values directly).
|
||||
67
docs/media/image/caddy-imgproxy/src/domain/extract-domain.md
Normal file
67
docs/media/image/caddy-imgproxy/src/domain/extract-domain.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# extractDomain
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`extractDomain` translates an incoming HTTP `Host` header into the
|
||||
S3 sub-path segment that the URL builder interpolates into the
|
||||
`s3://<bucket>/<domain>/<path>@<format>` template. It is the single
|
||||
place where the multi-tenant routing convention "the `images.` host
|
||||
prefix maps to the bare apex domain" is enforced.
|
||||
|
||||
The function is total — it never throws — so callers may apply it
|
||||
to any string. Hosts shorter than two labels (such as `localhost`)
|
||||
are returned unchanged because the heuristic only makes sense for
|
||||
fully-qualified domain names. Port suffixes are removed before
|
||||
splitting so `images.example.com:8080` and `images.example.com` map
|
||||
to the same value.
|
||||
|
||||
## Features
|
||||
|
||||
- Total function over arbitrary input strings.
|
||||
- Port-aware: strips `:<port>` before label splitting.
|
||||
- Single-label hosts (`localhost`) pass through unchanged.
|
||||
- Mirrors the upstream Go heuristic exactly.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { extractDomain } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
extractDomain("images.example.com:8080"); // "example.com"
|
||||
extractDomain("cdn.example.com"); // "cdn.example.com"
|
||||
extractDomain("localhost"); // "localhost"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { extractDomain } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
// Apply per-request to build a tenant key:
|
||||
function tenantKey(host: string): string {
|
||||
return extractDomain(host).replace(/\./g, "-");
|
||||
}
|
||||
tenantKey("images.acme.example.com"); // "acme-example-com"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `extractDomain(host: string): string`
|
||||
|
||||
- **Parameters:** `host` — the raw `Host` header value (with or
|
||||
without a port suffix).
|
||||
- **Returns:** the apex domain when the prefix is `images.`,
|
||||
otherwise the host unchanged (minus any port suffix).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Implemented as a two-step string scan: `indexOf(":")` to locate the
|
||||
optional port boundary, then `String.prototype.split(".")` to break
|
||||
the host into labels. The label check requires at least two segments
|
||||
because dropping the first label of a one-label host would yield the
|
||||
empty string. The function does not validate the host against any
|
||||
DNS spec — it accepts whatever the caller passes through.
|
||||
101
docs/media/image/caddy-imgproxy/src/error-codes/error-codes.md
Normal file
101
docs/media/image/caddy-imgproxy/src/error-codes/error-codes.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# error-codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `error-codes` module is the canonical home of the package's
|
||||
stable error-code registry and the typed `CaddyImgproxyError` class
|
||||
that carries those codes. Every failure path in the package raises a
|
||||
`CaddyImgproxyError` whose `code` is drawn from this registry;
|
||||
consumers discriminate on `error.code` rather than `instanceof` so
|
||||
the namespace survives module boundaries and runtime serialisation.
|
||||
|
||||
Codes are built with `@suaveplan/error`'s `defineCodes` helper, which
|
||||
emits the canonical `NAMESPACE.REASON` form (e.g.
|
||||
`"CADDY_IMGPROXY.INVALID_URL"`). The `CaddyImgproxyError` class
|
||||
extends `SuaveplanError<CaddyImgproxyCode>` so `code` narrows
|
||||
against the literal union at compile time.
|
||||
|
||||
## Features
|
||||
|
||||
- Built via `defineCodes` so the namespace + reason form is uniform.
|
||||
- `CaddyImgproxyError` carries `code`, `message`, `cause`, `context`,
|
||||
`timestamp`, and the standard `SuaveplanError` envelope.
|
||||
- Discriminating union literal (`CaddyImgproxyCode`) for exhaustive
|
||||
`switch` checks.
|
||||
- Every code in the registry is reachable from at least one call
|
||||
site exercised by the tests.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
CADDY_IMGPROXY_CODES,
|
||||
CaddyImgproxyError,
|
||||
} from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
try {
|
||||
// … call into the package
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof CaddyImgproxyError &&
|
||||
err.code === CADDY_IMGPROXY_CODES.INVALID_URL
|
||||
) {
|
||||
// handle parse failure
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
CADDY_IMGPROXY_CODES,
|
||||
CaddyImgproxyError,
|
||||
} from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
function classify(err: unknown): "parse" | "config" | "host" | "other" {
|
||||
if (!(err instanceof CaddyImgproxyError)) return "other";
|
||||
switch (err.code) {
|
||||
case CADDY_IMGPROXY_CODES.INVALID_URL:
|
||||
case CADDY_IMGPROXY_CODES.MISSING_DIMENSIONS:
|
||||
return "parse";
|
||||
case CADDY_IMGPROXY_CODES.INVALID_OPTIONS:
|
||||
case CADDY_IMGPROXY_CODES.INVALID_KEY:
|
||||
case CADDY_IMGPROXY_CODES.INVALID_SALT:
|
||||
return "config";
|
||||
case CADDY_IMGPROXY_CODES.INVALID_HOST:
|
||||
return "host";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CADDY_IMGPROXY_CODES`
|
||||
|
||||
A frozen object mapping reason names to their `CADDY_IMGPROXY.REASON`
|
||||
code strings. Reasons: `INVALID_URL`, `MISSING_DIMENSIONS`,
|
||||
`INVALID_OPTIONS`, `INVALID_KEY`, `INVALID_SALT`, `INVALID_HOST`.
|
||||
|
||||
### `class CaddyImgproxyError extends SuaveplanError<CaddyImgproxyCode>`
|
||||
|
||||
Public constructor: `(code, message, options?)`. Options carry
|
||||
`cause` and `context`.
|
||||
|
||||
### `type CaddyImgproxyCode`
|
||||
|
||||
Discriminating union literal extracted from the registry values.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` returns a frozen object indexed by reason name, with
|
||||
each value typed as `${Namespace}.${Reason}` for compile-time literal
|
||||
narrowing. The class deliberately exposes a public constructor —
|
||||
`SuaveplanError`'s own constructor is `protected` so subclasses must
|
||||
provide a concrete entry point. The `name = "CaddyImgproxyError"`
|
||||
assignment matters for stack-trace clarity; without it `new.target`
|
||||
would still set it correctly but explicit is clearer.
|
||||
74
docs/media/image/caddy-imgproxy/src/gravity/gravity.md
Normal file
74
docs/media/image/caddy-imgproxy/src/gravity/gravity.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# gravity
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `gravity` module exposes the bidirectional translation between the
|
||||
compact one- or two-character grammar codes (`t`, `tl`, `bl`, …) and
|
||||
the two-/four-letter compass tokens that imgproxy itself consumes
|
||||
(`no`, `nowe`, `sowe`, …). Every operation that takes a gravity
|
||||
parameter — `cover`, `crop` — routes through this module so the
|
||||
mapping lives in exactly one place.
|
||||
|
||||
The map is exported as a frozen `Readonly<Record<GravityCode, string>>`
|
||||
so consumers may inspect or build their own translation tables on top
|
||||
of it. The `gravityToImgproxy` helper handles unknown / empty input by
|
||||
falling back to `"ce"` (center), matching the Go default branch
|
||||
exactly.
|
||||
|
||||
## Features
|
||||
|
||||
- Total function — never throws.
|
||||
- Single source of truth for the gravity grammar.
|
||||
- Frozen map for safe sharing.
|
||||
- Center default for forward-compatibility with grammar extensions.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { gravityToImgproxy } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
gravityToImgproxy("t"); // "no"
|
||||
gravityToImgproxy("br"); // "soea"
|
||||
gravityToImgproxy(""); // "ce" (default)
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { GRAVITY_MAP } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
// Build a reverse lookup for parsing imgproxy URLs back into the
|
||||
// compact grammar.
|
||||
const reverse = Object.fromEntries(
|
||||
Object.entries(GRAVITY_MAP).map(([k, v]) => [v, k] as const)
|
||||
);
|
||||
reverse["sowe"]; // "bl"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `gravityToImgproxy(code: string): string`
|
||||
|
||||
- **Parameters:** `code` — one of `t|b|l|r|c|tl|tr|bl|br`, or any
|
||||
unknown value.
|
||||
- **Returns:** the imgproxy compass token, or `"ce"` when input is
|
||||
unknown or empty.
|
||||
|
||||
### `GRAVITY_MAP: Readonly<Record<GravityCode, string>>`
|
||||
|
||||
The frozen lookup table.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function uses an indexed object lookup rather than a `switch`
|
||||
statement because the map is exposed as a frozen value; the default
|
||||
branch handles both the empty-string and the unknown-code cases in a
|
||||
single line. Coverage for the unknown branch is exercised by the test
|
||||
`"defaults unknown codes to ce"`. The cast to `Record<string, string>`
|
||||
inside the function is necessary because the formal type uses the
|
||||
`GravityCode` union; at runtime any string may arrive (e.g. from a
|
||||
caller-constructed `ParsedImgproxyUrl`).
|
||||
121
docs/media/image/caddy-imgproxy/src/handler/handler.md
Normal file
121
docs/media/image/caddy-imgproxy/src/handler/handler.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# ImgproxyHandler
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`ImgproxyHandler` is the high-level orchestrator that wraps
|
||||
`parseImgproxyUrl`, `extractDomain`, and `buildImgproxyUrl` into a
|
||||
single `rewrite(request)` call suitable for use inside a CDN worker,
|
||||
an edge reverse proxy, or a build-time tooling pass. It owns the
|
||||
telemetry envelope around every rewrite, the structured logging on
|
||||
parse failures, and the discriminated `RewriteResult` that lets
|
||||
callers decide whether to forward to imgproxy, fall back, or return
|
||||
an error to the client.
|
||||
|
||||
The handler exposes three observable outcomes: `rewritten` (the path
|
||||
matched the grammar and a target imgproxy path was produced),
|
||||
`passthrough` (the path is `/unsafe/...` or `/health*`, meaning the
|
||||
caller should forward to imgproxy unchanged), and `invalid` (the
|
||||
path is not an imgproxy URL and the caller should fall back to the
|
||||
next handler in the chain).
|
||||
|
||||
## Features
|
||||
|
||||
- Injectable `Clock` for deterministic duration histograms.
|
||||
- Injectable `LoggerLike` for structured parse-failure warnings.
|
||||
- Injectable `SignerHashLike` for native / HSM / WebCrypto signing.
|
||||
- Discriminated `RewriteResult` — no exceptions on the happy path.
|
||||
- Telemetry: counter (`caddy_imgproxy_rewrite_total{outcome,
|
||||
operation}`) + histogram (`caddy_imgproxy_rewrite_duration_ms`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ImgproxyHandler, parseConfig } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const handler = new ImgproxyHandler({
|
||||
config: parseConfig({
|
||||
defaultWidth: 800,
|
||||
key: "aa11bbcc",
|
||||
salt: "deadbeef",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = handler.rewrite({
|
||||
path: "/uv16:9/avatar.png",
|
||||
host: "images.example.com",
|
||||
});
|
||||
|
||||
if (result.rewritten) {
|
||||
forwardToImgproxy(result.targetPath);
|
||||
} else if (result.reason === "passthrough") {
|
||||
forwardToImgproxy(originalPath);
|
||||
} else {
|
||||
fallbackToNextHandler();
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type { Clock, LoggerLike } from "@suaveplan/types";
|
||||
import { ImgproxyHandler, parseConfig } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const tick = createMonotonicTick();
|
||||
const clock: Clock = () => tick.nowMs();
|
||||
const logger: LoggerLike = {
|
||||
debug: (m, c) => log("debug", m, c),
|
||||
info: (m, c) => log("info", m, c),
|
||||
warn: (m, c) => log("warn", m, c),
|
||||
error: (m, c) => log("error", m, c),
|
||||
};
|
||||
|
||||
const handler = new ImgproxyHandler({
|
||||
config: parseConfig({}),
|
||||
clock,
|
||||
logger,
|
||||
});
|
||||
|
||||
const result = handler.rewrite({
|
||||
path: "/uvtr9:16h1080/hero.webp",
|
||||
host: "images.example.com",
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class ImgproxyHandler`
|
||||
|
||||
#### `new ImgproxyHandler(options: ImgproxyHandlerOptions)`
|
||||
|
||||
- `options.config` — validated `ImgproxyConfig`.
|
||||
- `options.clock` — optional wall-clock; defaults to `Date.now`.
|
||||
- `options.logger` — optional `LoggerLike`; defaults to a silent
|
||||
no-op.
|
||||
- `options.hash` — optional signer hash override.
|
||||
|
||||
#### `rewrite(request: RewriteRequest): RewriteResult`
|
||||
|
||||
Returns one of `{ rewritten: true; targetPath; operation }`,
|
||||
`{ rewritten: false; reason: "passthrough" }`, or
|
||||
`{ rewritten: false; reason: "invalid"; errorCode; errorMessage }`.
|
||||
|
||||
### `RewriteRequest`
|
||||
|
||||
- `path` — incoming URL path including leading `/`.
|
||||
- `host` — incoming `Host` header.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Empty host is treated as a hard error (`INVALID_HOST`) rather than a
|
||||
silent passthrough because the S3 path interpolation depends on the
|
||||
host. `/unsafe/...` and `/health*` are detected only when
|
||||
`parseImgproxyUrl` throws — the grammar deliberately does not match
|
||||
these paths, so we treat the parse failure as a signal that this is a
|
||||
direct imgproxy passthrough rather than a user-facing rewrite. Both
|
||||
the clock and the logger are captured into instance fields at
|
||||
construction time so a single handler is safe to share across many
|
||||
requests in a long-lived server.
|
||||
76
docs/media/image/caddy-imgproxy/src/operation/operation.md
Normal file
76
docs/media/image/caddy-imgproxy/src/operation/operation.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# operationToImgproxy
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`operationToImgproxy` converts a resolved operation + concrete pixel
|
||||
dimensions + gravity into the imgproxy processing-string segment that
|
||||
sits between the `/<signature>/` prefix and the `/plain/s3://…@<fmt>`
|
||||
suffix. It is the bridge between the compact grammar's abstract
|
||||
operations and imgproxy's concrete URL DSL.
|
||||
|
||||
The five operations behave exactly as the upstream Go module: `cover`
|
||||
and `crop` require a gravity prefix so anchoring takes effect during
|
||||
cropping; `fit` ignores gravity (it always preserves aspect ratio);
|
||||
`stretch` always emits `rs:force:W:H`; `resize` is the single-axis
|
||||
variant of `fit`. Passthrough (`""`) and unknown operations emit an
|
||||
empty string so the URL builder skips the processing segment entirely.
|
||||
|
||||
## Features
|
||||
|
||||
- Total function — every input returns a defined string, never
|
||||
throws.
|
||||
- Mirrors the Go switch table byte-for-byte.
|
||||
- Single-axis fallbacks (`g:<g>/w:W`, `w:W`, `h:H`) so callers can
|
||||
produce auto-axis resizes without re-implementing the table.
|
||||
- Empty-string fallback drives the URL builder to skip the segment.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { operationToImgproxy } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
operationToImgproxy("cover", 1920, 1080, "t");
|
||||
// → "g:no/rs:fill:1920:1080"
|
||||
|
||||
operationToImgproxy("fit", 800, 0, "c");
|
||||
// → "w:800"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
calculateDimensions,
|
||||
operationToImgproxy,
|
||||
parseImgproxyUrl,
|
||||
} from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const parsed = parseImgproxyUrl("/ucbl4:3w1920/photo.png");
|
||||
const { width, height } = calculateDimensions(parsed, 800);
|
||||
operationToImgproxy(parsed.operation, width, height, parsed.gravity);
|
||||
// → "g:sowe/c:1920:1440"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `operationToImgproxy(op: ImgproxyOperation, width: number, height: number, gravity: string): string`
|
||||
|
||||
- **Parameters:**
|
||||
- `op` — one of `""|cover|crop|fit|stretch|resize`.
|
||||
- `width`, `height` — non-negative integers; either may be zero.
|
||||
- `gravity` — a grammar code accepted by `gravityToImgproxy`.
|
||||
- **Returns:** the imgproxy processing string, or `""` when the
|
||||
operation cannot be expressed (e.g. `crop` without both dims).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The switch is exhaustive over the `ImgproxyOperation` union; the
|
||||
TypeScript compiler verifies no case escapes. Gravity is resolved
|
||||
once at the top of the function so the `cover`/`crop` branches share
|
||||
a single translation step. The empty-operation case returns `""`
|
||||
explicitly rather than falling through so the exhaustiveness check
|
||||
stays clean.
|
||||
98
docs/media/image/caddy-imgproxy/src/parse/parse-url.md
Normal file
98
docs/media/image/caddy-imgproxy/src/parse/parse-url.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# parseImgproxyUrl
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`parseImgproxyUrl` parses the compact imgproxy URL grammar used by the
|
||||
Caddy module — `/[s|u]<op><gravity><dims><modifiers>/<path>.<format>` —
|
||||
into a typed, frozen `ParsedImgproxyUrl`. It is the canonical entry
|
||||
point for every other helper in the package; the URL builder, the
|
||||
handler, and the dimensions calculator all consume the value it
|
||||
returns.
|
||||
|
||||
Behaviour mirrors the upstream Go implementation byte-for-byte so the
|
||||
TypeScript port and the Go module can be tested against a shared
|
||||
fixture corpus. The function never partially succeeds: invalid input
|
||||
throws a `CaddyImgproxyError` carrying a stable code from
|
||||
`CADDY_IMGPROXY_CODES`, and the parse-total counter ticks once per
|
||||
call with the matching outcome tag.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-pass regex grammar covering signed / unsigned prefix, five
|
||||
operations (`cover`, `crop`, `fit`, `stretch`, `resize`), nine
|
||||
gravities, aspect / pixel / square dimensions, and width / height /
|
||||
quality modifiers in arbitrary order.
|
||||
- Quality validation built into the grammar (`q1..q100`) — out-of-band
|
||||
values raise `INVALID_URL`.
|
||||
- Operation + dimension constraint enforcement (`MISSING_DIMENSIONS`).
|
||||
- Frozen, immutable return value safe to memoise.
|
||||
- OTel counter on every call (`caddy_imgproxy_parse_total`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseImgproxyUrl } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const parsed = parseImgproxyUrl("/uv16:9/avatar.png");
|
||||
parsed.operation; // "cover"
|
||||
parsed.aspectW; // 16
|
||||
parsed.path; // "avatar"
|
||||
parsed.format; // "png"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
CADDY_IMGPROXY_CODES,
|
||||
CaddyImgproxyError,
|
||||
parseImgproxyUrl,
|
||||
} from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
function tryParse(path: string): string | null {
|
||||
try {
|
||||
const parsed = parseImgproxyUrl(path);
|
||||
return `${parsed.operation || "passthrough"} → ${parsed.path}.${parsed.format}`;
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof CaddyImgproxyError &&
|
||||
err.code === CADDY_IMGPROXY_CODES.MISSING_DIMENSIONS
|
||||
) {
|
||||
return "needs dims";
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `parseImgproxyUrl(path: string): ParsedImgproxyUrl`
|
||||
|
||||
- **Parameters:** `path` — the URL path including leading `/`.
|
||||
- **Returns:** a frozen `ParsedImgproxyUrl` (see `types/types.ts`).
|
||||
- **Throws:**
|
||||
- `CaddyImgproxyError(INVALID_URL)` when the regex rejects the
|
||||
input — bad prefix, malformed dimensions, quality outside
|
||||
`1..100`.
|
||||
- `CaddyImgproxyError(MISSING_DIMENSIONS)` when an operation
|
||||
(`v|c|f|s|r`) is present without any dimension grammar.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The grammar regex is a single anchored expression with thirteen
|
||||
capture groups that mirror the field order of the Go regex
|
||||
exactly. The modifier sub-grammar (`w<int>`, `h<int>`, `q1..100`) is
|
||||
extracted via a global secondary regex so order is irrelevant — the
|
||||
last `w`/`h` wins (matching Go), and `q` always wins for quality.
|
||||
|
||||
`Number.parseInt` with radix 10 is used for every numeric extraction
|
||||
to avoid the legacy octal-from-leading-zero behaviour. Defensive
|
||||
defaults are deliberately omitted from branches the regex can never
|
||||
reach so coverage stays honest; the only fallbacks in the function
|
||||
body protect against future grammar evolution (e.g. an empty gravity
|
||||
capture defaulting to `"c"`).
|
||||
95
docs/media/image/caddy-imgproxy/src/signing/sign-path.md
Normal file
95
docs/media/image/caddy-imgproxy/src/signing/sign-path.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# signImgproxyPath
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`signImgproxyPath` computes the HMAC-SHA256 URL signature that
|
||||
imgproxy requires when it is configured with a key + salt. It is the
|
||||
single source of truth for how the TypeScript port and the upstream
|
||||
Go module agree on bytes: any URL signed here verifies against the Go
|
||||
implementation and vice-versa.
|
||||
|
||||
The function decodes the hex-encoded `key` and `salt` into raw bytes,
|
||||
concatenates the salt with the UTF-8 encoded `path`, runs HMAC-SHA256
|
||||
under the key, and base64url-encodes the digest without padding. Bad
|
||||
hex on either input raises a typed `CaddyImgproxyError`
|
||||
(`INVALID_KEY` / `INVALID_SALT`) so misconfiguration surfaces
|
||||
immediately rather than at the imgproxy boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- Verbatim byte-level port of the Go `signPath` function.
|
||||
- Pluggable hash via the optional `hash` parameter — supply a native
|
||||
/ HSM / WebCrypto-backed implementation when needed.
|
||||
- Hex validation stricter than `@suaveplan/crypto-hash`'s `fromHex`
|
||||
(lower-case only) to keep wire-format round-trips deterministic.
|
||||
- Throws typed `CaddyImgproxyError` on every failure path.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { signImgproxyPath } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const signature = signImgproxyPath({
|
||||
key: "aa11bbcc",
|
||||
salt: "deadbeef",
|
||||
path: "/g:ce/rs:fill:800:450/plain/s3://images/example.com/avatar@png",
|
||||
});
|
||||
signature; // "E3ytdnTM6THWihFdTWqY4YGQIV-6K969S-8jb4AaE4U"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { signImgproxyPath, type SignerHashLike } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
// Plug a WebCrypto-backed signer in a Cloudflare Worker:
|
||||
const wcHash: SignerHashLike = {
|
||||
hmac(_algo, key, data) {
|
||||
// ... call into crypto.subtle.sign synchronously via a vendored
|
||||
// implementation, return the digest as Uint8Array.
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
const sig = signImgproxyPath({
|
||||
key: "deadbeef",
|
||||
salt: "feedface",
|
||||
path: "/plain/s3://images/example.com/photo@webp",
|
||||
hash: wcHash,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `signImgproxyPath(options: SignImgproxyPathOptions): string`
|
||||
|
||||
- **Parameters:**
|
||||
- `options.key` — lower-hex HMAC key, even length, non-empty.
|
||||
- `options.salt` — lower-hex salt, even length, non-empty.
|
||||
- `options.path` — the imgproxy path being signed.
|
||||
- `options.hash` — optional `SignerHashLike` injection seam.
|
||||
- **Returns:** base64url-encoded signature, no padding.
|
||||
- **Throws:**
|
||||
- `CaddyImgproxyError(INVALID_KEY)` for malformed / empty / odd-length key.
|
||||
- `CaddyImgproxyError(INVALID_SALT)` for malformed / empty / odd-length salt.
|
||||
|
||||
### `SignerHashLike`
|
||||
|
||||
Structural type with a single `hmac(algorithm, key, data)` method
|
||||
returning a `Uint8Array`. Lets callers swap the default Noble-backed
|
||||
implementation for a native, HSM, or WebCrypto-backed alternative.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function pre-allocates the `salt || path` buffer in one
|
||||
`Uint8Array(saltBytes.length + pathBytes.length)` allocation so the
|
||||
hot path never reallocates. UTF-8 encoding goes through
|
||||
`TextEncoder`, which is available in every modern JS runtime
|
||||
(Node ≥ 14, all browsers, Cloudflare Workers, Deno, Bun). The Go
|
||||
reference uses `[]byte(path)` which is identical for ASCII paths and
|
||||
spec-compliant for arbitrary UTF-8 (the imgproxy upstream uses the
|
||||
same convention).
|
||||
89
docs/media/image/caddy-imgproxy/src/telemetry/telemetry.md
Normal file
89
docs/media/image/caddy-imgproxy/src/telemetry/telemetry.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# telemetry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `telemetry` module owns the OTel instruments emitted by every
|
||||
public entry point in `@suaveplan/caddy-imgproxy`. It declares three
|
||||
counters and one histogram, registers them against a single meter
|
||||
named `@suaveplan/caddy-imgproxy`, and exposes thin `record*`
|
||||
helpers so the consuming feature modules never need to import the
|
||||
raw OTel API.
|
||||
|
||||
When no `MeterProvider` is registered (the default in unit tests and
|
||||
production code that has not opted into telemetry), the OTel API
|
||||
returns a no-op meter and the helpers become no-ops. This makes the
|
||||
instruments safe to import unconditionally — the package never
|
||||
forces a runtime cost on consumers who do not want telemetry.
|
||||
|
||||
## Features
|
||||
|
||||
- Three counters: parse outcomes, rewrite outcomes (tagged with
|
||||
operation), sign mode.
|
||||
- One histogram: rewrite duration in milliseconds.
|
||||
- Helpers (`recordParseOutcome`, `recordRewriteOutcome`,
|
||||
`recordSignMode`, `recordRewriteDurationMs`) so feature code stays
|
||||
free of OTel boilerplate.
|
||||
- Discriminated tag literals (`ParseOutcome`, `RewriteOutcome`,
|
||||
`SignMode`) for compile-time safety on tag values.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { recordParseOutcome, recordSignMode } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
recordParseOutcome("ok");
|
||||
recordSignMode("signed");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
import { rewriteDurationHistogram } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
// Wire a real MeterProvider before importing the rest of the package
|
||||
// so the histogram is bound to an active exporter.
|
||||
const provider = new MeterProvider();
|
||||
metrics.setGlobalMeterProvider(provider);
|
||||
|
||||
// rewriteDurationHistogram now records into the live provider.
|
||||
expect(rewriteDurationHistogram).toBeDefined();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Counters
|
||||
|
||||
- `parseTotalCounter` — name `caddy_imgproxy_parse_total`, tagged
|
||||
with `outcome` ∈ `ok | invalid | missing_dims`.
|
||||
- `rewriteTotalCounter` — `caddy_imgproxy_rewrite_total`, tagged
|
||||
with `outcome` ∈ `rewritten | passthrough | invalid` and
|
||||
`operation`.
|
||||
- `signTotalCounter` — `caddy_imgproxy_sign_total`, tagged with
|
||||
`mode` ∈ `signed | unsafe`.
|
||||
|
||||
### Histogram
|
||||
|
||||
- `rewriteDurationHistogram` — `caddy_imgproxy_rewrite_duration_ms`,
|
||||
unit `ms`.
|
||||
|
||||
### Helpers
|
||||
|
||||
- `recordParseOutcome(outcome)`
|
||||
- `recordRewriteOutcome(outcome, operation)`
|
||||
- `recordSignMode(mode)`
|
||||
- `recordRewriteDurationMs(durationMs)`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The meter is bound at module import time via `metrics.getMeter(...)`.
|
||||
When no provider is registered the OTel API returns a no-op meter,
|
||||
so the instrument constructors do not throw. Helpers funnel through
|
||||
the counters/histogram via the standard `add(value, tags)` /
|
||||
`record(value)` API; the tag object literal is constructed inline
|
||||
each call so V8 can inline it into the call site under hot loops.
|
||||
98
docs/media/image/caddy-imgproxy/src/types/types.md
Normal file
98
docs/media/image/caddy-imgproxy/src/types/types.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `types` module declares every cross-feature type that flows
|
||||
between the package's runtime modules: `ParsedImgproxyUrl` (the
|
||||
output of the parser), `ImgproxyConfig` (the public configuration
|
||||
shape), `ImgproxyDimensions` (the resolved pixel pair), and the
|
||||
discriminated `RewriteResult` union that `ImgproxyHandler.rewrite`
|
||||
returns. Centralising them here keeps the package's contract in one
|
||||
place and prevents circular imports between feature modules.
|
||||
|
||||
These types are runtime-free — `types.ts` has no executable code and
|
||||
is excluded from coverage by the vitest config. Consumers that only
|
||||
want the type contract can import from `@suaveplan/caddy-imgproxy/types`
|
||||
without pulling in any runtime dependency.
|
||||
|
||||
## Features
|
||||
|
||||
- Single source of truth for the package's data contracts.
|
||||
- Discriminated union (`RewriteResult`) for branchless handling of
|
||||
the three rewrite outcomes.
|
||||
- Frozen-by-convention return values throughout the package — the
|
||||
types use `readonly` modifiers so accidental mutation is a
|
||||
compile-time error.
|
||||
- Closed unions (`ImgproxyOperation`, `GravityCode`, `HintType`)
|
||||
give the rest of the package exhaustive `switch` coverage.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type { ImgproxyConfig, ParsedImgproxyUrl } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
function describe(parsed: ParsedImgproxyUrl, config: ImgproxyConfig): string {
|
||||
return `${parsed.operation || "passthrough"} for ${config.s3Bucket}`;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type { RewriteResult } from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
function summarise(result: RewriteResult): string {
|
||||
if (result.rewritten) return `→ ${result.targetPath}`;
|
||||
if (result.reason === "passthrough") return "passthrough";
|
||||
return `invalid (${result.errorCode}): ${result.errorMessage}`;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ImgproxyOperation`
|
||||
|
||||
Union of `""|cover|crop|fit|stretch|resize`.
|
||||
|
||||
### `GravityCode`
|
||||
|
||||
Union of `t|b|l|r|c|tl|tr|bl|br`.
|
||||
|
||||
### `HintType`
|
||||
|
||||
Union of `""|w|h`.
|
||||
|
||||
### `ParsedImgproxyUrl`
|
||||
|
||||
Frozen, immutable parser output. Carries `signed`, `operation`,
|
||||
`gravity`, `aspectW`, `aspectH`, `pixelW`, `pixelH`, `hintType`,
|
||||
`hintValue`, `quality`, `path`, `format`, `isAspect`, `hasDims`.
|
||||
|
||||
### `ImgproxyConfig`
|
||||
|
||||
Validated configuration carrying `upstream`, `s3Bucket`,
|
||||
`defaultWidth`, and optional `key` / `salt`.
|
||||
|
||||
### `ImgproxyDimensions`
|
||||
|
||||
`{ width: number; height: number }` — output of
|
||||
`calculateDimensions`.
|
||||
|
||||
### `RewriteResult`
|
||||
|
||||
Discriminated union of `RewriteRewrittenResult`,
|
||||
`RewritePassthroughResult`, and `RewriteInvalidResult`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Every field uses `readonly` to communicate immutability through the
|
||||
type. The discriminated `RewriteResult` shape is deliberately tagged
|
||||
twice (`rewritten` for the boolean discriminator and `reason` for
|
||||
the failure sub-discriminator) so consumers can branch on either
|
||||
side without losing TypeScript narrowing. The optional `key` / `salt`
|
||||
on `ImgproxyConfig` carry an explicit `| undefined` because the
|
||||
package is built under `exactOptionalPropertyTypes`.
|
||||
@@ -0,0 +1,92 @@
|
||||
# buildImgproxyUrl
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`buildImgproxyUrl` composes the final imgproxy URL path from a parsed
|
||||
grammar representation. It is the orchestrator that ties together
|
||||
`calculateDimensions`, `operationToImgproxy`, and `signImgproxyPath`
|
||||
into the single string a downstream proxy or CDN edge needs to
|
||||
forward an image-rewrite request to imgproxy.
|
||||
|
||||
The function mirrors the upstream Go `buildImgproxyURL` semantics in
|
||||
every detail: processing string first, optional `q:<quality>` next,
|
||||
then the `plain/s3://<bucket>/<domain>/<path>@<format>` payload, and
|
||||
finally either a `/<base64url-signature>` prefix (when the config
|
||||
carries both a key and a salt) or `/unsafe`. Every call records a
|
||||
single `caddy_imgproxy_sign_total` sample tagged with the mode.
|
||||
|
||||
## Features
|
||||
|
||||
- Single source of truth for the URL composition order.
|
||||
- Telemetry on every call (sign-mode counter).
|
||||
- Honours the empty-string fallbacks from `operationToImgproxy` —
|
||||
passthrough URLs produce a clean `/unsafe/plain/...` shape.
|
||||
- Supports the injectable signer hash from `signImgproxyPath`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildImgproxyUrl,
|
||||
parseConfig,
|
||||
parseImgproxyUrl,
|
||||
} from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const parsed = parseImgproxyUrl("/uv16:9/avatar.png");
|
||||
const config = parseConfig({ defaultWidth: 800 });
|
||||
|
||||
buildImgproxyUrl({ parsed, domain: "example.com", config });
|
||||
// → "/unsafe/g:ce/rs:fill:800:450/plain/s3://images/example.com/avatar@png"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildImgproxyUrl,
|
||||
parseConfig,
|
||||
parseImgproxyUrl,
|
||||
} from "@suaveplan/caddy-imgproxy";
|
||||
|
||||
const config = parseConfig({
|
||||
s3Bucket: "media",
|
||||
defaultWidth: 1200,
|
||||
key: "aa11bbcc",
|
||||
salt: "deadbeef",
|
||||
});
|
||||
|
||||
// Quality + width hint + signed
|
||||
const parsed = parseImgproxyUrl("/uv16:9q90w1920/photo.png");
|
||||
const url = buildImgproxyUrl({ parsed, domain: "example.com", config });
|
||||
// → "/<signature>/g:ce/rs:fill:1920:1080/q:90/plain/s3://media/example.com/photo@png"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `buildImgproxyUrl(options: BuildImgproxyUrlOptions): string`
|
||||
|
||||
- **Parameters:**
|
||||
- `options.parsed` — output of `parseImgproxyUrl`.
|
||||
- `options.domain` — virtual-host segment for the S3 path.
|
||||
- `options.config` — validated `ImgproxyConfig`.
|
||||
- `options.hash` — optional signer hash override.
|
||||
- **Returns:** the full imgproxy path including the signature prefix.
|
||||
|
||||
### `BuildImgproxyUrlOptions`
|
||||
|
||||
Inputs interface — see above.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Signature mode is selected only when BOTH `config.key` and
|
||||
`config.salt` are non-empty strings. An empty string in either slot
|
||||
falls back to `/unsafe` so callers can disable signing at runtime
|
||||
without re-parsing the config. The `caddy_imgproxy_sign_total`
|
||||
counter is incremented exactly once per call — either as `signed` or
|
||||
as `unsafe` — even when the upstream signer throws (the throw
|
||||
propagates after the counter increments via the signer's own error
|
||||
path, since `signImgproxyPath` raises before this function records).
|
||||
29
docs/media/image/colour-convert-adapt/README.md
Normal file
29
docs/media/image/colour-convert-adapt/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# @suaveplan/colour-convert-adapt - Bradford chromatic adaptation between XYZ illuminants for ICC-style colour management
|
||||
|
||||
Bradford chromatic adaptation for XYZ colour coordinates. Chromatic adaptation adjusts a colour's appearance so that it looks the same under different light sources (illuminants). The Bradford transform is the industry-standard method, used in ICC colour management and many colour science toolkits.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [adapt](./src/adapt/adapt.md) — `adapt.ts` provides Bradford chromatic adaptation for XYZ colour coordinates. Chromatic adaptation adjusts a colour's a…
|
||||
- [adapt](./src/adapt/adapt.md) — `adapt.ts` provides Bradford chromatic adaptation for XYZ colour coordinates. Chromatic adaptation adjusts a colour's a…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colour-convert-adapt
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { bradfordAdapt } from "@suaveplan/colour-convert-adapt";
|
||||
|
||||
const d50 = bradfordAdapt({ x: 50, y: 50, z: 50 }, "D65", "D50");
|
||||
console.log(d50); // { x: ~52.9, y: ~50, z: ~37.7 }
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/adapt/adapt.md`](./src/adapt/adapt.md) — Bradford chromatic adaptation implementation and API reference
|
||||
48
docs/media/image/colour-convert-adapt/src/adapt/adapt.md
Normal file
48
docs/media/image/colour-convert-adapt/src/adapt/adapt.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# adapt
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`adapt.ts` provides Bradford chromatic adaptation for XYZ colour coordinates. Chromatic adaptation adjusts a colour's appearance so that it looks the same under different light sources (illuminants). The Bradford transform is the industry-standard method for this, used in ICC colour management and many colour science toolkits. When a photograph is taken under tungsten lighting but viewed under daylight, chromatic adaptation predicts how the colours should be shifted to preserve their perceived appearance.
|
||||
|
||||
## Features
|
||||
|
||||
- 11 built-in standard illuminants (A, B, C, D50/55/65/75, E, F2/F7/F11)
|
||||
- Custom XYZ illuminant support for specialised workflows
|
||||
- Default destination is D65, the most common reference white
|
||||
- Full XYZ round-trip fidelity when source and destination match
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { bradfordAdapt } from "@suaveplan/colour-convert-adapt";
|
||||
|
||||
const d50 = bradfordAdapt({ x: 50, y: 50, z: 50 }, "D65", "D50");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { bradfordAdapt } from "@suaveplan/colour-convert-adapt";
|
||||
|
||||
function matchIlluminant(colours: XYZ[], from: string, to: string) {
|
||||
return colours.map((c) => bradfordAdapt(c, from, to));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `bradfordAdapt(xyz: XYZ, source: string | XYZ, dest?: string | XYZ): XYZ`
|
||||
|
||||
Adapts an XYZ colour from one illuminant to another using the Bradford chromatic adaptation transform. The `source` and `dest` parameters may be a named illuminant string or a custom XYZ white point. Defaults to D65 when `dest` is omitted.
|
||||
|
||||
### `ILLUMINANTS: Record<string, XYZ>`
|
||||
|
||||
Map of standard CIE illuminant white points used by the Bradford transform.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Bradford adaptation uses a cone-response space (LMS) via the Bradford matrix and its inverse. The XYZ colour is first converted to LMS, scaled by the ratio of destination to source white point in LMS space, then converted back to XYZ. This preserves colour appearance under changing lighting conditions. The matrix coefficients are from the ICC specification. Small floating-point differences may occur in identity transformations due to the finite precision of the matrix coefficients, but these are typically on the order of 0.0001% and negligible for practical applications.
|
||||
32
docs/media/image/colour-convert-cieluv/README.md
Normal file
32
docs/media/image/colour-convert-cieluv/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# @suaveplan/colour-convert-cieluv - CIELUV, xyY, LMS, and Bradford chromatic adaptation for professional colour workflows
|
||||
|
||||
CIELUV, xyY, LMS, and Bradford chromatic adaptation conversions. This plugin provides colour-space transforms between RGB, CIELUV, xyY, and LMS cone-response space, including Bradford adaptation across 11 standard illuminants (A, B, C, D50/55/65/75, E, F2/F7/F11).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [colour-convert-cieluv](./src/colour-convert-cieluv/colour-convert-cieluv.md) — `colour-convert-cieluv.ts` extends the CIE colour family with CIELUV, xyY chromaticity, LMS cone response, and Bradford…
|
||||
- [colour-convert-cieluv](./src/colour-convert-cieluv/colour-convert-cieluv.md) — `colour-convert-cieluv.ts` extends the CIE colour family with CIELUV, xyY chromaticity, LMS cone response, and Bradford…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colour-convert-cieluv
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { rgbToLuv, bradfordAdapt } from "@suaveplan/colour-convert-cieluv";
|
||||
|
||||
const luv = rgbToLuv({ r: 128, g: 64, b: 32 });
|
||||
console.log(luv); // { l: ~32.9, u: ~40.6, v: ~37.8 }
|
||||
|
||||
const d50 = bradfordAdapt({ x: 50, y: 50, z: 50 }, "D65", "D50");
|
||||
console.log(d50); // { x: ~52.9, y: ~50, z: ~37.7 }
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/colour-convert-cieluv/colour-convert-cieluv.md`](./src/colour-convert-cieluv/colour-convert-cieluv.md) — CIELUV, xyY, LMS, and Bradford adaptation implementation
|
||||
@@ -0,0 +1,67 @@
|
||||
# colour-convert-cieluv
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`colour-convert-cieluv.ts` extends the CIE colour family with CIELUV, xyY chromaticity, LMS cone response, and Bradford chromatic adaptation. These spaces are essential for professional colour workflows including display calibration, print proofing, and cross-media colour reproduction.
|
||||
|
||||
## Features
|
||||
|
||||
- RGB ↔ CIELUV conversions
|
||||
- XYZ ↔ xyY conversions
|
||||
- XYZ ↔ LMS cone response conversions
|
||||
- Bradford chromatic adaptation with CIE standard illuminants
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { rgbToLuv, luvToRgb } from "@suaveplan/colour-convert-cieluv";
|
||||
|
||||
const luv = rgbToLuv({ r: 128, g: 64, b: 32 });
|
||||
const back = luvToRgb(luv);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { bradfordAdapt, ILLUMINANTS } from "@suaveplan/colour-convert-cieluv";
|
||||
|
||||
const d50 = bradfordAdapt(xyz, "D65", "D50");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### CIELUV
|
||||
|
||||
- `xyzToLuv(xyz: XYZ): LUV`
|
||||
- `luvToXyz(luv: LUV): XYZ`
|
||||
- `rgbToLuv(rgb: RGB): LUV`
|
||||
- `luvToRgb(luv: LUV): RGBA`
|
||||
|
||||
### xyY
|
||||
|
||||
- `xyzToxyY(xyz: XYZ): xyY`
|
||||
- `xyYToXyz(xy: xyY): XYZ`
|
||||
- `rgbToxyY(rgb: RGB): xyY`
|
||||
- `xyYToRgb(xy: xyY): RGBA`
|
||||
|
||||
### LMS
|
||||
|
||||
- `xyzToLms(xyz: XYZ): LMS`
|
||||
- `lmsToXyz(lms: LMS): XYZ`
|
||||
- `rgbToLms(rgb: RGB): LMS`
|
||||
- `lmsToRgb(lms: LMS): RGBA`
|
||||
|
||||
### Bradford Adaptation
|
||||
|
||||
- `bradfordAdapt(xyz: XYZ, source: string | XYZ, dest: string | XYZ): XYZ`
|
||||
- `ILLUMINANTS` — map of CIE standard illuminants
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Bradford adaptation uses the standard CIECAM02 matrix and its inverse. The implementation accepts either named illuminant strings (A, B, C, D50, D55, D65, D75, E, F2, F7, F11) or raw XYZ values for custom white points. All conversion pipelines are deterministic and side-effect-free.
|
||||
|
||||
The CIELUV implementation follows the standard CIE 1976 formula with D65 as the default reference white. Internal helper functions handle the low-luminance linear branch automatically, ensuring accurate results even for very dark colours. The module has no runtime dependencies beyond the base `@suaveplan/colours` types.
|
||||
32
docs/media/image/colour-convert-cmyk/README.md
Normal file
32
docs/media/image/colour-convert-cmyk/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# @suaveplan/colour-convert-cmyk - Bidirectional RGB <-> CMYK colour space conversion with black generation for print workflows
|
||||
|
||||
Bidirectional conversion between RGB and CMYK colour spaces using the standard Euclidean algorithm with black generation. CMYK is the subtractive model used in professional printing; these conversions are essential for print preview, pre-press workflows, and bridging screen colours with physical output.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [cmyk](./src/cmyk/cmyk.md) — `cmyk.ts` provides bidirectional conversion between RGB and CMYK colour spaces. CMYK is the subtractive model used in p…
|
||||
- [cmyk](./src/cmyk/cmyk.md) — `cmyk.ts` provides bidirectional conversion between RGB and CMYK colour spaces. CMYK is the subtractive model used in p…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colour-convert-cmyk
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { rgbToCmyk, cmykToRgb } from "@suaveplan/colour-convert-cmyk";
|
||||
|
||||
const ink = rgbToCmyk({ r: 59, g: 130, b: 246 });
|
||||
console.log(ink); // { c: 76, m: 47, y: 0, k: 4 }
|
||||
|
||||
const screen = cmykToRgb(ink);
|
||||
console.log(screen); // { r: 59, g: 130, b: 246, a: 1 }
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/cmyk/cmyk.md`](./src/cmyk/cmyk.md) — RGB↔CMYK conversion implementation and API reference
|
||||
47
docs/media/image/colour-convert-cmyk/src/cmyk/cmyk.md
Normal file
47
docs/media/image/colour-convert-cmyk/src/cmyk/cmyk.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# cmyk
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`cmyk.ts` provides bidirectional conversion between RGB and CMYK colour spaces. CMYK is the subtractive model used in professional printing, where colours are created by mixing cyan, magenta, yellow, and black inks. These conversions are essential for print preview, pre-press workflows, and any application that needs to bridge screen colours with physical output. While RGB is additive (light-emitting), CMYK is subtractive (light-absorbing), making direct translation necessary for accurate colour reproduction across media.
|
||||
|
||||
## Features
|
||||
|
||||
- RGB to CMYK conversion with black generation
|
||||
- CMYK to RGB conversion
|
||||
- Percentage-based CMYK output (0–100)
|
||||
- Handles edge cases like pure black and pure white
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { rgbToCmyk } from "@suaveplan/colour-convert-cmyk";
|
||||
|
||||
const cmyk = rgbToCmyk({ r: 128, g: 64, b: 32 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { rgbToCmyk, cmykToRgb } from "@suaveplan/colour-convert-cmyk";
|
||||
|
||||
const ink = rgbToCmyk({ r: 59, g: 130, b: 246 });
|
||||
const screen = cmykToRgb(ink);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `rgbToCmyk(rgb: RGB): CMYK`
|
||||
|
||||
Converts an RGB colour to CMYK percentages. The black channel (`k`) is generated from the maximum of the inverted RGB channels, and the cyan, magenta, and yellow channels are computed relative to this black. This is the standard Euclidean algorithm used in most graphics software.
|
||||
|
||||
### `cmykToRgb(cmyk: CMYK): RGBA`
|
||||
|
||||
Converts CMYK percentages back to RGB. The formula applies the subtractive model in reverse: each RGB channel is the product of `(1 - ink) × (1 - black)`. The alpha channel is always set to 1 because CMYK has no native alpha channel.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The conversion uses the standard Euclidean algorithm for RGB to CMYK. When the input colour is pure black (`k = 100%`), the cyan, magenta, and yellow channels are defined as zero to avoid division by zero. All CMYK values are rounded to the nearest integer percentage. The inverse conversion always produces alpha 1 because CMYK has no native alpha channel. Round-trip conversion may introduce small quantization errors due to the integer rounding of CMYK percentages.
|
||||
32
docs/media/image/colour-convert-hsluv/README.md
Normal file
32
docs/media/image/colour-convert-hsluv/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# @suaveplan/colour-convert-hsluv - HSLuv perceptually uniform colour space: conversions and harmony generators
|
||||
|
||||
HSLuv perceptually-uniform colour space with gamut-mapped ray intersection for sRGB. This plugin provides bidirectional RGB↔HSLuv/HPLuv conversions and uniform colour harmonies (complementary, triad, tetrad) that preserve perceptual lightness across hue rotations.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [colour-convert-hsluv](./src/colour-convert-hsluv/colour-convert-hsluv.md) — `colour-convert-hsluv.ts` implements the HSLuv perceptually uniform colour space. Unlike HSL, which stretches and compr…
|
||||
- [colour-convert-hsluv](./src/colour-convert-hsluv/colour-convert-hsluv.md) — `colour-convert-hsluv.ts` implements the HSLuv perceptually uniform colour space. Unlike HSL, which stretches and compr…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colour-convert-hsluv
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { rgbToHsluv, hsluvToRgb, uniformTriad } from "@suaveplan/colour-convert-hsluv";
|
||||
|
||||
const hsluv = rgbToHsluv({ r: 255, g: 0, b: 0 });
|
||||
console.log(hsluv); // { h: ~12.2, s: ~100, l: ~53.2 }
|
||||
|
||||
const colours = uniformTriad({ r: 59, g: 130, b: 246 });
|
||||
console.log(colours); // [primary, secondary, tertiary]
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/colour-convert-hsluv/colour-convert-hsluv.md`](./src/colour-convert-hsluv/colour-convert-hsluv.md) — HSLuv/HPLuv conversion and uniform harmonies implementation
|
||||
@@ -0,0 +1,53 @@
|
||||
# colour-convert-hsluv
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`colour-convert-hsluv.ts` implements the HSLuv perceptually uniform colour space. Unlike HSL, which stretches and compresses hue angles unevenly across lightness levels, HSLuv guarantees that a given saturation percentage produces the same visual intensity regardless of hue. This makes it ideal for generating harmonious colour palettes and accessible UI themes.
|
||||
|
||||
## Features
|
||||
|
||||
- RGB ↔ HSLuv bidirectional conversion with gamut mapping
|
||||
- RGB ↔ HPLuv (pastel variant) bidirectional conversion
|
||||
- Uniform colour harmonies: complementary, triad, and tetrad
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { rgbToHsluv, hsluvToRgb } from "@suaveplan/colour-convert-hsluv";
|
||||
|
||||
const hsluv = rgbToHsluv({ r: 255, g: 0, b: 0 });
|
||||
const back = hsluvToRgb(hsluv);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { uniformTriad } from "@suaveplan/colour-convert-hsluv";
|
||||
|
||||
const colours = uniformTriad({ r: 59, g: 130, b: 246 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Conversions
|
||||
|
||||
- `rgbToHsluv(rgb: RGB): HSLUV`
|
||||
- `hsluvToRgb(hsluv: HSLUV): RGBA`
|
||||
- `rgbToHpluv(rgb: RGB): HPLUV`
|
||||
- `hpluvToRgb(hpluv: HPLUV): RGBA`
|
||||
|
||||
### Harmonies
|
||||
|
||||
- `uniformComplementary(rgb: RGB): RGB[]`
|
||||
- `uniformTriad(rgb: RGB): RGB[]`
|
||||
- `uniformTetrad(rgb: RGB): RGB[]`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The HSLuv algorithm computes the maximum chroma for a given hue by intersecting rays from the white point with the sRGB gamut boundary in CIELUV space. Six rays are tested (one per RGB cube edge) and the minimum positive intersection distance becomes the saturation scale factor. This ensures that 100% saturation always touches the gamut boundary, regardless of hue or lightness.
|
||||
|
||||
All conversion pipelines are deterministic and free of side effects. The module has no runtime dependencies beyond the base `@suaveplan/colours` types, making it suitable for both server-side rendering and client-side bundling. Gamut mapping is performed automatically during RGB to HSLuv conversion, so out-of-range inputs are handled gracefully without throwing exceptions.
|
||||
32
docs/media/image/colour-filter-cct/README.md
Normal file
32
docs/media/image/colour-filter-cct/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# @suaveplan/colour-filter-cct - Correlated Colour Temperature (CCT) calculation and approximation for lighting and display calibration
|
||||
|
||||
Correlated Colour Temperature (CCT) conversions using the McCamy formula and black-body RGB approximation. Convert any RGB colour to its approximate colour temperature in Kelvin, or generate an RGB colour from a given temperature. Useful for white-balance correction, lighting design, and temperature-based theming.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [colour-filter-cct](./src/colour-filter-cct/colour-filter-cct.md) — `colour-filter-cct.ts` provides Correlated Colour Temperature (CCT) calculation and approximation. CCT describes the co…
|
||||
- [colour-filter-cct](./src/colour-filter-cct/colour-filter-cct.md) — `colour-filter-cct.ts` provides Correlated Colour Temperature (CCT) calculation and approximation. CCT describes the co…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colour-filter-cct
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { cctFromRgb, cctToRgb } from "@suaveplan/colour-filter-cct";
|
||||
|
||||
const kelvin = cctFromRgb({ r: 255, g: 240, b: 220 });
|
||||
console.log(kelvin); // ~6500
|
||||
|
||||
const warmWhite = cctToRgb(2700);
|
||||
console.log(warmWhite); // { r: ~255, g: ~169, b: ~87, a: 1 }
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/colour-filter-cct/colour-filter-cct.md`](./src/colour-filter-cct/colour-filter-cct.md) — CCT conversion implementation and API reference
|
||||
@@ -0,0 +1,52 @@
|
||||
# colour-filter-cct
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`colour-filter-cct.ts` provides Correlated Colour Temperature (CCT) calculation and approximation. CCT describes the colour appearance of a light source by comparing its chromaticity to that of an ideal black-body radiator. It is widely used in lighting design, photography, and display calibration.
|
||||
|
||||
## Features
|
||||
|
||||
- CCT from RGB via McCamy's formula
|
||||
- RGB approximation from CCT via black-body curves
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { cctFromRgb, cctToRgb } from "@suaveplan/colour-filter-cct";
|
||||
|
||||
const kelvin = cctFromRgb({ r: 255, g: 240, b: 220 });
|
||||
const rgb = cctToRgb(6500);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { cctFromRgb } from "@suaveplan/colour-filter-cct";
|
||||
|
||||
function classifyWhiteBalance(rgb: RGB): string {
|
||||
const cct = cctFromRgb(rgb);
|
||||
if (cct < 3300) return "warm";
|
||||
if (cct < 5300) return "neutral";
|
||||
return "cool";
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `cctFromRgb(rgb: RGB): number`
|
||||
|
||||
Returns the correlated colour temperature in Kelvin. Uses McCamy's formula based on CIE 1931 chromaticity coordinates derived from the input RGB.
|
||||
|
||||
### `cctToRgb(kelvin: number): RGBA`
|
||||
|
||||
Approximates an RGB colour for a given CCT in Kelvin. Input is clamped to the valid range of 1000K–40000K. Uses the standard black-body polynomial approximation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
McCamy's formula is accurate for CCT values between 2000K and 10000K. Outside this range the approximation becomes less reliable, though the function still returns a value. The inverse function (`cctToRgb`) uses the same polynomial coefficients as `@suaveplan/colours`' `fromTemperature` for consistency.
|
||||
|
||||
The implementation clamps input CCT values to the valid range of 1000K–40000K to prevent numerical instability. At the low end of this range the blue channel is zero because the black-body curve produces negligible short-wavelength energy. All conversions are deterministic and have no external runtime dependencies.
|
||||
29
docs/media/image/colour-filter-cmc/README.md
Normal file
29
docs/media/image/colour-filter-cmc/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# @suaveplan/colour-filter-cmc - CMC l:c Delta-E colour difference metric for textile and print quality control
|
||||
|
||||
CMC l:c Delta-E colour difference metric with hue-dependent chroma scaling and low-luminance branch handling. This plugin computes perceptual distance between two colours using the Colour Measurement Committee's l:c formula, widely used in textile and industrial colour matching where human-perceptible differences matter more than Euclidean distance.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [colour-filter-cmc](./src/colour-filter-cmc/colour-filter-cmc.md) — `colour-filter-cmc.ts` implements the CMC l:c Delta-E colour difference metric. Unlike Euclidean distance or simple Del…
|
||||
- [colour-filter-cmc](./src/colour-filter-cmc/colour-filter-cmc.md) — `colour-filter-cmc.ts` implements the CMC l:c Delta-E colour difference metric. Unlike Euclidean distance or simple Del…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colour-filter-cmc
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { cmcDistance } from "@suaveplan/colour-filter-cmc";
|
||||
|
||||
const d = cmcDistance({ r: 255, g: 0, b: 0 }, { r: 0, g: 0, b: 255 });
|
||||
console.log(d); // perceptual distance between red and blue
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/colour-filter-cmc/colour-filter-cmc.md`](./src/colour-filter-cmc/colour-filter-cmc.md) — CMC l:c Delta-E implementation and API reference
|
||||
@@ -0,0 +1,43 @@
|
||||
# colour-filter-cmc
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`colour-filter-cmc.ts` implements the CMC l:c Delta-E colour difference metric. Unlike Euclidean distance or simple Delta-E 76, CMC accounts for the non-uniformity of CIELAB space by weighting luminance and chroma differences separately. It is the industry standard for textile and print quality control.
|
||||
|
||||
## Features
|
||||
|
||||
- CMC l:c Delta-E calculation between two RGB colours
|
||||
- Configurable luminance and chroma weights
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { cmcDistance } from "@suaveplan/colour-filter-cmc";
|
||||
|
||||
const d = cmcDistance({ r: 255, g: 0, b: 0 }, { r: 0, g: 0, b: 255 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { cmcDistance } from "@suaveplan/colour-filter-cmc";
|
||||
|
||||
// Stricter luminance weighting for textile applications
|
||||
const d = cmcDistance(brand, sample, 1, 1);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `cmcDistance(a: RGB, b: RGB, luminanceWeight = 2, chromaWeight = 1): number`
|
||||
|
||||
Returns the CMC l:c Delta-E distance between two colours. Lower values indicate closer matches. A value below 1.0 is generally considered a commercial match in textile applications.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The algorithm first converts both colours to CIELAB, then applies the CMC l:c formula with hue-dependent scaling factors. The `t` factor adjusts for the eye's reduced sensitivity to chroma differences in blue hues. The `f` factor compensates for the increased sensitivity to chroma differences at low chroma values.
|
||||
|
||||
All calculations use double-precision floating point arithmetic. The implementation guards against negative values under the square root that can arise from floating-point error when comparing near-identical colours. Default weights of 2:1 for luminance to chroma match the standard textile industry convention.
|
||||
29
docs/media/image/colour-filter-greyscale/README.md
Normal file
29
docs/media/image/colour-filter-greyscale/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# @suaveplan/colour-filter-greyscale - Perceptual RGB-to-greyscale filter using the ITU-R BT.709 luminance coefficients
|
||||
|
||||
Converts any RGB colour to its perceptual greyscale equivalent using the ITU-R BT.709 luminance coefficients. Unlike simple channel averaging, this formula weights green most heavily to match human visual sensitivity, producing greyscale results that look correctly balanced to observers.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [greyscale](./src/greyscale/greyscale.md) — `greyscale.ts` converts any RGB colour to its perceptual greyscale equivalent using the ITU-R BT.709 luminance coeffici…
|
||||
- [greyscale](./src/greyscale/greyscale.md) — `greyscale.ts` converts any RGB colour to its perceptual greyscale equivalent using the ITU-R BT.709 luminance coeffici…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colour-filter-greyscale
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { greyscale } from "@suaveplan/colour-filter-greyscale";
|
||||
|
||||
const mono = greyscale({ r: 255, g: 128, b: 0 });
|
||||
console.log(mono); // { r: 150, g: 150, b: 150, a: 1 }
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/greyscale/greyscale.md`](./src/greyscale/greyscale.md) — ITU-R BT.709 luminance greyscale implementation and API reference
|
||||
@@ -0,0 +1,44 @@
|
||||
# greyscale
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`greyscale.ts` converts any RGB colour to its perceptual greyscale equivalent using the ITU-R BT.709 luminance coefficients. Unlike simple averaging of channels, this formula weights green most heavily because human vision is most sensitive to mid-wavelength light. This approach produces greyscale results that appear correctly balanced to human observers rather than mathematically averaged values that can look too dark or too bright.
|
||||
|
||||
## Features
|
||||
|
||||
- ITU-R BT.709 luminance-based greyscale conversion
|
||||
- Deterministic single-channel output
|
||||
- Alpha channel preserved at 1
|
||||
- Matches standard video and image processing conventions
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { greyscale } from "@suaveplan/colour-filter-greyscale";
|
||||
|
||||
const mono = greyscale({ r: 255, g: 128, b: 0 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { greyscale } from "@suaveplan/colour-filter-greyscale";
|
||||
|
||||
function desaturatePalette(colours: RGB[]) {
|
||||
return colours.map((c) => greyscale(c));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `greyscale(rgb: RGB): RGBA`
|
||||
|
||||
Returns a greyscale RGBA colour where all three channels are set to the perceptual luminance of the input. The alpha channel is always 1. The luminance is computed using the ITU-R BT.709 standard coefficients and rounded to the nearest integer.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The luminance formula `Y = 0.2126·R + 0.7152·G + 0.0722·B` is the standard recommendation from ITU-R BT.709 for high-definition video and is widely used in image processing. The coefficients sum to 1.0, ensuring that white input maps exactly to white output and black maps to black. Green receives the highest weight because the human eye has peak sensitivity in the green portion of the spectrum. Blue receives the lowest weight because human vision is least sensitive to short-wavelength light. This formula is also used in JPEG compression, HDTV signal processing, and many CSS `filter: grayscale()` implementations.
|
||||
29
docs/media/image/colour-filter-sepia/README.md
Normal file
29
docs/media/image/colour-filter-sepia/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# @suaveplan/colour-filter-sepia - Photographic sepia tone filter for RGB colours using the W3C CSS Filters 3x3 matrix
|
||||
|
||||
Applies a photographic sepia tone filter to RGB colours using the standard W3C CSS Filters 3×3 matrix. This plugin converts any colour into a warm brownish tone that simulates the look of aged photographs, with output clamped to the valid 8-bit sRGB range.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [sepia](./src/sepia/sepia.md) — `sepia.ts` applies a photographic sepia tone filter to RGB colours. Sepia simulates the warm brownish tone of old photo…
|
||||
- [sepia](./src/sepia/sepia.md) — `sepia.ts` applies a photographic sepia tone filter to RGB colours. Sepia simulates the warm brownish tone of old photo…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colour-filter-sepia
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { sepia } from "@suaveplan/colour-filter-sepia";
|
||||
|
||||
const vintage = sepia({ r: 255, g: 0, b: 0 });
|
||||
console.log(vintage); // { r: 100, g: 89, b: 69, a: 1 }
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/sepia/sepia.md`](./src/sepia/sepia.md) — Sepia matrix filter implementation and API reference
|
||||
49
docs/media/image/colour-filter-sepia/src/sepia/sepia.md
Normal file
49
docs/media/image/colour-filter-sepia/src/sepia/sepia.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# sepia
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`sepia.ts` applies a photographic sepia tone filter to RGB colours. Sepia simulates the warm brownish tone of old photographs by re-weighting the red, green, and blue channels through a fixed 3×3 matrix transformation.
|
||||
|
||||
## Features
|
||||
|
||||
- Deterministic sepia matrix applied to any RGB colour
|
||||
- Output clamped to valid 8-bit range
|
||||
- Zero runtime dependencies beyond base colour types
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sepia } from "@suaveplan/colour-filter-sepia";
|
||||
|
||||
const brown = sepia({ r: 255, g: 0, b: 0 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sepia } from "@suaveplan/colour-filter-sepia";
|
||||
import { Colour } from "@suaveplan/colours/colour";
|
||||
|
||||
function vintageTheme(base: RGB) {
|
||||
return {
|
||||
primary: sepia(base),
|
||||
secondary: sepia(new Colour(base).darken(50).toRgb()),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `sepia(rgb: RGB): RGBA`
|
||||
|
||||
Applies the standard sepia matrix to an RGB colour and returns the result with alpha set to 1. The matrix coefficients follow the W3C CSS Filters specification.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The sepia matrix is a fixed linear transform. No gamma correction or perceptual scaling is applied beyond the matrix coefficients themselves. Black and white are mapped to slightly different values due to the matrix weights summing to more than 1.0 for some channels. For example, white input produces an output of approximately (255, 255, 239) because the blue channel weight sum is less than 1.0. Saturated colours shift toward warm brown tones, while neutral greys take on a subtle warm cast.
|
||||
|
||||
The matrix coefficients are the standard values from the W3C CSS Filters specification: `[[0.393, 0.769, 0.189], [0.349, 0.686, 0.168], [0.272, 0.534, 0.131]]`. These weights were chosen to simulate the chemical process of sepia toning in traditional photography, where silver halide crystals are replaced with silver sulphide, shifting the spectral response toward longer wavelengths. The output channels are individually clamped to the 0–255 range after rounding to prevent overflow from the matrix multiplication.
|
||||
65
docs/media/image/colours/README.md
Normal file
65
docs/media/image/colours/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# @suaveplan/colours - Universal colour manipulation: parsing, conversion, analysis, mixing, blending, and palette generation
|
||||
|
||||
Universal colour manipulation library: parsing, conversion, analysis, mixing, blending, and palette generation. Parses hex (3/4/6/8 digit), CSS `rgb()`, `hsl()`, `lab()`, `lch()`, `oklab()`, `oklch()`, named colours, and plain objects. Converts between RGB, HSL, HSV, LAB, LCH, OKLAB, OKLCH, XYZ, and CMYK. Supports manipulation (lighten, darken, saturate, desaturate, rotate, alpha, fade, opaquer, negate, grayscale, mix), analysis (luminance, perceived brightness, contrast ratio, distance), and factories (random colours, temperature-based black-body curves, blend modes, gradient steps, harmonious palettes).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [colour](./src/colour/colour.md) — `colour.ts` implements the `Colour` class — an immutable, chainable colour value object. It wraps an internal `RGBA` st…
|
||||
- [colour-formatters](./src/colour/colour-formatters.md) — `colour-formatters.ts` provides deterministic string-formatting functions for every colour representation supported by…
|
||||
- [colour](./src/colour/colour.md) — `colour.ts` implements the `Colour` class — an immutable, chainable colour value object. It wraps an internal `RGBA` st…
|
||||
- [colour-factories](./src/colour-factories/colour-factories.md) — `colour-factories.ts` provides pure, deterministic helpers for generating colours and colour collections. It is complet…
|
||||
- [colour-factories](./src/colour-factories/colour-factories.md) — `colour-factories.ts` provides pure, deterministic helpers for generating colours and colour collections. It is complet…
|
||||
- [conversions](./src/conversions/conversions.md) — `conversions.ts` provides deterministic, side-effect-free mathematical transforms between colour spaces. Every function…
|
||||
- [conversions](./src/conversions/conversions.md) — `conversions.ts` provides deterministic, side-effect-free mathematical transforms between colour spaces. Every function…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` declares the canonical error-code registry for `@suaveplan/colours`. Every failure mode that the colou…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` declares the canonical error-code registry for `@suaveplan/colours`. Every failure mode that the colou…
|
||||
- [errors](./src/errors/errors.md) — `errors.ts` provides the typed error hierarchy for `@suaveplan/colours`. `ColourError` extends `SuaveplanError` so that…
|
||||
- [errors](./src/errors/errors.md) — `errors.ts` provides the typed error hierarchy for `@suaveplan/colours`. `ColourError` extends `SuaveplanError` so that…
|
||||
- [parser](./src/parser/parser.md) — `parser.ts` converts arbitrary colour input into a canonical `RGBA` object. It supports hex strings (3/4/6/8 digit), mo…
|
||||
- [parser](./src/parser/parser.md) — `parser.ts` converts arbitrary colour input into a canonical `RGBA` object. It supports hex strings (3/4/6/8 digit), mo…
|
||||
- [colour-types](./src/types/colour-types/colour-types.md) — `colour-types.ts` is the universal type system and Zod schema registry for `@suaveplan/colours`. It defines TypeSc…
|
||||
- [colour-types](./src/types/colour-types/colour-types.md) — `colour-types.ts` is the universal type system and Zod schema registry for `@suaveplan/colours`. It defines TypeSc…
|
||||
- [utils](./src/utils/utils.md) — `utils.ts` provides small, pure mathematical utilities used throughout `@suaveplan/colours`. Every function is determin…
|
||||
- [utils](./src/utils/utils.md) — `utils.ts` provides small, pure mathematical utilities used throughout `@suaveplan/colours`. Every function is determin…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/colours
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { Colour } from "@suaveplan/colours";
|
||||
|
||||
const c = new Colour("#ff9900");
|
||||
console.log(c.lighten(0.2).toHex()); // "#ffb34d"
|
||||
console.log(c.isDark()); // false
|
||||
console.log(c.contrast("#ffffff")); // WCAG contrast ratio
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/colour/colour.md`](./src/colour/colour.md) — `Colour` class and instance methods
|
||||
- [`src/colour/colour-formatters.md`](./src/colour/colour-formatters.md) — Output formatters (hex, rgb, hsl, etc.)
|
||||
- [`src/colour-factories/colour-factories.md`](./src/colour-factories/colour-factories.md) — Random colours, temperature, blend modes, palettes
|
||||
- [`src/conversions/conversions.md`](./src/conversions/conversions.md) — Colour-space conversion functions
|
||||
- [`src/parser/parser.md`](./src/parser/parser.md) — `parseColour` input parser
|
||||
- [`src/types/colour-types/colour-types.md`](./src/types/colour-types/colour-types.md) — TypeScript interfaces and Zod schemas
|
||||
- [`src/utils/utils.md`](./src/utils/utils.md) — Clamping and rounding utilities
|
||||
- [`src/errors/errors.md`](./src/errors/errors.md) — `ColourError` hierarchy and typed factories
|
||||
- [`src/error-codes/error-codes.md`](./src/error-codes/error-codes.md) — `COLOUR_CODES` registry
|
||||
|
||||
## Subpath Exports
|
||||
|
||||
- `@suaveplan/colours/colour` — `Colour` class
|
||||
- `@suaveplan/colours/parser` — `parseColour`
|
||||
- `@suaveplan/colours/conversions` — colour-space conversion functions
|
||||
- `@suaveplan/colours/types` — TypeScript interfaces and Zod schemas
|
||||
- `@suaveplan/colours/utils` — clamping and rounding utilities
|
||||
- `@suaveplan/colours/errors` — `ColourError` and factory functions
|
||||
- `@suaveplan/colours/error-codes` — `COLOUR_CODES` registry
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# colour-factories
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`colour-factories.ts` provides pure, deterministic helpers for generating colours and colour collections. It is completely stateless and has no side effects. All functions return either a single `Colour` instance or an array of `Colour` instances. These helpers extend the core `Colour` class with common creative and programmatic colour tasks.
|
||||
|
||||
## Features
|
||||
|
||||
- Random colour generation with optional alpha
|
||||
- Temperature-based black-body approximation (Kelvin to RGB)
|
||||
- Colour blending with standard Photoshop-style blend modes
|
||||
- Gradient step generation between two colours
|
||||
- Palette generation (analogous, monochromatic, triadic, tetradic, complementary, split-complementary)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomColour, steps } from "@suaveplan/colours/colour-factories";
|
||||
|
||||
const r = randomColour();
|
||||
const gradient = steps("#ff0000", "#0000ff", 5);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { palette, fromTemperature } from "@suaveplan/colours/colour-factories";
|
||||
|
||||
const warm = fromTemperature(3200);
|
||||
const brandPalette = palette("#3b82f6", "analogous", 5);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `randomColour(alpha?: number): Colour`
|
||||
|
||||
Returns a pseudo-random colour. If `alpha` is provided it is clamped to `[0,1]`.
|
||||
|
||||
### `fromTemperature(kelvin: number): Colour`
|
||||
|
||||
Approximates a black-body colour temperature in Kelvin. Valid range is roughly `1000K` to `40000K`. Values outside this range are clamped.
|
||||
|
||||
### `blend(bottom: ColourInput, top: ColourInput, mode: BlendMode): Colour`
|
||||
|
||||
Applies a standard blend mode to two colours and returns the result. Supported modes include `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `difference`, `exclusion`.
|
||||
|
||||
### `steps(start: ColourInput, end: ColourInput, count: number): Colour[]`
|
||||
|
||||
Generates `count` evenly spaced colours between `start` and `end` in OKLAB space for perceptual uniformity.
|
||||
|
||||
### `palette(base: ColourInput, type: PaletteType, count?: number): Colour[]`
|
||||
|
||||
Generates a harmonious palette from a base colour. `count` defaults to `5` for most types. Types:
|
||||
|
||||
- `analogous` — colours adjacent on the colour wheel
|
||||
- `monochromatic` — same hue, varying lightness
|
||||
- `triadic` — evenly spaced three-colour harmony
|
||||
- `tetradic` — four-colour rectangular harmony
|
||||
- `complementary` — opposite hue
|
||||
- `split-complementary` — opposite hue split by a small angle
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All factory functions create new `Colour` instances via the constructor; they never mutate existing instances. The `steps` function uses OKLAB interpolation because it preserves perceptual uniformity better than RGB or HSL. Temperature approximation uses the standard black-body polynomial from Tanner Helland.
|
||||
58
docs/media/image/colours/src/colour/colour-formatters.md
Normal file
58
docs/media/image/colours/src/colour/colour-formatters.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# colour-formatters
|
||||
|
||||
## Purpose
|
||||
|
||||
`colour-formatters.ts` provides deterministic string-formatting functions for every colour representation supported by `@suaveplan/colours`. Each formatter takes a canonical `RGBA` object and returns a human-readable or machine-parseable string. The module is pure, stateless, and side-effect-free.
|
||||
|
||||
## Features
|
||||
|
||||
- Hex formatters: 6-digit, 3-digit shorthand, and 8-digit with alpha
|
||||
- CSS functional notation: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hsv()`
|
||||
- CIE and OK colour spaces: `lab()`, `lch()`, `oklab()`, `oklch()`
|
||||
- Device colour: `cmyk()`
|
||||
- Named colour lookup with fallback to hex
|
||||
- Unified `formatColour` dispatcher
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { formatHex, formatRgb } from "@suaveplan/colours/colour";
|
||||
|
||||
const rgba = { r: 255, g: 128, b: 0, a: 1 };
|
||||
console.log(formatHex(rgba)); // "#ff8000"
|
||||
console.log(formatRgb(rgba)); // "rgb(255 128 0)"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { formatColour } from "@suaveplan/colours/colour";
|
||||
|
||||
function exportColour(rgba: RGBA, targetFormat: string) {
|
||||
return formatColour(rgba, targetFormat as any);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `formatHex(rgba: RGBA): string`
|
||||
- `formatHex3(rgba: RGBA): string | null`
|
||||
- `formatHex8(rgba: RGBA): string`
|
||||
- `formatRgb(rgba: RGBA): string`
|
||||
- `formatRgba(rgba: RGBA): string`
|
||||
- `formatHsl(rgba: RGBA): string`
|
||||
- `formatHsla(rgba: RGBA): string`
|
||||
- `formatHsv(rgba: RGBA): string`
|
||||
- `formatLab(rgba: RGBA): string`
|
||||
- `formatLch(rgba: RGBA): string`
|
||||
- `formatOklab(rgba: RGBA): string`
|
||||
- `formatOklch(rgba: RGBA): string`
|
||||
- `formatCmyk(rgba: RGBA): string`
|
||||
- `formatNamed(rgba: RGBA): string`
|
||||
- `formatColour(rgba: RGBA, format?: string): string`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All formatters use fixed-precision rounding appropriate to their colour space. Hex formatters pad single-digit components with leading zeros. The `formatHex3` formatter returns `null` when the colour cannot be exactly represented in three-digit shorthand. `formatNamed` iterates the `NAMED_COLOURS` map and falls back to `formatHex` for unknown colours.
|
||||
|
||||
Formatters are grouped together so that the `Colour` class can delegate all string output to a single module. This keeps the class focused on manipulation logic while the formatters handle presentation details.
|
||||
88
docs/media/image/colours/src/colour/colour.md
Normal file
88
docs/media/image/colours/src/colour/colour.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# colour
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`colour.ts` implements the `Colour` class — an immutable, chainable colour value object. It wraps an internal `RGBA` state and exposes methods for manipulation, analysis, conversion, and output formatting. The class is the primary public API surface for the package and is used by the `colour-factories` helpers.
|
||||
|
||||
## Features
|
||||
|
||||
- Immutable chainable manipulation (lighten, darken, saturate, desaturate, rotate, alpha, fade, opaquer)
|
||||
- Colour-space conversion methods (hex, RGB, HSL, HSV, LAB, LCH, OKLAB, OKLCH, CMYK)
|
||||
- Analysis methods (luminance, perceived brightness, contrast ratio, isDark, isLight, best contrast, distance)
|
||||
- Utility methods (negate, grayscale, mix, clone, equals, toString)
|
||||
- Pure functional implementation with no mutation of internal state
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Colour } from "@suaveplan/colours/colour";
|
||||
|
||||
const c = new Colour("#ff9900");
|
||||
console.log(c.lighten(0.2).toHex()); // "#ffb34d"
|
||||
console.log(c.isDark()); // false
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Colour } from "@suaveplan/colours/colour";
|
||||
|
||||
const brand = new Colour("#3b82f6");
|
||||
const text = brand.bestContrast("#ffffff", "#1f2937");
|
||||
console.log(text.toHex()); // "#ffffff"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor
|
||||
|
||||
- `new Colour(input: ColourInput)` — parses any supported colour input into an immutable `Colour` instance.
|
||||
|
||||
### Manipulation
|
||||
|
||||
- `lighten(amount: number): Colour`
|
||||
- `darken(amount: number): Colour`
|
||||
- `saturate(amount: number): Colour`
|
||||
- `desaturate(amount: number): Colour`
|
||||
- `rotate(degrees: number): Colour`
|
||||
- `alpha(a: number): Colour`
|
||||
- `fade(amount: number): Colour`
|
||||
- `opaquer(amount: number): Colour`
|
||||
- `negate(): Colour`
|
||||
- `grayscale(): Colour`
|
||||
- `mix(other: ColourInput, amount?: number): Colour`
|
||||
|
||||
### Analysis
|
||||
|
||||
- `luminance(): number`
|
||||
- `perceivedBrightness(): number`
|
||||
- `contrast(other: ColourInput): number`
|
||||
- `isDark(): boolean`
|
||||
- `isLight(): boolean`
|
||||
- `bestContrast(...candidates: (Colour | string)[]): Colour`
|
||||
- `distance(other: ColourInput, mode?: DistanceMode): number`
|
||||
- `equals(other: ColourInput): boolean`
|
||||
|
||||
### Output
|
||||
|
||||
- `toHex(): string`
|
||||
- `toHex3(): string`
|
||||
- `toHex8(): string`
|
||||
- `toRgb(): RGB`
|
||||
- `toRgba(): RGBA`
|
||||
- `toHsl(): HSL`
|
||||
- `toHsv(): HSV`
|
||||
- `toLab(): LAB`
|
||||
- `toLch(): LCH`
|
||||
- `toOklab(): OKLAB`
|
||||
- `toOklch(): OKLCH`
|
||||
- `toCmyk(): CMYK`
|
||||
- `toString(): string`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The internal `_rgba` state is never mutated. Every chainable method returns a new `Colour` instance. Conversion methods delegate to the `conversions` module. Analysis methods use the standard WCAG 2.1 luminance formula and sRGB gamma. The `bestContrast` helper uses the APCA-like perceived brightness for candidate ranking.
|
||||
76
docs/media/image/colours/src/conversions/conversions.md
Normal file
76
docs/media/image/colours/src/conversions/conversions.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# conversions
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`conversions.ts` provides deterministic, side-effect-free mathematical transforms between colour spaces. Every function takes a tuple of numbers and returns a tuple of numbers. No object mutation, no I/O, no dependency on any other package logic except basic math. These functions form the numerical foundation for the `Colour` class.
|
||||
|
||||
## Features
|
||||
|
||||
- RGB to HSL/HSLA and back
|
||||
- RGB to HSV and back
|
||||
- RGB to XYZ and back
|
||||
- XYZ to LAB/LCH and back
|
||||
- RGB to OKLAB/OKLCH and back
|
||||
- RGB to CMYK
|
||||
- Alpha passthrough on every conversion
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { rgbToHsl, hslToRgb } from "@suaveplan/colours/conversions";
|
||||
|
||||
const hsl = rgbToHsl({ r: 255, g: 128, b: 0 });
|
||||
const back = hslToRgb(hsl);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { colourSteps } from "@suaveplan/colours/colour-factories";
|
||||
|
||||
const gradient = colourSteps("#ff0000", "#0000ff", 5);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### RGB ↔ HSL
|
||||
|
||||
- `rgbToHsl(rgb: RGB): HSL`
|
||||
- `hslToRgb(hsl: HSL): RGBA`
|
||||
|
||||
### RGB ↔ HSV
|
||||
|
||||
- `rgbToHsv(rgb: RGB): HSV`
|
||||
- `hsvToRgb(hsv: HSV, alpha?: number): RGBA`
|
||||
|
||||
### RGB ↔ XYZ
|
||||
|
||||
- `rgbToXyz(rgb: RGB): XYZ`
|
||||
- `xyzToRgb(xyz: XYZ, alpha?: number): RGBA`
|
||||
|
||||
### XYZ ↔ LAB/LCH
|
||||
|
||||
- `xyzToLab(xyz: XYZ): LAB`
|
||||
- `labToXyz(lab: LAB): XYZ`
|
||||
- `labToLch(lab: LAB): LCH`
|
||||
- `lchToLab(lch: LCH): LAB`
|
||||
|
||||
### RGB ↔ OKLAB/OKLCH
|
||||
|
||||
- `rgbToOklab(rgb: RGB): OKLAB`
|
||||
- `oklabToRgb(oklab: OKLAB, alpha?: number): RGBA`
|
||||
- `oklabToOklch(oklab: OKLAB): OKLCH`
|
||||
- `oklchToRgb(oklch: OKLCH): RGBA`
|
||||
|
||||
### RGB ↔ CMYK
|
||||
|
||||
- `rgbToCmyk(rgb: RGB): CMYK`
|
||||
- `cmykToRgb(cmyk: CMYK): RGBA`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All transforms use double-precision arithmetic and standard colour-science matrices. RGB values are normalised to `[0,1]` before matrix multiplication and clamped back to `[0,255]` afterwards. OKLAB uses the Bradley coefficients. CMYK uses the simple 100% minus RGB ratio formula. Alpha is never modified by colour-space conversion.
|
||||
61
docs/media/image/colours/src/error-codes/error-codes.md
Normal file
61
docs/media/image/colours/src/error-codes/error-codes.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# error-codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`error-codes.ts` declares the canonical error-code registry for `@suaveplan/colours`. Every failure mode that the colour package can surface — from invalid hex strings to unsupported CSS colour formats — is assigned a stable, namespaced string via `defineCodes` from `@suaveplan/error`. This gives downstream log analysers, retry policies, and problem+json renderers a deterministic contract they can match against without parsing human-readable messages.
|
||||
|
||||
## Features
|
||||
|
||||
- Three stable codes under the `COLOUR.*` namespace
|
||||
- Type-safe `ColourCode` union derived from the registry object
|
||||
- Zero runtime side effects — the registry is a plain constant
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { COLOUR_CODES } from "@suaveplan/colours/error-codes";
|
||||
|
||||
function handleColourError(err: { code: string }) {
|
||||
if (err.code === COLOUR_CODES.INVALID_INPUT) {
|
||||
return { status: 400, message: "Invalid colour" };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { COLOUR_CODES, type ColourCode } from "@suaveplan/colours/error-codes";
|
||||
|
||||
const retryableCodes: ColourCode[] = [
|
||||
COLOUR_CODES.CONVERSION_FAILED,
|
||||
];
|
||||
|
||||
function shouldRetry(code: ColourCode): boolean {
|
||||
return retryableCodes.includes(code);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `COLOUR_CODES`
|
||||
|
||||
A frozen object mapping human-readable keys to namespaced string values:
|
||||
|
||||
- `INVALID_INPUT` → `"COLOUR.INVALID_INPUT"`
|
||||
- `UNSUPPORTED_FORMAT` → `"COLOUR.UNSUPPORTED_FORMAT"`
|
||||
- `CONVERSION_FAILED` → `"COLOUR.CONVERSION_FAILED"`
|
||||
|
||||
### `ColourCode`
|
||||
|
||||
A TypeScript union of all three code string literals. Useful for function parameters and discriminated unions.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is produced by `defineCodes("COLOUR", [...])`. If the namespace or code list ever changes, every test that asserts against `COLOUR_CODES.*` will fail, which is the desired behaviour — it prevents silent drift in the error contract.
|
||||
|
||||
Error codes are intentionally coarse-grained. They group related failure modes under a single code so that retry policies and alerting rules do not need to track an ever-expanding list of micro-codes. When more detail is required, the factory functions attach a context object with the raw input or format string.
|
||||
65
docs/media/image/colours/src/errors/errors.md
Normal file
65
docs/media/image/colours/src/errors/errors.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# errors
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`errors.ts` provides the typed error hierarchy for `@suaveplan/colours`. `ColourError` extends `SuaveplanError` so that callers receive structured failures with a stable code, a message key, an optional context object, and an optional cause chain. Typed factory functions make it easy to construct correctly-coded errors without hand-typing string literals.
|
||||
|
||||
## Features
|
||||
|
||||
- `ColourError` class with `code`, `message`, `context`, and `cause`
|
||||
- Factory functions for every failure mode
|
||||
- Context carries `input` and `format` for debugging
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { colourInvalid } from "@suaveplan/colours/errors";
|
||||
|
||||
function parseUserColour(input: string) {
|
||||
if (input.length === 0) {
|
||||
throw colourInvalid(input);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ColourError, COLOUR_CODES } from "@suaveplan/colours/errors";
|
||||
|
||||
function enrichError(err: unknown): ColourError {
|
||||
if (err instanceof ColourError) return err;
|
||||
return new ColourError(
|
||||
COLOUR_CODES.CONVERSION_FAILED,
|
||||
"Unexpected failure",
|
||||
{ cause: err instanceof Error ? err : undefined }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ColourError`
|
||||
|
||||
Extends `SuaveplanError<ColourCode>`.
|
||||
|
||||
- `constructor(code, messageKey, options?)` — `options` may include `cause` and `context`.
|
||||
- `context?: ColourErrorContext` — carries `input` and `format`.
|
||||
|
||||
### Factory Functions
|
||||
|
||||
- `colourInvalid(input?)`
|
||||
- `colourUnsupportedFormat(format)`
|
||||
- `colourConversionFailed(message)`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All factories are thin wrappers — they do not duplicate logic. The `ColourErrorContext` interface extends `Record<string, unknown>` so callers can safely add custom fields without losing type safety.
|
||||
|
||||
Every factory accepts an optional input or format value and embeds it in the error context. This makes debugging straightforward because the thrown error carries the exact value that caused the failure. Callers should never construct `ColourError` manually; always use the exported factories to guarantee the correct code and context shape.
|
||||
|
||||
The error hierarchy follows the suaveplan standard of extending `SuaveplanError` so that all packages in the project throw errors with a consistent shape. This allows top-level error handlers to treat colour errors the same way they treat network, validation, or auth errors.
|
||||
51
docs/media/image/colours/src/parser/parser.md
Normal file
51
docs/media/image/colours/src/parser/parser.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# parser
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`parser.ts` converts arbitrary colour input into a canonical `RGBA` object. It supports hex strings (3/4/6/8 digit), modern and legacy CSS `rgb()` and `hsl()` syntax, CSS Color Module Level 4/5 `lab()`, `lch()`, `oklab()`, and `oklch()` syntax, named CSS colours, and plain JavaScript objects in every supported colour space. The parser is pure and side-effect-free.
|
||||
|
||||
## Features
|
||||
|
||||
- Hex parsing with optional `#` prefix
|
||||
- Modern space-separated and legacy comma-separated CSS syntax
|
||||
- Named colour lookup via `NAMED_COLOURS`
|
||||
- Object dispatch for RGB, HSL, HSV, LAB, OKLAB, and CMYK objects
|
||||
- Strict validation with typed `ColourError` throws
|
||||
- Safe parsing via `tryParseColour` with configurable fallback
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseColour } from "@suaveplan/colours/parser";
|
||||
|
||||
const rgba = parseColour("#ff9900");
|
||||
console.log(rgba); // { r: 255, g: 153, b: 0, a: 1 }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { tryParseColour } from "@suaveplan/colours/parser";
|
||||
|
||||
const safe = tryParseColour(userInput, { r: 255, g: 255, b: 255, a: 1 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `parseColour(input: ColourInput): RGBA`
|
||||
|
||||
Returns a canonical `RGBA` object. Throws `ColourError` with code `COLOUR.INVALID_INPUT` or `COLOUR.UNSUPPORTED_FORMAT` on failure.
|
||||
|
||||
### `tryParseColour(input: ColourInput, fallback?: RGBA): RGBA`
|
||||
|
||||
Returns the parsed `RGBA` object, or `fallback` if parsing fails. The default fallback is black with full opacity (`{ r: 0, g: 0, b: 0, a: 1 }`).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Regex patterns are compiled once at module load and reused for every parse call. Object parsing uses property detection (e.g. `'r' in obj`) rather than `instanceof` to avoid cross-realm issues. Alpha values in hex8 are normalised with `roundAlpha` to three decimal places.
|
||||
|
||||
The parser is designed to be strict rather than permissive. It does not attempt to guess intent when input is ambiguous; instead it throws a typed `ColourError` immediately. This fail-fast behaviour makes debugging easier for callers and prevents subtle colour drift caused by silently accepted malformed input. The `tryParseColour` variant provides a convenient escape hatch for callers who need graceful degradation rather than exceptions.
|
||||
@@ -0,0 +1,61 @@
|
||||
# colour-types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`colour-types.ts` is the universal type system and Zod schema registry for `@suaveplan/colours`. It defines TypeScript interfaces for every colour space supported by the library, validates object inputs via Zod, and provides a lookup table of named CSS colours. Everything in this module is deterministic and side-effect free.
|
||||
|
||||
## Features
|
||||
|
||||
- TypeScript interfaces for RGB, RGBA, HSL, HSLA, HSV, LAB, LCH, OKLAB, OKLCH, CMYK
|
||||
- Zod schemas for runtime validation of each colour-space object
|
||||
- Named colour map with 19 CSS named colours plus transparent
|
||||
- Union types for `ColourInput`, `MixMode`, `DistanceMode`, `BlendMode`, and `PaletteType`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RgbSchema, NAMED_COLOURS } from "@suaveplan/colours/types";
|
||||
|
||||
const rgb = RgbSchema.parse({ r: 255, g: 128, b: 0 });
|
||||
const red = NAMED_COLOURS.get("red");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { LabSchema, type LAB } from "@suaveplan/colours/types";
|
||||
|
||||
function processLab(input: unknown): LAB {
|
||||
return LabSchema.parse(input);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Zod Schemas
|
||||
|
||||
- `RgbSchema`, `RgbaSchema`
|
||||
- `HslSchema`, `HslaSchema`
|
||||
- `HsvSchema`
|
||||
- `LabSchema`, `LchSchema`
|
||||
- `OklabSchema`, `OklchSchema`
|
||||
- `CmykSchema`
|
||||
|
||||
### Types
|
||||
|
||||
- `RGB`, `RGBA`, `HSL`, `HSLA`, `HSV`, `LAB`, `LCH`, `OKLAB`, `OKLCH`, `CMYK`
|
||||
- `ColourInput`, `MixMode`, `DistanceMode`, `BlendMode`, `PaletteType`
|
||||
|
||||
### Constants
|
||||
|
||||
- `NAMED_COLOURS` — `ReadonlyMap<string, RGBA>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Zod schemas use `.int()` and `.min().max()` bounds to match CSS channel constraints. `NAMED_COLOURS` is a native `Map` wrapped in `ReadonlyMap` to prevent accidental mutation while preserving `O(1)` lookups.
|
||||
|
||||
Type definitions are kept as simple readonly interfaces so they can be used in both runtime Zod validation and compile-time TypeScript checks without duplication. The union types for modes and palettes are derived from const arrays, which means adding a new mode updates both the type and any Zod schema that references it automatically.
|
||||
56
docs/media/image/colours/src/utils/utils.md
Normal file
56
docs/media/image/colours/src/utils/utils.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# utils
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`utils.ts` provides small, pure mathematical utilities used throughout `@suaveplan/colours`. Every function is deterministic and side-effect-free, making the module safe to import in any JavaScript runtime. These helpers enforce the numeric invariants that keep colour channels, percentages, and alpha values within their valid ranges.
|
||||
|
||||
## Features
|
||||
|
||||
- `clamp` — restricts a number to a closed interval
|
||||
- `isFiniteNumber` — type guard rejecting `NaN` and `Infinity`
|
||||
- `roundChannel` — clamps and rounds to `[0, 255]`
|
||||
- `roundPercent` — clamps and rounds to `[0, 100]`
|
||||
- `roundAlpha` — clamps and rounds to three decimal places in `[0, 1]`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { clamp, roundChannel } from "@suaveplan/colours/utils";
|
||||
|
||||
const value = clamp(300, 0, 255);
|
||||
console.log(value); // 255
|
||||
|
||||
const channel = roundChannel(128.6);
|
||||
console.log(channel); // 129
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { isFiniteNumber, roundAlpha } from "@suaveplan/colours/utils";
|
||||
|
||||
function safeAlpha(input: unknown): number {
|
||||
if (!isFiniteNumber(input)) return 1;
|
||||
return roundAlpha(input);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `clamp(val, min, max): number`
|
||||
- `isFiniteNumber(v): v is number`
|
||||
- `roundChannel(v): number`
|
||||
- `roundPercent(v): number`
|
||||
- `roundAlpha(v): number`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`roundAlpha` uses `Math.round(v * 1000) / 1000` to cap precision at three decimal places. This matches the CSS `opacity` resolution and prevents floating-point drift in serialised output.
|
||||
|
||||
All utilities are implemented as standalone functions with no shared state. They can be imported individually for tree-shaking or used together as a consistent numeric toolkit. Because every helper is pure, they are safe to call inside hot loops such as pixel shaders or animation frames without risk of side effects.
|
||||
|
||||
The clamp function uses simple comparison operators rather than Math.min and Math.max for slightly better performance in tight loops. The isFiniteNumber guard explicitly rejects NaN and Infinity, which is stricter than a plain typeof check and prevents invalid values from propagating through conversion chains.
|
||||
65
docs/media/image/compress-image/README.md
Normal file
65
docs/media/image/compress-image/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# @suaveplan/compress-image - Runtime-agnostic image compression utilities: resize, quality reduction, format conversion, and metadata stripping via pluggable adapters
|
||||
|
||||
Runtime-agnostic image compression utilities with pluggable adapters for Node.js (Sharp) and testing (MemoryAdapter). Supports resize, quality reduction, format conversion (JPEG, PNG, WebP, AVIF), and metadata stripping. The compressor core is pure TypeScript with zero runtime dependencies; only the Sharp adapter requires the native `sharp` library.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [memory](./src/adapters/memory/memory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [memory](./src/adapters/memory/memory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [sharp](./src/adapters/sharp/sharp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [sharp](./src/adapters/sharp/sharp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [compressor](./src/core/compressor/compressor.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [compressor](./src/core/compressor/compressor.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/compress-image-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [compress-image-error](./src/errors/compress-image-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [schemas](./src/schemas/compress-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [compress-options.schema](./src/schemas/compress-options.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [utils](./src/utils/image-utils.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [image-utils](./src/utils/image-utils.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/compress-image
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { createCompressor } from "@suaveplan/compress-image/compressor";
|
||||
import { SharpAdapter } from "@suaveplan/compress-image/adapters/sharp";
|
||||
import sharp from "sharp";
|
||||
|
||||
const compressor = createCompressor(new SharpAdapter(sharp));
|
||||
|
||||
const result = await compressor.compress(imageBuffer, {
|
||||
format: "webp",
|
||||
quality: 75,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
resizeMode: "cover",
|
||||
stripMetadata: true,
|
||||
});
|
||||
|
||||
console.log(result.compressionRatio, result.compressedSize);
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/core/compressor/compressor.md`](./src/core/compressor/compressor.md) — `ImageCompressor` core and `createCompressor`
|
||||
- [`src/adapters/memory/memory.md`](./src/adapters/memory/memory.md) — `MemoryAdapter` for testing without native deps
|
||||
- [`src/adapters/sharp/sharp.md`](./src/adapters/sharp/sharp.md) — `SharpAdapter` for Node.js production compression
|
||||
- [`src/errors/compress-image-error.md`](./src/errors/compress-image-error.md) — `CompressImageError` and error codes
|
||||
- [`src/schemas/compress-options.schema.md`](./src/schemas/compress-options.schema.md) — Zod schemas for compression options
|
||||
- [`src/types/compress-image.types.md`](./src/types/compress-image.types.md) — TypeScript interfaces for results and adapters
|
||||
- [`src/utils/image-utils.md`](./src/utils/image-utils.md) — Pure TypeScript image utilities
|
||||
|
||||
## Subpath Exports
|
||||
|
||||
- `.` — All public types, errors, utilities, adapters, and the compressor
|
||||
- `./compressor` — `ImageCompressor`, `createCompressor`
|
||||
- `./adapters/memory` — `MemoryAdapter`
|
||||
- `./adapters/sharp` — `SharpAdapter`, `SharpLike`, `SharpInstance`
|
||||
- `./errors` — `CompressImageError`, `COMPRESS_IMAGE_CODES`
|
||||
@@ -0,0 +1,81 @@
|
||||
# memory-adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a pure-TypeScript implementation of `ImageAdapter` that operates entirely on raw bytes — no native addons, no WebAssembly, no external codec dependencies. Its primary purpose is to enable 100% test coverage of the `ImageCompressor` class, Zod validation paths, and OTel instrumentation without requiring Sharp or any platform-specific library.
|
||||
|
||||
The adapter parses PNG IHDR chunks and JPEG SOF markers to derive real pixel dimensions from test fixtures, and synthesises `CompressResult` metadata (including a simulated `compressionRatio`) from the requested `quality` option.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements the full `ImageAdapter` interface (`compress`, `getInfo`, `resize`)
|
||||
- PNG dimension extraction via IHDR chunk parsing
|
||||
- JPEG dimension extraction via SOF0/SOF2 marker scanning
|
||||
- Synthetic `compressionRatio` computation: `ratio = 100 / quality`
|
||||
- Conservative alpha detection: PNG and WebP are flagged as potentially having alpha
|
||||
- Throws `CompressImageError` with code `UNSUPPORTED_FORMAT` for unrecognized inputs
|
||||
- Zero external runtime dependencies
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { MemoryAdapter } from '@suaveplan/compress-image/adapters/memory';
|
||||
import { createCompressor } from '@suaveplan/compress-image/compressor';
|
||||
|
||||
const compressor = createCompressor(new MemoryAdapter());
|
||||
const result = await compressor.compress(pngBuffer, { quality: 60, format: 'webp' });
|
||||
console.log(result.compressionRatio); // ~1.67
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MemoryAdapter } from '@suaveplan/compress-image/adapters/memory';
|
||||
|
||||
const adapter = new MemoryAdapter();
|
||||
|
||||
// getInfo: detect format and dimensions
|
||||
const info = await adapter.getInfo(pngBuffer);
|
||||
// { width: 1, height: 1, format: 'png', size: 68, hasAlpha: true }
|
||||
|
||||
// compress: returns original bytes with synthetic metadata
|
||||
const result = await adapter.compress(pngBuffer, {
|
||||
quality: 50,
|
||||
format: 'webp',
|
||||
width: 200,
|
||||
height: 200,
|
||||
});
|
||||
// result.compressionRatio === 2.0
|
||||
// result.data === pngBuffer (no actual re-encoding)
|
||||
|
||||
// resize: passthrough (no real pixel manipulation)
|
||||
const resized = await adapter.resize(pngBuffer, 100, 100, 'cover');
|
||||
// resized === pngBuffer
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class MemoryAdapter implements ImageAdapter`
|
||||
|
||||
#### `compress(input: Uint8Array, options: CompressOptions): Promise<CompressResult>`
|
||||
|
||||
Returns a `CompressResult` where `data` is identical to `input`. The `format` defaults to the detected input format. `compressionRatio` is computed as `100 / quality` (quality defaults to 80, giving ratio 1.25). `width` and `height` in the result reflect `options.width`/`options.height` when supplied, otherwise the parsed dimensions. Throws `CompressImageError(UNSUPPORTED_FORMAT)` for unrecognized input.
|
||||
|
||||
#### `getInfo(input: Uint8Array): Promise<ImageInfo>`
|
||||
|
||||
Returns `{ width, height, format, size, hasAlpha }` derived from magic-byte parsing. Alpha is `true` for PNG and WebP, `false` for JPEG. AVIF and unrecognized formats fall back to `{ width: 1, height: 1 }`. Throws `CompressImageError(UNSUPPORTED_FORMAT)` if no format is detected.
|
||||
|
||||
#### `resize(input: Uint8Array, width: number, height: number, mode?: ResizeMode): Promise<Uint8Array>`
|
||||
|
||||
Returns `input` unchanged. Validates that the format is recognizable before returning; throws `CompressImageError(UNSUPPORTED_FORMAT)` otherwise.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The simulated `compressionRatio = 100 / quality` formula is a deliberate simplification. Real compressors produce ratios that depend on image content complexity, format, and encoder state. The formula satisfies the test contract that "higher quality = lower ratio", while being deterministic and dependency-free.
|
||||
|
||||
`compressedSize` is `Math.max(1, Math.round(originalSize / compressionRatio))` — the `Math.max(1, ...)` guard prevents a zero byte count when the input buffer is a single byte at quality 100.
|
||||
|
||||
WebP and AVIF dimension parsing is not implemented because these formats require a full codec to parse the container headers. The fallback `{ width: 1, height: 1 }` is clearly documented so test authors set explicit `width`/`height` options when asserting on those fields.
|
||||
102
docs/media/image/compress-image/src/adapters/sharp/sharp.md
Normal file
102
docs/media/image/compress-image/src/adapters/sharp/sharp.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# sharp-adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a production-grade `ImageAdapter` implementation backed by the Sharp image processing library. Sharp delivers native-speed JPEG, PNG, WebP, and AVIF encoding through libvips, making it the recommended adapter for server-side workloads where pixel-accurate compression and real resizing are required.
|
||||
|
||||
Sharp is deliberately **not** a direct dependency of this package. The caller supplies a Sharp factory function at construction time, which eliminates the native-addon from the `@suaveplan/compress-image` dependency tree and allows the package to be used in environments where Sharp cannot be installed (e.g., edge runtimes, bundled frontends, or CI test runs).
|
||||
|
||||
## Features
|
||||
|
||||
- Full `ImageAdapter` implementation using the real Sharp pipeline
|
||||
- JPEG progressive encoding, PNG compression levels, WebP lossless, AVIF effort tuning
|
||||
- Metadata stripping by default; opt-in preservation via `stripMetadata: false`
|
||||
- `ResizeMode` mapped to Sharp's `fit` option (cover / contain / fill / inside / outside)
|
||||
- Caller-supplied Sharp factory for dependency injection
|
||||
- Wraps all Sharp errors in `CompressImageError` with cause-chain forwarding
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import sharp from 'sharp';
|
||||
import { SharpAdapter } from '@suaveplan/compress-image/adapters/sharp';
|
||||
import { createCompressor } from '@suaveplan/compress-image/compressor';
|
||||
|
||||
const compressor = createCompressor(new SharpAdapter(sharp));
|
||||
|
||||
const result = await compressor.compress(imageBuffer, {
|
||||
format: 'webp',
|
||||
quality: 75,
|
||||
width: 800,
|
||||
height: 600,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import sharp from 'sharp';
|
||||
import { SharpAdapter } from '@suaveplan/compress-image/adapters/sharp';
|
||||
|
||||
const adapter = new SharpAdapter(sharp);
|
||||
|
||||
// Get metadata
|
||||
const info = await adapter.getInfo(jpegBuffer);
|
||||
console.log(info.width, info.height, info.format, info.hasAlpha);
|
||||
|
||||
// Convert JPEG to AVIF with high compression effort
|
||||
const result = await adapter.compress(jpegBuffer, {
|
||||
format: 'avif',
|
||||
quality: 60,
|
||||
effort: 6,
|
||||
stripMetadata: true,
|
||||
});
|
||||
|
||||
// Resize to thumbnail keeping aspect ratio (contain mode)
|
||||
const thumb = await adapter.resize(jpegBuffer, 128, 128, 'contain');
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `interface SharpLike`
|
||||
|
||||
A minimal factory interface matching the Sharp module export signature. Implement this to inject a mock in tests.
|
||||
|
||||
```ts
|
||||
interface SharpLike {
|
||||
(input: Buffer | Uint8Array): SharpInstance;
|
||||
}
|
||||
```
|
||||
|
||||
### `interface SharpInstance`
|
||||
|
||||
The fluent pipeline instance returned by `SharpLike`. Covers the methods used by `SharpAdapter`.
|
||||
|
||||
### `class SharpAdapter implements ImageAdapter`
|
||||
|
||||
#### `constructor(sharp: SharpLike)`
|
||||
|
||||
Stores the provided Sharp factory. No I/O occurs at construction time.
|
||||
|
||||
#### `compress(input: Uint8Array, options: CompressOptions): Promise<CompressResult>`
|
||||
|
||||
Runs the Sharp pipeline with format conversion, quality settings, and optional resize. Returns a `CompressResult` with real output bytes and actual dimensions. Throws `CompressImageError(UNSUPPORTED_FORMAT)` for undetectable inputs; throws `CompressImageError(COMPRESS_FAILED)` for all other Sharp errors.
|
||||
|
||||
#### `getInfo(input: Uint8Array): Promise<ImageInfo>`
|
||||
|
||||
Calls `sharp(input).metadata()` to extract width, height, format, and alpha channel information. Throws `CompressImageError(UNSUPPORTED_FORMAT)` if Sharp returns a format not in the known set.
|
||||
|
||||
#### `resize(input: Uint8Array, width: number, height: number, mode?: ResizeMode): Promise<Uint8Array>`
|
||||
|
||||
Resizes using `sharp.resize()` with the mapped `fit` option. Returns a `Uint8Array` wrapping the Sharp output `Buffer`. Default mode is `'cover'`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Format encoding options are mapped per-format: JPEG receives `quality` and `progressive`; WebP receives `quality`, `lossless`, and `effort`; AVIF receives `quality` and `effort`; PNG receives `compressionLevel` (mapped from `effort`) but no quality parameter since PNG is lossless.
|
||||
|
||||
A second `.metadata()` pass on the output buffer is used to derive the final `width` and `height` in `CompressResult`. This adds a small overhead but guarantees accuracy after resize operations where the actual output dimensions may differ from the requested values (e.g., when using `inside` or `outside` fit modes).
|
||||
|
||||
Sharp's `Buffer` is wrapped in `new Uint8Array(buffer)` to satisfy the `Uint8Array` contract in `CompressResult.data`, ensuring interoperability with environments that do not use Node.js `Buffer`.
|
||||
@@ -0,0 +1,105 @@
|
||||
# compressor
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `ImageCompressor` class is the primary entry point for the `@suaveplan/compress-image` package. It orchestrates all image processing operations through a pluggable `ImageAdapter`, adds Zod-based option validation, dimension range checking, and OpenTelemetry instrumentation, and exposes a convenience `compressIfLarger` method for conditional compression pipelines.
|
||||
|
||||
Application code should interact with `ImageCompressor` (or the `createCompressor` factory) rather than calling adapters directly, to benefit from validation and observability.
|
||||
|
||||
## Features
|
||||
|
||||
- Pluggable adapter pattern: swap `MemoryAdapter` ↔ `SharpAdapter` without changing application code
|
||||
- Zod option validation with structured `CompressImageError(INVALID_OPTIONS)` on failure
|
||||
- Dimension range validation (1–32767 px) before delegating to adapter
|
||||
- OTel counter `compress_image.compress.total{format, outcome}` — incremented on every `compress()` call
|
||||
- OTel histogram `compress_image.compress.duration_ms{format, outcome}` — recorded per call
|
||||
- `compressIfLarger` for size-threshold-based conditional compression
|
||||
- `createCompressor` factory for functional-style initialization
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createCompressor } from '@suaveplan/compress-image/compressor';
|
||||
|
||||
// Default adapter: MemoryAdapter (testing)
|
||||
const compressor = createCompressor();
|
||||
const result = await compressor.compress(pngBuffer, { quality: 80 });
|
||||
console.log(result.compressionRatio, result.format);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import sharp from 'sharp';
|
||||
import { createCompressor } from '@suaveplan/compress-image/compressor';
|
||||
import { SharpAdapter } from '@suaveplan/compress-image/adapters/sharp';
|
||||
|
||||
const compressor = createCompressor(new SharpAdapter(sharp));
|
||||
|
||||
// Compress with resize and format conversion
|
||||
const result = await compressor.compress(rawBuffer, {
|
||||
format: 'webp',
|
||||
quality: 75,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
resizeMode: 'contain',
|
||||
stripMetadata: true,
|
||||
progressive: false,
|
||||
effort: 5,
|
||||
});
|
||||
|
||||
// Conditionally compress: only if > 200 KB
|
||||
const maybe = await compressor.compressIfLarger(rawBuffer, 200 * 1024, {
|
||||
format: 'jpeg',
|
||||
quality: 70,
|
||||
});
|
||||
|
||||
// Get info without compressing
|
||||
const info = await compressor.getInfo(rawBuffer);
|
||||
|
||||
// Resize only
|
||||
const thumb = await compressor.resize(rawBuffer, 128, 128, 'cover');
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class ImageCompressor`
|
||||
|
||||
#### `constructor(adapter?: ImageAdapter)`
|
||||
|
||||
Creates a new compressor. Defaults to `MemoryAdapter` when no adapter is provided.
|
||||
|
||||
#### `setAdapter(adapter: ImageAdapter): void`
|
||||
|
||||
Replaces the active adapter. Useful for switching between testing and production adapters at runtime.
|
||||
|
||||
#### `compress(input: Uint8Array, options?: CompressOptions): Promise<CompressResult>`
|
||||
|
||||
Validates options with Zod, checks dimension bounds, delegates to the adapter, and records OTel metrics. Returns a `CompressResult`. Throws `CompressImageError(INVALID_OPTIONS)` on bad options; throws `CompressImageError(UNSUPPORTED_FORMAT)` or `CompressImageError(COMPRESS_FAILED)` from the adapter.
|
||||
|
||||
#### `getInfo(input: Uint8Array): Promise<ImageInfo>`
|
||||
|
||||
Delegates directly to `adapter.getInfo()` — no option validation required.
|
||||
|
||||
#### `resize(input: Uint8Array, width: number, height: number, mode?: ResizeMode): Promise<Uint8Array>`
|
||||
|
||||
Validates `width` and `height` ranges, then delegates to `adapter.resize()`. Throws `CompressImageError(INVALID_OPTIONS)` for out-of-range dimensions.
|
||||
|
||||
#### `compressIfLarger(input: Uint8Array, maxBytes: number, options?: CompressOptions): Promise<CompressResult>`
|
||||
|
||||
Returns the original bytes wrapped in a synthetic `CompressResult` (`compressionRatio = 1`) when `input.length <= maxBytes`. Otherwise delegates to `compress()`. Always returns `CompressResult` so downstream code has a consistent return type.
|
||||
|
||||
### `createCompressor(adapter?: ImageAdapter): ImageCompressor`
|
||||
|
||||
Equivalent to `new ImageCompressor(adapter)`. Returns a configured `ImageCompressor`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
OTel instruments are lazily initialized on first use inside `getCounter()` and `getDuration()`. This avoids registering metrics at module load time, which would interfere with test environments that configure the OTel SDK after imports are resolved.
|
||||
|
||||
The `format` OTel attribute defaults to `'unknown'` when `options.format` is not specified, since the detected format is not available until the adapter resolves it. This is a deliberate trade-off: the counter fires unconditionally so monitoring captures 100% of calls.
|
||||
|
||||
`compressIfLarger` calls `adapter.getInfo()` for the bypass path to populate `format`, `width`, and `height` in the returned `CompressResult`. This costs one metadata parse but gives callers a usable result regardless of whether compression ran.
|
||||
@@ -0,0 +1,82 @@
|
||||
# compress-image-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the structured error class and stable code registry for the `@suaveplan/compress-image` package. All compression failures are surfaced through `CompressImageError`, which extends the project-wide `SuaveplanError` base class to guarantee consistent wire format, cause-chain traversal, OTel span-event integration, and Zod-validated JSON/binary serialization.
|
||||
|
||||
Centralising codes in `COMPRESS_IMAGE_CODES` (produced by `defineCodes`) ensures that every error string is unique within the project namespace and will not silently change across package updates.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable `COMPRESS_IMAGE_CODES` registry with four codes covering all failure surfaces
|
||||
- `CompressImageError` class with typed `code` property for narrow `instanceof` discrimination
|
||||
- Full cause-chain forwarding via the `cause` option
|
||||
- Compatible with the `SuaveplanError` OTel adapter and redaction middleware
|
||||
- Zero external runtime dependencies beyond `@suaveplan/error`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CompressImageError, COMPRESS_IMAGE_CODES } from '@suaveplan/compress-image/errors';
|
||||
|
||||
throw new CompressImageError(
|
||||
'Unsupported input format: tiff',
|
||||
COMPRESS_IMAGE_CODES.UNSUPPORTED_FORMAT,
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CompressImageError, COMPRESS_IMAGE_CODES } from '@suaveplan/compress-image/errors';
|
||||
|
||||
async function runCompress(input: Uint8Array) {
|
||||
try {
|
||||
await adapter.compress(input, { quality: 80 });
|
||||
} catch (err) {
|
||||
throw new CompressImageError(
|
||||
'Compression pipeline failed',
|
||||
COMPRESS_IMAGE_CODES.COMPRESS_FAILED,
|
||||
{ cause: err instanceof Error ? err : undefined },
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `COMPRESS_IMAGE_CODES`
|
||||
|
||||
An object map produced by `defineCodes('COMPRESS_IMAGE', { ... })`. Each value is a stable string code of the form `COMPRESS_IMAGE.<KEY>`.
|
||||
|
||||
| Key | Description |
|
||||
|---|---|
|
||||
| `UNSUPPORTED_FORMAT` | Input image format is not recognized or not supported by the adapter |
|
||||
| `COMPRESS_FAILED` | The compression operation itself threw an error |
|
||||
| `INVALID_OPTIONS` | Zod validation rejected the provided `CompressOptions` |
|
||||
| `ADAPTER_NOT_SET` | `compress` / `resize` / `getInfo` was called before an adapter was configured |
|
||||
|
||||
### `CompressImageError`
|
||||
|
||||
```ts
|
||||
class CompressImageError extends SuaveplanError {
|
||||
constructor(message: string, code: string, options?: { cause?: Error }): void
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
|---|---|---|
|
||||
| `message` | `string` | Human-readable description of the failure |
|
||||
| `code` | `string` | A value from `COMPRESS_IMAGE_CODES` |
|
||||
| `options.cause` | `Error \| undefined` | Originating error for cause-chain traversal |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`SuaveplanError` stores `code` on its instance and enforces the Zod-validated wire format so errors survive cross-process transport. The `name = 'CompressImageError'` assignment enables `instanceof` checks even after deserialization with the error registry.
|
||||
|
||||
The constructor signature deviates slightly from `DatetimeError` — it accepts a plain `string` message rather than a messageKey because image compression errors carry enough context inline and do not require i18n resolution.
|
||||
@@ -0,0 +1,103 @@
|
||||
# compress-options.schema
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a Zod schema (`CompressOptionsSchema`) that validates and normalises
|
||||
`CompressOptions` at runtime before any image-processing work begins. Keeping
|
||||
validation in a dedicated schema file separates concerns cleanly: the
|
||||
compressor delegates option-checking to this module and never needs to repeat
|
||||
range guards inline.
|
||||
|
||||
## Features
|
||||
|
||||
- `format` — optional enum restricted to `"jpeg" | "png" | "webp" | "avif"`
|
||||
- `quality` — integer `1–100`; rejects floats and out-of-range values
|
||||
- `width` / `height` — positive integers capped at 32767 (JPEG/PNG spec limit)
|
||||
- `resizeMode` — optional enum of five Sharp `fit` strategies
|
||||
- `stripMetadata` / `progressive` / `lossless` — optional booleans
|
||||
- `effort` — integer `0–6` for compression effort / speed trade-off
|
||||
- Re-exports `CompressOptionsInput` (pre-parse) and `CompressOptionsParsed` (post-parse) type aliases for strong typing at call sites
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CompressOptionsSchema } from '@suaveplan/compress-image/schemas';
|
||||
|
||||
const result = CompressOptionsSchema.safeParse({ quality: 85, format: 'webp' });
|
||||
if (!result.success) {
|
||||
console.error(result.error.issues);
|
||||
} else {
|
||||
const opts = result.data; // CompressOptionsParsed
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CompressOptionsSchema } from '@suaveplan/compress-image/schemas';
|
||||
import type { CompressOptionsInput } from '@suaveplan/compress-image/schemas';
|
||||
|
||||
function toCompressOptions(raw: unknown): CompressOptionsInput {
|
||||
// Strict parse — throws ZodError on invalid input
|
||||
return CompressOptionsSchema.parse(raw);
|
||||
}
|
||||
|
||||
// Combining with ImageCompressor validation:
|
||||
// The ImageCompressor calls CompressOptionsSchema.safeParse internally and
|
||||
// maps ZodError issues into a structured CompressImageError(INVALID_OPTIONS).
|
||||
// Direct use of this schema is useful in API route handlers that want to
|
||||
// validate options before constructing an ImageCompressor.
|
||||
const validated = CompressOptionsSchema.safeParse({
|
||||
format: 'avif',
|
||||
quality: 60,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
resizeMode: 'cover',
|
||||
effort: 4,
|
||||
stripMetadata: true,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CompressOptionsSchema`
|
||||
|
||||
`ZodObject` schema validating `CompressOptions`. All fields are optional.
|
||||
|
||||
| Field | Type | Constraints |
|
||||
|----------------|-----------------------------------|----------------------------|
|
||||
| `format` | `"jpeg" \| "png" \| "webp" \| "avif"` | enum |
|
||||
| `quality` | `number` | int, 1–100 |
|
||||
| `width` | `number` | int, 1–32767 |
|
||||
| `height` | `number` | int, 1–32767 |
|
||||
| `resizeMode` | `ResizeMode` | enum of 5 strategies |
|
||||
| `stripMetadata`| `boolean` | optional |
|
||||
| `progressive` | `boolean` | optional |
|
||||
| `lossless` | `boolean` | optional |
|
||||
| `effort` | `number` | int, 0–6 |
|
||||
|
||||
### `CompressOptionsInput`
|
||||
|
||||
`z.input<typeof CompressOptionsSchema>` — the shape of data accepted before parsing (all fields optional).
|
||||
|
||||
### `CompressOptionsParsed`
|
||||
|
||||
`z.output<typeof CompressOptionsSchema>` — the shape returned after successful parsing.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema intentionally mirrors the `CompressOptions` interface defined in
|
||||
`compress-image.types.ts`. No coercion is applied; callers are expected to
|
||||
pass the correct JS types. Integer constraints (`z.number().int()`) are
|
||||
necessary because Sharp interprets quality as a C `int`; passing `80.5` would
|
||||
produce unpredictable output.
|
||||
|
||||
Effort range `0–6` follows the WebP/AVIF encoder convention. Sharp maps this
|
||||
field directly to the codec's speed parameter, where 0 is fastest (lower
|
||||
quality at equal bit-rate) and 6 is slowest (best compression).
|
||||
|
||||
The schema is intentionally stateless and side-effect-free — it is safe to
|
||||
import and reuse across workers, edge functions, and test helpers.
|
||||
@@ -0,0 +1,149 @@
|
||||
# compress-image.types
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines all public TypeScript types and interfaces for the
|
||||
`@suaveplan/compress-image` package. Keeping type definitions in a single
|
||||
dedicated file avoids circular imports between adapter and compressor modules
|
||||
and makes it straightforward for consumers to import only the types they need
|
||||
without pulling in any runtime code.
|
||||
|
||||
## Features
|
||||
|
||||
- `ImageFormat` — union of four supported output formats (`jpeg`, `png`, `webp`, `avif`)
|
||||
- `ResizeMode` — five resize strategies mirroring the Sharp `fit` option
|
||||
- `ColorSpace` — two colour-space choices (`srgb`, `p3`)
|
||||
- `CompressOptions` — all parameters accepted by `ImageCompressor.compress()`
|
||||
- `CompressResult` — structured result returned after a compress call
|
||||
- `ImageInfo` — image metadata returned by `getInfo()` without modifying the image
|
||||
- `ImageAdapter` — pluggable backend interface implemented by `MemoryAdapter` and `SharpAdapter`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
CompressOptions,
|
||||
CompressResult,
|
||||
ImageFormat,
|
||||
} from '@suaveplan/compress-image';
|
||||
|
||||
function makeWebpOptions(quality: number): CompressOptions {
|
||||
return { format: 'webp' as ImageFormat, quality };
|
||||
}
|
||||
|
||||
async function printResult(result: CompressResult): Promise<void> {
|
||||
console.log(`Compressed to ${result.compressedSize} bytes (ratio ${result.compressionRatio.toFixed(2)})`);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { ImageAdapter, CompressOptions, CompressResult, ImageInfo, ResizeMode } from '@suaveplan/compress-image';
|
||||
|
||||
// Implement a custom adapter backed by a WASM codec:
|
||||
class WasmAdapter implements ImageAdapter {
|
||||
async compress(input: Uint8Array, options: CompressOptions): Promise<CompressResult> {
|
||||
// delegate to WASM encoder...
|
||||
return {
|
||||
data: input,
|
||||
format: options.format ?? 'jpeg',
|
||||
width: 100,
|
||||
height: 100,
|
||||
originalSize: input.length,
|
||||
compressedSize: input.length,
|
||||
compressionRatio: 1,
|
||||
};
|
||||
}
|
||||
|
||||
async getInfo(input: Uint8Array): Promise<ImageInfo> {
|
||||
return { width: 100, height: 100, format: 'jpeg', size: input.length, hasAlpha: false };
|
||||
}
|
||||
|
||||
async resize(input: Uint8Array, _w: number, _h: number, _mode?: ResizeMode): Promise<Uint8Array> {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ImageFormat`
|
||||
|
||||
```ts
|
||||
type ImageFormat = "jpeg" | "png" | "webp" | "avif";
|
||||
```
|
||||
|
||||
### `ResizeMode`
|
||||
|
||||
```ts
|
||||
type ResizeMode = "cover" | "contain" | "fill" | "inside" | "outside";
|
||||
```
|
||||
|
||||
Mirrors the Sharp `fit` option. `cover` crops to fill; `contain` letterboxes; `fill` stretches; `inside` shrinks only; `outside` enlarges only.
|
||||
|
||||
### `ColorSpace`
|
||||
|
||||
```ts
|
||||
type ColorSpace = "srgb" | "p3";
|
||||
```
|
||||
|
||||
### `CompressOptions`
|
||||
|
||||
All fields optional.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------------|---------------|----------------------------------------------------------|
|
||||
| `format` | `ImageFormat` | Target format. Defaults to detected input format. |
|
||||
| `quality` | `number` | 1–100 quality level. Default 80. |
|
||||
| `width` | `number` | Target width in pixels. |
|
||||
| `height` | `number` | Target height in pixels. |
|
||||
| `resizeMode` | `ResizeMode` | Resize strategy. Default `"cover"`. |
|
||||
| `stripMetadata` | `boolean` | Strip EXIF and other metadata. Default `true`. |
|
||||
| `progressive` | `boolean` | Enable progressive JPEG. Default `false`. |
|
||||
| `lossless` | `boolean` | WebP lossless encoding. Default `false`. |
|
||||
| `effort` | `number` | Compression effort 0–6. Default 4. |
|
||||
|
||||
### `CompressResult`
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------------|---------------|------------------------------------------------------|
|
||||
| `data` | `Uint8Array` | Compressed image bytes. |
|
||||
| `format` | `ImageFormat` | Output format. |
|
||||
| `width` | `number` | Output width in pixels. |
|
||||
| `height` | `number` | Output height in pixels. |
|
||||
| `originalSize` | `number` | Input byte length. |
|
||||
| `compressedSize` | `number` | Output byte length. |
|
||||
| `compressionRatio` | `number` | `originalSize / compressedSize` (>1 = effective compression). |
|
||||
|
||||
### `ImageInfo`
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------|---------------|------------------------------------------|
|
||||
| `width` | `number` | Image width in pixels. |
|
||||
| `height` | `number` | Image height in pixels. |
|
||||
| `format` | `ImageFormat` | Detected image format. |
|
||||
| `size` | `number` | File size in bytes. |
|
||||
| `hasAlpha` | `boolean` | Whether the image has an alpha channel. |
|
||||
|
||||
### `ImageAdapter`
|
||||
|
||||
Interface for pluggable image-processing backends.
|
||||
|
||||
| Method | Signature | Description |
|
||||
|-------------|---------------------------------------------------------------------------|-----------------------------------|
|
||||
| `compress` | `(input: Uint8Array, options: CompressOptions) => Promise<CompressResult>`| Compress with given options. |
|
||||
| `getInfo` | `(input: Uint8Array) => Promise<ImageInfo>` | Extract metadata without encoding.|
|
||||
| `resize` | `(input: Uint8Array, w: number, h: number, mode?: ResizeMode) => Promise<Uint8Array>` | Resize to target dimensions. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
These types are pure TypeScript interfaces — they emit no runtime code and add
|
||||
no bundle weight. The `ImageAdapter` interface is the primary extension point:
|
||||
any backend (Sharp, WebAssembly, Canvas API, remote service) that satisfies
|
||||
the three-method contract can be plugged into `ImageCompressor` via the
|
||||
`setAdapter()` method or the `createCompressor()` factory.
|
||||
|
||||
`ColorSpace` is defined here for forward compatibility but is not yet consumed
|
||||
by any built-in adapter. It is available for custom adapters that support
|
||||
wide-gamut output.
|
||||
89
docs/media/image/compress-image/src/utils/image-utils.md
Normal file
89
docs/media/image/compress-image/src/utils/image-utils.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# image-utils
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides low-level, dependency-free utility functions for working with raw image bytes. These utilities power both the `MemoryAdapter` (testing) and the `SharpAdapter` (production), and are also available to application code for format detection and dimension extraction without instantiating a full adapter.
|
||||
|
||||
All functions operate on `Uint8Array` buffers and are designed to be pure and free of side effects, enabling tree-shaking and straightforward unit testing.
|
||||
|
||||
## Features
|
||||
|
||||
- Magic-byte format detection for JPEG, PNG, WebP, and AVIF
|
||||
- PNG IHDR chunk dimension parser (big-endian uint32 at fixed offsets)
|
||||
- JPEG SOF0/SOF2 marker scanner for baseline and progressive dimension extraction
|
||||
- Aspect ratio calculation using the Euclidean GCD algorithm
|
||||
- Dimension range validation with descriptive `CompressImageError` on failure
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { detectFormat, parsePngDimensions } from '@suaveplan/compress-image';
|
||||
|
||||
const format = detectFormat(buffer); // 'png' | 'jpeg' | 'webp' | 'avif' | null
|
||||
|
||||
const dims = parsePngDimensions(buffer);
|
||||
if (dims) {
|
||||
console.log(dims.width, dims.height);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
detectFormat,
|
||||
parseJpegDimensions,
|
||||
validateDimensions,
|
||||
calculateAspectRatio,
|
||||
} from '@suaveplan/compress-image';
|
||||
|
||||
function analyzeImage(data: Uint8Array) {
|
||||
const format = detectFormat(data);
|
||||
if (!format) throw new Error('Unknown format');
|
||||
|
||||
let dims: { width: number; height: number } | null = null;
|
||||
if (format === 'jpeg') dims = parseJpegDimensions(data);
|
||||
else if (format === 'png') dims = parsePngDimensions(data);
|
||||
|
||||
if (!dims) throw new Error('Could not determine dimensions');
|
||||
|
||||
// Validate target dimensions before a resize
|
||||
validateDimensions(800, 600);
|
||||
|
||||
const ratio = calculateAspectRatio(dims.width, dims.height);
|
||||
console.log(`Aspect ratio: ${ratio.width}:${ratio.height}`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `detectFormat(data: Uint8Array): ImageFormat | null`
|
||||
|
||||
Inspects magic bytes at the beginning of `data` to determine the image format. Returns `null` for unrecognized signatures. Requires at least 4 bytes; WebP and AVIF detection requires 12 bytes.
|
||||
|
||||
### `validateDimensions(width?: number, height?: number): void`
|
||||
|
||||
Validates that the provided dimensions (when present) are positive integers not exceeding 32767. Throws `CompressImageError` with code `INVALID_OPTIONS` on any violation.
|
||||
|
||||
### `calculateAspectRatio(width: number, height: number): { width: number; height: number }`
|
||||
|
||||
Returns the reduced aspect ratio using the Euclidean GCD. For example `(1920, 1080)` returns `{ width: 16, height: 9 }`.
|
||||
|
||||
### `parsePngDimensions(data: Uint8Array): { width: number; height: number } | null`
|
||||
|
||||
Reads the IHDR chunk at the fixed offset in a PNG file (bytes 16–23) and returns the width and height. Returns `null` if the magic bytes are wrong, the buffer is too short, or the IHDR tag is not found.
|
||||
|
||||
### `parseJpegDimensions(data: Uint8Array): { width: number; height: number } | null`
|
||||
|
||||
Scans the JPEG segment stream for SOF0 (0xFF 0xC0) or SOF2 (0xFF 0xC2) markers and reads the frame header dimensions. Returns `null` if no SOF marker is found or the buffer is too short.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The AVIF detection reads the `ftyp` box major brand field at bytes 8–11. This covers `avif` (still image) and `avis` (AVIF image sequence). Files with compatible brands listed only in the compatible-brands array (not the major brand) are not detected; this is an acceptable limitation for a magic-byte heuristic.
|
||||
|
||||
`parseJpegDimensions` handles 0xFF padding bytes and segment-length arithmetic correctly, but does not attempt to parse progressive scans beyond SOF2. DNL (Define Number of Lines) segments used in some streaming encoders are not supported.
|
||||
|
||||
Dimension overflow: width and height are read as unsigned 32-bit big-endian integers and immediately checked against 0 to reject pathological inputs before they can propagate.
|
||||
134
docs/media/image/emblem-solver/README.md
Normal file
134
docs/media/image/emblem-solver/README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# @suaveplan/emblem-solver - Minimal-rectangle SVG converter for pixelated sprite emblems: layered occlusion, overlapping rect optimisation, dual-axis maximal rectangles, strip decomposition, and exhaustive layer ordering
|
||||
|
||||
Minimal-rectangle SVG converter for pixelated sprite emblems.
|
||||
|
||||
Converts a pixel grid (`RGBA[]`) into the smallest possible set of
|
||||
axis-aligned coloured rectangles by combining four covering strategies
|
||||
(dual-axis maximal rectangles + greedy cover, per-island independent
|
||||
cover, strip decomposition, adjacent-rect merge), pairwise + triple-wise
|
||||
overlap reductions, and exhaustive z-stacking search across up to 8! layer
|
||||
orderings. Outputs a deterministic SVG string ready for direct embedding
|
||||
or further compression.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [converter](./src/converter/converter.md) — This module is the public-facing entry point of `@suaveplan/emblem-solver`. It coordinates the entire pipeline — schema…
|
||||
- [converter](./src/converter/converter.md) — This module is the public-facing entry point of `@suaveplan/emblem-solver`. It coordinates the entire pipeline — schema…
|
||||
- [errors](./src/errors/errors.md) — This module exposes the typed error surface for `@suaveplan/emblem-solver`. It defines the stable, namespaced code regi…
|
||||
- [errors](./src/errors/errors.md) — This module exposes the typed error surface for `@suaveplan/emblem-solver`. It defines the stable, namespaced code regi…
|
||||
- [fixtures](./src/fixtures/fixtures.md) — This module is the canonical test-fixture surface for the package. Each exported function returns a deterministic `RGBA…
|
||||
- [fixtures](./src/fixtures/fixtures.md) — This module is the canonical test-fixture surface for the package. Each exported function returns a deterministic `RGBA…
|
||||
- [analyse](./src/grid/analyse/analyse.md) — This module owns `analyseGrid`, the diagnostic phase of the conversion pipeline. It walks an RGBA grid once, buckets op…
|
||||
- [analyse](./src/grid/analyse/analyse.md) — This module owns `analyseGrid`, the diagnostic phase of the conversion pipeline. It walks an RGBA grid once, buckets op…
|
||||
- [helpers](./src/helpers/helpers.md) — This module is the shared kernel of utility functions used by the analyser, the solver strategies, and the optimisers.…
|
||||
- [helpers](./src/helpers/helpers.md) — This module is the shared kernel of utility functions used by the analyser, the solver strategies, and the optimisers.…
|
||||
- [parse](./src/io/parse/parse.md) — This module owns the two import paths into the solver: `parseRGBABuffer` turns a raw RGBA `Uint8Array` (e.g. the output…
|
||||
- [parse](./src/io/parse/parse.md) — This module owns the two import paths into the solver: `parseRGBABuffer` turns a raw RGBA `Uint8Array` (e.g. the output…
|
||||
- [merge](./src/optimizer/merge/merge.md) — This module owns `mergeAdjacentRects`, the post-processing pass that combines pairs of edge-sharing same-extent rectang…
|
||||
- [merge](./src/optimizer/merge/merge.md) — This module owns `mergeAdjacentRects`, the post-processing pass that combines pairs of edge-sharing same-extent rectang…
|
||||
- [overlap](./src/optimizer/overlap/overlap.md) — This module owns `optimiseOverlap`, the deepest cover-reduction pass in the package. It looks for three patterns that t…
|
||||
- [overlap](./src/optimizer/overlap/overlap.md) — This module owns `optimiseOverlap`, the deepest cover-reduction pass in the package. It looks for three patterns that t…
|
||||
- [redundancy](./src/optimizer/redundancy/redundancy.md) — This module owns `eliminateRedundant`, the final cross-layer optimisation the converter runs after every layer's cover…
|
||||
- [redundancy](./src/optimizer/redundancy/redundancy.md) — This module owns `eliminateRedundant`, the final cross-layer optimisation the converter runs after every layer's cover…
|
||||
- [schemas](./src/schemas/schemas.md) — This module owns the runtime validation surface for `@suaveplan/emblem-solver`. It exposes `ConvertOptionsSchema`, a Zo…
|
||||
- [schemas](./src/schemas/schemas.md) — This module owns the runtime validation surface for `@suaveplan/emblem-solver`. It exposes `ConvertOptionsSchema`, a Zo…
|
||||
- [greedy](./src/solver/greedy/greedy.md) — This module provides the greedy selector that drives the primary covering strategy. `enhancedGreedyCover` picks rectang…
|
||||
- [greedy](./src/solver/greedy/greedy.md) — This module provides the greedy selector that drives the primary covering strategy. `enhancedGreedyCover` picks rectang…
|
||||
- [histogram](./src/solver/histogram/histogram.md) — This module enumerates every distinct maximal-rectangle candidate inside an opaque-pixel grid. It is the candidate pool…
|
||||
- [histogram](./src/solver/histogram/histogram.md) — This module enumerates every distinct maximal-rectangle candidate inside an opaque-pixel grid. It is the candidate pool…
|
||||
- [layer](./src/solver/layer/layer.md) — This module owns `processLayerMultiStrategy`, the function the converter calls once per layer in the current ordering.…
|
||||
- [layer](./src/solver/layer/layer.md) — This module owns `processLayerMultiStrategy`, the function the converter calls once per layer in the current ordering.…
|
||||
- [ordering](./src/solver/ordering/ordering.md) — This module provides the layer-ordering machinery the converter consults when it has to decide which colour sits on the…
|
||||
- [ordering](./src/solver/ordering/ordering.md) — This module provides the layer-ordering machinery the converter consults when it has to decide which colour sits on the…
|
||||
- [strip](./src/solver/strip/strip.md) — This module implements `stripDecomposition`, a complementary cover strategy that ignores the maximal-rect candidate poo…
|
||||
- [strip](./src/solver/strip/strip.md) — This module implements `stripDecomposition`, a complementary cover strategy that ignores the maximal-rect candidate poo…
|
||||
- [build](./src/svg/build/build.md) — This module owns `buildSVG`, the deterministic SVG serialiser the converter uses to produce its final `<svg>` string. I…
|
||||
- [build](./src/svg/build/build.md) — This module owns `buildSVG`, the deterministic SVG serialiser the converter uses to produce its final `<svg>` string. I…
|
||||
- [telemetry](./src/telemetry/telemetry.md) — This module owns the OpenTelemetry instruments emitted by `convert()`. It exposes a counter (`emblem_solver.convert.tot…
|
||||
- [telemetry](./src/telemetry/telemetry.md) — This module owns the OpenTelemetry instruments emitted by `convert()`. It exposes a counter (`emblem_solver.convert.tot…
|
||||
- [types](./src/types/types.md) — This module is the single source of truth for the public type surface of `@suaveplan/emblem-solver`. It defines the imm…
|
||||
- [types](./src/types/types.md) — This module is the single source of truth for the public type surface of `@suaveplan/emblem-solver`. It defines the imm…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @suaveplan/emblem-solver
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { convert, fromIndexedGrid } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = fromIndexedGrid(
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
new Map([[1, { r: 255, g: 0, b: 0 }]])
|
||||
);
|
||||
|
||||
const { svg, totalRects, ratio } = convert(pixels, width, height);
|
||||
console.log(svg); // <svg …>…</svg>
|
||||
console.log(totalRects); // 2 — the cross collapses to two overlapping rects
|
||||
console.log(ratio.toFixed(1)); // 2.5 — naïve per-pixel would have been 5 rects
|
||||
```
|
||||
|
||||
## API surface
|
||||
|
||||
| Symbol | Subpath | Purpose |
|
||||
|---|---|---|
|
||||
| `convert` | root | Public orchestrator |
|
||||
| `analyseGrid` | `/grid` | Per-colour islands, density, fragmentation |
|
||||
| `parseRGBABuffer`, `fromIndexedGrid` | `/io` | Input adapters |
|
||||
| `buildSVG` | `/svg` | Standalone serialiser |
|
||||
| `dualAxisMaximalRects`, `enhancedGreedyCover`, `perIslandCover`, `stripDecomposition`, `processLayerMultiStrategy`, `permutations`, `factorial`, `getStaticOrdering` | `/solver` | Composition primitives |
|
||||
| `mergeAdjacentRects`, `optimiseOverlap`, `eliminateRedundant` | `/optimizer` | Composition primitives |
|
||||
| `ConvertOptionsSchema` | `/schemas` | Zod input validation |
|
||||
| `EmblemSolverError`, `EMBLEM_SOLVER_CODES` | `/errors` | Typed errors |
|
||||
|
||||
## Options
|
||||
|
||||
```ts
|
||||
interface ConvertOptions {
|
||||
/** Default: "auto" */
|
||||
layerOrder?: "auto" | "area-desc" | "area-asc" | "island-aware" | "perimeter-desc";
|
||||
/** Default: 40320 (= 8!) */
|
||||
maxPermutations?: number;
|
||||
/** Default: Math.random — supply a seeded RNG for deterministic auto-ordering on > 8-colour inputs */
|
||||
rng?: () => number;
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
All failures throw `EmblemSolverError` whose `.code` is one of:
|
||||
|
||||
- `EMBLEM_SOLVER.INVALID_GRID` — dimensions non-positive or
|
||||
`pixels.length !== width * height`.
|
||||
- `EMBLEM_SOLVER.INVALID_OPTIONS` — Zod rejected the options bag.
|
||||
- `EMBLEM_SOLVER.INVALID_PIXEL` — reserved for future use.
|
||||
|
||||
## Observability
|
||||
|
||||
`convert()` emits two OpenTelemetry instruments via the meter
|
||||
`@suaveplan/emblem-solver`:
|
||||
|
||||
- Counter `emblem_solver.convert.total`
|
||||
- Histogram `emblem_solver.convert.duration_ms` (unit: `ms`)
|
||||
|
||||
Instruments are registered lazily on first call, so importing the package
|
||||
has no observability side effects.
|
||||
|
||||
## Tier
|
||||
|
||||
Universal: zero browser / Node / React dependencies. Runs unchanged on
|
||||
Bun, Node, Deno, and modern browsers.
|
||||
|
||||
## License
|
||||
|
||||
MIT — Suaveplan Team.
|
||||
|
||||
98
docs/media/image/emblem-solver/src/converter/converter.md
Normal file
98
docs/media/image/emblem-solver/src/converter/converter.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# `converter/converter.ts` — public `convert()` orchestrator
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the public-facing entry point of `@suaveplan/emblem-solver`.
|
||||
It coordinates the entire pipeline — schema validation, grid validation,
|
||||
grid analysis, ordering search, per-layer covering, cross-layer redundancy
|
||||
elimination, SVG serialisation, and OpenTelemetry instrument recording —
|
||||
into a single deterministic call: `convert(pixels, width, height,
|
||||
options?)`.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated options with strict-mode unknown-key rejection.
|
||||
- Grid validation (`width > 0`, `height > 0`, `pixels.length === w*h`).
|
||||
- Two orderings paths: exhaustive permutation search when
|
||||
`factorial(colourCount) ≤ maxPermutations`, otherwise four static
|
||||
heuristics plus up to 1000 RNG-driven random samples.
|
||||
- Injectable `rng` for deterministic auto-ordering on > 8-colour inputs.
|
||||
- OTel counter + histogram recorded around the algorithmic core.
|
||||
- Early-return for fully-transparent input — emits a viewBox-only SVG and
|
||||
zero layers.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { convert, fromIndexedGrid } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = fromIndexedGrid(
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
new Map([[1, { r: 255, g: 0, b: 0 }]])
|
||||
);
|
||||
const result = convert(pixels, width, height);
|
||||
console.log(result.totalRects, "rects;", result.svg.length, "bytes");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { convert } from "@suaveplan/emblem-solver";
|
||||
|
||||
// Seeded RNG → deterministic auto-mode output on >8-colour inputs.
|
||||
function mulberry32(seed: number): () => number {
|
||||
let s = seed >>> 0;
|
||||
return () => {
|
||||
s = (s + 0x6d2b79f5) >>> 0;
|
||||
let t = s;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
const a = convert(pixels, w, h, {
|
||||
layerOrder: "auto",
|
||||
maxPermutations: 16,
|
||||
rng: mulberry32(123),
|
||||
});
|
||||
const b = convert(pixels, w, h, {
|
||||
layerOrder: "auto",
|
||||
maxPermutations: 16,
|
||||
rng: mulberry32(123),
|
||||
});
|
||||
console.assert(a.svg === b.svg, "deterministic round-trip");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `convert(pixels, w, h, options?)` | fn | Returns `ConvertResult` |
|
||||
|
||||
| Throws | When |
|
||||
|---|---|
|
||||
| `EmblemSolverError(INVALID_OPTIONS)` | Zod schema rejects `options` |
|
||||
| `EmblemSolverError(INVALID_GRID)` | non-positive dims or `pixels.length` mismatch |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The orchestrator uses `Date.now()` for the duration histogram —
|
||||
millisecond resolution is sufficient for OTel and the clock is universal
|
||||
across Bun, Node, Deno, and browsers.
|
||||
The fisher-Yates shuffle uses the injected `rng` directly so a seeded
|
||||
source is byte-stable across runs. The heuristic branch is deliberate
|
||||
about its sample budget: `min(maxPermutations - 4, 1000)` because the
|
||||
four static strategies are evaluated first, and beyond 1000 samples the
|
||||
upstream measurements showed diminishing returns. `eliminateRedundant`
|
||||
mutates `bestLayers` in place inside `processOrdering`, which is safe
|
||||
because `processOrdering` returns a freshly-allocated array per
|
||||
invocation.
|
||||
83
docs/media/image/emblem-solver/src/errors/errors.md
Normal file
83
docs/media/image/emblem-solver/src/errors/errors.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# `errors/errors.ts` — typed error hierarchy
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module exposes the typed error surface for `@suaveplan/emblem-solver`.
|
||||
It defines the stable, namespaced code registry `EMBLEM_SOLVER_CODES` and
|
||||
the `EmblemSolverError` class that extends `SuaveplanError`. Every failure
|
||||
path inside the package — schema validation, grid validation, IO checks —
|
||||
funnels through this hierarchy so callers can pattern-match on `error.code`
|
||||
to distinguish user input mistakes from defensive guards.
|
||||
|
||||
## Features
|
||||
|
||||
- Codes are produced by `defineCodes("EMBLEM_SOLVER", [...] as const)`,
|
||||
yielding the namespaced strings `EMBLEM_SOLVER.INVALID_GRID`,
|
||||
`EMBLEM_SOLVER.INVALID_OPTIONS`, and `EMBLEM_SOLVER.INVALID_PIXEL`.
|
||||
- The registry is frozen — consumers can rely on identity equality across
|
||||
the module.
|
||||
- `EmblemSolverError` extends `SuaveplanError<EmblemSolverCode>`, so
|
||||
`error.code` narrows the union and `instanceof EmblemSolverError`
|
||||
discriminates the throw site.
|
||||
- Supports the full `SuaveplanErrorOptions` bag: `cause`, `context`,
|
||||
`traceId`, and `clock` for testing.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
EMBLEM_SOLVER_CODES,
|
||||
EmblemSolverError,
|
||||
} from "@suaveplan/emblem-solver/errors";
|
||||
|
||||
if (badInput) {
|
||||
throw new EmblemSolverError(
|
||||
EMBLEM_SOLVER_CODES.INVALID_GRID,
|
||||
"emblem-solver.invalid_grid"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
try {
|
||||
convert(pixels, w, h, options);
|
||||
} catch (err) {
|
||||
if (err instanceof EmblemSolverError) {
|
||||
switch (err.code) {
|
||||
case EMBLEM_SOLVER_CODES.INVALID_OPTIONS:
|
||||
return reportInputProblem(err);
|
||||
case EMBLEM_SOLVER_CODES.INVALID_GRID:
|
||||
return reportShapeProblem(err);
|
||||
case EMBLEM_SOLVER_CODES.INVALID_PIXEL:
|
||||
return reportPixelProblem(err);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `EMBLEM_SOLVER_CODES` | const | Frozen `{ INVALID_GRID, INVALID_OPTIONS, INVALID_PIXEL }` map |
|
||||
| `EmblemSolverCode` | type | Union of the namespaced strings |
|
||||
| `EmblemSolverError` | class | `extends SuaveplanError<EmblemSolverCode>` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` runs at module load and `Object.freeze`s the registry,
|
||||
guaranteeing identity stability — consumers that import via the root barrel
|
||||
or the `./errors` subpath see the same object. The class constructor is
|
||||
deliberately thin: it forwards to `SuaveplanError`'s `protected` constructor
|
||||
and sets `name = "EmblemSolverError"` so stack frames disambiguate. The
|
||||
`INVALID_PIXEL` code is reserved for future use (it appears in the code
|
||||
registry so consumers can switch on it ahead of any algorithmic change that
|
||||
introduces an unrepresentable RGBA value); the test suite exercises it via
|
||||
the round-trip code coverage check.
|
||||
73
docs/media/image/emblem-solver/src/fixtures/fixtures.md
Normal file
73
docs/media/image/emblem-solver/src/fixtures/fixtures.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# `fixtures/fixtures.ts` — canonical regression grids
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the canonical test-fixture surface for the package. Each
|
||||
exported function returns a deterministic `RGBA[]` grid with known
|
||||
algorithmic properties — chessboards, donuts, crosses, T-shapes, stars,
|
||||
flags, shields, and crests. Together they form the regression suite that
|
||||
locks the solver's quality numbers in place: any change that regresses
|
||||
the rect count on one of these fixtures fails the test gate.
|
||||
|
||||
## Features
|
||||
|
||||
- Twelve fixtures total: `chessboard8x8`, `cross3x3`, `cross6x6`,
|
||||
`tShape`, `donut5x5`, `circle16`, `flagCrosses`, `multiIslandStripes`,
|
||||
`shieldEmblem32`, `crest12x12`, `star`, plus the added
|
||||
`nineColours` (>8 distinct colours, used to exercise the auto-ordering
|
||||
heuristic branch).
|
||||
- Every fixture is purely declarative — built from a `number[][]` palette
|
||||
index map and `fromIndexedGrid`. No randomness, no `Date.now`, no
|
||||
module-load side effects.
|
||||
- Index `0` is reserved for transparency; every fixture follows that
|
||||
convention.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { chessboard8x8 } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = chessboard8x8();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { convert, crest12x12 } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = crest12x12();
|
||||
const result = convert(pixels, width, height, { layerOrder: "auto" });
|
||||
console.log(`crest compressed to ${result.totalRects} rects`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Returns |
|
||||
|---|---|
|
||||
| `chessboard8x8()` | 8x8 alternating black/white grid |
|
||||
| `cross3x3()` / `cross6x6()` | Plus-sign crosses |
|
||||
| `tShape()` | 5x4 T-shape |
|
||||
| `donut5x5()` | Frame with transparent centre |
|
||||
| `circle16()` | 16-px pixel circle |
|
||||
| `flagCrosses()` | 16x16 layered three-colour flag |
|
||||
| `multiIslandStripes()` | 10x2 alternating two-colour stripes |
|
||||
| `shieldEmblem32()` | 32x32 three-colour shield |
|
||||
| `crest12x12()` | 12x12 four-colour crest |
|
||||
| `star()` | Irregular star outline |
|
||||
| `nineColours()` | 4x4 grid with nine distinct colours |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The fixtures are exported as zero-argument functions rather than constants
|
||||
so each test call builds a fresh pixel array — the solver does not
|
||||
mutate its input, but defensive copies keep test isolation strong. The
|
||||
`nineColours` grid exists specifically to push `factorial(colourCount) >
|
||||
maxPermutations` so the heuristic + sampling branch in `convert()` is
|
||||
exercised; without it that branch would be unreachable and coverage would
|
||||
drop. Fixtures are excluded from the package's coverage threshold
|
||||
(`coverage.exclude` in `vitest.config.ts`) because they are test
|
||||
material, not production code.
|
||||
75
docs/media/image/emblem-solver/src/grid/analyse/analyse.md
Normal file
75
docs/media/image/emblem-solver/src/grid/analyse/analyse.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# `grid/analyse/analyse.ts` — colour groups, islands, and shape stats
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `analyseGrid`, the diagnostic phase of the conversion
|
||||
pipeline. It walks an RGBA grid once, buckets opaque pixels by colour, runs
|
||||
a 4-connected flood-fill on each bucket to recover the islands, and
|
||||
computes per-colour `density` (`pixels / bbox area`) and `fragmentation`
|
||||
(`islands / pixels`). The result feeds the layer-ordering heuristics and
|
||||
the per-island cover strategy in the solver pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- Single linear pass over the grid to populate `opaquePixels` and the
|
||||
colour buckets.
|
||||
- 4-connected BFS flood-fill — diagonals are treated as separate islands,
|
||||
matching the upstream reference and the convention used by every
|
||||
pixel-art tool.
|
||||
- Density and fragmentation are pure functions of the bucket and its
|
||||
bounding box; the solver uses both to score the `island-aware` ordering.
|
||||
- Output is structurally shared with the rest of the package via
|
||||
`GridAnalysis` — solver kernels consume `colourGroups`/`colourAnalysis`
|
||||
directly.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { analyseGrid } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { colourGroups, colourAnalysis } = analyseGrid(pixels, w, h);
|
||||
console.log("colours:", colourGroups.size);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { analyseGrid } from "@suaveplan/emblem-solver";
|
||||
|
||||
function pickBackgroundColour(pixels, w, h) {
|
||||
const a = analyseGrid(pixels, w, h);
|
||||
let best = "";
|
||||
let bestScore = -Infinity;
|
||||
for (const [key, info] of a.colourAnalysis) {
|
||||
const score = info.totalPixels * info.density;
|
||||
if (score > bestScore) {
|
||||
best = key;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `analyseGrid(pixels, w, h)` | fn | Returns `GridAnalysis` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The flood-fill uses an explicit stack (`pop()`) rather than recursion to
|
||||
avoid blowing the call stack on large emblems. We `add` to `visited`
|
||||
*before* enqueueing the neighbour, which guarantees each pixel is touched
|
||||
at most twice (once when discovered, once when its packed index is popped).
|
||||
The neighbour set is computed as a small inline array of `-1`-sentinel
|
||||
checks for the four cardinal directions; benchmarks against a `for (const
|
||||
[dx,dy] …)` form showed the inline form measurably faster in V8 and Bun.
|
||||
The `Map<string, Set<number>>` colour-group layout is intentional: the
|
||||
string key is the canonical colour identity, while the packed-index `Set`
|
||||
gives O(1) `has`/`add`/`delete` for the cover strategies.
|
||||
85
docs/media/image/emblem-solver/src/helpers/helpers.md
Normal file
85
docs/media/image/emblem-solver/src/helpers/helpers.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# `helpers/helpers.ts` — packing, keys, and grid primitives
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the shared kernel of utility functions used by the analyser,
|
||||
the solver strategies, and the optimisers. It owns the canonical
|
||||
representation of a pixel index (a packed `y * width + x` integer), the
|
||||
canonical colour key (`"r,g,b"`), the `computeBounds` reduction over a
|
||||
packed-pixel set, and the boolean-grid builders that drive the
|
||||
maximal-rectangle sweep and per-layer covering.
|
||||
|
||||
## Features
|
||||
|
||||
- `pack` / `unpackX` / `unpackY` give the package a single, branch-free way
|
||||
to convert between `(x, y)` and a single integer. Using a `Set<number>`
|
||||
of packed indices is dramatically cheaper than a `Set<string>` of
|
||||
`"x,y"`.
|
||||
- `colourKey` / `parseKey` round-trip an RGBA without committing to alpha;
|
||||
the solver does not need alpha for keying.
|
||||
- `computeBounds` is the only place we touch unpack-loop machinery for
|
||||
bounds computation; everywhere else delegates here.
|
||||
- `isFullyAvailable`, `rectCoversAnyRequired`, and `countRequired` are the
|
||||
three predicates the optimiser relies on; they share an early-exit
|
||||
pattern that keeps the solver O(area × candidates) rather than
|
||||
O(area² × candidates).
|
||||
- `buildAvailableGrid` / `buildRequiredGrid` allocate the 2-D `boolean[][]`
|
||||
masks used by the cover strategies.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { pack, unpackX, unpackY } from "@suaveplan/emblem-solver";
|
||||
|
||||
const w = 8;
|
||||
const idx = pack(3, 2, w); // 19
|
||||
unpackX(idx, w); // 3
|
||||
unpackY(idx, w); // 2
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildAvailableGrid,
|
||||
buildRequiredGrid,
|
||||
isFullyAvailable,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
const opaque = new Set<number>([0, 1, 2, 3]);
|
||||
const earlier = new Set<number>();
|
||||
const avail = buildAvailableGrid(2, 2, opaque, earlier);
|
||||
const req = buildRequiredGrid(opaque, 2, 2);
|
||||
isFullyAvailable({ x: 0, y: 0, w: 2, h: 2 }, avail); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `pack(x, y, w)` | fn | Row-major packed index |
|
||||
| `unpackX(i, w)` | fn | Recover `x` |
|
||||
| `unpackY(i, w)` | fn | Recover `y` |
|
||||
| `colourKey(rgba)` | fn | `"r,g,b"` |
|
||||
| `parseKey(str)` | fn | `RGBA` with `a:1` |
|
||||
| `computeBounds(set, w)` | fn | Tight bounding `Rect` |
|
||||
| `isFullyAvailable(rect, grid)` | fn | All cells `true`? |
|
||||
| `rectCoversAnyRequired(rect, req)` | fn | Any overlap? |
|
||||
| `countRequired(rect, req)` | fn | Overlap count |
|
||||
| `buildAvailableGrid(w, h, opaque, earlier)` | fn | 2-D opaque mask minus earlier layers |
|
||||
| `buildRequiredGrid(set, w, h)` | fn | 2-D mask from packed pixel set |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The functions are deliberately monomorphic over `boolean[][]`/`number` for
|
||||
predictable JIT performance. The bounding-box reduction uses `±Infinity`
|
||||
sentinels so it works for any non-empty set. The available-grid builder
|
||||
skips earlier-layer pixels in O(1) per packed index — important when the
|
||||
solver iterates orderings exhaustively over a colour set. None of these
|
||||
helpers allocate beyond the grids they explicitly build; the predicates
|
||||
return as soon as their early-exit condition is met, so worst-case work is
|
||||
bounded by `rect.w * rect.h`.
|
||||
71
docs/media/image/emblem-solver/src/io/parse/parse.md
Normal file
71
docs/media/image/emblem-solver/src/io/parse/parse.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# `io/parse/parse.ts` — RGBA buffer + indexed-grid IO
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the two import paths into the solver: `parseRGBABuffer`
|
||||
turns a raw RGBA `Uint8Array` (e.g. the output of `<canvas>.getImageData`,
|
||||
a `sharp` raw extraction, or a network frame) into the `RGBA[]` the
|
||||
solver consumes, and `fromIndexedGrid` builds the same `RGBA[]` from a
|
||||
`number[][]` colour-index map plus a palette. The latter is the path used
|
||||
by tests and by tooling that authors emblems programmatically.
|
||||
|
||||
## Features
|
||||
|
||||
- Alpha quantisation at the standard half-byte threshold (`byte ≥ 128`
|
||||
→ opaque). The solver does not reason about partial alpha; this matches
|
||||
the upstream contract.
|
||||
- Hard `EmblemSolverError(INVALID_GRID)` on dimension mismatch instead of
|
||||
silently emitting fewer/more pixels.
|
||||
- Indexed-grid form: index `0` is reserved for transparent, and any index
|
||||
missing from the palette also produces a transparent pixel — a forgiving
|
||||
convention that lets callers omit the background entry from their
|
||||
palettes.
|
||||
- Output is a fresh `RGBA[]` with no shared references to the input.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseRGBABuffer } from "@suaveplan/emblem-solver";
|
||||
|
||||
const pixels = parseRGBABuffer(new Uint8Array(w * h * 4), w, h);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { fromIndexedGrid, convert } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = fromIndexedGrid(
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
new Map([[1, { r: 255, g: 0, b: 0 }]])
|
||||
);
|
||||
const result = convert(pixels, width, height);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `parseRGBABuffer(buf, w, h)` | fn | `RGBA[]` of length `w*h`, alpha quantised |
|
||||
| `fromIndexedGrid(grid, palette)` | fn | `{ pixels, width, height }` |
|
||||
|
||||
Both throw `EmblemSolverError(EMBLEM_SOLVER_CODES.INVALID_GRID)` on
|
||||
non-positive dimensions or buffer-length mismatch.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`parseRGBABuffer` does a tight 4-stride loop over the input, reading via
|
||||
the optional-chaining default of `0` so a truncated buffer is rejected
|
||||
before it can produce a malformed pixel. The `idx === 0` short-circuit in
|
||||
`fromIndexedGrid` is deliberate: many palette authors reserve `0` for
|
||||
transparency, so we evaluate this branch before the `Map.get` lookup. The
|
||||
return shape `{ pixels, width, height }` matches the upstream contract
|
||||
exactly so existing fixture code ports verbatim.
|
||||
67
docs/media/image/emblem-solver/src/optimizer/merge/merge.md
Normal file
67
docs/media/image/emblem-solver/src/optimizer/merge/merge.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# `optimizer/merge/merge.ts` — adjacent-rect merge pass
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `mergeAdjacentRects`, the post-processing pass that
|
||||
combines pairs of edge-sharing same-extent rectangles into a single larger
|
||||
rectangle, provided the merged span lies entirely inside the available
|
||||
mask. It is the cheapest of the optimiser passes and runs after every
|
||||
covering strategy in the layer pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- Merges along both axes: two rects with the same `y` and `h` merge
|
||||
horizontally when adjacent; two rects with the same `x` and `w` merge
|
||||
vertically when adjacent.
|
||||
- Iterates until a full pass produces no merges, so chains of three or
|
||||
more rects collapse to one in `O(n²)` per pass and `O(k · n²)` overall
|
||||
for `k` passes (bounded by the rect count).
|
||||
- Available-mask check (`isFullyAvailable`) guarantees the merged span
|
||||
never paints transparent cells.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { mergeAdjacentRects } from "@suaveplan/emblem-solver";
|
||||
|
||||
const merged = mergeAdjacentRects(
|
||||
[
|
||||
{ x: 0, y: 0, w: 1, h: 1 },
|
||||
{ x: 1, y: 0, w: 1, h: 1 },
|
||||
],
|
||||
fullyAvailableGrid
|
||||
);
|
||||
// merged === [{ x: 0, y: 0, w: 2, h: 1 }]
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { mergeAdjacentRects } from "@suaveplan/emblem-solver";
|
||||
|
||||
let rects = greedyCover;
|
||||
rects = mergeAdjacentRects(rects, available);
|
||||
rects = mergeAdjacentRects(rects, available); // idempotent — no further change
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `mergeAdjacentRects(rects, available)` | fn | Greedily merge edge-sharing same-extent rects |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The inner `tryMerge` first tests the horizontal merge (same `y`, same
|
||||
`h`, sharing an edge in `x`), then the vertical merge (same `x`, same
|
||||
`w`, sharing an edge in `y`). Either form checks the combined rectangle
|
||||
against `available` before accepting; this avoids painting through
|
||||
transparent pixels. The outer loop terminates when a full pass produces
|
||||
zero merges, so each call is idempotent. We deliberately do not try
|
||||
*non*-same-extent merges here — that is the job of `optimiseOverlap`,
|
||||
which can produce overlapping output and is therefore a strictly stronger
|
||||
(but more expensive) operation.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `optimizer/overlap/overlap.ts` — overlap-based reductions
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `optimiseOverlap`, the deepest cover-reduction pass in
|
||||
the package. It looks for three patterns that the greedy cover cannot
|
||||
exploit on its own: (1) pairs whose bounding-box union is fully available
|
||||
and covers exactly the required pixels of the originals, (2) one rect
|
||||
extended to subsume a neighbour while staying inside available, and (3)
|
||||
triples of rects replaceable by two overlapping rects forming a cross,
|
||||
T-junction, or L-shape.
|
||||
|
||||
## Features
|
||||
|
||||
- Pass 1 — pairwise bbox merge with required-set inclusion check.
|
||||
- Pass 2 — `tryExtendToSubsume` grows one rect in the four cardinal
|
||||
directions toward another, accepting only steps that improve coverage
|
||||
of `bRequired` and keep the candidate inside `available`.
|
||||
- Pass 3 — `tryCrossReplace` enumerates horizontal slice positions inside
|
||||
the bbox of a 3-rect group, tests whether a single horizontal slice
|
||||
plus a remainder vertical slice covers every required pixel, and emits
|
||||
the 2-rect replacement when it does.
|
||||
- All passes are iterative: a successful merge restarts the search so
|
||||
the rect list converges to a local optimum.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { optimiseOverlap } from "@suaveplan/emblem-solver";
|
||||
|
||||
const reduced = optimiseOverlap(
|
||||
greedyCover,
|
||||
requiredGrid,
|
||||
availableGrid,
|
||||
rows,
|
||||
cols
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { optimiseOverlap } from "@suaveplan/emblem-solver";
|
||||
|
||||
let rects = greedyCover;
|
||||
do {
|
||||
const next = optimiseOverlap(rects, required, available, rows, cols);
|
||||
if (next.length === rects.length) break;
|
||||
rects = next;
|
||||
} while (true);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `optimiseOverlap(rects, required, available, rows, cols)` | fn | Apply pairwise, extend, and cross-replace reductions |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The pass 1 inclusion check uses `Set<number>` of packed pixel indices so
|
||||
the comparison is O(k) where `k` is the cell count of the union. Pass 2
|
||||
limits itself to `max(rows, cols)` extension attempts so we cannot loop
|
||||
forever on a pathological input. Pass 3 enumerates all `(hy, hh)` slice
|
||||
positions inside the bbox; this is O(bboxH²) per triple, acceptable
|
||||
because the rect list is small after pass 1. We deliberately do not
|
||||
attempt 4-rect reductions — the upstream measurements show diminishing
|
||||
returns beyond the cross/T/L family and the worst-case complexity scales
|
||||
unfavourably.
|
||||
@@ -0,0 +1,61 @@
|
||||
# `optimizer/redundancy/redundancy.ts` — cross-layer redundancy elimination
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `eliminateRedundant`, the final cross-layer optimisation
|
||||
the converter runs after every layer's cover has been built. It removes
|
||||
rectangles that produce no visible output: either because higher-z
|
||||
rectangles of any colour overdraw them, or because sibling rectangles in
|
||||
the same layer already cover the same cells. The pass operates in place
|
||||
on the layer list and is the only pass that mutates `layer.rects`.
|
||||
|
||||
## Features
|
||||
|
||||
- Composites every layer into a per-cell `target` grid first (lower-z to
|
||||
higher-z), then walks rect by rect deciding whether removal would
|
||||
change the composited output.
|
||||
- A rect is removed iff *every* one of its required cells is either
|
||||
covered by a sibling rect at the same z-index or overdrawn by a higher-z
|
||||
rect of any colour.
|
||||
- In-place mutation via `Array.prototype.splice` — the converter calls
|
||||
this once per ordering, so the in-place form keeps allocations bounded.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { eliminateRedundant } from "@suaveplan/emblem-solver";
|
||||
|
||||
eliminateRedundant(layers, width, height);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { eliminateRedundant } from "@suaveplan/emblem-solver";
|
||||
|
||||
const before = layers.reduce((s, l) => s + l.rects.length, 0);
|
||||
eliminateRedundant(layers, width, height);
|
||||
const after = layers.reduce((s, l) => s + l.rects.length, 0);
|
||||
console.log(`pruned ${before - after} redundant rects`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `eliminateRedundant(layers, width, height)` | fn | In-place prune of fully-occluded rects |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The compositing step builds a `(string | null)[][]` grid keyed by the
|
||||
canonical colour key — using a string rather than a packed integer makes
|
||||
the per-cell comparison a single `===` against the layer's fill string,
|
||||
which V8 deduplicates aggressively. The two-pass design (composite first,
|
||||
then walk rect by rect) avoids the quadratic blow-up of repeatedly
|
||||
re-compositing after each removal. The removable indices are pushed in
|
||||
ascending order and spliced in reverse, so we never break the index
|
||||
correspondence mid-removal.
|
||||
79
docs/media/image/emblem-solver/src/schemas/schemas.md
Normal file
79
docs/media/image/emblem-solver/src/schemas/schemas.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# `schemas/schemas.ts` — Zod options schema
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the runtime validation surface for `@suaveplan/emblem-solver`.
|
||||
It exposes `ConvertOptionsSchema`, a Zod schema that mirrors the static
|
||||
`ConvertOptions` interface, applies sensible defaults, and rejects unknown
|
||||
keys. `convert()` runs every caller-supplied options object through
|
||||
`safeParse` so typos or out-of-range values fail loudly with
|
||||
`EmblemSolverError(INVALID_OPTIONS)` rather than silently degrading the
|
||||
solver.
|
||||
|
||||
## Features
|
||||
|
||||
- `layerOrder` defaults to `"auto"` and is validated as a closed enum over
|
||||
the five supported strategies.
|
||||
- `maxPermutations` defaults to `40320 = 8!` — the threshold beyond which
|
||||
exhaustive permutation search is abandoned in favour of heuristics +
|
||||
random sampling. The schema enforces `int().min(1)` so a zero or negative
|
||||
value is rejected at the boundary.
|
||||
- `rng` is an optional `() => number` validated with `z.custom<…>()`. It is
|
||||
the only injection point for the package — supplying it makes the
|
||||
heuristic + sampling path fully deterministic.
|
||||
- `.strict()` rejects unknown keys, so misspelled options never silently
|
||||
succeed.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ConvertOptionsSchema } from "@suaveplan/emblem-solver/schemas";
|
||||
|
||||
const parsed = ConvertOptionsSchema.parse({ layerOrder: "area-desc" });
|
||||
parsed.maxPermutations; // 40320 (default applied)
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { ConvertOptionsSchema } from "@suaveplan/emblem-solver/schemas";
|
||||
import { EMBLEM_SOLVER_CODES, EmblemSolverError } from
|
||||
"@suaveplan/emblem-solver/errors";
|
||||
|
||||
function safeOptions(raw: unknown) {
|
||||
const r = ConvertOptionsSchema.safeParse(raw);
|
||||
if (!r.success) {
|
||||
throw new EmblemSolverError(
|
||||
EMBLEM_SOLVER_CODES.INVALID_OPTIONS,
|
||||
`bad options: ${r.error.issues
|
||||
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
||||
.join("; ")}`
|
||||
);
|
||||
}
|
||||
return r.data;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `ConvertOptionsSchema` | `z.ZodObject` | Strict schema for `ConvertOptions` |
|
||||
| `ConvertOptionsParsed` | type | Post-defaults shape of the parsed result |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema lives in its own module so the rest of the package does not pay
|
||||
the Zod cost at import time when consumers do not need validation (e.g.
|
||||
hot-loop callers that constructed options at startup). `convert()` always
|
||||
runs through this schema, but downstream tooling can call
|
||||
`ConvertOptionsSchema.parse` ahead of time and pass the parsed result into
|
||||
its own caching layer. We use `z.custom<() => number>` rather than
|
||||
`z.function()` because Zod 4 deprecated runtime function validation in
|
||||
favour of a custom predicate; this matches the catalog `zod ^4.x`. The
|
||||
`rng` field stays optional (rather than defaulting to `Math.random`) so the
|
||||
test harness can detect whether a caller injected a deterministic source.
|
||||
72
docs/media/image/emblem-solver/src/solver/greedy/greedy.md
Normal file
72
docs/media/image/emblem-solver/src/solver/greedy/greedy.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# `solver/greedy/greedy.ts` — deterministic greedy set-cover
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module provides the greedy selector that drives the primary covering
|
||||
strategy. `enhancedGreedyCover` picks rectangles from a candidate pool
|
||||
using a deterministic three-key score (covered count, efficiency, centroid
|
||||
proximity). `perIslandCover` runs the same selector independently on each
|
||||
4-connected island so the algorithm never wastes budget on rectangles that
|
||||
span the transparent gap between two disjoint islands.
|
||||
|
||||
## Features
|
||||
|
||||
- Three-key score: `covered * 1000 + efficiency * 100 + proximity * 10`.
|
||||
The factors are deliberately spaced so a more-covering rect always beats
|
||||
a more-efficient one, and ties are broken by centroid proximity.
|
||||
- Falls back to emitting `1×1` rectangles for any residual uncovered
|
||||
pixels — guarantees the cover is complete even when no candidate
|
||||
intersects the residual.
|
||||
- `perIslandCover` filters the candidate pool per island so the selector
|
||||
never considers a spanning rect; this halves selection work on
|
||||
multi-island colours.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
dualAxisMaximalRects,
|
||||
enhancedGreedyCover,
|
||||
} from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const candidates = dualAxisMaximalRects(available, rows, cols);
|
||||
const cover = enhancedGreedyCover(candidates, required, cols, rows);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
analyseGrid,
|
||||
perIslandCover,
|
||||
dualAxisMaximalRects,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
const a = analyseGrid(pixels, w, h);
|
||||
const info = a.colourAnalysis.get("255,0,0")!;
|
||||
const candidates = dualAxisMaximalRects(available, h, w);
|
||||
const cover = perIslandCover(info.islands, candidates, w, h);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `enhancedGreedyCover(candidates, required, w, h)` | fn | Greedy cover with three-key score |
|
||||
| `perIslandCover(islands, candidates, w, h)` | fn | Independent cover per island |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The centroid term is computed against the running set of *uncovered*
|
||||
required pixels, not the original set — this keeps selections clustered
|
||||
even as the residual shrinks. We use a precomputed `maxDist =
|
||||
sqrt(w² + h²)` so the proximity term is normalised to `[0, 1]`; without
|
||||
this, the third tiebreaker could dominate the second on large grids.
|
||||
`perIslandCover` materialises a fresh `boolean[][]` per island; this is
|
||||
acceptable because the typical island count is small (≤ 10 on the
|
||||
upstream fixtures) and the allocation cost is dominated by the cover work
|
||||
inside.
|
||||
@@ -0,0 +1,67 @@
|
||||
# `solver/histogram/histogram.ts` — dual-axis maximal rectangles
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module enumerates every distinct maximal-rectangle candidate inside an
|
||||
opaque-pixel grid. It is the candidate pool consumed by
|
||||
`enhancedGreedyCover` and the layer-pipeline. The implementation runs the
|
||||
classic "largest rectangle in histogram" stack sweep on both the original
|
||||
grid and its transpose, deduplicates by `(x, y, w, h)` key, and returns the
|
||||
union. Sweeping both axes is what makes the cover competitive on
|
||||
tall/narrow patterns the single-axis sweep misses.
|
||||
|
||||
## Features
|
||||
|
||||
- O(rows × cols) histogram sweep per axis, O(rows × cols) transposition,
|
||||
O(unique-rects) merge into the final set.
|
||||
- Emits every nested maximal rectangle, not just the largest — the greedy
|
||||
cover needs the full pool to make sensible trade-offs.
|
||||
- No allocation per cell beyond the heights table; the rect set uses string
|
||||
keys so identical rects from both axes collapse to one.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { dualAxisMaximalRects } from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const available = [
|
||||
[true, true, false],
|
||||
[true, true, false],
|
||||
];
|
||||
const rects = dualAxisMaximalRects(available, 2, 3);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
dualAxisMaximalRects,
|
||||
enhancedGreedyCover,
|
||||
} from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const candidates = dualAxisMaximalRects(available, rows, cols);
|
||||
candidates.sort((a, b) => b.w * b.h - a.w * a.h);
|
||||
const cover = enhancedGreedyCover(candidates, required, cols, rows);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `dualAxisMaximalRects(available, rows, cols)` | fn | Deduplicated maximal rects across both sweep axes |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The internal `histogramSweep` builds a column-by-column heights table and
|
||||
then walks each row with a monotonic stack. When the stack pops, every
|
||||
height ≥ the popped column's height yields a distinct maximal rect of
|
||||
width `x - leftBound` and height `rectH`. We emit *every* nested height so
|
||||
the greedy cover can pick a smaller-but-better-positioned rect when the
|
||||
maximal-tall one would force an awkward residual. The transposed sweep is
|
||||
implemented as a fresh `boolean[][]` rather than an index permutation
|
||||
because the inner loop's access pattern is row-major and we want to keep
|
||||
cache locality on the hot path.
|
||||
84
docs/media/image/emblem-solver/src/solver/layer/layer.md
Normal file
84
docs/media/image/emblem-solver/src/solver/layer/layer.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# `solver/layer/layer.ts` — multi-strategy per-layer cover
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `processLayerMultiStrategy`, the function the converter
|
||||
calls once per layer in the current ordering. It runs every covering
|
||||
strategy the package implements (dual-axis histogram + greedy, per-island
|
||||
cover, strip decomposition, adjacent-rect merge) and returns the result
|
||||
with the fewest rectangles. Running every strategy is what makes the
|
||||
upstream algorithm robust — no single strategy wins on every emblem.
|
||||
|
||||
## Features
|
||||
|
||||
- Strategy A (dual-axis histogram → greedy → overlap optimise) is always
|
||||
evaluated; it is the workhorse.
|
||||
- Strategy B (per-island cover) only runs when the colour has more than
|
||||
one island — pure optimisation, since a single-island colour would
|
||||
produce the same answer.
|
||||
- Strategy C (strip decomposition) is only adopted when the strips fully
|
||||
cover the required pixels; the layer pipeline does not stitch in a
|
||||
partial strip cover.
|
||||
- Strategy D (adjacent-rect merge) runs against the best-so-far so we
|
||||
never reject a free merge.
|
||||
- The final winner is the smallest of all four (or three when B is
|
||||
skipped).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
analyseGrid,
|
||||
processLayerMultiStrategy,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
const a = analyseGrid(pixels, w, h);
|
||||
const ownPixels = a.colourGroups.get("255,0,0")!;
|
||||
const rects = processLayerMultiStrategy(
|
||||
"255,0,0",
|
||||
ownPixels,
|
||||
a,
|
||||
new Set<number>()
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { processLayerMultiStrategy } from "@suaveplan/emblem-solver";
|
||||
|
||||
const earlier = new Set<number>();
|
||||
for (const key of orderedKeys) {
|
||||
const ownPixels = analysis.colourGroups.get(key)!;
|
||||
const rects = processLayerMultiStrategy(
|
||||
key,
|
||||
ownPixels,
|
||||
analysis,
|
||||
earlier
|
||||
);
|
||||
layers.push({ key, rects /* ... */ });
|
||||
for (const p of ownPixels) earlier.add(p);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `processLayerMultiStrategy(key, ownPixels, analysis, earlier)` | fn | Smallest rect cover across all strategies |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The strip strategy only contributes when it fully covers the required
|
||||
mask. We compute coverage by intersecting each valid strip with the
|
||||
`required` grid (via `pack`) and comparing the resulting count to a
|
||||
single pre-tallied `totalRequired`. The adjacent-rect merge pass runs
|
||||
*after* the strategies have produced their winners; running it earlier
|
||||
would force a merge on results that would have been pruned anyway. The
|
||||
final reduce uses a strictly-less-than comparison (`a.length <= b.length`)
|
||||
so the first result wins on ties — this preserves deterministic ordering
|
||||
across runs.
|
||||
@@ -0,0 +1,74 @@
|
||||
# `solver/ordering/ordering.ts` — layer-ordering search primitives
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module provides the layer-ordering machinery the converter consults
|
||||
when it has to decide which colour sits on the bottom of the z-stack and
|
||||
which paints on top. `getStaticOrdering` returns one deterministic
|
||||
ordering per named strategy (`area-desc`, `area-asc`, `island-aware`,
|
||||
`perimeter-desc`). `permutations` lazily yields every n! ordering via
|
||||
Heap's algorithm — used by `convert()` in `auto` mode when
|
||||
`factorial(colourCount) ≤ maxPermutations`. `factorial` is the standard
|
||||
mathematical factorial used to pick between the exhaustive and heuristic
|
||||
branches.
|
||||
|
||||
## Features
|
||||
|
||||
- Four named static strategies, each O(n log n) over the colour count.
|
||||
- `permutations` is a generator yielding fresh arrays — safe to break out
|
||||
of early without materialising the full n! list.
|
||||
- `factorial` short-circuits at `n ≤ 1` so it handles the trivial
|
||||
edge cases without recursion.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
getStaticOrdering,
|
||||
permutations,
|
||||
factorial,
|
||||
} from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const order = getStaticOrdering(analysis, "area-desc");
|
||||
factorial(5); // 120
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { permutations } from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
let best = Infinity;
|
||||
let bestPerm: string[] | undefined;
|
||||
for (const perm of permutations(["r", "g", "b"])) {
|
||||
const score = scoreOrdering(perm);
|
||||
if (score < best) {
|
||||
best = score;
|
||||
bestPerm = [...perm];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `getStaticOrdering(analysis, strategy)` | fn | Sorted colour keys for a named strategy |
|
||||
| `permutations<T>(arr)` | gen | Yields every permutation (Heap's algorithm) |
|
||||
| `factorial(n)` | fn | `factorial(0) === factorial(1) === 1` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`islandAwareScore` mixes four signals: coverage (how much of the grid the
|
||||
colour occupies), occlusion benefit (the visual mass it removes from
|
||||
higher layers), island penalty (more islands ⇒ more rectangles needed),
|
||||
and hole ratio (bbox area / pixel count — large hole ratios suggest the
|
||||
colour sits below another). The coefficients (`2.0`, `3.0`, `1.5`, `0.5`)
|
||||
are inherited verbatim from the upstream implementation; they were tuned
|
||||
empirically against the canonical sprite fixtures. `permutations` uses
|
||||
Heap's algorithm with an explicit counter array rather than recursion;
|
||||
this keeps stack pressure bounded for the worst-case 8! enumeration.
|
||||
64
docs/media/image/emblem-solver/src/solver/strip/strip.md
Normal file
64
docs/media/image/emblem-solver/src/solver/strip/strip.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `solver/strip/strip.ts` — horizontal/vertical strip decomposition
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module implements `stripDecomposition`, a complementary cover strategy
|
||||
that ignores the maximal-rect candidate pool entirely. It finds maximal
|
||||
horizontal runs of required pixels, merges vertically-adjacent runs that
|
||||
share both `x` and `length`, and runs the transposed variant (vertical runs
|
||||
merged horizontally). The smaller of the two orientations wins. This
|
||||
strategy is what gives `convert()` the right answer on tall thin shapes
|
||||
that the histogram + greedy combo fragments.
|
||||
|
||||
## Features
|
||||
|
||||
- O(rows × cols) per axis; tail-call-free `while` loops.
|
||||
- Two orientations evaluated, smaller wins — deterministic.
|
||||
- Produces non-overlapping rects (unlike the overlap optimiser); the layer
|
||||
pipeline only adopts the strip result if it covers the entire required
|
||||
mask, otherwise it falls back to the greedy cover.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { stripDecomposition } from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const required = [
|
||||
[true, true, true],
|
||||
[true, true, true],
|
||||
];
|
||||
const rects = stripDecomposition(required, 2, 3);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
stripDecomposition,
|
||||
isFullyAvailable,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
const strips = stripDecomposition(required, rows, cols);
|
||||
const valid = strips.filter((r) => isFullyAvailable(r, available));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `stripDecomposition(required, rows, cols)` | fn | Smaller of horizontal-strips / vertical-strips |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`mergeRuns` sorts runs by their primary coordinate, then their length, then
|
||||
their secondary coordinate. The merge loop walks the sorted list once and
|
||||
collapses consecutive entries that share `(x, length)` (horizontal) or
|
||||
`(y, length)` (vertical) into a single taller/wider rect. Because the sort
|
||||
is stable on JavaScript's `Array.prototype.sort` and we break on a length
|
||||
mismatch, the merge terminates in O(n). The dual-orientation evaluation
|
||||
ensures we never make the cover worse by picking the wrong axis — the
|
||||
chess-board test exercises this branch heavily.
|
||||
61
docs/media/image/emblem-solver/src/svg/build/build.md
Normal file
61
docs/media/image/emblem-solver/src/svg/build/build.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# `svg/build/build.ts` — SVG serialiser
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `buildSVG`, the deterministic SVG serialiser the
|
||||
converter uses to produce its final `<svg>` string. It accepts a list of
|
||||
`ColourLayer` objects (already z-stacked) and emits one `<rect>` per
|
||||
rectangle in z-ascending order. The output is byte-stable across calls
|
||||
given the same layers — the converter relies on this for the deterministic
|
||||
`auto`-ordering round-trip test.
|
||||
|
||||
## Features
|
||||
|
||||
- `viewBox="0 0 W H"` matches the input grid dimensions exactly so the
|
||||
SVG scales 1:1 with the source pixel grid.
|
||||
- `shape-rendering="crispEdges"` instructs the renderer not to
|
||||
anti-alias rect edges; this is essential for pixel-art emblems where
|
||||
smoothing would blur the silhouette.
|
||||
- Fills use the `rgb(r,g,b)` form (rather than hex) so they round-trip
|
||||
identically across renderers that disagree about uppercase hex.
|
||||
- Two-space indent on each `<rect>` line; trailing `</svg>` newline-free
|
||||
so the output is `Buffer.from(out).length` bytes regardless of platform
|
||||
line endings.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { buildSVG } from "@suaveplan/emblem-solver";
|
||||
|
||||
const svg = buildSVG(layers, width, height);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { buildSVG } from "@suaveplan/emblem-solver";
|
||||
|
||||
const reordered = layers.map((l, i) => ({ ...l, zIndex: i }));
|
||||
const svg = buildSVG(reordered, width, height);
|
||||
console.log(svg.length, "bytes");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `buildSVG(layers, w, h)` | fn | Serialise z-stacked layers to an SVG string |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function sorts a copy of the layer list (never the caller's reference)
|
||||
so callers can pass `result.layers` without losing the original ordering
|
||||
metadata. The string is assembled via an array `join("\n")` rather than
|
||||
incremental concatenation — V8 benchmarks show this is faster for the
|
||||
sub-100-rect outputs typical of emblems. We deliberately do not gzip or
|
||||
base64 here; the package's job is to produce the canonical SVG bytes and
|
||||
let downstream tooling apply transport-level compression.
|
||||
69
docs/media/image/emblem-solver/src/telemetry/telemetry.md
Normal file
69
docs/media/image/emblem-solver/src/telemetry/telemetry.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# `telemetry/telemetry.ts` — OpenTelemetry instruments
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the OpenTelemetry instruments emitted by `convert()`. It
|
||||
exposes a counter (`emblem_solver.convert.total`) and a histogram
|
||||
(`emblem_solver.convert.duration_ms`), both lazily initialised on first
|
||||
use so importing the package does not register instruments unless they
|
||||
are actually needed.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy registration — no instruments are created at module load; the
|
||||
first call to `getConvertCounter()` or `getConvertDuration()` reaches
|
||||
into `metrics.getMeter("@suaveplan/emblem-solver")` and caches the
|
||||
result.
|
||||
- Instruments are independent objects so a downstream OTel SDK can
|
||||
configure views per instrument.
|
||||
- Test helpers (`__resetTelemetryForTests`, `__peekTelemetryForTests`)
|
||||
let the test suite assert lazy-registration behaviour without forcing
|
||||
the production code to expose internals.
|
||||
- Histogram unit is `ms`; emitting an OTel view that converts to seconds
|
||||
is the consumer's responsibility.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
getConvertCounter,
|
||||
getConvertDuration,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
getConvertCounter().add(1);
|
||||
getConvertDuration().record(12.5);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { getConvertCounter } from "@suaveplan/emblem-solver";
|
||||
|
||||
metrics.setGlobalMeterProvider(myProvider);
|
||||
// the next call lazily creates the counter against `myProvider`'s meter:
|
||||
getConvertCounter().add(1, { layerOrder: "auto" });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `getConvertCounter()` | fn | Lazy `Counter` for `emblem_solver.convert.total` |
|
||||
| `getConvertDuration()` | fn | Lazy `Histogram` for `emblem_solver.convert.duration_ms` |
|
||||
| `__resetTelemetryForTests()` | fn | Test-only: drop cached instruments |
|
||||
| `__peekTelemetryForTests()` | fn | Test-only: inspect cached instruments |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `metrics.getMeter` call is deferred until first use so that a host
|
||||
that swaps the global meter provider after import still sees its
|
||||
provider's instruments. Without this, instruments would be bound to the
|
||||
no-op default and silently discard recordings. The test helpers are
|
||||
exported from the module (rather than placed in a separate `__internal`
|
||||
file) because they need to reach the module-scope `let` bindings; keeping
|
||||
them here avoids exposing those bindings through a different surface.
|
||||
92
docs/media/image/emblem-solver/src/types/types.md
Normal file
92
docs/media/image/emblem-solver/src/types/types.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# `types/types.ts` — public type contract
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the single source of truth for the public type surface of
|
||||
`@suaveplan/emblem-solver`. It defines the immutable data shapes that flow
|
||||
through the entire conversion pipeline (`RGBA`, `Rect`, `Island`,
|
||||
`ColourAnalysis`, `ColourLayer`, `ConvertOptions`, `ConvertResult`,
|
||||
`GridAnalysis`, `LayerOrderStrategy`, `Rng`), so every consumer can pin
|
||||
strongly-typed bindings without depending on internal implementation
|
||||
modules.
|
||||
|
||||
Types live in `types/` (per CLAUDE.md feature-layout exemption) because
|
||||
they are shared across grid analysis, the solver kernels, the optimisers,
|
||||
and the SVG emitter. Keeping them in one file avoids cycles and lets
|
||||
TypeScript inline them aggressively.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure data; zero runtime, zero side effects.
|
||||
- `RGBA.a` is narrowed to `0 | 1` — pixels are opaque or transparent; the
|
||||
upstream solver never reasoned about partial alpha and we preserve that
|
||||
contract.
|
||||
- `Rect` uses `{x, y, w, h}` (not `width/height`) to match the upstream
|
||||
on-the-wire layout.
|
||||
- `Island.pixels` is a `Set<number>` of packed indices to keep flood-fill
|
||||
inexpensive.
|
||||
- `ConvertOptions.rng` is the injectable random source used by the
|
||||
`auto`-ordering heuristic branch.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type { RGBA, ConvertOptions } from "@suaveplan/emblem-solver";
|
||||
|
||||
const px: RGBA = { r: 255, g: 0, b: 0, a: 1 };
|
||||
const opts: ConvertOptions = { layerOrder: "area-desc" };
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type {
|
||||
ColourAnalysis,
|
||||
ColourLayer,
|
||||
GridAnalysis,
|
||||
Rng,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
function pickBottomLayer(analysis: GridAnalysis): ColourAnalysis | undefined {
|
||||
return [...analysis.colourAnalysis.values()].sort(
|
||||
(a, b) => b.totalPixels - a.totalPixels
|
||||
)[0];
|
||||
}
|
||||
|
||||
const seeded: Rng = (() => {
|
||||
let s = 1;
|
||||
return () => {
|
||||
s = (s * 1664525 + 1013904223) >>> 0;
|
||||
return s / 0x1_0000_0000;
|
||||
};
|
||||
})();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Notes |
|
||||
|---|---|---|
|
||||
| `RGBA` | interface | `{ r, g, b, a: 0 \| 1 }` |
|
||||
| `Rect` | interface | `{ x, y, w, h }` |
|
||||
| `Island` | interface | `{ pixels: Set<number>, bounds, size }` |
|
||||
| `ColourAnalysis` | interface | Per-colour analysis (density, fragmentation, islands) |
|
||||
| `ColourLayer` | interface | A z-stacked layer with `rects` + `analysis` |
|
||||
| `LayerOrderStrategy` | union | `"auto" \| "area-desc" \| "area-asc" \| "island-aware" \| "perimeter-desc"` |
|
||||
| `Rng` | type | `() => number` in `[0, 1)` |
|
||||
| `ConvertOptions` | interface | Public input to `convert()` |
|
||||
| `ConvertResult` | interface | Public output of `convert()` |
|
||||
| `GridAnalysis` | interface | Diagnostic data returned by `analyseGrid` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Every interface marked `readonly` enforces the immutability contract at the
|
||||
type level — solver internals deliberately mutate the working sets they
|
||||
allocate; the public types do not advertise mutability. `ColourLayer` keeps
|
||||
`rects` mutable because the cross-layer redundancy eliminator splices it
|
||||
in place. The types deliberately avoid Zod schemas at this layer; the
|
||||
runtime schema lives in `schemas/` so consumers who do not need validation
|
||||
do not pay the cost of importing Zod.
|
||||
86
docs/media/image/exif/README.md
Normal file
86
docs/media/image/exif/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# @suaveplan/exif - Dependency-free, runtime-agnostic EXIF/XMP/IPTC metadata reader, writer, and stripper for JPEG and PNG images
|
||||
|
||||
Dependency-free, runtime-agnostic EXIF/XMP/IPTC metadata reader, writer, and stripper for JPEG and PNG images. Parses JPEG segment chains to locate and decode the EXIF IFD (TIFF byte-order aware, including GPS and SubIFD), IPTC IIM (caption, keywords, copyright), and XMP Dublin Core (raw XML string extraction). Writes back a revised EXIF IFD without re-encoding the image pixels, and strips all metadata segments for privacy-preserving image export. Includes a `ByteReader`/`ByteWriter` pair with rational arithmetic helpers and CRC-32 for PNG chunks.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [crc32](./src/binary/crc32/crc32.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [crc32](./src/binary/crc32/crc32.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [rational](./src/binary/rational/rational.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [rational](./src/binary/rational/rational.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [reader](./src/binary/reader/byte-reader.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [byte-reader](./src/binary/reader/byte-reader.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [writer](./src/binary/writer/byte-writer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [byte-writer](./src/binary/writer/byte-writer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [read-metadata](./src/core/read-metadata.md) — The main entry point for reading image metadata. Orchestrates format detection, JPEG segment parsing, and EXIF/XMP/IPTC…
|
||||
- [strip-metadata](./src/core/strip-metadata.md) — Removes metadata segments from a JPEG image buffer and returns a clean image with only the pixel data intact. Privacy-s…
|
||||
- [write-metadata](./src/core/write-metadata.md) — Writes or updates EXIF metadata in a JPEG image buffer. Merges the provided patch over any existing EXIF data and rebui…
|
||||
- [errors](./src/errors/exif-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [exif-error](./src/errors/exif-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [exif](./src/exif/read-exif.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [read-exif](./src/exif/read-exif.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [format](./src/format/detect.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [detect](./src/format/detect.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [ifd](./src/ifd/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [parse](./src/ifd/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [iptc](./src/iptc/read-iptc.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [read-iptc](./src/iptc/read-iptc.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [jpeg](./src/jpeg/segments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [segments](./src/jpeg/segments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [xmp](./src/xmp/read-xmp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [read-xmp](./src/xmp/read-xmp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/exif
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { readMetadata, writeMetadata, stripMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Read all metadata
|
||||
const meta = readMetadata(imageBuffer);
|
||||
console.log(meta.format); // "jpeg"
|
||||
console.log(meta.exif?.make); // "Canon"
|
||||
console.log(meta.exif?.gps?.latitude);
|
||||
|
||||
// Write / update metadata
|
||||
const updated = writeMetadata(imageBuffer, { orientation: 1, software: "My App" });
|
||||
|
||||
// Strip all metadata for privacy
|
||||
const clean = stripMetadata(imageBuffer);
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/core/read-metadata.md`](./src/core/read-metadata.md) — Main entry point for reading image metadata
|
||||
- [`src/core/write-metadata.md`](./src/core/write-metadata.md) — Write or update EXIF metadata in JPEG buffers
|
||||
- [`src/core/strip-metadata.md`](./src/core/strip-metadata.md) — Remove metadata segments for privacy-safe export
|
||||
- [`src/binary/reader/byte-reader.md`](./src/binary/reader/byte-reader.md) — `ByteReader` with endianness support
|
||||
- [`src/binary/writer/byte-writer.md`](./src/binary/writer/byte-writer.md) — `ByteWriter` for TIFF reconstruction
|
||||
- [`src/binary/rational/rational.md`](./src/binary/rational/rational.md) — Rational number arithmetic helpers
|
||||
- [`src/binary/crc32/crc32.md`](./src/binary/crc32/crc32.md) — CRC-32 for PNG chunk validation
|
||||
- [`src/format/detect.md`](./src/format/detect.md) — Magic-byte image format detection
|
||||
- [`src/jpeg/segments.md`](./src/jpeg/segments.md) — JPEG segment chain parsing
|
||||
- [`src/ifd/parse.md`](./src/ifd/parse.md) — TIFF IFD parsing (GPS, SubIFD)
|
||||
- [`src/exif/read-exif.md`](./src/exif/read-exif.md) — Raw EXIF/TIFF buffer parsing
|
||||
- [`src/iptc/read-iptc.md`](./src/iptc/read-iptc.md) — IPTC IIM caption/keywords/copyright parsing
|
||||
- [`src/xmp/read-xmp.md`](./src/xmp/read-xmp.md) — XMP Dublin Core raw XML extraction
|
||||
- [`src/errors/exif-error.md`](./src/errors/exif-error.md) — `ExifError` and `EXIF_CODES` registry
|
||||
|
||||
## Subpath Exports
|
||||
|
||||
| Subpath | Description |
|
||||
| --- | --- |
|
||||
| `@suaveplan/exif` | Main API: readMetadata, writeMetadata, stripMetadata |
|
||||
| `@suaveplan/exif/binary` | ByteReader, ByteWriter, rational utilities, CRC-32 |
|
||||
| `@suaveplan/exif/format` | detectImageFormat — magic byte detection |
|
||||
| `@suaveplan/exif/exif` | readExifFromTiffBuffer — raw TIFF/EXIF parsing |
|
||||
| `@suaveplan/exif/iptc` | readIptc — IPTC IIM parsing |
|
||||
| `@suaveplan/exif/xmp` | readXmp — XMP Dublin Core parsing |
|
||||
| `@suaveplan/exif/errors` | ExifError, EXIF_CODES |
|
||||
55
docs/media/image/exif/src/binary/crc32/crc32.md
Normal file
55
docs/media/image/exif/src/binary/crc32/crc32.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# CRC-32
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the standard IEEE 802.3 CRC-32 algorithm (polynomial `0xEDB88320`, reflected) used for PNG chunk integrity verification. The implementation is a pure TypeScript, lookup-table-based computation that works in any JavaScript runtime without native bindings or Node.js built-ins.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-buffer `crc32()` for straightforward integrity checks
|
||||
- Multi-chunk `crc32Multi()` that avoids buffer concatenation allocation
|
||||
- Pre-computed 256-entry lookup table for O(n) throughput
|
||||
- Returns unsigned 32-bit integer (safe for comparison with `>>>0` normalised values)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { crc32 } from "@suaveplan/exif/binary";
|
||||
|
||||
// Standard check value: crc32("123456789") === 0xCBF43926
|
||||
const encoder = new TextEncoder();
|
||||
const checkValue = crc32(encoder.encode("123456789"));
|
||||
console.log(checkValue.toString(16)); // "cbf43926"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { crc32Multi } from "@suaveplan/exif/binary";
|
||||
|
||||
// PNG chunk CRC: covers chunk type bytes + chunk data bytes
|
||||
function pngChunkCrc(type: Uint8Array, data: Uint8Array): number {
|
||||
return crc32Multi([type, data]);
|
||||
}
|
||||
|
||||
const typeBytes = new TextEncoder().encode("IHDR");
|
||||
const ihdrData = new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 0, 0, 0]);
|
||||
const checksum = pngChunkCrc(typeBytes, ihdrData);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `crc32(data: Uint8Array): number`
|
||||
|
||||
Computes the CRC-32 of `data`. Returns an unsigned 32-bit integer. The standard test vector is: `crc32(new TextEncoder().encode("123456789")) === 0xCBF43926`.
|
||||
|
||||
### `crc32Multi(chunks: Uint8Array[]): number`
|
||||
|
||||
Computes the CRC-32 of all `chunks` as if they were concatenated, without allocating a combined buffer. Returns an unsigned 32-bit integer. Equivalent to `crc32(concat(...chunks))`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The lookup table is built once at module load time using the standard reflected polynomial `0xEDB88320` (the bit-reversal of `0x04C11DB7`). Each byte of input XORs into the low 8 bits of the running CRC, selects a table entry, then is combined with the shifted CRC via XOR. The final `>>> 0` ensures the result is a non-negative JavaScript number even when the high bit is set. `crc32Multi` avoids buffer allocation by maintaining the running CRC across chunk boundaries.
|
||||
67
docs/media/image/exif/src/binary/rational/rational.md
Normal file
67
docs/media/image/exif/src/binary/rational/rational.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Rational
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides conversion and formatting utilities for TIFF RATIONAL values — pairs of integers representing numerator and denominator. EXIF metadata stores many camera settings (exposure time, f-number, focal length, GPS coordinates) as RATIONAL pairs, so correct round-trip conversion between floating-point numbers and rational pairs is essential for accurate metadata reading and writing.
|
||||
|
||||
## Features
|
||||
|
||||
- `numberToRational` — floating-point to rational via Stern-Brocot tree approximation with configurable max denominator
|
||||
- `rationalToNumber` — rational to float with zero-denominator guard
|
||||
- `rationalToString` — human-readable "n/d" formatting
|
||||
- `simplifyRational` — GCD reduction to lowest terms with sign normalisation
|
||||
- Handles negative values and zero correctly
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { numberToRational, rationalToNumber, rationalToString } from "@suaveplan/exif/binary";
|
||||
|
||||
const r = numberToRational(0.01); // exposure 1/100 s
|
||||
console.log(rationalToString(r)); // "1/100"
|
||||
console.log(rationalToNumber(r)); // 0.01
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { numberToRational, simplifyRational, rationalToString } from "@suaveplan/exif/binary";
|
||||
|
||||
// Limit denominator for compact storage
|
||||
const focalLength = numberToRational(50.0, 100);
|
||||
console.log(rationalToString(focalLength)); // "50/1"
|
||||
|
||||
// GPS coordinate degree representation
|
||||
const degrees = numberToRational(40.7128, 1_000_000);
|
||||
const simple = simplifyRational(degrees);
|
||||
console.log(rationalToString(simple));
|
||||
|
||||
// Simplify a raw EXIF rational
|
||||
const raw = { numerator: 6, denominator: 4 };
|
||||
console.log(rationalToString(simplifyRational(raw))); // "3/2"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `numberToRational(n: number, maxDenominator?: number): Rational`
|
||||
|
||||
Converts a floating-point number to the closest `Rational` with denominator ≤ `maxDenominator` (default `1_000_000`). Uses the Stern-Brocot tree / mediant algorithm to guarantee the best approximation for the given denominator bound. Throws `ExifError(INVALID_DATA)` for non-finite inputs.
|
||||
|
||||
### `rationalToNumber(r: Rational): number`
|
||||
|
||||
Returns `numerator / denominator`. Returns `0` when `denominator === 0` to avoid propagating `NaN` or `Infinity` into metadata fields.
|
||||
|
||||
### `rationalToString(r: Rational): string`
|
||||
|
||||
Returns the string `"${numerator}/${denominator}"`, e.g. `"1/100"`.
|
||||
|
||||
### `simplifyRational(r: Rational): Rational`
|
||||
|
||||
Reduces `r` to lowest terms using Euclidean GCD. Normalises negative denominators to the numerator. Returns `{ numerator: 0, denominator: 1 }` when numerator is zero.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Stern-Brocot tree search is bounded by a maximum of 1000 iterations in addition to the denominator limit, ensuring termination even for pathological inputs. The mediant criterion (`|medVal - abs| < 1e-10`) provides exact representation for most photographic fractions such as `1/100`, `1/250`, `2.8`, `50.0`. The GCD in `simplifyRational` uses the iterative Euclidean algorithm to avoid stack overflow on large inputs.
|
||||
117
docs/media/image/exif/src/binary/reader/byte-reader.md
Normal file
117
docs/media/image/exif/src/binary/reader/byte-reader.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# ByteReader
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a low-level, bounds-checked binary reader for TIFF/EXIF byte streams. It wraps a `Uint8Array` with a configurable byte order and exposes typed read methods for all TIFF primitive types. The runtime-agnostic implementation avoids Node.js `Buffer` and browser-specific APIs, relying solely on `DataView` and standard typed arrays.
|
||||
|
||||
## Features
|
||||
|
||||
- Big-endian and little-endian support with runtime `setByteOrder()` switching
|
||||
- Full TIFF primitive coverage: uint8/16/32, int16/32, float32/64, rational, signed rational
|
||||
- Bounds-checked reads that throw `ExifError` with `EXIF.PARSE_ERROR` on overflow
|
||||
- String reads with null-termination handling (`readString`, `readNullTermString`)
|
||||
- `slice()` factory that creates a sub-reader sharing the same byte order
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ByteReader } from "@suaveplan/exif/binary";
|
||||
|
||||
const data = new Uint8Array([0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08]);
|
||||
const reader = new ByteReader(data, "BE");
|
||||
|
||||
const magic = reader.readUint16(2); // 0x002A
|
||||
const ifdOffset = reader.readUint32(4); // 8
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ByteReader } from "@suaveplan/exif/binary";
|
||||
|
||||
// Switch byte order mid-stream (e.g. after reading TIFF header)
|
||||
const reader = new ByteReader(tiffBuffer, "BE");
|
||||
const bom = reader.readUint16(0);
|
||||
if (bom === 0x4949) reader.setByteOrder("LE"); // Intel = little-endian
|
||||
|
||||
// Read a RATIONAL (8 bytes: numerator + denominator)
|
||||
const exposureTime = reader.readRational(offset);
|
||||
console.log(`${exposureTime.numerator}/${exposureTime.denominator}`);
|
||||
|
||||
// Sub-slice for IFD parsing
|
||||
const ifdReader = reader.slice(ifdOffset, ifdOffset + ifdSize);
|
||||
const entryCount = ifdReader.readUint16(0);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `constructor(data: Uint8Array, byteOrder?: ByteOrder): ByteReader`
|
||||
|
||||
Creates a new reader wrapping `data`. Default `byteOrder` is `"BE"` (big-endian).
|
||||
|
||||
### `setByteOrder(order: ByteOrder): void`
|
||||
|
||||
Switches the active byte order for all subsequent reads.
|
||||
|
||||
### `get length: number`
|
||||
|
||||
Returns the total byte length of the underlying buffer.
|
||||
|
||||
### `readUint8(offset: number): number`
|
||||
|
||||
Reads one unsigned byte at `offset`.
|
||||
|
||||
### `readUint16(offset: number): number`
|
||||
|
||||
Reads a 2-byte unsigned integer using the active byte order.
|
||||
|
||||
### `readUint32(offset: number): number`
|
||||
|
||||
Reads a 4-byte unsigned integer using the active byte order.
|
||||
|
||||
### `readInt16(offset: number): number`
|
||||
|
||||
Reads a 2-byte signed integer using the active byte order.
|
||||
|
||||
### `readInt32(offset: number): number`
|
||||
|
||||
Reads a 4-byte signed integer using the active byte order.
|
||||
|
||||
### `readFloat32(offset: number): number`
|
||||
|
||||
Reads a 4-byte IEEE 754 single-precision float using the active byte order.
|
||||
|
||||
### `readFloat64(offset: number): number`
|
||||
|
||||
Reads an 8-byte IEEE 754 double-precision float using the active byte order.
|
||||
|
||||
### `readBytes(offset: number, length: number): Uint8Array`
|
||||
|
||||
Returns a copy of `length` bytes starting at `offset`.
|
||||
|
||||
### `readString(offset: number, length: number): string`
|
||||
|
||||
Reads `length` bytes as a Latin-1 string, stopping at the first null byte.
|
||||
|
||||
### `readNullTermString(offset: number): string`
|
||||
|
||||
Scans forward from `offset` until a null byte, then reads the preceding bytes as a string.
|
||||
|
||||
### `readRational(offset: number): Rational`
|
||||
|
||||
Reads an 8-byte TIFF RATIONAL (two unsigned 32-bit integers: numerator then denominator).
|
||||
|
||||
### `readSRational(offset: number): Rational`
|
||||
|
||||
Reads an 8-byte TIFF SRATIONAL (two signed 32-bit integers).
|
||||
|
||||
### `slice(start: number, end: number): ByteReader`
|
||||
|
||||
Returns a new `ByteReader` over `data.slice(start, end)` with the same byte order.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All read methods call `assertBounds` before touching `DataView` to ensure clear error messages on malformed input. The `view` is constructed from the array's `buffer`, `byteOffset`, and `byteLength` to correctly handle sliced `Uint8Array` inputs that do not start at offset 0 in their underlying `ArrayBuffer`. The `readString` and `readNullTermString` methods use Latin-1 (one byte per character) which matches TIFF/EXIF ASCII strings; UTF-8 decoding is not attempted here as EXIF ASCII tags are defined as 7-bit ASCII.
|
||||
100
docs/media/image/exif/src/binary/writer/byte-writer.md
Normal file
100
docs/media/image/exif/src/binary/writer/byte-writer.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# ByteWriter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a dynamic binary writer for building TIFF/EXIF byte streams from scratch. It maintains an auto-growing `Uint8Array` buffer and exposes typed write methods for all TIFF primitive types. This is the write-side counterpart to `ByteReader`, sharing the same byte-order model and error infrastructure.
|
||||
|
||||
## Features
|
||||
|
||||
- Auto-growing buffer (doubles capacity on overflow, no pre-allocation required)
|
||||
- Big-endian and little-endian support with runtime `setByteOrder()` switching
|
||||
- Full TIFF primitive coverage: uint8/16/32, int16, rational, raw bytes, strings
|
||||
- Fluent API — all write methods return `this` for chaining
|
||||
- `toUint8Array()` snapshot of the written bytes at any point
|
||||
- `ExifError` with `EXIF.WRITE_ERROR` on non-Latin-1 string characters
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ByteWriter } from "@suaveplan/exif/binary";
|
||||
|
||||
const writer = new ByteWriter(64, "BE");
|
||||
writer.writeUint16(0x4D4D); // big-endian BOM
|
||||
writer.writeUint16(0x002A); // TIFF magic
|
||||
writer.writeUint32(8); // IFD0 offset
|
||||
|
||||
const bytes = writer.toUint8Array();
|
||||
// Uint8Array [ 0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08 ]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ByteWriter } from "@suaveplan/exif/binary";
|
||||
|
||||
// Fluent chaining
|
||||
const exifHeader = new ByteWriter()
|
||||
.setByteOrder("LE")
|
||||
.writeBytes(new Uint8Array([0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) // "Exif\0\0"
|
||||
.writeUint16(0x4949) // LE BOM
|
||||
.writeUint16(0x002A)
|
||||
.writeUint32(8)
|
||||
.toUint8Array();
|
||||
|
||||
// Writing a RATIONAL (e.g. exposure time 1/100)
|
||||
const writer = new ByteWriter();
|
||||
writer.writeRational({ numerator: 1, denominator: 100 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `constructor(initialSize?: number, byteOrder?: ByteOrder): ByteWriter`
|
||||
|
||||
Creates a new writer. `initialSize` defaults to 256 bytes. `byteOrder` defaults to `"BE"`.
|
||||
|
||||
### `get position: number`
|
||||
|
||||
Returns the current write cursor position (total bytes written so far).
|
||||
|
||||
### `setByteOrder(order: ByteOrder): this`
|
||||
|
||||
Switches the active byte order. Returns `this` for chaining.
|
||||
|
||||
### `writeUint8(value: number): this`
|
||||
|
||||
Writes one unsigned byte.
|
||||
|
||||
### `writeUint16(value: number): this`
|
||||
|
||||
Writes a 2-byte unsigned integer using the active byte order.
|
||||
|
||||
### `writeUint32(value: number): this`
|
||||
|
||||
Writes a 4-byte unsigned integer using the active byte order.
|
||||
|
||||
### `writeInt16(value: number): this`
|
||||
|
||||
Writes a 2-byte signed integer using the active byte order.
|
||||
|
||||
### `writeBytes(bytes: Uint8Array): this`
|
||||
|
||||
Appends a raw byte array.
|
||||
|
||||
### `writeString(str: string): this`
|
||||
|
||||
Writes a Latin-1 string without null termination. Throws `ExifError(WRITE_ERROR)` if any character is outside the Latin-1 range (> U+00FF).
|
||||
|
||||
### `writeRational(r: Rational): this`
|
||||
|
||||
Writes an 8-byte TIFF RATIONAL (two unsigned 32-bit integers: numerator then denominator).
|
||||
|
||||
### `toUint8Array(): Uint8Array`
|
||||
|
||||
Returns a copy of all bytes written so far (from position 0 to the current cursor).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The buffer starts at `initialSize` and doubles whenever the next write would overflow. This amortizes allocation cost to O(log n) reallocations for n total bytes written. The `view` is recreated after each reallocation since `DataView` binds to a specific `ArrayBuffer` instance. `toUint8Array()` always returns a fresh slice to prevent the caller from holding a reference to the internal buffer.
|
||||
65
docs/media/image/exif/src/core/read-metadata.md
Normal file
65
docs/media/image/exif/src/core/read-metadata.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# readMetadata
|
||||
|
||||
## Purpose
|
||||
|
||||
The main entry point for reading image metadata. Orchestrates format detection, JPEG segment parsing, and EXIF/XMP/IPTC decoding into a unified `ImageMetadata` result. Callers do not need to interact with the lower-level parsers directly — this function handles the full pipeline from raw `Uint8Array` to structured metadata.
|
||||
|
||||
## Features
|
||||
|
||||
- Auto-detects image format via magic bytes
|
||||
- For JPEG: parses all APP segments and dispatches to EXIF, XMP, and IPTC readers
|
||||
- For PNG: reads IHDR chunk dimensions
|
||||
- `ReadOptions` to skip individual metadata types for performance
|
||||
- Throws `ExifError(UNSUPPORTED_FORMAT)` for completely unrecognised formats
|
||||
- Gracefully skips malformed EXIF segments without throwing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readMetadata } from "@suaveplan/exif";
|
||||
|
||||
const metadata = readMetadata(imageBuffer);
|
||||
console.log(metadata.format); // "jpeg"
|
||||
console.log(metadata.exif?.make); // "Canon"
|
||||
console.log(metadata.exif?.dateTimeOriginal);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { readMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Skip XMP for faster processing
|
||||
const metadata = readMetadata(imageBuffer, { skipXmp: true });
|
||||
|
||||
if (metadata.exif?.gps?.latitude) {
|
||||
const { degrees, minutes, seconds, ref } = metadata.exif.gps.latitude;
|
||||
const decimal = (degrees + minutes / 60 + seconds / 3600) * (ref === "S" ? -1 : 1);
|
||||
console.log("GPS lat:", decimal.toFixed(6));
|
||||
}
|
||||
|
||||
if (metadata.iptc?.keywords) {
|
||||
console.log("Keywords:", metadata.iptc.keywords.join(", "));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `readMetadata(buffer: Uint8Array, options?: ReadOptions): ImageMetadata`
|
||||
|
||||
Reads all available metadata from `buffer`.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `buffer` — raw image bytes
|
||||
- `options.skipExif` — skip EXIF APP1 parsing (default: false)
|
||||
- `options.skipIptc` — skip IPTC APP13 parsing (default: false)
|
||||
- `options.skipXmp` — skip XMP APP1 parsing (default: false)
|
||||
|
||||
**Returns:** `ImageMetadata` with `format` always set, and optional `exif`, `iptc`, `xmp`, `width`, `height`.
|
||||
|
||||
**Throws:** `ExifError(UNSUPPORTED_FORMAT)` if `detectImageFormat` returns `"unknown"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The EXIF APP1 segment begins with a 6-byte `"Exif\0\0"` prefix before the TIFF header; this is stripped before calling `readExifFromTiffBuffer`. The XMP APP1 segment begins with the null-terminated namespace URI `"http://ns.adobe.com/xap/1.0/\0"` before the XML payload. Image dimensions are extracted from EXIF (imageWidth/imageHeight fields) when present and from the PNG IHDR chunk as a fallback. Malformed EXIF data is silently skipped to avoid crashing on images with corrupt metadata.
|
||||
60
docs/media/image/exif/src/core/strip-metadata.md
Normal file
60
docs/media/image/exif/src/core/strip-metadata.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# stripMetadata
|
||||
|
||||
## Purpose
|
||||
|
||||
Removes metadata segments from a JPEG image buffer and returns a clean image with only the pixel data intact. Privacy-sensitive metadata (GPS location, camera serial numbers, timestamps) is removed without re-encoding or degrading the image quality. The JPEG stream is rebuilt by concatenating only the non-metadata segments with the original compressed image data.
|
||||
|
||||
## Features
|
||||
|
||||
- Strips EXIF APP1 segments (camera, GPS, orientation metadata)
|
||||
- Strips XMP APP1 segments (Adobe editorial metadata)
|
||||
- Strips IPTC APP13 segments (Photoshop 3.0 news/editorial metadata)
|
||||
- Each strip operation is independently controllable via `StripOptions`
|
||||
- Non-destructive: compressed image data (SOS + payload + EOI) is preserved byte-for-byte
|
||||
- Throws `ExifError(UNSUPPORTED_FORMAT)` for non-JPEG inputs
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { stripMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Strip all metadata (default behaviour)
|
||||
const clean = stripMetadata(jpegBuffer);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { stripMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Keep IPTC (editorial attribution) but remove GPS and XMP
|
||||
const stripped = stripMetadata(jpegBuffer, {
|
||||
stripExif: true,
|
||||
stripXmp: true,
|
||||
stripIptc: false,
|
||||
});
|
||||
|
||||
// Verify the result is still a valid JPEG
|
||||
console.log(stripped[0] === 0xFF && stripped[1] === 0xD8); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `stripMetadata(buffer: Uint8Array, options?: StripOptions): Uint8Array`
|
||||
|
||||
Strips metadata from a JPEG buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `buffer` — raw JPEG image bytes
|
||||
- `options.stripExif` — strip EXIF APP1 segments (default: `true`)
|
||||
- `options.stripIptc` — strip IPTC APP13 segments (default: `true`)
|
||||
- `options.stripXmp` — strip XMP APP1 segments (default: `true`)
|
||||
|
||||
**Returns:** New `Uint8Array` with the specified segments removed.
|
||||
|
||||
**Throws:** `ExifError(UNSUPPORTED_FORMAT)` if `buffer` is not a JPEG image.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function calls `parseJpegSegments` to obtain the structural segment list, then rebuilds the JPEG by writing the SOI marker, each non-stripped segment (marker + length + data), and finally the raw SOS-onward bytes from the original buffer. The SOS segment and all subsequent compressed data are appended verbatim — no re-encoding occurs. Default options strip all three metadata types (EXIF, XMP, IPTC) to provide the safest default for privacy-sensitive use cases. Passing `{ stripExif: false, stripIptc: false, stripXmp: false }` is a no-op that returns a structurally identical (though freshly allocated) buffer.
|
||||
64
docs/media/image/exif/src/core/write-metadata.md
Normal file
64
docs/media/image/exif/src/core/write-metadata.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# writeMetadata
|
||||
|
||||
## Purpose
|
||||
|
||||
Writes or updates EXIF metadata in a JPEG image buffer. Merges the provided patch over any existing EXIF data and rebuilds the APP1 segment, inserting one if the image does not already have EXIF. The compressed image data is preserved byte-for-byte — no re-encoding occurs.
|
||||
|
||||
## Features
|
||||
|
||||
- Merges patch fields over existing EXIF data when an APP1 segment is present
|
||||
- Inserts a new APP1 segment when no EXIF exists
|
||||
- Writes IFD0 fields: Make, Model, Software, DateTime, Orientation, ImageWidth, ImageHeight
|
||||
- Uses big-endian (Motorola) TIFF format for maximum compatibility
|
||||
- Rebuilds valid JPEG stream with correct APP1 marker and length
|
||||
- Throws `ExifError(UNSUPPORTED_FORMAT)` for non-JPEG inputs
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { writeMetadata } from "@suaveplan/exif";
|
||||
|
||||
const updated = writeMetadata(jpegBuffer, {
|
||||
software: "My App 1.0",
|
||||
orientation: 1,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { writeMetadata, readMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Read → patch → write round-trip
|
||||
const before = readMetadata(jpegBuffer);
|
||||
const after = writeMetadata(jpegBuffer, {
|
||||
...before.exif,
|
||||
orientation: 6, // rotate 90° CW
|
||||
software: "Suaveplan Processor",
|
||||
});
|
||||
|
||||
const verify = readMetadata(after);
|
||||
console.log(verify.exif?.orientation); // 6
|
||||
console.log(verify.exif?.software); // "Suaveplan Processor"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `writeMetadata(buffer: Uint8Array, patch: Partial<ExifData>): Uint8Array`
|
||||
|
||||
Updates EXIF metadata in a JPEG buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `buffer` — raw JPEG image bytes
|
||||
- `patch` — `ExifData` fields to set. Fields not included in `patch` retain their existing values from the original EXIF (if present).
|
||||
|
||||
**Returns:** New `Uint8Array` containing the updated JPEG.
|
||||
|
||||
**Throws:** `ExifError(UNSUPPORTED_FORMAT)` if `buffer` is not a JPEG image.
|
||||
|
||||
**Supported fields:** `make`, `model`, `software`, `dateTime`, `orientation`, `imageWidth`, `imageHeight`. GPS and ExifIFD sub-IFD writing are not yet supported.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The TIFF stream is always written in big-endian (Motorola, `MM`) format for maximum compatibility with reading software. IFD entries are sorted by tag number as required by the TIFF specification. String values (ASCII type) are null-terminated; values longer than 3 characters are stored in the data area rather than inline. The rebuilt JPEG inserts the new APP1 immediately after the SOI marker (before any other APP segments), which is the canonical position for EXIF. Any existing EXIF APP1 segment is replaced to prevent duplicate segments.
|
||||
99
docs/media/image/exif/src/errors/exif-error.md
Normal file
99
docs/media/image/exif/src/errors/exif-error.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# ExifError
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides the stable error code registry and base error class for the `@suaveplan/exif` package. Every error thrown by the package uses `ExifError` with a code from `EXIF_CODES`, enabling programmatic error handling without string matching on error messages.
|
||||
|
||||
The design mirrors the `@suaveplan/error` pattern but keeps the package dependency-free: no runtime `@suaveplan/*` imports are needed.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable code registry via `EXIF_CODES` constant object
|
||||
- `ExifError` class with `code` and optional `cause` chaining
|
||||
- TypeScript `ExifErrorCode` union type for exhaustive switch statements
|
||||
- Four distinct codes covering all error surfaces: format detection, parsing, writing, and data validation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ExifError, EXIF_CODES } from "@suaveplan/exif/errors";
|
||||
|
||||
throw new ExifError("Buffer is not a JPEG or PNG image", EXIF_CODES.UNSUPPORTED_FORMAT);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ExifError, EXIF_CODES, type ExifErrorCode } from "@suaveplan/exif/errors";
|
||||
|
||||
function handleExifError(err: ExifError): void {
|
||||
switch (err.code as ExifErrorCode) {
|
||||
case "EXIF.UNSUPPORTED_FORMAT":
|
||||
console.warn("Unsupported image format:", err.message);
|
||||
break;
|
||||
case "EXIF.PARSE_ERROR":
|
||||
console.error("Parsing failed:", err.message, err.cause);
|
||||
break;
|
||||
case "EXIF.WRITE_ERROR":
|
||||
console.error("Write failed:", err.message);
|
||||
break;
|
||||
case "EXIF.INVALID_DATA":
|
||||
console.error("Invalid metadata:", err.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// With cause chaining
|
||||
try {
|
||||
const data = parseIfd(reader, 0);
|
||||
} catch (cause) {
|
||||
throw new ExifError("IFD parse failed at offset 0", EXIF_CODES.PARSE_ERROR, { cause });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EXIF_CODES`
|
||||
|
||||
```ts
|
||||
const EXIF_CODES: {
|
||||
UNSUPPORTED_FORMAT: "EXIF.UNSUPPORTED_FORMAT";
|
||||
PARSE_ERROR: "EXIF.PARSE_ERROR";
|
||||
WRITE_ERROR: "EXIF.WRITE_ERROR";
|
||||
INVALID_DATA: "EXIF.INVALID_DATA";
|
||||
}
|
||||
```
|
||||
|
||||
Stable string code registry. Use these constants rather than literal strings to guard against typos.
|
||||
|
||||
### `ExifErrorCode`
|
||||
|
||||
```ts
|
||||
type ExifErrorCode = "EXIF.UNSUPPORTED_FORMAT" | "EXIF.PARSE_ERROR" | "EXIF.WRITE_ERROR" | "EXIF.INVALID_DATA";
|
||||
```
|
||||
|
||||
Union type of all valid codes, suitable for exhaustive switches.
|
||||
|
||||
### `ExifError`
|
||||
|
||||
```ts
|
||||
class ExifError extends Error {
|
||||
readonly code: ExifErrorCode;
|
||||
constructor(message: string, code: ExifErrorCode, options?: { cause?: unknown }): ExifError;
|
||||
}
|
||||
```
|
||||
|
||||
- **message** — human-readable description of the error
|
||||
- **code** — one of the `EXIF_CODES` values for programmatic handling
|
||||
- **options.cause** — upstream error to chain (accessible via `err.cause`)
|
||||
|
||||
The `name` property is always `"ExifError"` for `instanceof`-free type narrowing via `err.name`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This module is intentionally minimal. It carries no runtime dependencies and exports only what is needed for error handling across the package. The `EXIF_CODES` object is declared `as const` so TypeScript narrows each property to its literal type, enabling the `ExifErrorCode` union to be derived without duplication.
|
||||
|
||||
`ExifError` extends `Error` directly rather than `@suaveplan/error`'s base class to keep the package fully dependency-free. This is a deliberate trade-off: the package is a leaf node in the dependency graph and should impose zero peer requirements on consumers.
|
||||
74
docs/media/image/exif/src/exif/read-exif.md
Normal file
74
docs/media/image/exif/src/exif/read-exif.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# readExifFromTiffBuffer
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Reads EXIF metadata from a raw TIFF byte buffer. Handles the full TIFF IFD chain: IFD0 for image-level tags, ExifIFD (0x8769) for camera settings, and GPS IFD (0x8825) for geolocation data. Supports both big-endian (Motorola, `MM`) and little-endian (Intel, `II`) TIFF byte orders.
|
||||
|
||||
## Features
|
||||
|
||||
- Reads both Motorola (big-endian) and Intel (little-endian) TIFF streams
|
||||
- Maps IFD0 tags: Make, Model, Software, DateTime, Orientation, ImageWidth, ImageHeight
|
||||
- Follows ExifIFD: ExposureTime, FNumber, ISO, FocalLength, FocalLengthIn35mm, Flash, WhiteBalance, ExposureMode, ExposureProgram, DateTimeOriginal, DateTimeDigitized
|
||||
- Follows GPS IFD: latitude, longitude, altitude, timestamp, datestamp, speed, direction
|
||||
- Returns all parsed tags in `rawTags` for custom access
|
||||
- Throws `ExifError(PARSE_ERROR)` on invalid TIFF header
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readExifFromTiffBuffer } from "@suaveplan/exif/exif";
|
||||
|
||||
// Buffer starts at TIFF header (after "Exif\0\0" prefix is stripped)
|
||||
const exif = readExifFromTiffBuffer(tiffBuffer);
|
||||
console.log(exif.make, exif.model);
|
||||
console.log(exif.exposureTime); // { numerator: 1, denominator: 100 }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { readExifFromTiffBuffer } from "@suaveplan/exif/exif";
|
||||
import { rationalToNumber } from "@suaveplan/exif/binary";
|
||||
|
||||
const exif = readExifFromTiffBuffer(tiffBuffer);
|
||||
|
||||
if (exif.gps?.latitude) {
|
||||
const { degrees, minutes, seconds, ref } = exif.gps.latitude;
|
||||
const decimal = degrees + minutes / 60 + seconds / 3600;
|
||||
const signed = ref === "S" ? -decimal : decimal;
|
||||
console.log("Latitude:", signed.toFixed(6));
|
||||
}
|
||||
|
||||
if (exif.exposureTime) {
|
||||
const shutter = rationalToNumber(exif.exposureTime);
|
||||
console.log(`Shutter: ${shutter}s`);
|
||||
}
|
||||
|
||||
// Access raw tag by number
|
||||
const customTag = exif.rawTags?.[0x013B]; // Artist tag
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `readExifFromTiffBuffer(buffer: Uint8Array): ExifData`
|
||||
|
||||
Parses EXIF metadata from a raw TIFF buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `buffer` — `Uint8Array` starting at the TIFF byte-order mark (`MM` or `II`), not at the JPEG `Exif\0\0` prefix
|
||||
|
||||
**Returns:** `ExifData` with all recognised tags populated.
|
||||
|
||||
**Throws:** `ExifError(PARSE_ERROR)` if:
|
||||
|
||||
- The buffer is shorter than 8 bytes
|
||||
- The byte-order mark is not `0x4D4D` (BE) or `0x4949` (LE)
|
||||
- The TIFF magic number is not `0x002A`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The 6-byte `"Exif\0\0"` prefix must be stripped before calling this function — the buffer must start at the TIFF header. The `readMetadata` orchestrator handles this stripping automatically. GPS coordinate rationals are expected to be an array of three RATIONAL values (degrees, minutes, seconds) as per the EXIF GPS IFD specification. The GPS timestamp is parsed from three RATIONAL values and formatted as `HH:MM:SS`. All collected IFD entries from both IFD0 and ExifIFD are merged into `rawTags` for consumer access.
|
||||
66
docs/media/image/exif/src/format/detect.md
Normal file
66
docs/media/image/exif/src/format/detect.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# detectImageFormat
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Identifies the image format of a raw byte buffer by inspecting its leading magic bytes. This is the entry point for all metadata operations in `@suaveplan/exif`: the correct parser is selected based on the detected format, so detection must be reliable before any other processing occurs.
|
||||
|
||||
The implementation avoids file extensions and MIME type strings — it reads only the binary signature, which is the only trustworthy signal for format identification.
|
||||
|
||||
## Features
|
||||
|
||||
- Detects JPEG (3-byte signature), PNG (8-byte signature), WebP (12-byte RIFF signature), and TIFF (both big-endian and little-endian variants)
|
||||
- Returns `'unknown'` rather than throwing for unrecognised formats
|
||||
- Zero dependencies — pure TypeScript with no external imports
|
||||
- Handles buffers shorter than the minimum signature length gracefully
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { detectImageFormat } from "@suaveplan/exif/format";
|
||||
|
||||
const jpegBytes = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0, ...]);
|
||||
console.log(detectImageFormat(jpegBytes)); // "jpeg"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { detectImageFormat } from "@suaveplan/exif/format";
|
||||
import { readMetadata } from "@suaveplan/exif";
|
||||
|
||||
async function processImage(buffer: Uint8Array) {
|
||||
const format = detectImageFormat(buffer);
|
||||
|
||||
switch (format) {
|
||||
case "jpeg":
|
||||
case "png":
|
||||
return readMetadata(buffer);
|
||||
case "webp":
|
||||
case "tiff":
|
||||
throw new Error(`Format ${format} metadata reading not yet supported`);
|
||||
case "unknown":
|
||||
throw new Error("Unrecognised image format");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `detectImageFormat(buffer: Uint8Array): ImageFormat`
|
||||
|
||||
Inspects the leading bytes of `buffer` and returns one of:
|
||||
|
||||
- `"jpeg"` — buffer starts with `FF D8 FF`
|
||||
- `"png"` — buffer starts with `89 50 4E 47 0D 0A 1A 0A`
|
||||
- `"webp"` — buffer starts with `RIFF` and bytes 8–11 are `WEBP`
|
||||
- `"tiff"` — buffer starts with `49 49 2A 00` (LE) or `4D 4D 00 2A` (BE)
|
||||
- `"unknown"` — none of the above match, or buffer is shorter than 4 bytes
|
||||
|
||||
Does not throw. All inputs are handled.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The magic-byte checks are ordered by specificity: JPEG and PNG checks come first because they are the primary supported formats. WebP requires a 12-byte buffer to read both the RIFF header and the WEBP brand. TIFF detection handles both byte orders since TIFF files start with their own byte-order mark. The function returns `"unknown"` for buffers shorter than 4 bytes rather than attempting a partial match, to avoid false positives on truncated data.
|
||||
86
docs/media/image/exif/src/ifd/parse.md
Normal file
86
docs/media/image/exif/src/ifd/parse.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# IFD Parse
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements TIFF IFD (Image File Directory) parsing, the core data structure used by EXIF to store metadata. Each IFD entry is a 12-byte record containing a tag number, data type, count, and either an inline value or an offset to data elsewhere in the TIFF stream. This module handles all 12 standard TIFF data types and provides typed accessor helpers for downstream EXIF, GPS, and sub-IFD readers.
|
||||
|
||||
## Features
|
||||
|
||||
- Parses all 12 TIFF primitive types: BYTE, ASCII, SHORT, LONG, RATIONAL, SBYTE, UNDEFINED, SSHORT, SLONG, SRATIONAL, FLOAT, DOUBLE
|
||||
- Inline vs. offset-stored value selection based on total data size
|
||||
- Graceful handling of malformed entries (skips rather than throwing)
|
||||
- Typed accessors: `getTagNumber`, `getTagString`, `getTagRational`, `getTagRationalArray`
|
||||
- Bounds-checked with `ExifError(PARSE_ERROR)` on structural violations
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ByteReader } from "@suaveplan/exif/binary";
|
||||
import { parseIfd, getTagString } from "@suaveplan/exif";
|
||||
|
||||
const reader = new ByteReader(tiffBuffer, "BE");
|
||||
const ifd0 = parseIfd(reader, 8);
|
||||
|
||||
const make = getTagString(ifd0, 0x010F); // Make tag
|
||||
const model = getTagString(ifd0, 0x0110); // Model tag
|
||||
console.log(`${make} ${model}`);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ByteReader } from "@suaveplan/exif/binary";
|
||||
import { parseIfd, getTagNumber, getTagRational } from "@suaveplan/exif";
|
||||
|
||||
const reader = new ByteReader(tiffBuffer, "LE");
|
||||
|
||||
// Parse IFD0 then follow ExifIFD pointer
|
||||
const ifd0 = parseIfd(reader, 8);
|
||||
const exifIfdOffset = getTagNumber(ifd0, 0x8769); // ExifIFD pointer
|
||||
|
||||
if (exifIfdOffset !== undefined) {
|
||||
const exifIfd = parseIfd(reader, exifIfdOffset);
|
||||
const exposureTime = getTagRational(exifIfd, 0x829A);
|
||||
const fNumber = getTagRational(exifIfd, 0x829D);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `IfdEntry`
|
||||
|
||||
```ts
|
||||
interface IfdEntry {
|
||||
tag: number;
|
||||
type: number;
|
||||
count: number;
|
||||
value: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
### `parseIfd(reader: ByteReader, ifdOffset: number): Map<number, IfdEntry>`
|
||||
|
||||
Parses the IFD at `ifdOffset` within `reader`. Returns a `Map<tag, IfdEntry>`. Throws `ExifError(PARSE_ERROR)` if the IFD count field extends beyond the buffer. Malformed individual entries are silently skipped.
|
||||
|
||||
### `getTagNumber(entries: Map<number, IfdEntry>, tag: number): number | undefined`
|
||||
|
||||
Extracts a SHORT or LONG tag as a number.
|
||||
|
||||
### `getTagString(entries: Map<number, IfdEntry>, tag: number): string | undefined`
|
||||
|
||||
Extracts an ASCII tag as a string.
|
||||
|
||||
### `getTagRational(entries: Map<number, IfdEntry>, tag: number): Rational | undefined`
|
||||
|
||||
Extracts a single RATIONAL or SRATIONAL tag.
|
||||
|
||||
### `getTagRationalArray(entries: Map<number, IfdEntry>, tag: number): Rational[] | undefined`
|
||||
|
||||
Extracts an array of RATIONAL values (e.g. GPS coordinate triples where each axis is three RATIONALs for degrees/minutes/seconds).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Inline storage applies when `typeSize * count <= 4`: the value is packed into the 4-byte value/offset field of the IFD entry. For larger values, the field contains a file offset to the actual data. ASCII strings have trailing nulls stripped. The `UNDEFINED` type (7) returns a raw `Uint8Array`. Sign extension for SBYTE uses the left-shift/right-shift trick `<< 24 >> 24`.
|
||||
73
docs/media/image/exif/src/iptc/read-iptc.md
Normal file
73
docs/media/image/exif/src/iptc/read-iptc.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# readIptc
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Parses IPTC IIM (Information Interchange Model) metadata from a Photoshop 3.0 APP13 JPEG segment payload. IPTC is the editorial metadata standard used by news agencies and photo distributors for fields such as headline, caption, copyright, keywords, and geographic information. The parser traverses the Photoshop 8BIM resource block structure to locate the IPTC-NAA data block (resource ID 0x0404) and then iterates its Record 2 datasets.
|
||||
|
||||
## Features
|
||||
|
||||
- Parses the Photoshop 3.0 + 8BIM wrapper before accessing IPTC data
|
||||
- Extracts all standard Record 2 editorial fields: headline, caption, copyright, creator, keywords, city, country code, country name, source
|
||||
- Handles repeatable fields (keywords) correctly as arrays
|
||||
- Returns an empty object gracefully if no IPTC data is present
|
||||
- Zero external dependencies — pure TypeScript byte-level parsing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readIptc } from "@suaveplan/exif/iptc";
|
||||
|
||||
const iptc = readIptc(app13SegmentData);
|
||||
console.log(iptc.headline);
|
||||
console.log(iptc.keywords); // string[]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseJpegSegments, isIptcSegment } from "@suaveplan/exif";
|
||||
import { readIptc } from "@suaveplan/exif/iptc";
|
||||
|
||||
const segments = parseJpegSegments(jpegBuffer);
|
||||
const iptcSeg = segments.find(isIptcSegment);
|
||||
|
||||
if (iptcSeg) {
|
||||
const iptc = readIptc(iptcSeg.data);
|
||||
console.log("Headline:", iptc.headline);
|
||||
console.log("Copyright:", iptc.copyright);
|
||||
console.log("Creator:", iptc.creator);
|
||||
console.log("Keywords:", iptc.keywords?.join(", "));
|
||||
console.log("Location:", [iptc.city, iptc.country].filter(Boolean).join(", "));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `readIptc(data: Uint8Array): IptcData`
|
||||
|
||||
Parses IPTC data from a raw APP13 segment payload (the full `data` field of a `JpegSegment` with marker `0xFFED`).
|
||||
|
||||
**Returns:** `IptcData` object with all recognisable fields populated, or an empty `{}` if the data does not match the Photoshop 3.0 + 8BIM + IPTC structure.
|
||||
|
||||
**Does not throw.** Malformed or truncated data results in partial or empty output.
|
||||
|
||||
### Dataset mapping
|
||||
|
||||
| Dataset code | IptcData field |
|
||||
|---|---|
|
||||
| Record 2, DS 5 (0x05) | `headline` |
|
||||
| Record 2, DS 80 (0x50) | `creator` |
|
||||
| Record 2, DS 120 (0x78) | `caption` |
|
||||
| Record 2, DS 25 (0x19) | `keywords[]` |
|
||||
| Record 2, DS 90 (0x5A) | `city` |
|
||||
| Record 2, DS 100 (0x64) | `countryCode` |
|
||||
| Record 2, DS 101 (0x65) | `country` |
|
||||
| Record 2, DS 116 (0x74) | `copyright` |
|
||||
| Record 2, DS 115 (0x73) | `source` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Photoshop 3.0 wrapper uses Pascal strings for resource names (1-byte length followed by the name, padded to an even byte boundary). The 8BIM block iteration stops on the first non-`8BIM` signature to avoid reading into compressed image data. IPTC record markers are `0x1C` followed by record number and dataset number. Only Record 2 (Application Record) datasets are extracted; Record 1 (Envelope Record) envelope fields are skipped. The string decoder uses Latin-1 (one byte per character); IPTC IIM data is specified as 7-bit ASCII.
|
||||
83
docs/media/image/exif/src/jpeg/segments.md
Normal file
83
docs/media/image/exif/src/jpeg/segments.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# JPEG Segments
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Parses the segment structure of a JPEG file and identifies metadata segments (EXIF, XMP, IPTC). JPEG is a container format: each segment is introduced by a 2-byte marker (`0xFF` + type), followed by a 2-byte big-endian length and payload. This module provides the structural parser that upstream readers (EXIF, IPTC, XMP) rely on to locate their data.
|
||||
|
||||
## Features
|
||||
|
||||
- `parseJpegSegments` — iterates all APP/marker segments up to and including SOS
|
||||
- Handles padding bytes (multiple consecutive `0xFF`) between segments
|
||||
- `isExifSegment` — identifies APP1 segments with the `Exif\0\0` header
|
||||
- `isXmpSegment` — identifies APP1 segments with the XMP namespace header
|
||||
- `isIptcSegment` — identifies APP13 segments with the `Photoshop 3.0\0` header
|
||||
- Bounds-checking with `ExifError(PARSE_ERROR)` on malformed input
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseJpegSegments, isExifSegment } from "@suaveplan/exif";
|
||||
|
||||
const segments = parseJpegSegments(jpegBuffer);
|
||||
const exifSeg = segments.find(isExifSegment);
|
||||
if (exifSeg) {
|
||||
console.log("EXIF data found, length:", exifSeg.data.length);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseJpegSegments, isExifSegment, isXmpSegment, isIptcSegment } from "@suaveplan/exif";
|
||||
|
||||
const segments = parseJpegSegments(jpegBuffer);
|
||||
|
||||
for (const seg of segments) {
|
||||
if (isExifSegment(seg)) {
|
||||
console.log(`EXIF APP1 at offset ${seg.offset}, ${seg.data.length} bytes`);
|
||||
} else if (isXmpSegment(seg)) {
|
||||
console.log(`XMP APP1 at offset ${seg.offset}`);
|
||||
} else if (isIptcSegment(seg)) {
|
||||
console.log(`IPTC APP13 at offset ${seg.offset}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `JpegSegment`
|
||||
|
||||
```ts
|
||||
interface JpegSegment {
|
||||
marker: number; // e.g. 0xFFE1 for APP1
|
||||
data: Uint8Array; // payload (length field excluded)
|
||||
offset: number; // byte offset of marker in original buffer
|
||||
}
|
||||
```
|
||||
|
||||
### `parseJpegSegments(buffer: Uint8Array): JpegSegment[]`
|
||||
|
||||
Parses all JPEG segments from `buffer`. Returns segments from SOI through SOS (inclusive). Throws `ExifError(PARSE_ERROR)` if:
|
||||
|
||||
- The buffer does not start with `FF D8` (SOI)
|
||||
- A segment's length field is less than 2
|
||||
- A segment's data extends beyond the buffer
|
||||
|
||||
### `isExifSegment(seg: JpegSegment): boolean`
|
||||
|
||||
Returns `true` if `seg` is an APP1 segment (`0xFFE1`) whose payload begins with `Exif\0\0`.
|
||||
|
||||
### `isXmpSegment(seg: JpegSegment): boolean`
|
||||
|
||||
Returns `true` if `seg` is an APP1 segment whose payload begins with `http://ns.adobe.com/xap/1.0/\0`.
|
||||
|
||||
### `isIptcSegment(seg: JpegSegment): boolean`
|
||||
|
||||
Returns `true` if `seg` is an APP13 segment (`0xFFED`) whose payload begins with `Photoshop 3.0\0`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The parser skips padding `0xFF` bytes between markers as permitted by the JFIF specification. Standalone markers (SOI, EOI, RST0–RST7) have no length field and are handled as zero-data segments. Parsing stops at the SOS marker to avoid processing compressed image data as segments. The `data` field of each segment excludes the 2-byte length field, so `data.length === segmentLength - 2`.
|
||||
93
docs/media/image/exif/src/xmp/read-xmp.md
Normal file
93
docs/media/image/exif/src/xmp/read-xmp.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# readXmp
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Parses XMP (Extensible Metadata Platform) metadata from a raw XMP XML string extracted from a JPEG APP1 segment. XMP is Adobe's XML-based metadata format built on RDF/XML. This module extracts the most commonly used Dublin Core fields (dc:creator, dc:description, dc:rights, dc:subject, dc:title) and XMP Rights fields using pattern-based extraction rather than a full XML parser, trading completeness for zero dependencies.
|
||||
|
||||
## Features
|
||||
|
||||
- Extracts dc:creator as a string array (from rdf:Seq)
|
||||
- Extracts dc:description as a language-keyed Record (from rdf:Alt)
|
||||
- Extracts dc:rights or xmpRights:UsageTerms as a plain string
|
||||
- Extracts dc:subject as a string array (from rdf:Bag)
|
||||
- Extracts dc:title as a language-keyed Record (from rdf:Alt)
|
||||
- Always returns the raw XMP XML string in the `raw` field
|
||||
- Never throws — partial results on malformed or missing fields
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readXmp } from "@suaveplan/exif/xmp";
|
||||
|
||||
const xml = `<x:xmpmeta><rdf:RDF>
|
||||
<rdf:Description>
|
||||
<dc:creator><rdf:Seq><rdf:li>Jane Doe</rdf:li></rdf:Seq></dc:creator>
|
||||
<dc:rights>© 2024 Jane Doe</dc:rights>
|
||||
</rdf:Description>
|
||||
</rdf:RDF></x:xmpmeta>`;
|
||||
|
||||
const xmp = readXmp(xml);
|
||||
console.log(xmp.creator); // ["Jane Doe"]
|
||||
console.log(xmp.rights); // "© 2024 Jane Doe"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { readXmp } from "@suaveplan/exif/xmp";
|
||||
|
||||
const xml = `<x:xmpmeta>
|
||||
<rdf:RDF>
|
||||
<rdf:Description>
|
||||
<dc:title>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang="en">Sunset over mountains</rdf:li>
|
||||
<rdf:li xml:lang="fr">Coucher de soleil sur les montagnes</rdf:li>
|
||||
</rdf:Alt>
|
||||
</dc:title>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>landscape</rdf:li>
|
||||
<rdf:li>sunset</rdf:li>
|
||||
<rdf:li>mountains</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>`;
|
||||
|
||||
const xmp = readXmp(xml);
|
||||
console.log(xmp.title?.["en"]); // "Sunset over mountains"
|
||||
console.log(xmp.title?.["fr"]); // "Coucher de soleil sur les montagnes"
|
||||
console.log(xmp.subject); // ["landscape", "sunset", "mountains"]
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `readXmp(xmlString: string): XmpData`
|
||||
|
||||
Parses XMP metadata from `xmlString`.
|
||||
|
||||
**Returns:** `XmpData` with all recognised fields populated. Always includes `raw: xmlString`. Fields absent from the XMP are `undefined` in the result.
|
||||
|
||||
**Does not throw.** Returns `{ raw: xmlString }` on empty or unrecognised input.
|
||||
|
||||
### `XmpData`
|
||||
|
||||
```ts
|
||||
interface XmpData {
|
||||
raw: string;
|
||||
creator?: string[];
|
||||
description?: Record<string, string>;
|
||||
rights?: string;
|
||||
subject?: string[];
|
||||
title?: Record<string, string>;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The extraction uses three internal helpers: `extractTextNode` for simple inline values, `extractRdfList` for `rdf:Bag`/`rdf:Seq` item arrays, and `extractRdfAlt` for `rdf:Alt` language maps. Pattern matching is tolerant of namespace prefixes (any prefix or no prefix). The `rdf:Alt` extraction captures optional `xml:lang` attributes and defaults to `"x-default"` when absent. This approach handles the most common XMP output from Lightroom, Photoshop, and camera firmware without the complexity and size of a full DOM parser.
|
||||
88
docs/media/image/image-quantisation/README.md
Normal file
88
docs/media/image/image-quantisation/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# @suaveplan/image-quantisation - Image complexity analysis and adaptive colour quantisation with pool detection, spatial-aware anti-aliasing cleanup, and multiple quantisation algorithms
|
||||
|
||||
Image complexity analysis and adaptive colour quantisation with pool detection, spatial-aware anti-aliasing cleanup, and multiple quantisation algorithms. Designed for emblem generation pipelines where pixel-art inputs need minimal processing while photographic inputs need aggressive reduction.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [classify](./src/analysis/classify/classify.md) — Analyse a raw RGBA pixel buffer and classify its complexity into one of four tiers: pixel-art, vector-clean, vector-aa,…
|
||||
- [classify](./src/analysis/classify/classify.md) — Analyse a raw RGBA pixel buffer and classify its complexity into one of four tiers: pixel-art, vector-clean, vector-aa,…
|
||||
- [colour](./src/analysis/colour/colour.md) — Low-level colour distance and keying helpers used by the analysis and spatial-cleanup modules. These functions are pure…
|
||||
- [colour](./src/analysis/colour/colour.md) — Low-level colour distance and keying helpers used by the analysis and spatial-cleanup modules. These functions are pure…
|
||||
- [edges](./src/analysis/edges/edges.md) — Analyse colour transitions between horizontally and vertically adjacent pixels to determine whether an image has sharp…
|
||||
- [edges](./src/analysis/edges/edges.md) — Analyse colour transitions between horizontally and vertically adjacent pixels to determine whether an image has sharp…
|
||||
- [pools](./src/analysis/pools/pools.md) — Find contiguous regions of identical colour via 4-connected flood fill. Pool size distribution is the primary signal fo…
|
||||
- [pools](./src/analysis/pools/pools.md) — Find contiguous regions of identical colour via 4-connected flood fill. Pool size distribution is the primary signal fo…
|
||||
- [errors](./src/errors/image-quantisation-error.md) — Structured error type for all `@suaveplan/image-quantisation` failures. Extends `SuaveplanError` so that callers receiv…
|
||||
- [image-quantisation-error](./src/errors/image-quantisation-error.md) — Structured error type for all `@suaveplan/image-quantisation` failures. Extends `SuaveplanError` so that callers receiv…
|
||||
- [pipeline](./src/pipeline/pipeline.md) — Full workflow: analyse complexity → spatial cleanup → quantise. Automatically selects the best strategy based on image…
|
||||
- [pipeline](./src/pipeline/pipeline.md) — Full workflow: analyse complexity → spatial cleanup → quantise. Automatically selects the best strategy based on image…
|
||||
- [quantise](./src/quantise/quantise.md) — Reduce an RGBA pixel buffer to a target number of colours using the `image-q` library. This module is the primary quant…
|
||||
- [quantise](./src/quantise/quantise.md) — Reduce an RGBA pixel buffer to a target number of colours using the `image-q` library. This module is the primary quant…
|
||||
- [schemas](./src/schemas/options.schema.md) — Zod schemas for runtime validation of quantisation, spatial cleanup, and pipeline options. Every public entry point tha…
|
||||
- [options.schema](./src/schemas/options.schema.md) — Zod schemas for runtime validation of quantisation, spatial cleanup, and pipeline options. Every public entry point tha…
|
||||
- [cleanup](./src/spatial/cleanup/cleanup.md) — Merge small anti-aliasing pools into their nearest spatial neighbours by colour distance. This preserves intentional de…
|
||||
- [cleanup](./src/spatial/cleanup/cleanup.md) — Merge small anti-aliasing pools into their nearest spatial neighbours by colour distance. This preserves intentional de…
|
||||
- [types](./src/types/common.md) — The `common.ts` module defines the core domain types used across the `@suaveplan/image-quantisation` package. Every pub…
|
||||
- [common](./src/types/common.md) — The `common.ts` module defines the core domain types used across the `@suaveplan/image-quantisation` package. Every pub…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/image-quantisation
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import {
|
||||
adaptivePipeline,
|
||||
analyseComplexity,
|
||||
quantise,
|
||||
} from "@suaveplan/image-quantisation";
|
||||
|
||||
// Full adaptive pipeline: analyse -> cleanup -> quantise
|
||||
const result = adaptivePipeline(rgbaPixels, width, height);
|
||||
|
||||
console.log(result.analysis.tier); // 'pixel-art' | 'vector-aa' | 'photographic'
|
||||
console.log(result.analysis.uniqueColours);
|
||||
console.log(result.finalColours); // colour count after quantisation
|
||||
// result.pixels is ready for downstream consumers
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/analysis/classify/classify.md`](src/analysis/classify/classify.md) — Complexity classification
|
||||
- [`src/analysis/colour/colour.md`](src/analysis/colour/colour.md) — Colour distance and keying utilities
|
||||
- [`src/analysis/edges/edges.md`](src/analysis/edges/edges.md) — Edge sharpness analysis
|
||||
- [`src/analysis/pools/pools.md`](src/analysis/pools/pools.md) — Pool detection and distribution
|
||||
- [`src/errors/image-quantisation-error.md`](src/errors/image-quantisation-error.md) — Structured error types
|
||||
- [`src/pipeline/pipeline.md`](src/pipeline/pipeline.md) — Adaptive pipeline
|
||||
- [`src/quantise/quantise.md`](src/quantise/quantise.md) — Colour quantisation
|
||||
- [`src/schemas/options.schema.md`](src/schemas/options.schema.md) — Zod validation schemas
|
||||
- [`src/spatial/cleanup/cleanup.md`](src/spatial/cleanup/cleanup.md) — Spatial AA cleanup
|
||||
- [`src/types/common.md`](src/types/common.md) — Core type definitions
|
||||
|
||||
## Features
|
||||
|
||||
- Image complexity classification into four tiers: pixel-art, vector-clean, vector-aa, photographic
|
||||
- Pool detection via 4-connected flood fill for contiguous same-colour regions
|
||||
- Pool distribution statistics (count, min, max, mean, median, singleton ratio)
|
||||
- Edge analysis with sharpness ratio and mean colour distance
|
||||
- Spatial AA cleanup that merges small pools into nearest spatial neighbours
|
||||
- Three quantisation algorithms: wuquant (Wu), neuquant (neural network), rgbquant (fast)
|
||||
- Three colour distance formulas: euclidean, manhattan, ciede2000
|
||||
- Dithering options: none, floyd-steinberg, riemersma
|
||||
- Full adaptive pipeline: analyse, cleanup, quantise in one call
|
||||
- Colour utility functions for distance calculation and key generation
|
||||
- Zod schemas for runtime option validation
|
||||
- OpenTelemetry counter and histogram instrumentation
|
||||
- Structured errors with stable codes via `@suaveplan/error`
|
||||
- Property-based tests with fast-check
|
||||
- 100% test coverage (Istanbul)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Complexity Classification
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Analyse a raw RGBA pixel buffer and classify its complexity into one of four tiers: pixel-art, vector-clean, vector-aa, or photographic. The tier determines the optimal processing strategy for emblem generation pipelines. This module combines pool detection, edge analysis, and heuristics into a single high-level entry point.
|
||||
|
||||
## Features
|
||||
|
||||
- Four-tier classification based on pool distribution and edge sharpness
|
||||
- Automatic palette size recommendation
|
||||
- Dithering recommendation for photographic content
|
||||
- Pure TypeScript with zero external dependencies for analysis
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { analyseComplexity } from "@suaveplan/image-quantisation/analysis/classify";
|
||||
|
||||
const analysis = analyseComplexity(pixels, 32, 32);
|
||||
console.log(analysis.tier); // "pixel-art" | "photographic" | ...
|
||||
console.log(analysis.recommendedColours); // suggested palette size
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { analyseComplexity } from "@suaveplan/image-quantisation/analysis/classify";
|
||||
|
||||
const analysis = analyseComplexity(pixels, 128, 128);
|
||||
|
||||
const strategy =
|
||||
analysis.tier === "pixel-art"
|
||||
? "skip_quantise"
|
||||
: analysis.tier === "photographic"
|
||||
? "heavy_dither"
|
||||
: "light_merge";
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `analyseComplexity(pixels, width, height): ComplexityAnalysis`
|
||||
|
||||
Runs pool detection, edge analysis, and tier classification in one call.
|
||||
|
||||
Returns a `ComplexityAnalysis` object containing:
|
||||
|
||||
- `tier` — classification result
|
||||
- `uniqueColours` — number of distinct colours
|
||||
- `poolDistribution` — pool size statistics
|
||||
- `edgeAnalysis` — edge sharpness metrics
|
||||
- `recommendedColours` — suggested palette size (2–256)
|
||||
- `recommendDithering` — boolean
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Classification heuristics are tuned for emblem-generation workflows:
|
||||
|
||||
- Pixel art: ≤32 colours, mean pool size >20
|
||||
- Photographic: >70% singleton pools, sharpness <0.05
|
||||
- Vector-AA: bimodal distribution with many tiny edge pools
|
||||
|
||||
Recommended colours are clamped to a minimum of 2 to satisfy quantiser constraints. The heuristics were derived from empirical testing on a corpus of pixel-art, vector, and photographic images. They are intentionally simple to keep the analysis fast and deterministic. Future versions may expose the threshold constants as configurable parameters.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Colour Utilities
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Low-level colour distance and keying helpers used by the analysis and spatial-cleanup modules. These functions are pure, stateless, and operate on the `RGBA` type. They enable exact colour matching for pool detection, perceptual distance for edge analysis, and canonical string keys for hash-based lookups. All operations are synchronous and allocate no intermediate objects beyond the return value.
|
||||
|
||||
## Features
|
||||
|
||||
- Euclidean RGB distance
|
||||
- Perceptually weighted Euclidean distance (ITU-R BT.601)
|
||||
- Canonical colour key generation for exact matching
|
||||
- Round-trip colour key parsing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
rgbDistance,
|
||||
colourKey,
|
||||
} from "@suaveplan/image-quantisation/analysis/colour";
|
||||
|
||||
const a = { r: 255, g: 0, b: 0, a: 255 };
|
||||
const b = { r: 0, g: 255, b: 0, a: 255 };
|
||||
|
||||
console.log(rgbDistance(a, b)); // ~360.6
|
||||
console.log(colourKey(a)); // "255,0,0,255"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
weightedDistance,
|
||||
parseColourKey,
|
||||
} from "@suaveplan/image-quantisation/analysis/colour";
|
||||
|
||||
// Weighted distance accounts for human eye sensitivity
|
||||
const dist = weightedDistance(
|
||||
{ r: 100, g: 100, b: 100, a: 255 },
|
||||
{ r: 100, g: 110, b: 100, a: 255 }
|
||||
);
|
||||
|
||||
const parsed = parseColourKey("100,110,100,255");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `rgbDistance(a, b): number`
|
||||
|
||||
Euclidean distance in RGB space. Range 0–441.67.
|
||||
|
||||
### `weightedDistance(a, b): number`
|
||||
|
||||
ITU-R BT.601 weighted Euclidean distance. Range 0–255.
|
||||
|
||||
### `colourKey(c): string`
|
||||
|
||||
Returns `"r,g,b,a"`.
|
||||
|
||||
### `parseColourKey(key): RGBA`
|
||||
|
||||
Parses a comma-separated key back into `RGBA`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`rgbDistance` ignores the alpha channel. `weightedDistance` uses coefficients 0.299 (R), 0.587 (G), 0.114 (B) to approximate perceptual difference. The alpha channel is included in `colourKey` so that transparent pools are not merged with opaque ones during flood fill. All four functions are pure and have no side effects, making them safe to use in property-based tests and benchmarks. They are also fully deterministic: given the same inputs they always produce the same outputs, which is important for reproducible image processing pipelines.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Edge Analysis
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Analyse colour transitions between horizontally and vertically adjacent pixels to determine whether an image has sharp edges (pixel art) or soft edges (photographic). Edge metrics are a critical input to the complexity classifier, providing a signal that is orthogonal to pool distribution. Two images may have the same number of pools but very different edge characteristics.
|
||||
|
||||
## Features
|
||||
|
||||
- Examines every right and bottom neighbour pair
|
||||
- Counts hard edges (perceptually distinct) and soft edges (nearly identical)
|
||||
- Computes sharpness ratio and mean edge distance
|
||||
- Configurable hard/soft thresholds
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { analyseEdges } from "@suaveplan/image-quantisation/analysis/edges";
|
||||
|
||||
const edges = analyseEdges(pixels, 32, 32);
|
||||
console.log(edges.sharpnessRatio); // 0.0 – 1.0
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { analyseEdges } from "@suaveplan/image-quantisation/analysis/edges";
|
||||
|
||||
// Stricter thresholds for high-contrast artwork
|
||||
const edges = analyseEdges(pixels, 64, 64, 20, 5);
|
||||
|
||||
if (edges.sharpnessRatio > 0.5) {
|
||||
console.log("High-contrast pixel art detected");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `analyseEdges(pixels, width, height, hardThreshold?, softThreshold?): EdgeAnalysis`
|
||||
|
||||
- `pixels` — RGBA `Uint8Array`
|
||||
- `width`, `height` — image dimensions
|
||||
- `hardThreshold` — default 30
|
||||
- `softThreshold` — default 10
|
||||
|
||||
Returns `EdgeAnalysis` with `totalEdges`, `hardEdges`, `softEdges`, `sharpnessRatio`, `meanEdgeDistance`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function uses `weightedDistance` (ITU-R BT.601) for perceptually fair comparisons. Pairs are counted once each (right and bottom neighbours only) to avoid double counting. A solid-colour image produces zero hard edges and zero mean distance, while a checkerboard produces a very high sharpness ratio. The thresholds can be adjusted for different content types: stricter thresholds treat more edges as hard, which is useful for high-contrast artwork, while lenient thresholds classify most edges as soft, which is better for smooth gradients.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Pool Detection
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Find contiguous regions of identical colour via 4-connected flood fill. Pool size distribution is the primary signal for image complexity classification. A pixel-art image may have only a handful of large pools, while a photograph may have thousands of singleton pools. This module provides both detection and statistical summary functions.
|
||||
|
||||
## Features
|
||||
|
||||
- 4-connected BFS flood fill
|
||||
- Returns pools sorted by size descending
|
||||
- Computes distribution statistics
|
||||
- Zero dependencies beyond colour utilities
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { detectPools } from "@suaveplan/image-quantisation/analysis/pools";
|
||||
|
||||
const pools = detectPools(pixels, 32, 32);
|
||||
console.log(pools.length); // number of distinct colour regions
|
||||
console.log(pools[0]!.size); // largest region
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
detectPools,
|
||||
poolDistribution,
|
||||
} from "@suaveplan/image-quantisation/analysis/pools";
|
||||
|
||||
const pools = detectPools(pixels, 64, 64);
|
||||
const dist = poolDistribution(pools);
|
||||
|
||||
if (dist.singletonRatio > 0.7) {
|
||||
console.log("Photographic — many 1-pixel pools");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `detectPools(pixels, width, height): Pool[]`
|
||||
|
||||
Returns an array of `Pool` objects sorted by size descending.
|
||||
|
||||
### `poolDistribution(pools): PoolDistribution`
|
||||
|
||||
Computes count, min, max, mean, median, p90, singletons, and singletonRatio.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Flood fill uses a stack-based BFS with a `Uint8Array` visited mask for memory efficiency. The `pixels` buffer is read-only. Pools are sorted by size descending so that the dominant colour regions appear first, which is useful for downstream algorithms that only need to inspect the largest pools. The visited mask uses a single byte per pixel, which keeps memory overhead low even for large images. Because every pixel is visited exactly once, the overall time complexity is O(width * height) in the worst case. For images with many small pools, the BFS queue may grow large, but the `Uint8Array` visited mask ensures that each pixel is never enqueued more than once.
|
||||
@@ -0,0 +1,70 @@
|
||||
# QuantisationError
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Structured error type for all `@suaveplan/image-quantisation` failures. Extends `SuaveplanError` so that callers receive stable codes, i18n-ready message keys, and optional cause chains. This unified error type eliminates the need for consumers to catch multiple different exception classes and enables programmatic branching on the `code` property.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable `QUANTISATION.*` error codes via `defineCodes`
|
||||
- Typed `code` property for programmatic discrimination
|
||||
- i18n-ready message keys with no hard-coded user strings
|
||||
- OTel span-event compatibility via `toSpanEvent`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
QuantisationError,
|
||||
QUANTISATION_CODES,
|
||||
} from "@suaveplan/image-quantisation/errors";
|
||||
|
||||
throw new QuantisationError(
|
||||
QUANTISATION_CODES.INVALID_DIMENSIONS,
|
||||
"quantisation.invalid_dimensions"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
QuantisationError,
|
||||
QUANTISATION_CODES,
|
||||
} from "@suaveplan/image-quantisation/errors";
|
||||
|
||||
try {
|
||||
quantise(pixels, 0, 1);
|
||||
} catch (err) {
|
||||
if (err instanceof QuantisationError) {
|
||||
console.log(err.code); // "QUANTISATION.INVALID_DIMENSIONS"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `QUANTISATION_CODES`
|
||||
|
||||
Constant object exported from `defineCodes("QUANTISATION", [...])`.
|
||||
|
||||
- `INVALID_PIXEL_BUFFER`
|
||||
- `INVALID_COLOR_COUNT`
|
||||
- `INVALID_DIMENSIONS`
|
||||
- `PROCESSING_FAILED`
|
||||
|
||||
### `QuantisationError`
|
||||
|
||||
Constructor: `new QuantisationError(code, messageKey, options?)`
|
||||
|
||||
- `code` — one of the `QUANTISATION_CODES` values
|
||||
- `messageKey` — i18n message resolver key
|
||||
- `options?.cause` — optional underlying `Error`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Error codes are registered at module load time. The `defineCodes` utility guarantees that every code string is prefixed with the namespace, so collisions across packages are impossible. The `INVALID_COLOR_COUNT` code is reserved for future use when palette-size validation is tightened. Consumers should always test against the exported `QUANTISATION_CODES` constant rather than hard-coding strings, because the exact prefix may change during major version bumps. The `QuantisationError` class is kept in its own feature module so that consumers who only need the error type can import it without pulling in the full quantisation logic.
|
||||
67
docs/media/image/image-quantisation/src/pipeline/pipeline.md
Normal file
67
docs/media/image/image-quantisation/src/pipeline/pipeline.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Adaptive Pipeline
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Full workflow: analyse complexity → spatial cleanup → quantise. Automatically selects the best strategy based on image content. This is the recommended high-level entry point for emblem-generation pipelines because it encapsulates the decision logic for when to skip quantisation, when to apply spatial cleanup, and when to use dithering.
|
||||
|
||||
## Features
|
||||
|
||||
- One-call image preparation for emblem-solver
|
||||
- Skips quantisation for pixel-art images
|
||||
- Applies spatial cleanup for vector-aa and vector-clean tiers
|
||||
- Auto-selects dithering for photographic content
|
||||
- Zod validation for all options
|
||||
- OTel counter and histogram instrumentation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { adaptivePipeline } from "@suaveplan/image-quantisation/pipeline";
|
||||
|
||||
const result = adaptivePipeline(pixels, 32, 32);
|
||||
console.log(result.analysis.tier);
|
||||
console.log(result.finalColours);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { adaptivePipeline } from "@suaveplan/image-quantisation/pipeline";
|
||||
|
||||
const result = adaptivePipeline(pixels, 64, 64, {
|
||||
maxColours: 16,
|
||||
dither: "floyd-steinberg",
|
||||
spatial: { maxDistance: 25, maxPoolSize: 2 },
|
||||
skipQuantisationForPixelArt: false,
|
||||
});
|
||||
|
||||
if (result.spatialCleanupApplied) {
|
||||
console.log("Spatial cleanup merged small AA pools");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `adaptivePipeline(pixels, width, height, options?): AdaptivePipelineResult`
|
||||
|
||||
- `pixels` — RGBA `Uint8Array`
|
||||
- `width`, `height` — image dimensions
|
||||
- `options` — `AdaptivePipelineOptions` (optional)
|
||||
|
||||
Returns `AdaptivePipelineResult` with:
|
||||
|
||||
- `pixels` — final processed buffer
|
||||
- `analysis` — complexity analysis of the original
|
||||
- `quantisation` — quantisation result or `null`
|
||||
- `spatialCleanupApplied` — boolean
|
||||
- `finalColours` — unique colour count after processing
|
||||
|
||||
Throws `QuantisationError` on invalid input or options.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The pipeline never mutates the input buffer. Each stage creates a new `Uint8Array` when modification is required. OTel instruments record the complexity tier and whether spatial cleanup was applied as attributes. The final colour count is computed by packing each RGBA tuple into a 32-bit integer and counting distinct values in a `Set`. This design makes the pipeline safe to call repeatedly on the same buffer without copying at the call site. The `skipQuantisationForPixelArt` default is true because pixel-art images already have a low colour count and quantisation would only degrade quality.
|
||||
66
docs/media/image/image-quantisation/src/quantise/quantise.md
Normal file
66
docs/media/image/image-quantisation/src/quantise/quantise.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Quantise
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Reduce an RGBA pixel buffer to a target number of colours using the `image-q` library. This module is the primary quantisation entry point, supporting three palette-generation algorithms and three dithering modes. It wraps the low-level `image-q` API with ergonomic options, Zod validation, and OpenTelemetry instrumentation.
|
||||
|
||||
## Features
|
||||
|
||||
- Wu quant (median-cut, best quality)
|
||||
- Neuquant (neural network, good for photos)
|
||||
- RGBQuant (fastest, good for previews)
|
||||
- Euclidean, Manhattan, and CIEDE2000 distance formulas
|
||||
- Floyd-Steinberg and Riemersma dithering
|
||||
- Zod option validation
|
||||
- OTel counter and histogram instrumentation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { quantise } from "@suaveplan/image-quantisation/quantise";
|
||||
|
||||
const result = quantise(pixels, 32, 32, {
|
||||
colours: 16,
|
||||
algorithm: "wuquant",
|
||||
});
|
||||
|
||||
console.log(result.paletteSize); // ≤ 16
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { quantise } from "@suaveplan/image-quantisation/quantise";
|
||||
|
||||
const result = quantise(pixels, 64, 64, {
|
||||
colours: 32,
|
||||
algorithm: "neuquant",
|
||||
distance: "ciede2000",
|
||||
dither: "floyd-steinberg",
|
||||
});
|
||||
|
||||
// result.pixels is a new Uint8Array with quantised colours
|
||||
// result.palette is the generated colour table
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `quantise(pixels, width, height, options?): QuantiseResult`
|
||||
|
||||
- `pixels` — RGBA `Uint8Array`
|
||||
- `width`, `height` — image dimensions
|
||||
- `options` — `QuantiseOptions` (optional)
|
||||
|
||||
Throws `QuantisationError` on invalid input dimensions, mismatched buffer size, or invalid options.
|
||||
|
||||
### `quantiseAdaptive(pixels, width, height, recommendedColours, recommendDithering, overrides?): QuantiseResult`
|
||||
|
||||
Convenience wrapper that sets algorithm to `"wuquant"` and dither based on the boolean flag. `overrides` can override any individual field.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function validates inputs with Zod schemas before delegating to `image-q`. OTel instruments are lazily initialised on first use to avoid side effects at module load time. All algorithms run synchronously. The `mapDither` helper translates our dither enum values into the strings expected by `image-q`. Performance varies by algorithm: Wu quant is generally the best balance of quality and speed, Neuquant excels on photographic content, and RGBQuant is fastest but may produce slightly lower quality. Callers should benchmark on their specific image corpus to choose the best algorithm.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Option Schemas
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod schemas for runtime validation of quantisation, spatial cleanup, and pipeline options. Every public entry point that accepts options validates them through these schemas before processing. This catches misconfigured callers early and produces descriptive error messages that can be forwarded to end users or logged without additional formatting.
|
||||
|
||||
## Features
|
||||
|
||||
- `QuantiseOptionsSchema` — validates colour count, algorithm, distance formula, and dithering
|
||||
- `SpatialCleanupOptionsSchema` — validates merge thresholds
|
||||
- `AdaptivePipelineOptionsSchema` — validates pipeline overrides
|
||||
- Descriptive error messages suitable for structured logging
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { QuantiseOptionsSchema } from "@suaveplan/image-quantisation/schemas";
|
||||
|
||||
const result = QuantiseOptionsSchema.safeParse({ colours: 16 });
|
||||
if (!result.success) {
|
||||
console.log(result.error.issues);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AdaptivePipelineOptionsSchema,
|
||||
SpatialCleanupOptionsSchema,
|
||||
} from "@suaveplan/image-quantisation/schemas";
|
||||
|
||||
const raw = {
|
||||
maxColours: 32,
|
||||
spatial: { maxDistance: 20 },
|
||||
};
|
||||
|
||||
const parsed = AdaptivePipelineOptionsSchema.parse(raw);
|
||||
// parsed.spatial is fully typed and defaults are applied by Zod
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `QuantiseOptionsSchema`
|
||||
|
||||
- `colours` — optional integer 2–256
|
||||
- `algorithm` — optional enum `"neuquant" | "rgbquant" | "wuquant"`
|
||||
- `distance` — optional enum `"euclidean" | "manhattan" | "ciede2000"`
|
||||
- `dither` — optional enum `"none" | "floyd-steinberg" | "riemersma"`
|
||||
|
||||
### `SpatialCleanupOptionsSchema`
|
||||
|
||||
- `maxDistance` — optional non-negative number
|
||||
- `maxPoolSize` — optional non-negative integer
|
||||
|
||||
### `AdaptivePipelineOptionsSchema`
|
||||
|
||||
- `maxColours` — optional integer 2–256
|
||||
- `dither` — optional dither enum
|
||||
- `spatial` — optional nested `SpatialCleanupOptionsSchema`
|
||||
- `skipSpatialCleanup` — optional boolean
|
||||
- `skipQuantisationForPixelArt` — optional boolean
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Schemas use `.optional()` for every field so that callers can supply partial options and the implementation supplies defaults. Validation failures throw `QuantisationError` with code `PROCESSING_FAILED`. The schemas are co-located with the types they validate to reduce drift. The `QuantiseOptionsSchema` enforces a minimum of 2 colours because single-colour palettes are not supported by the underlying `image-q` library. The `AdaptivePipelineOptionsSchema` nests `SpatialCleanupOptionsSchema` so that pipeline callers can override cleanup parameters in a single options object.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Spatial Cleanup
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Merge small anti-aliasing pools into their nearest spatial neighbours by colour distance. This preserves intentional detail while removing AA noise. Unlike global k-means quantisation, spatial cleanup only merges pools that are physically adjacent, so a small blue dot next to a red region stays blue while a near-red edge pixel gets absorbed into the red region.
|
||||
|
||||
## Features
|
||||
|
||||
- Spatially-aware merge (not global k-means)
|
||||
- Configurable colour distance and pool size thresholds
|
||||
- Returns a new pixel buffer (non-destructive)
|
||||
- Fast pixel-to-pool index lookup via `Int32Array`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
detectPools,
|
||||
spatialCleanup,
|
||||
} from "@suaveplan/image-quantisation";
|
||||
|
||||
const pools = detectPools(pixels, 32, 32);
|
||||
const cleaned = spatialCleanup(pixels, 32, 32, pools);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
detectPools,
|
||||
spatialCleanup,
|
||||
countUniqueColours,
|
||||
} from "@suaveplan/image-quantisation";
|
||||
|
||||
const pools = detectPools(pixels, 64, 64);
|
||||
const before = countUniqueColours(pixels, 64, 64);
|
||||
|
||||
const cleaned = spatialCleanup(pixels, 64, 64, pools, {
|
||||
maxDistance: 25,
|
||||
maxPoolSize: 3,
|
||||
});
|
||||
|
||||
const after = countUniqueColours(cleaned, 64, 64);
|
||||
console.log(`Removed ${before - after} colours`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `spatialCleanup(pixels, width, height, pools, options?): Uint8Array`
|
||||
|
||||
- `pixels` — RGBA `Uint8Array`
|
||||
- `width`, `height` — image dimensions
|
||||
- `pools` — array of `Pool` from `detectPools`
|
||||
- `options.maxDistance` — default 30
|
||||
- `options.maxPoolSize` — default 4
|
||||
|
||||
Returns a new `Uint8Array` with merged pixels.
|
||||
|
||||
### `countUniqueColours(pixels, width, height): number`
|
||||
|
||||
Counts distinct colours in the buffer using a 32-bit packed key.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The algorithm builds an adjacency map for pools that are physically adjacent in the image. For each small pool, it finds the neighbour with the minimum `rgbDistance`. If the distance is within `maxDistance`, the pool is recoloured. The pixel-to-pool index is stored in an `Int32Array` initialised to `-1` for fast O(1) lookup during adjacency building.
|
||||
114
docs/media/image/image-quantisation/src/types/common.md
Normal file
114
docs/media/image/image-quantisation/src/types/common.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Common Types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `common.ts` module defines the core domain types used across the `@suaveplan/image-quantisation` package. Every public function operates on these shapes, so consumers should import them when writing TypeScript wrappers or adapters. Keeping types in a single module ensures consistency between analysis, quantisation, and pipeline stages. The types are designed to be serialisable, tree-shakeable, and free of runtime dependencies.
|
||||
|
||||
## Features
|
||||
|
||||
- Strongly-typed RGBA pixel representation
|
||||
- Pool and edge-analysis result shapes
|
||||
- Quantisation option and result types
|
||||
- Adaptive pipeline input/output types
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { RGBA, QuantiseOptions } from "@suaveplan/image-quantisation/types";
|
||||
|
||||
const pixel: RGBA = { r: 255, g: 128, b: 64, a: 255 };
|
||||
|
||||
const opts: QuantiseOptions = {
|
||||
colours: 16,
|
||||
algorithm: "wuquant",
|
||||
distance: "euclidean",
|
||||
dither: "none",
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AdaptivePipelineOptions,
|
||||
ComplexityAnalysis,
|
||||
} from "@suaveplan/image-quantisation/types";
|
||||
|
||||
function buildPipelineOpts(
|
||||
analysis: ComplexityAnalysis
|
||||
): AdaptivePipelineOptions {
|
||||
return {
|
||||
maxColours: analysis.recommendedColours,
|
||||
dither: analysis.recommendDithering ? "floyd-steinberg" : "none",
|
||||
spatial: { maxDistance: 25, maxPoolSize: 3 },
|
||||
skipSpatialCleanup: false,
|
||||
skipQuantisationForPixelArt: true,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `RGBA`
|
||||
|
||||
Interface with `r`, `g`, `b`, `a` in the range 0–255.
|
||||
|
||||
### `Pool`
|
||||
|
||||
Represents a contiguous region of identical colour found via 4-connected flood fill.
|
||||
|
||||
### `PoolDistribution`
|
||||
|
||||
Statistics object: `count`, `min`, `max`, `mean`, `median`, `p90`, `singletons`, `singletonRatio`.
|
||||
|
||||
### `EdgeAnalysis`
|
||||
|
||||
Metrics: `totalEdges`, `hardEdges`, `softEdges`, `sharpnessRatio`, `meanEdgeDistance`.
|
||||
|
||||
### `ComplexityTier`
|
||||
|
||||
Union type: `"pixel-art" | "vector-clean" | "vector-aa" | "photographic"`.
|
||||
|
||||
### `ComplexityAnalysis`
|
||||
|
||||
Full analysis including tier, pools, edges, and recommendations.
|
||||
|
||||
### `PaletteAlgorithm`
|
||||
|
||||
`"neuquant" | "rgbquant" | "wuquant"`.
|
||||
|
||||
### `DistanceFormula`
|
||||
|
||||
`"euclidean" | "manhattan" | "ciede2000"`.
|
||||
|
||||
### `DitherAlgorithm`
|
||||
|
||||
`"none" | "floyd-steinberg" | "riemersma"`.
|
||||
|
||||
### `QuantiseOptions`
|
||||
|
||||
Optional fields: `colours`, `algorithm`, `distance`, `dither`.
|
||||
|
||||
### `QuantiseResult`
|
||||
|
||||
Result with `pixels`, `palette`, `paletteSize`, and the resolved `options`.
|
||||
|
||||
### `SpatialCleanupOptions`
|
||||
|
||||
Optional fields: `maxDistance`, `maxPoolSize`.
|
||||
|
||||
### `AdaptivePipelineOptions`
|
||||
|
||||
Optional fields: `maxColours`, `dither`, `spatial`, `skipSpatialCleanup`, `skipQuantisationForPixelArt`.
|
||||
|
||||
### `AdaptivePipelineResult`
|
||||
|
||||
Full pipeline result with analysis, optional quantisation, and final pixel buffer.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All interfaces are pure data shapes with no methods. They are designed to be serialisable and tree-shakeable. The `Pool` interface uses `Set<number>` for pixel indices, which is efficient for BFS traversal but may be replaced with a denser representation in future versions if memory becomes a concern for very large images.
|
||||
59
docs/media/image/image/README.md
Normal file
59
docs/media/image/image/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# @suaveplan/image - Universal image metadata extraction, format detection, resize math, crop validation, and transform pipeline planning
|
||||
|
||||
Universal image metadata extraction, format detection, resize math, crop validation, and transform pipeline planning. Zero browser or Node dependencies — runs in Bun, Node, and browsers alike. Provides `ImageProcessor` for planning image transforms, `PipelineBuilder` for fluent composition, and header-based dimension parsers for JPEG, PNG, WebP, GIF, and AVIF.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` declares the canonical error-code registry for `@suaveplan/image`. Every failure mode that the image p…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` declares the canonical error-code registry for `@suaveplan/image`. Every failure mode that the image p…
|
||||
- [errors](./src/errors/errors.md) — `errors.ts` provides the typed error hierarchy for `@suaveplan/image`. `ImageError` extends `SuaveplanError` so that ca…
|
||||
- [errors](./src/errors/errors.md) — `errors.ts` provides the typed error hierarchy for `@suaveplan/image`. `ImageError` extends `SuaveplanError` so that ca…
|
||||
- [dimensions](./src/processor/dimensions/dimensions.md) — `dimensions.ts` extracts width and height from raw image headers for the four supported raster formats: PNG, JPEG, GIF,…
|
||||
- [dimensions](./src/processor/dimensions/dimensions.md) — `dimensions.ts` extracts width and height from raw image headers for the four supported raster formats: PNG, JPEG, GIF,…
|
||||
- [instruments](./src/processor/instruments/instruments.md) — `instruments.ts` lazily initialises the OpenTelemetry counters and histograms used by `ImageProcessor`. Each instrument…
|
||||
- [instruments](./src/processor/instruments/instruments.md) — `instruments.ts` lazily initialises the OpenTelemetry counters and histograms used by `ImageProcessor`. Each instrument…
|
||||
- [pipeline-builder](./src/processor/pipeline-builder/pipeline-builder.md) — `pipeline-builder.ts` implements a fluent, chainable API for constructing image transform pipelines. Each method append…
|
||||
- [pipeline-builder](./src/processor/pipeline-builder/pipeline-builder.md) — `pipeline-builder.ts` implements a fluent, chainable API for constructing image transform pipelines. Each method append…
|
||||
- [processor](./src/processor/processor/processor.md) — `processor.ts` is the orchestration layer of `@suaveplan/image`. `ImageProcessor` accepts a `Uint8Array` of image bytes…
|
||||
- [processor](./src/processor/processor/processor.md) — `processor.ts` is the orchestration layer of `@suaveplan/image`. `ImageProcessor` accepts a `Uint8Array` of image bytes…
|
||||
- [image-detect](./src/types/image-detect/image-detect.md) — `image-detect.ts` provides fast, deterministic format detection from raw binary headers. It inspects the first bytes of…
|
||||
- [image-detect](./src/types/image-detect/image-detect.md) — `image-detect.ts` provides fast, deterministic format detection from raw binary headers. It inspects the first bytes of…
|
||||
- [image-types](./src/types/image-types/image-types.md) — `image-types.ts` is the universal type system and pure-function toolkit for image metadata. It defines Zod schemas…
|
||||
- [image-types](./src/types/image-types/image-types.md) — `image-types.ts` is the universal type system and pure-function toolkit for image metadata. It defines Zod schemas…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/image
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { ImageProcessor } from "@suaveplan/image";
|
||||
|
||||
const proc = new ImageProcessor({ maxPixels: 50_000_000 });
|
||||
const meta = proc.extractMetadata(imageBytes);
|
||||
console.log(`${meta.format} ${meta.width}x${meta.height}`);
|
||||
|
||||
const result = proc.planTransform(meta, {
|
||||
operations: [
|
||||
{ type: "resize", options: { width: 400 } },
|
||||
{ type: "format", format: "webp" },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/processor/processor/processor.md`](./src/processor/processor/processor.md) — `ImageProcessor` and `PipelineBuilder`
|
||||
- [`src/processor/instruments/instruments.md`](./src/processor/instruments/instruments.md) — OpenTelemetry instrumentation helpers
|
||||
- [`src/processor/dimensions/dimensions.md`](./src/processor/dimensions/dimensions.md) — Header dimension parsers for JPEG, PNG, WebP, GIF, AVIF
|
||||
- [`src/processor/pipeline-builder/pipeline-builder.md`](./src/processor/pipeline-builder/pipeline-builder.md) — Fluent transform pipeline builder
|
||||
- [`src/types/image-types/image-types.md`](./src/types/image-types/image-types.md) — TypeScript interfaces and Zod schemas
|
||||
- [`src/types/image-detect/image-detect.md`](./src/types/image-detect/image-detect.md) — Format detection types and constants
|
||||
- [`src/errors/errors.md`](./src/errors/errors.md) — `ImageError` hierarchy and typed factories
|
||||
- [`src/error-codes/error-codes.md`](./src/error-codes/error-codes.md) — Stable `IMAGE.*` error code registry
|
||||
|
||||
66
docs/media/image/image/src/error-codes/error-codes.md
Normal file
66
docs/media/image/image/src/error-codes/error-codes.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# error-codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`error-codes.ts` declares the canonical error-code registry for `@suaveplan/image`. Every failure mode that the image package can surface — from unsupported formats to dimension violations — is assigned a stable, namespaced string via `defineCodes` from `@suaveplan/error`. This gives downstream log analysers, retry policies, and problem+json renderers a deterministic contract they can match against without parsing human-readable messages.
|
||||
|
||||
## Features
|
||||
|
||||
- Seven stable codes under the `IMAGE.*` namespace
|
||||
- Type-safe `ImageCode` union derived from the registry object
|
||||
- Zero runtime side effects — the registry is a plain constant
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { IMAGE_CODES } from "@suaveplan/image/error-codes";
|
||||
|
||||
function handleImageError(err: { code: string }) {
|
||||
if (err.code === IMAGE_CODES.FORMAT_UNSUPPORTED) {
|
||||
return { status: 415, message: "Format not supported" };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { IMAGE_CODES, type ImageCode } from "@suaveplan/image/error-codes";
|
||||
|
||||
const retryableCodes: ImageCode[] = [
|
||||
IMAGE_CODES.DECODE_FAILED,
|
||||
IMAGE_CODES.ENCODE_FAILED,
|
||||
];
|
||||
|
||||
function shouldRetry(code: ImageCode): boolean {
|
||||
return retryableCodes.includes(code);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `IMAGE_CODES`
|
||||
|
||||
A frozen object mapping human-readable keys to namespaced string values:
|
||||
|
||||
- `NOT_FOUND` → `"IMAGE.NOT_FOUND"`
|
||||
- `DECODE_FAILED` → `"IMAGE.DECODE_FAILED"`
|
||||
- `ENCODE_FAILED` → `"IMAGE.ENCODE_FAILED"`
|
||||
- `FORMAT_UNSUPPORTED` → `"IMAGE.FORMAT_UNSUPPORTED"`
|
||||
- `TOO_LARGE` → `"IMAGE.TOO_LARGE"`
|
||||
- `PROCESSING_FAILED` → `"IMAGE.PROCESSING_FAILED"`
|
||||
- `DIMENSIONS_INVALID` → `"IMAGE.DIMENSIONS_INVALID"`
|
||||
|
||||
### `ImageCode`
|
||||
|
||||
A TypeScript union of all seven code string literals. Useful for function parameters and discriminated unions.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is produced by `defineCodes("IMAGE", [...])`. If the namespace or code list ever changes, every test that asserts against `IMAGE_CODES.*` will fail, which is the desired behaviour — it prevents silent drift in the error contract.
|
||||
|
||||
Namespaces matter in a package ecosystem. Without the `IMAGE.` prefix, a generic name like `NOT_FOUND` could collide with `DRIVE.NOT_FOUND` or `CONTACTS.NOT_FOUND`. The `defineCodes` utility enforces a namespace at compile time and produces a frozen object, so consumers cannot accidentally mutate the registry at runtime. When adding a new failure mode, the code must be added to the `defineCodes` array first; only then can it be used in `src/` or tests. This two-step discipline ensures the error surface remains intentional and auditable.
|
||||
75
docs/media/image/image/src/errors/errors.md
Normal file
75
docs/media/image/image/src/errors/errors.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# errors
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`errors.ts` provides the typed error hierarchy for `@suaveplan/image`. `ImageError` extends `SuaveplanError` so that callers receive structured failures with a stable code, a message key, an optional context object, and an optional cause chain. Typed factory functions make it easy to construct correctly-coded errors without hand-typing string literals.
|
||||
|
||||
## Features
|
||||
|
||||
- `ImageError` class with `code`, `message`, `context`, and `cause`
|
||||
- Factory functions for every failure mode
|
||||
- Backwards-compatibility aliases for upstream consumers
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { imageFormatUnsupported } from "@suaveplan/image/errors";
|
||||
|
||||
function processImage(data: Uint8Array) {
|
||||
if (data.length === 0) {
|
||||
throw imageFormatUnsupported("empty buffer");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ImageError, IMAGE_CODES } from "@suaveplan/image/errors";
|
||||
|
||||
function enrichError(err: unknown): ImageError {
|
||||
if (err instanceof ImageError) return err;
|
||||
return new ImageError(
|
||||
IMAGE_CODES.PROCESSING_FAILED,
|
||||
"Unexpected failure",
|
||||
{ cause: err instanceof Error ? err : undefined }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ImageError`
|
||||
|
||||
Extends `SuaveplanError<ImageCode>`.
|
||||
|
||||
- `constructor(code, messageKey, options?)` — `options` may include `cause` and `context`.
|
||||
- `context?: ImageErrorContext` — carries `format`, `width`, `height`, `pixels`, `maxPixels`, `path`.
|
||||
|
||||
### Factory Functions
|
||||
|
||||
- `imageNotFound(path?)`
|
||||
- `imageDecodeFailed(format?, cause?)`
|
||||
- `imageEncodeFailed(format?, cause?)`
|
||||
- `imageFormatUnsupported(format?)`
|
||||
- `imageTooLarge(pixels, maxPixels)`
|
||||
- `imageProcessingFailed(message, cause?)`
|
||||
- `imageDimensionsInvalid(width, height)`
|
||||
|
||||
### Backwards-Compatibility Aliases
|
||||
|
||||
- `unknownFormat()` → alias for `imageFormatUnsupported()`
|
||||
- `invalidDimensions(w, h)` → alias for `imageDimensionsInvalid(w, h)`
|
||||
- `invalidCrop(msg)` → alias for `imageProcessingFailed("Invalid crop region: " + msg)`
|
||||
- `metadataParseFailed(cause?)` → alias for `imageDecodeFailed(undefined, cause)`
|
||||
- `invalidTransform(msg)` → alias for `imageProcessingFailed("Invalid transform: " + msg)`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All aliases are thin wrappers — they do not duplicate logic. The `ImageErrorContext` interface extends `Record<string, unknown>` so callers can safely add custom fields without losing type safety.
|
||||
|
||||
Because `ImageError` extends `SuaveplanError`, it inherits cause-chain traversal and OTel span-event adaptation for free. When surfacing an image failure inside an HTTP handler, the error can be passed directly to the logger or telemetry layer without manual unpacking. Canonical factories should be preferred for new code; backwards-compatibility aliases exist only to ease migration from upstream packages that used the old naming convention. Each factory constructs the error with a consistent message key derived from the code name, so telemetry dashboards can aggregate by message key while retry policies discriminate by code.
|
||||
@@ -0,0 +1,48 @@
|
||||
# dimensions
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`dimensions.ts` extracts width and height from raw image headers for the four supported raster formats: PNG, JPEG, GIF, and BMP. It is a pure, side-effect-free module consumed by `ImageProcessor`. Short or malformed buffers return `{ width: 0, height: 0 }` rather than throwing, so callers can decide whether zero dimensions are fatal.
|
||||
|
||||
## Features
|
||||
|
||||
- PNG IHDR dimension parsing
|
||||
- JPEG SOF0 marker scanning (skips APP segments)
|
||||
- GIF logical screen dimension parsing
|
||||
- BMP DIB header dimension parsing (handles negative height for top-down bitmaps)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseDimensions } from "@suaveplan/image/processor";
|
||||
|
||||
const dims = parseDimensions(pngBytes, "png");
|
||||
console.log(`${dims.width}x${dims.height}`);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseDimensions } from "@suaveplan/image/processor";
|
||||
import { detectFormat } from "@suaveplan/image/types";
|
||||
|
||||
function getDimensions(data: Uint8Array) {
|
||||
const format = detectFormat(data);
|
||||
if (!format) return null;
|
||||
return parseDimensions(data, format);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `parseDimensions(data, format): { width: number; height: number }`
|
||||
|
||||
Dispatches to the appropriate parser based on `format`. Returns `{ width: 0, height: 0 }` for unsupported formats or buffers that are too short.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
JPEG parsing walks the segment chain starting at offset 2 (after the SOI marker). It stops on the first SOF0-SOF15 marker (excluding DHT, DAC, and DHP) and reads the 16-bit width and height fields. BMP parsing uses `Math.abs` on the height field because Windows bitmaps can encode top-down images with a negative height value. TIFF is detected at the format level but dimension parsing is intentionally not implemented yet; callers receive `{ width: 0, height: 0 }` for TIFF rather than an exception, which lets `ImageProcessor` decide whether to treat the format as supported for metadata extraction while deferring dimension work. All four parsers validate buffer length before reading every multi-byte field, so truncated or corrupt headers never throw — they simply return zero dimensions. This design keeps `extractMetadata` robust against partial reads from network streams or damaged files.
|
||||
@@ -0,0 +1,54 @@
|
||||
# instruments
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`instruments.ts` lazily initialises the OpenTelemetry counters and histograms used by `ImageProcessor`. Each instrument is created on first access to avoid registering metrics at module-load time, keeping the package `sideEffects: false`. A matching tracer helper provides span creation for `extractMetadata` and `planTransform`.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy OTel counter and histogram creation
|
||||
- Four instruments: two counters + two histograms
|
||||
- Tracer helper for span creation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
This module is internal — consumers do not interact with it directly. `ImageProcessor` calls the helpers automatically.
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
If you need to override the global meter provider in tests, install a custom provider via `@opentelemetry/api` before constructing `ImageProcessor`:
|
||||
|
||||
```ts
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
metrics.setGlobalMeterProvider(myTestProvider);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getExtractCounter(): Counter`
|
||||
|
||||
Returns the `image.metadata.extracted_total` counter, creating it on first call.
|
||||
|
||||
### `getExtractDuration(): Histogram`
|
||||
|
||||
Returns the `image.metadata.extract_duration_ms` histogram.
|
||||
|
||||
### `getPlanCounter(): Counter`
|
||||
|
||||
Returns the `image.transform.planned_total` counter.
|
||||
|
||||
### `getPlanDuration(): Histogram`
|
||||
|
||||
Returns the `image.transform.plan_duration_ms` histogram.
|
||||
|
||||
### `getTracer(): Tracer`
|
||||
|
||||
Returns the `@suaveplan/image` tracer from the global tracer provider.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The module-level caches mean that switching meter providers after the first instrument has been created has no effect. In practice this is fine because the global provider is set once at application startup. Tests that need deterministic instrumentation should set the provider before the first `ImageProcessor` call.
|
||||
@@ -0,0 +1,63 @@
|
||||
# pipeline-builder
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`pipeline-builder.ts` implements a fluent, chainable API for constructing image transform pipelines. Each method appends an operation to an internal list and returns `this`, so callers can compose resize, crop, rotate, flip, quality, and format changes in a single expression. The builder validates dimensions at call time via Zod-derived guards, surfacing structured `ImageError`s immediately rather than deferring failures to execution.
|
||||
|
||||
## Features
|
||||
|
||||
- Fluent chaining API
|
||||
- Runtime validation of resize and crop dimensions
|
||||
- Quality clamping to `[1, 100]`
|
||||
- Immutable pipeline output via spread copy
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ImageProcessor } from "@suaveplan/image/processor";
|
||||
|
||||
const proc = new ImageProcessor();
|
||||
const pipeline = proc
|
||||
.createPipeline()
|
||||
.resize({ width: 400 })
|
||||
.toFormat("webp")
|
||||
.quality(75)
|
||||
.build();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ImageProcessor } from "@suaveplan/image/processor";
|
||||
|
||||
const proc = new ImageProcessor();
|
||||
const result = proc
|
||||
.createPipeline()
|
||||
.resize({ width: 800, height: 600, fit: "cover" })
|
||||
.crop({ x: 0, y: 0, width: 400, height: 300 })
|
||||
.rotate(90)
|
||||
.apply(metadata, proc);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `PipelineBuilder`
|
||||
|
||||
- `resize(options: ResizeOptions): this` — validates width/height are positive.
|
||||
- `crop(region: CropRegion): this` — validates width/height are positive.
|
||||
- `rotate(degrees: number): this`
|
||||
- `flip(direction: "horizontal" | "vertical"): this`
|
||||
- `quality(value: number): this` — clamps to `[1, 100]`.
|
||||
- `toFormat(format: ImageFormat): this`
|
||||
- `build(): TransformPipeline` — returns a frozen snapshot of operations.
|
||||
- `apply(metadata, processor): TransformResult` — delegates to `processor.planTransform`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Negative or zero dimensions in `resize` and `crop` throw `IMAGE_CODES.DIMENSIONS_INVALID` synchronously. The `quality` clamping is a defensive convenience — passing `150` silently becomes `100`, while passing `0` becomes `1`. This avoids silent truncation bugs in downstream pipelines.
|
||||
|
||||
The internal `ops` array is a plain JavaScript array of discriminated objects. Each fluent method pushes a new object and returns `this`, so multiple calls on the same builder accumulate operations in call order. `build()` spreads the array into a new object, so the returned `TransformPipeline` is immutable — further mutations to the builder do not affect previously built pipelines. The `apply` method is sugar for calling `build()` followed by `processor.planTransform(metadata, pipeline)`, making it easy to use the builder in one-shot expressions without storing the intermediate pipeline object.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user