image-ssim
image-ssim is a tiny CommonJS implementation of the structural similarity index for JavaScript and TypeScript. Give it two already-decoded images with pixel data, width, height, and a channel count, and it returns an SSIM score plus a mean contrast-structure score. It works in Node or a browser, but it does not open PNG or JPEG files, resize images, produce a visual diff, or choose a pass threshold for you. The package is best understood as one old, dependency-free comparison function rather than a complete screenshot-testing tool.
Keep it when an existing test suite depends on its exact scoring behavior. For a new project, ssim.js is the safer SSIM choice, while looks-same or pixelmatch covers screenshot-test workflow that image-ssim leaves to your code.
Use it if
- You already have equal-sized raw pixel buffers and need a simple perceptual similarity number
- You maintain CommonJS code that already depends on the package and want to preserve its existing comparison behavior
- You need the returned mean contrast-structure score as well as the overall SSIM score
- You want a dependency-free comparator that can run against Canvas ImageData in a browser
- You want an actively maintained SSIM implementation: npm 0.2.0 is the only published version and the repository's last push was in 2017
- You need file loading, resizing, alignment, or a visual diff: the public API only accepts decoded pixel data and returns two numeric scores
- Your images can differ in dimensions: compare throws 'Images have different sizes!' instead of aligning or cropping them
- Your dimensions may leave a one-pixel window at an edge: an open issue demonstrates that this can return NaN because the variance divisor becomes zero
- You want modern ESM packaging or a documented options object: the package exposes CommonJS and seven positional compare arguments, so tuning later parameters means supplying every earlier one
Setup reality
Installation is only npm install image-ssim, with no runtime dependencies, native build, peer dependency, credential, or configuration file. That easy install hides the work the package deliberately leaves to you. compare does not accept paths, Buffers containing compressed files, browser Blob objects, or HTMLImageElement values. In Node you must add a decoder such as pngjs or sharp and turn both inputs into raw data with known channel counts. In a browser you draw each image to an equally sized canvas and pass getImageData output. Width and height must match exactly or the call throws. Channels are numeric: Grey 1, GreyAlpha 2, RGB 3, and RGBAlpha 4. The default call uses 8 by 8 windows, K1 0.01, K2 0.03, luminance conversion enabled, and 8 bits per component. Those settings are positional rather than an options object. Alpha is multiplied into luminance as if the background were black, which may not match how a transparent asset is composited in your UI. TypeScript declarations are shipped, but the package metadata does not advertise a types field and the declaration uses the older export-equals style. Plan to use a CommonJS require or a compatible TypeScript interop setting, validate finite output, and define your own acceptance threshold from representative images rather than assuming every score below 1 means failure.
Patterns
Compare two RGBA pixel bufferscompare-rgba-buffers
const { compare, Channels } = require('image-ssim');
const result = compare(
{ data: actualPixels, width, height, channels: Channels.RGBAlpha },
{ data: expectedPixels, width, height, channels: Channels.RGBAlpha }
);
console.log(result.ssim, result.mcs);data must contain decoded channel values, not compressed PNG or JPEG bytes, and both dimensions must match.
Decode and compare PNG files in Nodedecode-png-files
const fs = require('node:fs');
const { PNG } = require('pngjs');
const { compare, Channels } = require('image-ssim');
const left = PNG.sync.read(fs.readFileSync('actual.png'));
const right = PNG.sync.read(fs.readFileSync('expected.png'));
const result = compare(
{ data: left.data, width: left.width, height: left.height, channels: Channels.RGBAlpha },
{ data: right.data, width: right.width, height: right.height, channels: Channels.RGBAlpha }
);pngjs is a separate dependency; image-ssim itself does not read image files. Check dimensions before compare if mismatch is an expected test outcome.
Compare browser canvas pixelscompare-canvas-images
const a = canvasA.getContext('2d').getImageData(0, 0, canvasA.width, canvasA.height);
const b = canvasB.getContext('2d').getImageData(0, 0, canvasB.width, canvasB.height);
const result = ImageSSIM.compare(
{ data: a.data, width: a.width, height: a.height, channels: ImageSSIM.Channels.RGBAlpha },
{ data: b.data, width: b.width, height: b.height, channels: ImageSSIM.Channels.RGBAlpha }
);Canvas access can throw a security error when a cross-origin image was drawn without suitable CORS headers.
Turn a similarity score into a test assertionenforce-threshold
const { ssim } = compare(actual, expected);
const minimum = 0.985;
if (!Number.isFinite(ssim) || ssim < minimum) {
throw new Error(`SSIM ${ssim} is below ${minimum}`);
}The package supplies no universal pass threshold. Calibrate one against representative approved and rejected images, and reject NaN explicitly.
Handle dimension mismatches before comparisonguard-image-size
function compareSameSize(a, b) {
if (a.width !== b.width || a.height !== b.height) {
return { comparable: false, reason: `${a.width}x${a.height} != ${b.width}x${b.height}` };
}
return { comparable: true, result: compare(a, b) };
}Calling compare directly with different dimensions throws an Error; the library does not crop, pad, or resize either input.
Compare three-channel RGB datacompare-rgb-data
const result = compare(
{ data: rgbA, width, height, channels: Channels.RGB },
{ data: rgbB, width, height, channels: Channels.RGB }
);The array must be tightly packed RGBRGB data. Passing RGBA bytes while declaring RGB shifts every pixel after the first.
Compare one-channel grayscale datacompare-grayscale-data
const result = compare(
{ data: grayA, width, height, channels: Channels.Grey },
{ data: grayB, width, height, channels: Channels.Grey }
);Grey expects one sample per pixel. GreyAlpha is the separate two-channel mode and consumes two samples per pixel.
Composite transparency before comparingchoose-alpha-background
function compositeOnWhite(rgba) {
const rgb = new Uint8Array((rgba.length / 4) * 3);
for (let s = 0, d = 0; s < rgba.length; s += 4) {
const a = rgba[s + 3] / 255;
rgb[d++] = rgba[s] * a + 255 * (1 - a);
rgb[d++] = rgba[s + 1] * a + 255 * (1 - a);
rgb[d++] = rgba[s + 2] * a + 255 * (1 - a);
}
return rgb;
}RGBAlpha mode multiplies luminance by alpha, effectively compositing onto black. Pre-composite when the real page background is another color.
Set a custom SSIM window sizechange-window-size
const result = compare(actual, expected, 4);The default is 8. Avoid a size that leaves a one-pixel edge window because the current variance calculation can produce NaN there.
Compare RGB channel sums without luminance weightsdisable-luminance-weighting
const result = compare(
actual,
expected,
8, // windowSize
0.01, // K1
0.03, // K2
false // luminance
);With luminance false, RGB values are summed rather than weighted. The positional API requires retaining all preceding defaults.
Set the component bit depthcompare-higher-bit-depth
const imageA = { data: samplesA, width, height, channels: Channels.Grey };
const imageB = { data: samplesB, width, height, channels: Channels.Grey };
const result = compare(imageA, imageB, 8, 0.01, 0.03, true, 16);bitsPerComponent changes the SSIM dynamic range only. Supply decoded sample values on the matching 0 to 65535 scale; the library does not unpack 16-bit file bytes.
Compare a batch against one baselinecompare-image-batch
const scores = candidates.map(({ name, image }) => {
const result = compare(image, baseline);
return { name, ssim: result.ssim, valid: Number.isFinite(result.ssim) };
});
const worstFirst = scores.sort((a, b) => a.ssim - b.ssim);compare is synchronous and walks every pixel. Large screenshot batches can block the event loop, so move the loop to workers when latency matters.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ssim.js | npm | You want a maintained SSIM implementation with a newer API and performance-focused algorithms |
| looks-same | npm | You compare PNG screenshots and want file handling, tolerance controls, and diff images included |
| pixelmatch | npm | You need a small pixel-level comparator that can write a visible diff rather than an SSIM score |