image-q
image-q is a TypeScript image-quantization library for reducing full RGBA pixel data to a smaller color palette. It supplies WuQuant, RGBQuant, NeuQuant, multiple color-distance formulas, nearest-color mapping, several error-diffusion dithering kernels, progress-aware async wrappers, and SSIM comparison. It works on pixel buffers and browser canvas data, not encoded PNG, JPEG, GIF, or WebP files.
Choose image-q when the job is specifically palette construction and RGBA remapping, especially when algorithm choice matters. Do not install it expecting an image-file toolkit or background processing.
Use it if
- You already have decoded RGBA pixels and need a 256-color or smaller palette in JavaScript
- You want to compare WuQuant, RGBQuant, and NeuQuant without wiring separate implementations
- You need alpha-aware quantization and a choice of nearest-color or error-diffusion output
- You need the same deterministic quantization code in a browser and Node.js, with bundled TypeScript declarations
- You need to read or write image files: image-q accepts pixel containers but includes no PNG, JPEG, GIF, or WebP decoder or encoder
- You need resizing, cropping, rotation, metadata, or format conversion; this package only handles palettes, quantization, color math, and SSIM
- You expect async calls to use another CPU core: buildPalette and applyPalette yield between generator steps with setImmediate or a timer but still perform JavaScript computation on the main thread
- You are sensitive to stale dependencies: version 4.0.0 publishes @types/node 16.9.1 as a runtime dependency, even though it is compile-time type material
- You want fast perceptual matching on large images: the README labels CIEDE2000 very slow, and dithering plus expensive distance formulas can be costly in pure JavaScript
Setup reality
Install image-q and import its namespace; version 4.0.0 publishes both ESM and CommonJS entry points and bundled declarations, with no native compilation or peer dependency. The first surprise is that an image filename is not valid input. Decode the file elsewhere, then construct iq.utils.PointContainer from an ImageData, HTML canvas, HTML image element, RGBA Uint8Array, Uint32Array, or Node Buffer plus explicit width and height. After quantization, image-q gives you a PointContainer or palette, not a PNG or GIF, so another library must encode the returned bytes. The byte input must represent four RGBA bytes per pixel and match width times height; the Buffer conversion views four-byte words directly. The convenient defaults are WuQuant, 256 colors, BT.709 Euclidean distance with alpha, and Floyd-Steinberg dithering. Async methods improve event-loop responsiveness by scheduling generator steps, but they do not create worker threads, shorten total CPU work, provide cancellation, or set a memory ceiling. Progress callbacks receive numeric progress from the algorithms, yet there is no AbortSignal. Browser canvas reads can throw when the canvas is tainted by a cross-origin image. The published runtime dependency on @types/node can also introduce global Node types into installations that expected a browser-only utility.
Patterns
Reduce RGBA pixels to 64 colorsquantize-rgba-sync
import * as iq from 'image-q'
const input = iq.utils.PointContainer.fromUint8Array(rgba, width, height)
const palette = iq.buildPaletteSync([input], { colors: 64 })
const output = iq.applyPaletteSync(input, palette)
const quantizedRgba = output.toUint8Array()rgba must contain four bytes per pixel. The result is raw RGBA data and still needs an image encoder.
Use the yielding API with progress callbacksquantize-with-progress
const palette = await iq.buildPalette([input], {
colors: 128,
onProgress: (value) => console.log('palette', value)
})
const output = await iq.applyPalette(input, palette, {
onProgress: (value) => console.log('image', value)
})These calls yield between generator steps but stay on the same JavaScript thread; use a worker for CPU isolation.
Quantize browser ImageDataread-image-data
const source = iq.utils.PointContainer.fromImageData(imageData)
const palette = iq.buildPaletteSync([source], { colors: 32 })
const result = iq.applyPaletteSync(source, palette)
const next = new ImageData(
new Uint8ClampedArray(result.toUint8Array()),
result.getWidth(),
result.getHeight()
)ImageData is decoded pixel data. Creating it does not encode a downloadable PNG or JPEG.
Build a point container from a canvasread-browser-canvas
const pixels = iq.utils.PointContainer.fromHTMLCanvasElement(canvas)
const palette = iq.buildPaletteSync([pixels], { colors: 16 })
const reduced = iq.applyPaletteSync(pixels, palette, {
imageQuantization: 'nearest'
})getImageData fails on a canvas tainted by an image loaded without suitable cross-origin permission.
Read pixels from a loaded image elementread-html-image
await image.decode()
const pixels = iq.utils.PointContainer.fromHTMLImageElement(image)
const palette = iq.buildPaletteSync([pixels], { colors: 48 })Wait for the image to decode first. The helper creates an internal canvas and is subject to the same cross-origin rules.
Wrap a decoded Node.js RGBA bufferread-node-buffer
const pixels = iq.utils.PointContainer.fromBuffer(
decodedRgbaBuffer,
width,
height
)
const palette = iq.buildPaletteSync([pixels], { colors: 256 })Pass decoded RGBA bytes, not the bytes of a PNG or JPEG file. The buffer length must be divisible by four.
Build one palette from several imagesshare-palette
const palette = iq.buildPaletteSync(frames, {
colors: 128,
paletteQuantization: 'wuquant'
})
const reducedFrames = frames.map((frame) =>
iq.applyPaletteSync(frame, palette)
)Sampling all frames first produces a shared palette, useful for animation, but holds every supplied PointContainer in application memory.
Select RGBQuant instead of the defaultchoose-palette-algorithm
const palette = iq.buildPaletteSync([input], {
colors: 32,
paletteQuantization: 'rgbquant',
colorDistanceFormula: 'euclidean-bt709-noalpha'
})The defaults are wuquant and euclidean-bt709. Ignoring alpha is appropriate only when transparency should not affect matching.
Apply an Atkinson dithering kernelchoose-dithering-kernel
const output = iq.applyPaletteSync(input, palette, {
colorDistanceFormula: 'euclidean-bt709',
imageQuantization: 'atkinson'
})The distance formula used while applying should normally match the one used to build the palette.
Map each pixel to its nearest palette colordisable-dithering
const flat = iq.applyPaletteSync(input, palette, {
imageQuantization: 'nearest'
})nearest avoids diffusion artifacts and is faster, but gradients usually band more visibly than with the default Floyd-Steinberg mode.
Sort and export palette colorsinspect-palette
palette.sort()
const paletteRgba = palette
.getPointContainer()
.toUint8Array()sort mutates palette order and clears its nearest-color cache; export returns four RGBA bytes per palette entry.
Calculate SSIM after quantizationcompare-image-quality
const score = iq.quality.ssim(input, output)
console.log({ score })Both PointContainers must have identical dimensions or ssim throws. A higher score indicates closer structural similarity.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| sharp | npm | Node services that need decoding, resizing, quantization, encoding, and high native-code throughput in one pipeline |
| jimp | npm | Pure JavaScript image loading and editing matter more than having a large choice of quantization algorithms |
| imagemin | npm | Build pipelines that optimize encoded image files through format-specific plugins |