Skip to content

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.

ts
import { encode, encodeToString, toBase64Url } from 'hazehash/encode'

The input is a plain object, the same shape in every function:

ts
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.

ts
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.

ts
import { encode } from 'hazehash/encode'

const bytes = encode({ data: rgba, width: 1280, height: 959 })
bytes.length // 28

Converting 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.

ts
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:

ts
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.

BudgetCharactersMean ΔEComment
16228.41the smallest practical hash
20277.89still 14% better than ThumbHash
24327.54a good size and quality compromise
28387.29the default
36487.00more 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.