Encoding Images
An image becomes a hash through three functions from hazehash/encode. They take straight (non-premultiplied) sRGB RGBA pixels, so an ImageData from a canvas fits as it is. Encode on the server or at build time and ship only the string; the encoder is the larger half of the library (about 6.3 KB gzip) and a browser that only shows placeholders does not need it.
import { encode, encodeToString, toBase64Url } from 'hazehash/encode'The input is a plain object, the same shape in every function:
interface RgbaImage {
data: Uint8Array | Uint8ClampedArray // RGBA, length 4 · width · height
width: number
height: number
}Encoding to a string
encodeToString(image, options?)
string
Encodes the image and returns the hash as base64url without padding, ready to store in a text column or to send in JSON. It is the function most code needs.
import { encodeToString } from 'hazehash/encode'
const hash = encodeToString({ data: rgba, width: 1280, height: 959 })
// "Ed7UwRWKKv5znm6a7sC1tziHDNpMuCikxrYpIg"image
RgbaImage
The pixels to encode. data must hold exactly 4 · width · height bytes, and width and height must be integers of at least 1; anything else throws a PlaceholderError with the code InvalidInput. The function describes one picture, so for an animated image pass a single frame.
options
EncodeOptions · optional
See Encoder options below.
Encoding to bytes
encode(image, options?)
Uint8Array
The same as encodeToString(), but returns the raw bytes. Use it for a binary column (BYTEA, BLOB), which is about 25% smaller than the text form; see Storing Hashes.
import { encode } from 'hazehash/encode'
const bytes = encode({ data: rgba, width: 1280, height: 959 })
bytes.length // 28Converting between the two forms
toBase64Url(bytes)
string
Turns the bytes of a hash into its base64url string. The opposite function, toBytes(), is exported by the decoder; see Decoding Hashes.
import { encode, toBase64Url } from 'hazehash/encode'
toBase64Url(encode({ data: rgba, width, height })) // the same string as encodeToString()Encoder options
budget
number · default: 28
The maximum size of the result in bytes, header included. It is a ceiling, not a fixed size: a simple image, such as a flat colour or a gradient, uses fewer bytes, and a hash that fits is never padded. The smallest value that works is 7 bytes, or 9 when the hash carries alpha; anything lower throws BudgetTooSmall.
The benchmark covers budgets from 16 to 48 bytes. See Choosing a budget.
profile
'fast' | 'default' | 'high' · default: 'default'
How hard the encoder searches. 'fast' tries fewer grids and takes about 1 ms for a 64×48 input, 'default' tries all of them and takes about 5 ms (images with alpha are slower, around 100 ms), and 'high' also refines the coefficients against the analysis image for about 1% lower error and takes about 14 ms. The time grows with the input size only through the downscale. Any other value throws InvalidInput.
alpha
'auto' | boolean · default: 'auto'
Whether the hash stores transparency. 'auto' adds the alpha block only when the image has a pixel whose alpha is below 254/255, true always adds it, and false never does, so a transparent image is encoded as if it were opaque.
analysisSize
number · default: 64
The long side, in pixels, of the grid the image is downscaled to before analysis. Values are rounded and limited to 32–128. A larger grid rarely helps, because the hash is a very coarse description anyway.
weights
{ L?: number; C?: number; A?: number } · default: { L: 1, C: 1, A: 1 }
How much each channel counts in the error the encoder minimises: L for brightness, C for the two colour channels and A for alpha. A larger weight spends more of the budget on that channel. A weight must be a finite number of at least 0, otherwise the call throws InvalidInput.
Example — every option at once:
const hash = encodeToString(
{ data: rgba, width: 1280, height: 959 },
{
budget: 24,
profile: 'high',
alpha: false,
analysisSize: 96,
weights: { L: 1, C: 1.5, A: 1 },
},
)Choosing a budget
A hash of n bytes is ceil(4n / 3) characters in base64url. Mean ΔE is the perceptual error in OKLab (times 100), averaged over 513 images; lower is better.
| Budget | Characters | Mean ΔE | Comment |
|---|---|---|---|
| 16 | 22 | 8.41 | the smallest practical hash |
| 20 | 27 | 7.89 | still 14% better than ThumbHash |
| 24 | 32 | 7.54 | a good size and quality compromise |
| 28 | 38 | 7.29 | the default |
| 36 | 48 | 7.00 | more detail when size is no issue |
Going from 28 to 24 bytes costs about 3.4% more error for 14% less data, and from 28 to 36 bytes gains 4.0% for 29% more data. Use 24 bytes when storage dominates and 28 otherwise. See Benchmark for the method.
Notes
- The core assumes sRGB and ignores ICC profiles and EXIF orientation. The Node.js helper and the command line convert to sRGB and apply the orientation through
sharp. - The result is deterministic: the same pixels and options always give the same bytes.
- Encoding is not meant for the browser's main thread on every render. Compute the hash once, ahead of time, and store it.