npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@adzazueta/color-extractor

v0.3.0

Published

Extract perceptually meaningful observed colors from images in browsers and Node.js

Readme

@adzazueta/color-extractor

Extract perceptually meaningful observed colors from images in browsers and Node.js.

The neutral color extraction API uses deterministic CIELAB K-means by default and also supports deterministic MMCQ quantization. Both algorithms return observed colors, population evidence, perceptual rankings, and algorithm diagnostics without assigning semantic roles.

The 0.3 release uses a stable color-oriented API that returns observed-color evidence without semantic role assignment.

This package is ESM-only. Use import with the documented package entrypoints.

import { extractColors } from '@adzazueta/color-extractor'

const result = await extractColors(image)
const topId = result.rankings.perceptual[0]
const top = result.colors.find(color => color.id === topId)
console.log(top?.id, top?.hex, top?.score)

Installation

To install the package:

npm install @adzazueta/color-extractor

For Node.js image decoding, install the optional sharp peer dependency:

npm install sharp

The Node entrypoint requires Node.js ^20.19.0 || >=22.12.0. Browser and core consumers do not need sharp.

Quick start

Universal (root)

import { extractColors } from '@adzazueta/color-extractor'

const result = await extractColors(image)

// Resolve rankings to colors
const colorsById = new Map(
  result.colors.map(color => [color.id, color]),
)
const perceptual = result.rankings.perceptual.map(
  id => colorsById.get(id)!,
)
console.log(perceptual[0]?.hex)

Browser — file input

import { extractColors } from '@adzazueta/color-extractor'

const fileInput = document.querySelector<HTMLInputElement>('input[type="file"]')
const file = fileInput?.files?.[0]
if (file) {
  const result = await extractColors(file)
  console.log(result.colors[0]?.hex)
}

Browser — ImageData

import { extractColorsFromImageData } from '@adzazueta/color-extractor/browser'

const canvas = document.querySelector('canvas')
const ctx = canvas?.getContext('2d')
if (ctx) {
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  const result = await extractColorsFromImageData(imageData)
  console.log(result.colors)
}

Node — local path

import { extractColors } from '@adzazueta/color-extractor/node'

const result = await extractColors('./photo.jpg')
console.log(result.rankings.perceptual)

Node — Buffer

import { readFile } from 'node:fs/promises'
import { extractColors } from '@adzazueta/color-extractor/node'

const buffer = await readFile('./photo.jpg')
const result = await extractColors(buffer)
console.log(result.metadata.validPixels)

Core — pixel buffer

import { extractColorsFromPixels } from '@adzazueta/color-extractor/core'

const result = await extractColorsFromPixels({
  data: new Uint8Array([/* RGBA bytes: width * height * 4 */]),
  width: 200,
  height: 150,
  channels: 4,
})
console.log(result.colors.length)

Public entrypoints

| Import path | Primary functions | Runtime | | --- | --- | --- | | @adzazueta/color-extractor | extractColors | Browser or Node through package export conditions | | @adzazueta/color-extractor/browser | extractColors, extractColorsFromImageData | Browser | | @adzazueta/color-extractor/node | extractColors | Node.js | | @adzazueta/color-extractor/core | extractColorsFromPixels | Any; no decoder dependencies |

The root import uses package export conditions: Node resolves the Node entrypoint, browser-oriented resolution uses the Browser entrypoint, and the default condition is Browser. Use an explicit subpath when you need deterministic runtime selection.

All entrypoints expose the relevant public types and VERSION. All entrypoints also expose ColorExtractorError, COLOR_EXTRACTOR_ERROR_CODES, and DEFAULT_NEUTRAL_OPTIONS. Root, browser, and node expose the ExtractColorOptions union type covering all runtime-specific option shapes (BrowserExtractColorOptions | NodeExtractColorOptions); core exposes CoreExtractColorOptions. Generated TypeScript declarations are the complete export reference.

Use explicit subpath imports when you need runtime-specific types, browser decoding helpers, or the Core pixel API.

Primary signatures

extractColors(input, options?): Promise<ExtractColorResult>
extractColorsFromImageData(imageData, options?): Promise<ExtractColorResult> // browser
extractColorsFromPixels(input, options?): Promise<ExtractColorResult> // core

extractColors is overloaded by the root entrypoint for Browser and Node inputs. extractColorsFromImageData is available from /browser, and extractColorsFromPixels is available from /core.

Supported inputs by runtime

| Runtime | Supported inputs | | --- | --- | | Browser | File, Blob, http:// or https:// URL string, HTMLImageElement, ImageBitmap, HTMLCanvasElement, ImageData | | Node.js | Buffer, Uint8Array, ArrayBuffer, URL string (http:///https://), non-empty local path string | | Core | { data: Uint8Array \| Uint8ClampedArray, width: number, height: number, channels: 3 \| 4 } |

Browser URL requests must be allowed by CORS. Browser strings other than http:// and https:// URLs are unsupported. Node URL strings are classified as remote when they start with http:// or https://; non-empty other strings are treated as local filesystem paths. An empty Node string is unsupported.

Neutral result model

extractColors returns an ExtractColorResult:

type ExtractColorResult = {
  colors: ObservedColor[]
  rankings: PaletteRankings
  metadata: ExtractionMetadata
}

Colors

type ObservedColor = {
  id: ColorId        // e.g. "color-a85f46"
  hex: string         // e.g. "#a85f46"
  rgb: RgbColor       // { r: number, g: number, b: number }
  lab: LabColor       // { L: number, a: number, b: number }
  chroma: number      // sqrt(a² + b²)
  population: number  // count among sampled valid pixels
  proportion: number  // population / validPixels
  score: number       // normalized perceptual score (0–1)
  hsl?: HslColor      // only when result.includeHsl is true
}

Every color is an observed color from the sampled valid pixels of the supplied image. No color is generated, adjusted, or assigned a UI role.

The score is relative within a single extraction result — it is not globally comparable across different images or extractions.

colors is sorted by ID (lexicographic), not by relevance. Use rankings to resolve order.

Rankings

type PaletteRankings = {
  perceptual: ColorId[]   // rawScore desc → population desc → chroma desc → id asc
  population: ColorId[]   // population desc → rawScore desc → chroma desc → id asc
  chroma: ColorId[]       // chroma desc → rawScore desc → population desc → id asc
}

Every ranking contains exactly the same IDs as colors. Rankings are permutations of the returned set.

A common consumer pattern to resolve ranked colors:

const colorsById = new Map(
  result.colors.map(color => [color.id, color]),
)
const perceptual = result.rankings.perceptual.map(
  id => colorsById.get(id)!,
)

This is consumer code — the ranking is resolved directly using result.rankings.

Metadata

type LabKmeansAlgorithmDetails = {
  algorithm: 'lab-kmeans'
  requestedClusters: number
  producedCandidates: number
  iterations: number
}

type MmcqAlgorithmDetails = {
  algorithm: 'mmcq'
  requestedBoxes: number
  producedCandidates: number
  histogramBits: number
  occupiedBins: number
  splits: number
}

type AlgorithmDetails =
  | LabKmeansAlgorithmDetails
  | MmcqAlgorithmDetails

type ExtractionMetadata = {
  algorithm: 'lab-kmeans' | 'mmcq'
  algorithmVersion: string
  packageVersion: string
  runtime: 'browser' | 'node' | 'core'
  decoder: 'canvas' | 'image-data' | 'sharp' | 'pixels'
  sampledWidth: number
  sampledHeight: number
  sampledPixels: number
  validPixels: number
  candidateCount: number
  returnedColors: number
  returnedPopulation: number
  coverage: number       // returnedPopulation / validPixels
  algorithmDetails: AlgorithmDetails
}

metadata.algorithm and metadata.algorithmDetails.algorithm always match. coverage may be less than 1 when maxColors limits the result. validPixels and populations refer to the sampled pixels that passed filtering, not necessarily every source pixel.

Neutral Color Defaults

The default options for neutral color extraction (extractColors) are exported as DEFAULT_NEUTRAL_OPTIONS:

import { DEFAULT_NEUTRAL_OPTIONS } from '@adzazueta/color-extractor'

DEFAULT_NEUTRAL_OPTIONS contains the common neutral defaults: algorithm: 'lab-kmeans', sampling.maxDimension: 150, filtering values alphaThreshold: 128, minBrightness: 10, maxBrightness: 245, minSaturation: 8, result values maxColors: 5, includeHsl: false, Lab K-means values clusters: 8, iterations: 7, MMCQ boxes: 8, and perceptual ranking values chromaFloor: 12, lowChromaPenalty: 0.1. Runtime-specific decode options (decode) and Node remote options (remote) are resolved separately per runtime.

Configuration

All options are optional. Defaults favor useful perceptual output and bounded resource use.

Algorithm selection

| Option | Default | Version | Description | | --- | --- | --- | --- | | algorithm | 'lab-kmeans' | 1.0.0 | Selects 'lab-kmeans' or 'mmcq'. Both algorithms use the same neutral normalization, scoring, rankings, and metadata contract. | | algorithm: 'mmcq' | - | mmcq-v2 | Uses a 5-bit-per-channel RGB histogram and selects the nearest observed sample from each final box. |

Lab K-means generates Lab-space candidates. MMCQ builds a 5-bit-per-channel RGB histogram, splits color boxes, and selects the nearest observed sample in each final box. The selected version is reported in metadata.algorithmVersion.

The shared neutral ranking uses chroma * log(population + 1). When a candidate's chroma is below chromaFloor, the score is multiplied by lowChromaPenalty. The normalized score is relative to the current extraction result.

Common options (every runtime)

| Group | Option | Default | Description | | --- | --- | --- | --- | | sampling | maxDimension | 150 | Constrain the longest image dimension to this size across browser, Node, and /core runtimes. Integer range: 1–4096. In /core, downsamples grid sampling to this maximum dimension without modifying the source pixel buffer. | | filtering | alphaThreshold | 128 | Ignore pixels below this alpha value. Integer range: 0–255. | | filtering | minBrightness | 10 | Ignore near-black pixels below this sRGB brightness. Number range: 0–255. | | filtering | maxBrightness | 245 | Ignore near-white pixels above this sRGB brightness. Number range: 0–255. Must be >= minBrightness. | | filtering | minSaturation | 8 | Ignore low-saturation pixels below this HSL percentage. Number range: 0–100. | | result | maxColors | 5 | Maximum number of colors in the returned result. Integer range: 1–64. | | result | includeHsl | false | Include HSL values in each color. | | advanced.labKmeans | clusters | max(8, maxColors) | Internal cluster count. Integer range: 1–64. Must be >= maxColors. | | advanced.labKmeans | iterations | 7 | K-means refinement passes. Integer range: 1–100. | | advanced.mmcq | boxes | max(8, maxColors) | Requested MMCQ color boxes. Integer range: 1–64. Must be >= maxColors. | | advanced.perceptualRanking | chromaFloor | 12 | Chroma below which the low-chroma penalty applies. Number range: 0–150. | | advanced.perceptualRanking | lowChromaPenalty | 0.1 | Score multiplier for low-chroma candidates. Number range: 0–1. | | signal | — | — | AbortSignal for cancellation. |

Note: result.maxColors caps the returned color count but is independent of the internal cluster/box count. The active algorithm's count defaults to at least 8 and must be >= maxColors.

Note: When algorithm is 'lab-kmeans', advanced.mmcq is rejected. When algorithm is 'mmcq', advanced.labKmeans is rejected. advanced.perceptualRanking is valid for both algorithms.

Note: There is no ranking.strategy option. All three rankings are always returned.

Browser decode options

| Option | Default | Description | | --- | --- | --- | | decode.maxPixels | 25_000_000 | Maximum decoded image dimensions (width × height). Integer range: 1–100,000,000. |

Node decode options

| Option | Default | Description | | --- | --- | --- | | decode.maxPixels | 25_000_000 | Maximum decoded image dimensions (width × height). Integer range: 1–100,000,000. | | decode.animated | 'first-frame' | 'first-frame' extracts only the first frame. | | decode.svg | 'disabled' | SVG decoding policy. Enable only for trusted sources. | | decode.respectOrientation | true | Apply EXIF orientation. | | decode.normalizeColorProfile | true | Convert supported images to sRGB. |

Node remote options

| Option | Default | Description | | --- | --- | --- | | remote.timeoutMs | 10_000 | Request and body timeout in milliseconds. Integer range: 1–300,000. | | remote.maxBytes | 10_000_000 | Maximum remote response or local file input size in bytes. Integer range: 1–1,000,000,000. | | remote.maxRedirects | 3 | Maximum redirect hops. Integer range: 0–20. | | remote.allowedProtocols | ['http:', 'https:'] | Non-empty list of URL protocols allowed for remote input. Values may only be 'http:' or 'https:'. | | remote.allowPrivateNetworks | false | Permit private or reserved addresses. Do not enable for untrusted URLs. | | remote.validateContentType | true | Reject non-image response content types when available. |

Option validation

Unknown, legacy, invalid, null, or runtime-incompatible options fail with COLOR_EXTRACTOR_INVALID_OPTIONS. Only undefined means that an option was omitted and should receive its default. Valid zero values are preserved (e.g. lowChromaPenalty: 0).

Full example

import { extractColors } from '@adzazueta/color-extractor'

const result = await extractColors(image, {
  sampling: { maxDimension: 300 },
  filtering: {
    alphaThreshold: 16,
    minSaturation: 5,
  },
  result: {
    maxColors: 8,
    includeHsl: true,
  },
  advanced: {
    labKmeans: { clusters: 12, iterations: 10 },
    perceptualRanking: { chromaFloor: 10, lowChromaPenalty: 0.05 },
  },
})

For MMCQ, select the algorithm and configure its boxes instead:

const result = await extractColors(image, {
  algorithm: 'mmcq',
  result: { maxColors: 6 },
  advanced: {
    mmcq: { boxes: 8 },
    perceptualRanking: { chromaFloor: 12, lowChromaPenalty: 0.1 },
  },
})

Cancellation

Pass an AbortSignal to cancel extraction in progress:

const controller = new AbortController()

setTimeout(() => controller.abort(), 5000)

try {
  const result = await extractColors(image, {
    signal: controller.signal,
  })
} catch (error) {
  if (error instanceof ColorExtractorError && error.code === 'COLOR_EXTRACTOR_ABORTED') {
    console.log('Cancelled')
  }
}

Cancellation is checked before work, during supported asynchronous decoding/fetching, after filtering, and after candidate generation. MMCQ also checks during histogram construction, box splitting, and final candidate construction. K-means checks between synchronous refinement iterations; it does not interrupt an iteration that is already running.

An already-aborted signal rejects immediately without decode, fetch, or sharp work.

Error handling

Library-generated validation, decoding, fetching, and extraction failures use ColorExtractorError with a stable code property. Platform exceptions that are not recognized by an adapter may propagate unchanged.

import { ColorExtractorError, extractColors } from '@adzazueta/color-extractor'

try {
  await extractColors(image)
} catch (error) {
  if (error instanceof ColorExtractorError) {
    console.error(error.code)   // stable machine-readable code
  }
}

Error codes

| Code | Stage | Typical cause | | --- | --- | --- | | COLOR_EXTRACTOR_UNSUPPORTED_INPUT | Pre-processing | Invalid input type, missing fields, or data length mismatch. | | COLOR_EXTRACTOR_INVALID_OPTIONS | Option resolution | Unknown, legacy, or out-of-range options. | | COLOR_EXTRACTOR_ABORTED | Any | Operation cancelled via AbortSignal. | | COLOR_EXTRACTOR_DECODE_FAILED | Decode | Image bytes could not be decoded. | | COLOR_EXTRACTOR_CORS_ERROR | Decode (browser) | Canvas readback blocked by CORS. | | COLOR_EXTRACTOR_FETCH_FAILED | Fetch | Non-2xx response, network failure, response-body read failure, or empty response. | | COLOR_EXTRACTOR_INPUT_TOO_LARGE | Input | Browser/Node remote response or Node local file exceeded maxBytes. | | COLOR_EXTRACTOR_IMAGE_TOO_LARGE | Decode | Image dimensions exceeded maxPixels. | | COLOR_EXTRACTOR_TIMEOUT | Fetch | URL request exceeded timeoutMs. | | COLOR_EXTRACTOR_UNSAFE_URL | Fetch (Node) | URL rejected by safety policy. | | COLOR_EXTRACTOR_UNSUPPORTED_FORMAT | Decode (Node) | Format not supported by the decoder. | | COLOR_EXTRACTOR_SHARP_MISSING | Decode (Node) | sharp is not installed. | | COLOR_EXTRACTOR_NO_VALID_PIXELS | Filtering | All pixels removed by filtering criteria. |

The code field is the stable machine contract. Error message text may change between releases.

Browser notes

  • Supported inputs: File, Blob, http:// or https:// URL string, HTMLImageElement, ImageBitmap, HTMLCanvasElement, ImageData.
  • URL strings are fetched via fetch() and subject to CORS.
  • Browser neutral extraction does not expose Node's remote options. Browser remote fetches use the built-in 10-second timeout and 10 MB response limit.
  • Decoded images are sampled via OffscreenCanvas when available, otherwise a DOM canvas fallback, to sampling.maxDimension while preserving aspect ratio. ImageData uses a software fallback only when no canvas API is available.
  • Browser decode.maxPixels is checked after platform image decoding because browser APIs do not provide portable pre-decode dimensions.
  • The browser bundle contains no Node.js dependencies.

Node notes

  • Supported inputs: Buffer, Uint8Array, ArrayBuffer, URL string, non-empty local path string.
  • Strings starting with http:// or https:// are fetched as remote URLs.
  • Non-empty strings that are not HTTP(S) URLs are treated as local filesystem paths. An empty string is unsupported.
  • SVG decoding is disabled by default. Enable with decode.svg: 'enabled' for trusted sources.
  • Animated images return the first frame by default (decode.animated: 'first-frame').
  • EXIF orientation is applied by default. Color profiles are normalized to sRGB.
  • SSRF protection: private network requests are blocked by default (remote.allowPrivateNetworks: false).
  • sharp is an optional peer dependency — install it only when using Node decode paths.

Core notes

  • The core entrypoint accepts already-decoded pixel buffers only.
  • Input requires { data, width, height, channels } with channels: 3 | 4.
  • Data length is strictly validated: data.length === width * height * channels.
  • The core entrypoint does not resize, decode, or fetch images.
  • sampling.maxDimension controls grid downsampling without modifying the source pixel buffer.
  • There is no DOM, filesystem, fetch, or sharp dependency.

Format support

Browser support follows the platform decoder. PNG, JPEG, GIF, WebP, and BMP are widely available; AVIF and SVG availability depends on the browser.

Node support follows the installed sharp and libvips build. Common formats include PNG, JPEG, WebP, GIF, AVIF, TIFF, BMP, and ICO.

Known limitations

  • Lab K-means (default) and MMCQ (algorithm: 'mmcq') are the available neutral extraction algorithms.
  • No role assignment — every color is an observed color with no semantic label.
  • No generated or adjusted colors (harmony fallback, lightness adjustment).
  • No public ranking helper — use the Map pattern shown above.
  • Cancellation granularity is between synchronous pipeline stages and between K-means iterations, not inside one synchronous K-means iteration.
  • Browser and Node decoders may produce slightly different pixel values from the same image.

color-engine boundary

Semantic role selection (primary, secondary, accent), harmony generation, lightness adjustment, and fallback policies are out of scope for this package.

extractColors returns only observed-color evidence. Consumers that need role-labeled colors should compose the extractor with a separate engine layer:

const extracted = await extractColors(image)
const theme = colorEngineAdapter(extracted)  // illustrative — adapter name defined by the engine package

The exact engine adapter API is defined by @adzazueta/color-engine and is not part of this package.

Versioning

This package follows semantic versioning. The 0.x major version zero line may introduce breaking changes. The public API surface is documented in this README and in generated TypeScript declarations.

Contributing

See CONTRIBUTING.md for the contribution model, bug reporting, feature requests, and development conventions.

Security

See SECURITY.md for responsible vulnerability-reporting guidance.

License

MIT © 2026 Alexis D. Zazueta