# HazeHash — AI Reference `hazehash` / `hazehash-vue` / `hazehash-nuxt` — compact image placeholders: an image becomes a 16–48 byte hash (28 by default, 38 base64url characters) from which a blurred preview is rebuilt in a fraction of a millisecond. The hash also carries the aspect ratio and, when needed, the alpha channel. Three npm packages from one pnpm workspace. Version 0.1.1. This document is hand-written for AI agents and other tools that generate code against this package: every signature, default, and behavior note below is verified directly against the TypeScript source and the built package (not summarized from prose docs), and prose is kept to the minimum needed to use the API correctly. For human-readable narrative docs, see the interactive site instead: - Full docs (EN): https://npm.vuecraft.ru/en/packages/hazehash/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/hazehash/guide/overview - GitHub: https://github.com/macrulezru/hazehash - npm: https://www.npmjs.com/package/hazehash Links below starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Package map — what to import from where | Package | Install | Peer deps | Provides | |---|---|---|---| | `hazehash` | `npm install hazehash` | `sharp >=0.33` (OPTIONAL — only `hazehash/node`, the CLI) | encoder, decoder, canvas helper, Node.js file helpers, the `hazehash` command (sections 2–6). | | `hazehash-vue` | `npm install hazehash hazehash-vue` | `hazehash`, `vue ^3.3.0` | `PlaceholderImage` component, `usePlaceholder()` composable (sections 7–8). | | `hazehash-nuxt` | `npm install hazehash-nuxt` | `nuxt ^3.9.0`, `sharp >=0.33` (optional) | Nuxt module (section 9). Depends on `hazehash`, `hazehash-vue`, `@nuxt/kit`. | Entry points of `hazehash` (all ESM + CJS, all `sideEffects: false`, Node.js >= 20): | Import | Contents | |---|---| | `hazehash` | `decode`, `getAspectRatio`, `getAverageColor`, `toBytes`, `PlaceholderError`, types `DecodeOptions`, `RgbaImage`, `PlaceholderErrorCode` | | `hazehash/decode` | the same as `hazehash` | | `hazehash/encode` | `encode`, `encodeToString`, `toBase64Url`, `PlaceholderError`, types `EncodeOptions`, `RgbaImage`, `PlaceholderErrorCode` | | `hazehash/canvas` | `drawToCanvas`, `toImageData`, type `CanvasLike`, type `DecodeOptions` | | `hazehash/node` | `encodeFile`, `encodeFileToString`, `encodeFileDetailed`, type `EncodedFile`, type `EncodeOptions` (needs optional `sharp`) | The decoder entry points never import the encoder. Minified + gzip: decoder ~2.6 KB, encoder ~6.3 KB, `hazehash/canvas` ~0.05 KB on top of the decoder. The core files (everything except `canvas`, `node`, `sharp` loader, CLI) never touch `window`, `document`, `Buffer`, `eval`, dynamic `import()` or `node:` modules — a test enforces it — so they run in browsers, Node.js 20+, Cloudflare Workers and other runtimes. --- ## 2. Types ```ts interface RgbaImage { data: Uint8Array | Uint8ClampedArray // RGBA, STRAIGHT (non-premultiplied) sRGB, length must be exactly 4 * width * height width: number // integer >= 1 height: number // integer >= 1 } interface EncodeOptions { budget?: number // default 28. Max bytes of the result, header included. A CEILING: simple images produce fewer bytes. Floor(n); capped at 1024 internally analysisSize?: number // default 64. Long side of the analysis grid; rounded, clamped to 32..128 alpha?: 'auto' | boolean // default 'auto': alpha block only if min alpha < 254/255. true: always. false: never (transparency ignored) weights?: { L?: number; C?: number; A?: number } // default 1,1,1. Channel weights in the error metric. Must be finite and >= 0 profile?: 'fast' | 'default' | 'high' // default 'default'. ~1 ms / ~5 ms (alpha input ~100 ms) / ~14 ms for a 64x48 input } interface DecodeOptions { size?: number // default 32. Long side of the output; rounded, clamped to 4..128 dither?: boolean // default true. Deterministic dithering; false = plain rounding } type PlaceholderErrorCode = | 'InvalidInput' | 'BudgetTooSmall' // encoder | 'InvalidLength' | 'InvalidCharacter' | 'UnsupportedVersion' // decoder class PlaceholderError extends Error { readonly code: PlaceholderErrorCode } // name === 'PlaceholderError'; message = code, or `${code}: ${detail}` ``` Hash forms: a base64url string (RFC 4648 §5, NO padding; 28 bytes = 38 characters, n bytes = ceil(4n/3) characters) or the raw `Uint8Array`. Every function that reads a hash accepts both. Valid length: 7..1024 bytes (9 minimum when the header says alpha). A string whose length is 1 mod 4, or with a character outside `A-Za-z0-9-_`, is invalid (`=` padding is rejected). Truncated hashes are VALID (missing bits read as 0 → less detail). --- ## 3. Encoder — `hazehash/encode` ```ts function encode(image: RgbaImage, options?: EncodeOptions): Uint8Array function encodeToString(image: RgbaImage, options?: EncodeOptions): string // base64url, no padding function toBase64Url(bytes: Uint8Array): string ``` - Deterministic: the same pixels and options always give the same bytes. - Core assumes sRGB and ignores ICC profiles and EXIF orientation. It describes ONE picture — for an animated image pass a single frame. - Throws `PlaceholderError`: `InvalidInput` (buffer length != 4*w*h, width/height not integers >= 1, non-finite number option, negative weight, unknown `profile`, `alpha` neither 'auto' nor boolean), `BudgetTooSmall` (`budget` < 7, or < 9 when the image has alpha; also if the result cannot fit the budget). - The benchmarked range is budget 16–48. Table (mean ΔE in OKLab x100 over 513 images; lower is better): 16 → 22 chars, 8.41; 20 → 27, 7.89; 24 → 32, 7.54; 28 → 38, 7.29 (default); 36 → 48, 7.00. At 28 bytes: 49% lower error than BlurHash, 20% lower than ThumbHash. ```ts import { encodeToString } from 'hazehash/encode' const hash = encodeToString({ data: rgba, width: 1280, height: 959 }) // "Ed7UwRWKKv5znm6a7sC1tziHDNpMuCikxrYpIg" ``` --- ## 4. Decoder — `hazehash` / `hazehash/decode` ```ts function decode(hash: string | Uint8Array, options?: DecodeOptions): RgbaImage function getAspectRatio(hash: string | Uint8Array): number // header only; quantized in 2^(1/8) ≈ 9% steps function getAverageColor(hash: string | Uint8Array): { r: number; g: number; b: number; a: number } // header only; r,g,b integers 0..255 sRGB; a 0..1 (always 1 without alpha) function toBytes(base64url: string): Uint8Array ``` - `decode()` output: `{ width, height, data: Uint8ClampedArray }`, straight sRGB RGBA, long side = `size`, short side from the stored aspect ratio (`r >= 1` → `size x max(1, round(size/r))`; else `max(1, round(size*r)) x size`). A 1280x959 photo's hash gives 32x25. Hash without alpha → opaque pixels. ~0.3 ms. - The stored aspect ratio is for DRAWING the placeholder; use the real image dimensions for page layout. - Throws `PlaceholderError`: `InvalidLength`, `InvalidCharacter`, `UnsupportedVersion` (first 2 header bits != 0; only version 1 exists). A malformed hash never hangs or crashes — it decodes or throws. - Version 1 decoding is FROZEN: a stored hash decodes the same in every release. ```ts import { decode, getAspectRatio, getAverageColor, toBytes } from 'hazehash' const { width, height, data } = decode(hash) // 32, 25, Uint8ClampedArray(3200) getAverageColor(hash) // { r: 152, g: 142, b: 126, a: 1 } toBytes(hash).length // 28 ``` --- ## 5. Canvas helper — `hazehash/canvas` Needs the global `ImageData` (browser, or a worker with `OffscreenCanvas`). ```ts interface CanvasLike { width: number; height: number getContext(type: '2d'): { putImageData(data: ImageData, x: number, y: number): void } | null } function drawToCanvas(hash: string | Uint8Array, canvas: CanvasLike, options?: DecodeOptions): void // RESIZES the canvas to the decoded size, then putImageData; if getContext returns null the canvas is resized and left empty function toImageData(hash: string | Uint8Array, options?: DecodeOptions): ImageData // decodes into a COPY ImageData accepts ``` The canvas ends up with the preview's pixel size (e.g. 32x25) — stretch it with CSS. Give the box `background-color` (from `getAverageColor`) and `aspect-ratio` (from `getAspectRatio`) first; they work before any script and on the server. --- ## 6. Node.js helpers and the CLI ### 6.1 `hazehash/node` (needs optional peer `sharp >=0.33`) ```ts function encodeFileDetailed(pathOrBuffer: string | Uint8Array, options?: EncodeOptions): Promise function encodeFile(pathOrBuffer: string | Uint8Array, options?: EncodeOptions): Promise function encodeFileToString(pathOrBuffer: string | Uint8Array, options?: EncodeOptions): Promise interface EncodedFile { bytes: Uint8Array; hash: string; width: number; height: number } // width/height = pixel size AS DISPLAYED (after EXIF rotation) ``` The image is read with `sharp`, EXIF-rotated, converted to sRGB, given an alpha channel, then encoded. Without `sharp`: `PlaceholderError('InvalidInput', 'the optional dependency "sharp" is required to read image files (npm i sharp)')`. One image per call. ### 6.2 `hazehash` command (bin `hazehash`; `npm i -D hazehash sharp`; `npx hazehash --help`) Global: `-h, --help`, `-V, --version`, `--color` / `--no-color`. Colours off when piped, with `NO_COLOR`, on a dumb terminal; `FORCE_COLOR` forces them on. Exit codes: `0` ok; `1` at least one input failed (others still written); `2` wrong command line. - `hazehash encode ` — input = file, folder, quoted glob (`"photos/**/*.jpg"`) or `-` (one image from stdin). - `-b, --budget ` (default 28; min 7, 9 with alpha) - `-p, --profile fast|default|high` - `-a, --analysis-size ` (32–128, default 64) - `--alpha auto|true|false` - `-r, --recursive` (folders; a `**` glob always recurses) - `-f, --format text|json|csv` — text: hash alone for ONE image, `pathhash` lines for several; json: `{ "path": "hash" }`; csv: header row. `--details` adds hash bytes, image size, file size to json/csv (csv columns: `file,hash,bytes,width,height,fileBytes`). - `--hex` (hex instead of base64url), `-o, --output `, `--pretty` / `--plain` (table with summary by default in a terminal; plain when piped; the table drops columns on narrow terminals), `-q, --quiet`. - Missing input: `✖ : no image found`, exit 1. - `hazehash decode ` — no `-o`: draws in the terminal with coloured half-blocks. `-o, --output ` (.png/.webp/.jpg, needs sharp), `-s, --size ` (4–128, default 32), `--scale ` (with `-o`; 1–32, default 8), `--no-dither`. Invalid hash: `✖ this is not a HazeHash: …`, exit 1. - `hazehash info ` — header facts; `--json` prints: `hash, characters, bytes, headerBytes, dataBytes, version, aspectRatio, nearestRatio, alpha, lumaGrid, chromaGrid, alphaGrid (null without alpha), storedCoefficients, averageColor (hex), averageOklab, riceParameters, scaleCodes`. --- ## 7. `hazehash-vue` — `PlaceholderImage` ```ts import { PlaceholderImage, usePlaceholder } from 'hazehash-vue' ``` Props (no emits, no slots; root is ONE `
`, so `class`/`style` fall through): ```ts hash?: string // base64url hash. Without it only the renders src?: string // real image URL. Without it only the placeholder renders alt?: string // default '' width?: number | string // with height: defines the aspect ratio, passed to height?: number | string size?: number // default 32 — long side of the decoded preview (4..128) fade?: number // default 300 — fade-in ms of the real image; 0 = no transition ``` Rendering: - SERVER: `
` with the real `` inside. NO canvas, so server markup == first client render (no hydration mismatch). No `` needed. - AFTER MOUNT: `