inspect-image
v0.4.0
Published
Exact image color statistics, palettes, dominant colors and crop suggestions
Readme
inspect-image
Synchronous, deterministic image color analysis for Bun and Node-compatible runtimes.
inspect-image reports dimensions, encoded/decoded byte sizes, RGB + HSL/OkHSL distributions, spatial acutance/activity, frequent integer colors, dominant color, alpha statistics, tiled analyses, and conservative crop suggestions.
Install
bun add inspect-imageUsage
import {readFile} from 'node:fs/promises'
import inspectImage from 'inspect-image'
const result = inspectImage(encodedBytes)
const fromDisk = inspectImage(await readFile('photo.png'))File access is deliberately outside the library. Read files with the importer’s runtime APIs and pass their bytes to inspectImage().
Inputs
inspectImage() accepts encoded PNG, JPEG, GIF, BMP and TIFF bytes synchronously, or tightly packed raw RGB/RGBA pixels:
inspectImage({
width: 2,
height: 1,
channels: 4,
data: new Uint8Array([255, 0, 0, 255, 0, 0, 255, 128]),
})JPEG EXIF orientation is applied before analysis. GIF analysis uses the first frame. TIFF analysis uses the first page. ICC/profile conversion and animation aggregation are intentionally outside the synchronous core.
Result semantics
channelsis the normalized decoded layout: 3 for RGB and 4 when the source structurally carries alpha. A fully opaque RGBA PNG therefore remains 4-channel and includes an opacity probe.fileSizeis the encoded byte count. It is absent only for direct raw-pixel input.bufferSizeis the normalized decoded RGB/RGBA byte count.- PNG
tRNScolor-key RGB is preserved instead of being replaced with black by the decoder. - Frequent colors count every pixel after rounding the selected HSL space to integer hue/saturation/lightness. Alpha never splits a color group; returned opacity is the group mean.
- Crop comparison deliberately ignores alpha and requires at least one pair of adjacent/touching full outer edges with the same rounded color.
- Hue average and median are circular rather than treating 359° and 1° as far apart.
OkHSL conversion is provided by the dependency-free okhsl package. Its RGB inverse intentionally returns integer OkHSL channels, which exactly matches this library’s grouping and crop semantics.
Options
type Options = {
frequentColorsCount?: number
hueSaturationThreshold?: number
colorSpace?: 'okhsl' | 'hsl'
rows?: number
columns?: number
dominantColorAlgorithm?: 'k_means' | 'median_cut' | false
dominantColorClusters?: number
maxPixels?: number
}Defaults:
frequentColorsCount: 10(0disables frequency collection)hueSaturationThreshold: 1(pixels with lower saturation are excluded from the hue probe)colorSpace: 'okhsl'rows: 1,columns: 1dominantColorAlgorithm: 'k_means'dominantColorClusters: 5(accepted range 1–16)maxPixels: 40_000_000
When rows/columns are both 1, probes and colors are returned at the top level. Otherwise base metadata is returned with a row-major tiles array. Uneven dimensions are split with floor boundaries so every pixel belongs to exactly one tile.
Distribution probes
Each probe contains minimum, maximum, average, median and distribution brackets.
Non-hue probes use 8 non-overlapping percentage brackets:
0%, >0% <5%, ≥5% <20%, ≥20% <50%, ≥50% <80%, ≥80% <95%, ≥95% <100%, 100%.
Hue uses 12 circular 30° brackets centered on multiples of 30°: [345°,15°), [15°,45°), …, [315°,345°). The first bracket wraps across 360° → 0°. Pixels below hueSaturationThreshold are omitted from the hue probe.
RGB uses 255 as 100%; saturation/lightness/opacity/acutance/activity use 100. RGB medians are exact. Continuous HSL/opacity/spatial medians use 0.01-unit histograms; default OkHSL inverse values are integers by definition of the okhsl dependency.
Spatial probes use encoded-sRGB luma Y = 0.2126R + 0.7152G + 0.0722B and ignore alpha. activity is each pixel's mean absolute luma difference from its existing orthogonal neighbors, normalized so a black/white difference is 100%. acutance is the ratio of the clipped 3×3 luma range to the clipped 5×5 luma range; pixels whose 5×5 neighborhood is uniform are omitted from that probe. Spatial neighborhoods are clipped only at image boundaries and always cross tile boundaries, so changing rows/columns does not change a pixel's spatial measurement.
Frequent colors
Every pixel contributes. The selected HSL values are rounded to:
- hue: 0–359
- saturation: 0–100
- lightness: 0–100
Ties are deterministic and sorted by the packed integer color key. Opacity is averaged per integer color when alpha exists.
Dominant color
Dominant analysis is deterministic and bounded:
- Every pixel contributes to a 5-bit-per-channel RGB population histogram (at most 32,768 buckets).
- Each non-empty bucket is represented by its population-weighted mean RGB and converted to Oklab.
- Deterministic weighted k-means or weighted median-cut operates on those buckets.
- The largest population cluster is represented by its weighted Oklab mean and converted back to the requested HSL space.
This avoids both random sampling bias and the unbounded high-entropy clustering cost seen in exhaustive per-color implementations. Alpha does not affect cluster assignment but is population-averaged for the winning cluster.
Crop suggestions
A crop is returned only when at least one adjacent edge pair (top+left, top+right, bottom+left, bottom+right) is entirely the same rounded color. Opposite-only borders do not qualify. Once qualified, only fully uniform rows/columns of that same color are stripped, making the suggestion conservative rather than blindly bounding every non-border-colored pixel.
Decoder behavior
- PNG: RGB/RGBA, grayscale, grayscale-alpha, indexed transparency, color-key
tRNS, 8/16-bit normalization through pngjs. - JPEG: synchronous jpeg-js decode plus EXIF orientations 1–8.
- GIF: first frame through omggif, preserving transparent-index alpha semantics.
- BMP: bmp-ts decode, including 32-bit alpha layout.
- TIFF: first page through utif2, with alpha inferred from samples/extra-sample metadata.
Encoded dimensions are checked against maxPixels as early as each codec permits. The limit is an application guard, not a security sandbox; isolate untrusted decoders with process-level resource limits when needed.
Development
bun run validateThat runs strict TypeScript checking, ESLint, the complete test suite, and the production build.
