mrkeyoor.com_
Sun 09 Aug 06:58 UTC
npmTestingupdated 09 Aug 2026

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.

Verdict

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.

API stability3/5The public surface is just compare, Channels, an image shape, and a two-number result, and that surface has not changed since the sole 0.2.0 release. That makes existing calls predictable, but it is stability by inactivity rather than a supported compatibility policy. Seven positional arguments also make extensions difficult without breaking or complicating callers.
Docs2/5The README explains the purpose, links a live browser demo and generated API pages, and points to the algorithm's origins. It does not show the basic Node or browser call, explain how to decode an image, discuss score interpretation, document alpha treatment, or warn about window edge cases. Most practical guidance has to be recovered from tests and source.
Maintenance1/5npm has one release, version 0.2.0, and the GitHub repository was last pushed in May 2017. It is not archived and the package is not marked deprecated, but a reproducible NaN edge case reported in August 2026 remains open. The old Gulp 3, Mocha 2, Browserify 10, and TypeScript 2 development stack is further evidence that no current maintenance cycle is visible.
Ecosystem3/5The package recorded 3,637,453 downloads in the measured npm week and its raw image shape works naturally with browser ImageData and decoders such as pngjs. Still, the repository has 94 stars, the README names no integrations, and the package provides no loaders, reporters, test-runner adapters, diff renderer, or modern module entry point of its own.

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
Skip it if

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

PackageRegistryPick it when
ssim.jsnpmYou want a maintained SSIM implementation with a newer API and performance-focused algorithms
looks-samenpmYou compare PNG screenshots and want file handling, tolerance controls, and diff images included
pixelmatchnpmYou need a small pixel-level comparator that can write a visible diff rather than an SSIM score