Skip to content

Releases: lokesh/color-thief

v3.5.0 — Region extraction; Web Worker path deprecated

Choose a tag to compare

@lokesh lokesh released this 02 Aug 23:30

Region extraction 🖼️

Sample colors from part of an image instead of the whole thing — the most-requested evergreen feature, open since 2021 (#176, with earlier attempts in #44 and #90). Thanks @runshotgun for the original ask.

New region option

Coordinates are fractions of the image size (0–1) from the top-left, so a region is resolution-independent — the same values work on a thumbnail and the full-size original.

// Colors from the bottom third — e.g. for a gradient overlapping the image
const palette = await getPalette(img, {
    region: { x: 0, y: 0.66, width: 1, height: 0.34 },
});

// Center crop
const color = await getColor(img, {
    region: { x: 0.25, y: 0.25, width: 0.5, height: 0.5 },
});

Works everywhere an option object does: getColor, getPalette, getSwatches, getPaletteProgressive, the *Sync functions, observe(), and the CLI — in both browser and Node.

colorthief image.jpg --region 0,0.66,1,0.34

What changed

  • Cropping happens on the decoded pixel buffer right after loading, before sampling — so every entry point gets region support, including custom loaders and quantizers supplied via configure().
  • Regions are validated up front, so a malformed rect throws before the image is decoded rather than after.
  • A region running past the right or bottom edge is clamped to the image; out-of-range or zero-sized values throw.
  • Proportions are relative to the region, not the whole image.

⚠️ The worker option is now a no-op

worker: true is still accepted but ignored, and logs a one-time deprecation warning. The isWorkerSupported, extractInWorker, and terminateWorker exports from colorthief/internals are now no-op shims. Nothing breaks in v3 — all of it is removed in v4.

It cost more than it saved. Only quantization ran off-thread. Decoding, pixel sampling, and the structured clone of the pixel array all stayed on the main thread — and serializing Array<[r, g, b]> (one small array per sampled pixel) ran several times longer than the quantization it avoided:

Image Quantize (what the worker saved) Structured clone
0.3 MP, quality 10 0.7 ms 2.9 ms
2 MP, quality 10 2.5 ms 19.9 ms
12 MP, quality 10 12.9 ms 199 ms

The gap widens as images get larger — the opposite of how the feature was meant to scale.

It was also returning different colors. The worker carried a hand-inlined copy of MMCQ that quantized in RGB while the main path defaults to OKLCH, and it skipped the few-color short-circuit and filter relaxation. Two calls differing only by worker: true disagreed. Routing the flag through the normal pipeline makes them agree.

Getting extraction off the main thread

Run Color Thief inside your own worker and hand it an ImageBitmap. Bitmaps are transferable, so pixels move without being copied and the whole pipeline — decode, sampling, quantization — runs off-thread:

// main.js
const bitmap = await createImageBitmap(await (await fetch(url)).blob());
const worker = new Worker('./palette-worker.js', { type: 'module' });
worker.postMessage({ bitmap }, [bitmap]); // transferred, not cloned
worker.onmessage = (e) => render(e.data.palette);
// palette-worker.js
import { getPalette } from 'colorthief';

self.onmessage = async ({ data }) => {
    const palette = await getPalette(data.bitmap, { colorCount: 5 });
    // Color objects don't survive structured clone — send plain data
    self.postMessage({ palette: palette.map((c) => c.hex()) });
};

Smaller bundles

Deleting the inlined quantizer shrank every build:

Bundle Before After
dist/index.js 55.6 kB 46.2 kB −17%
dist/internals.js 45.0 kB 39.4 kB −13%
dist/umd/color-thief.global.js 30.0 kB 22.3 kB −25%

Notes

  • Backward compatible — no breaking changes. The only behavior change is that worker: true now returns the same palette as the default path instead of a different one.
  • The deprecation warning fires once per page load, not once per call.

Full changelog: v3.4.1...v3.5.0

3.4.1

Choose a tag to compare

@lokesh lokesh released this 02 Aug 22:45

Patch release fixing a bundler warning reported in #283.

Fixed

WasmQuantizer fell back to importing ../../dist/wasm/color_thief_wasm.js when constructed with no arguments, but that file has never been included in the published package. Bundlers resolving colorthief/internals — for example to use oklchToRgb — reported it as an unresolvable module. Applications built and ran correctly (the dead code was tree-shaken away), but every build printed a warning.

The wasm-bindgen glue module is now supplied by the caller:

import { WasmQuantizer } from 'colorthief/internals';
const q = new WasmQuantizer(await import('./pkg/color_thief_wasm.js'));
await q.init();

init() with no module now throws a message pointing at the wasm-pack build step. Neither of the old code paths could succeed from an npm install, so nothing that previously worked has changed.

Thanks to @magic-akari for the report and diagnosis.

Full Changelog: v3.4.0...v3.4.1

v3.4.0 — Wide-gamut (Display P3) support

Choose a tag to compare

@lokesh lokesh released this 01 Jul 18:56

Wide-gamut (Display P3) support 🎨

Color Thief can now read and report wide-gamut colors, fixing incorrect results on P3-tagged images where out-of-sRGB colors were collapsed down to sRGB (thanks @LeaVerou — #266).

New gamut option

Opt in per call (or via configure): 'srgb' (default), 'display-p3', or 'auto'.

const palette = await getPalette(img, { gamut: 'display-p3' });
palette[0].css();   // 'color(display-p3 …)'
palette[0].gamut;   // 'display-p3'

// 'auto' upgrades to P3 only when the image actually uses out-of-sRGB colors
const auto = await getPalette(img, { gamut: 'auto' });

What changed

  • Browser loaders read the image through a P3 canvas (getContext('2d', { colorSpace: 'display-p3' })) with feature detection and automatic sRGB fallback.
  • Gamut-aware OKLCH quantization preserves the extra saturation instead of clamping it away.
  • Color objects now carry .gamut. .css() emits color(display-p3 …) and .oklch() reports the true wider chroma, while .rgb()/.array()/.hex() stay sRGB (gamut-mapped) so existing consumers keep working. Use .rgb('display-p3') for raw P3 components.
  • Threaded through the async, sync, worker, and progressive paths.

Notes

  • Backward compatible — default behavior is unchanged sRGB.
  • Node output is sRGB for now; P3 there is tracked as a follow-up.

Full changelog: v3.3.2...v3.4.0

v3.3.2

Choose a tag to compare

@lokesh lokesh released this 01 Jul 18:57

Performance & accuracy improvements

  • Better results for images with few colors — improved performance and accuracy of extraction when an image contains only a handful of distinct colors (#281).
  • Docs: consolidated planning docs into ROADMAP.md, added roadmap items, and moved transparency detection to intentionally-left-out.

Full changelog: v3.3.1...v3.3.2

v3.3.1

Choose a tag to compare

@lokesh lokesh released this 01 Jul 17:29

Packaging and docs fixes for the 3.3.0 CLI release.

  • Add a colorthief/cli export (./cli) for programmatic access to the CLI entry.
  • Apply npm pkg fix corrections: normalize the bin path and repository URL for a clean publish.
  • Update README CLI references to colorthief-cli.

v3.3.0

Choose a tag to compare

@lokesh lokesh released this 04 Mar 19:28

Adds colorthief CLI with color, palette, and swatches subcommands. Supports --json, --css, and ANSI output formats, stdin piping, multi-file input, and a friendly error message when sharp is not installed.

image

v3.2.0

Choose a tag to compare

@lokesh lokesh released this 04 Mar 17:49

Browser bundlers no longer warn about sharp - Bundlers like webpack (Angular), Vite, and Rollup previously emitted a "Can't resolve 'sharp'" warning when using colorthief in browser-only projects. The library now ships separate browser builds that contain no references to sharp or Node.js-specific code. Bundlers automatically pick the right build via the "browser" condition in package.json exports. No changes needed on your end. The API is identical. (#279)

v3.1.0

Choose a tag to compare

@lokesh lokesh released this 04 Mar 17:05

Changed palette functions to default to utilizing OKLCH (previously RGB)

v3.0.0

Choose a tag to compare

@lokesh lokesh released this 02 Mar 04:45
a79371f

Full TypeScript rewrite with a unified browser + Node.js API.

Highlights:

  • Rich Color objects with .hex(), .rgb(), .hsl(), .oklch(), .css()
  • OKLCH perceptually uniform quantization
  • Semantic swatches (Vibrant, Muted, DarkVibrant, DarkMuted, LightVibrant, LightMuted)
  • Live extraction with observe() for video, canvas, and img elements
  • Web Worker offloading
  • Progressive 3-pass extraction
  • WCAG contrast ratios and text color recommendations
  • AbortSignal support
  • Zero runtime dependencies

v.2.7.0

Choose a tag to compare

@lokesh lokesh released this 28 Feb 09:03

New features:

  • Configurable pixel filtering — New options: ignoreWhite, whiteThreshold, alphaThreshold, and minSaturation let callers control which pixels are
    included in color extraction
  • Options object API — getColor and getPalette now accept an options object (e.g., { colorCount: 5, quality: 10, ignoreWhite: false }) in addition to the
    existing positional arguments
  • New browser input types — getPalette/getColor now accept HTMLCanvasElement, ImageData, and ImageBitmap in addition to HTMLImageElement
  • TypeScript type definitions — Added .d.ts files for both browser (color-thief.d.ts) and Node (color-thief-node.d.ts)

Bug fixes & robustness:

  • Fallback color for edge cases — New computeFallbackColor() averages all pixels when the quantizer can't produce a palette (e.g., solid-color or
    all-white images), preventing null returns
  • Progressive filter relaxation — If filtering removes all pixels, filters are progressively relaxed (first allowing white, then transparent) before
    falling back
  • Fixed variable scope leak — i in getImageData changed from implicit global to let
  • Input validation — Browser version now throws descriptive errors for unloaded images, missing sources, and CORS-tainted canvases
  • Null-safe palette access — All call sites now check palette for null before accessing palette[0]

Chores & tooling:

  • Removed file-type dependency from Node version (unused)
  • Removed dist/ from git tracking (kept in npm package via .npmignore)
  • Added GitHub Actions CI workflow
  • Replaced CanvasImage class with a simpler getPixelData() function
  • Updated README to reflect current project setup
  • Version bump to 2.7.0 across package.json and source files