llms.txt@apps/website/public · git:20260716.548266e · 2026-07-16 · sha256 29582875c21e0b8d
llms.txt@apps/website/public git:20260716.548266eB
Immutable. This exact content is served forever at /api/v1/blob/29582875c21e0b8d.
# BlazeDiff
> BlazeDiff is a high-performance diff ecosystem. Originally built in JavaScript as a pixel-perfect image comparison library that's 1.5x faster than [pixelmatch](https://github.com/mapbox/pixelmatch). Now, BlazeDiff has evolved into a comprehensive suite of blazing-fast diff tools including image comparison, image diff analysis deterministically + agent-in-the-loop verdict, object diffing, perceptual quality metrics, framework-agnostic UI renderers, and React components for visualizing differences.
**Install (npm):** `npm install @blazediff/core`
**Install (JSR):** `deno add jsr:@blazediff/core`
## Performance
- **Native (Rust)**: 3-4x faster than odiff, 8x faster than pixelmatch on 4K images
- **WASM**: ~58% faster than pixelmatch, up to ~5x on 4K (browser, edge, any wasm host)
- **Image Pixel-by-Pixel (JS)**: ~50% faster than pixelmatch (up to 88% on identical images)
- **SSIM**: ~25% faster than ssim.js, ~70% faster with Hitchhiker's SSIM
- **Object Diff**: ~55% faster than microdiff (up to 96% on identical arrays)
---
# Packages
# @blazediff/core
High-performance pixel-by-pixel image comparison library. 1.5x faster than [pixelmatch](https://github.com/mapbox/pixelmatch) while maintaining identical accuracy.
[View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md)
## Installation
```sh
npm install @blazediff/core
```
## Features
- **1.5x faster** than pixelmatch on average, **88% faster** on identical images
- **100% API compatible** with pixelmatch - drop-in replacement
- **Zero dependencies**
## API Reference
### `blazediff(image1, image2, output, width, height, options?)`
Compares two images pixel by pixel and returns the number of different pixels.
#### Parameters
| Parameter | Type | Description |
| --------- | --------------------------------------------------- | ------------------------------------------- |
| `image1` | `Buffer \| Uint8Array \| Uint8ClampedArray` | Image data of the first image |
| `image2` | `Buffer \| Uint8Array \| Uint8ClampedArray` | Image data of the second image |
| `output` | `Buffer \| Uint8Array \| Uint8ClampedArray \| null` | Output buffer for the diff image (optional) |
| `width` | `number` | Width of the images in pixels |
| `height` | `number` | Height of the images in pixels |
| `options` | `Options` | Comparison options (optional) |
##### Options
| Option | Type | Default | Description |
| ----------------- | --------- | ------------- | ------------------------------------------------ |
| `threshold` | `number` | `0.1` | Matching threshold (0-1). Lower = more sensitive |
| `includeAA` | `boolean` | `false` | Include anti-aliased pixels in diff count |
| `alpha` | `number` | `0.1` | Opacity of original image in diff output |
| `aaColor` | `[R,G,B]` | `[255,255,0]` | Color of anti-aliased pixels (yellow) |
| `diffColor` | `[R,G,B]` | `[255,0,0]` | Color of different pixels (red) |
| `diffColorAlt` | `[R,G,B]` | `null` | Alternative color for dark differences |
| `diffMask` | `boolean` | `false` | Draw diff as a mask with transparent background |
| `fastBufferCheck` | `boolean` | `true` | Use fast buffer comparison for identical images |
> **Info:** **Threshold Guidelines:** - `0.0` - Exact match only - `0.05` - Strict comparison - `0.1` - Default balanced comparison - `0.2` - Lenient comparison
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/core)
- [Examples →](/docs/pixel-comparison/vanilla-javascript)
---
# @blazediff/ssim
Fast SSIM (Structural Similarity Index) implementations for perceptual image quality assessment. Includes standard SSIM, MS-SSIM (Multi-Scale SSIM), and Hitchhiker's SSIM for various use cases and performance requirements.
## Installation
```sh
npm install @blazediff/ssim
```
## Features
- **Three variants** - Standard SSIM, MS-SSIM, and Hitchhiker's SSIM (~4x faster)
- **MATLAB-compatible** - Standard SSIM matches reference implementation with \<0.01% error
- **SSIM map output** - Optional grayscale visualization of similarity
## API Reference
### `ssim(image1, image2, output, width, height, options?)`
Compares two images using standard SSIM metric and returns a similarity score.
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------------- | ------------------------------------------------- |
| `image1` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the first image |
| `image2` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the second image |
| `output` | `Buffer`, `Uint8Array`, `Uint8ClampedArray`, or `undefined` | Optional output buffer for SSIM map visualization |
| `width` | `number` | Width of the images in pixels |
| `height` | `number` | Height of the images in pixels |
| `options` | `SsimOptions` | Comparison options (optional) |
##### Options
| Option | Type | Default | Description |
| ------------ | -------- | ------- | --------------------------------- |
| `windowSize` | `number` | `11` | Size of the Gaussian window |
| `k1` | `number` | `0.01` | Algorithm parameter for luminance |
| `k2` | `number` | `0.03` | Algorithm parameter for contrast |
| `L` | `number` | `255` | Dynamic range of pixel values |
##### Returns
`number` - SSIM score between 0 and 1
### `msssim(image1, image2, output, width, height, options?)`
Compares two images using MS-SSIM (Multi-Scale SSIM) metric.
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------------- | --------------------------------------------------- |
| `image1` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the first image |
| `image2` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the second image |
| `output` | `Buffer`, `Uint8Array`, `Uint8ClampedArray`, or `undefined` | Optional output buffer for SSIM map at finest scale |
| `width` | `number` | Width of the images in pixels |
| `height` | `number` | Height of the images in pixels |
| `options` | `MsssimOptions` | Comparison options (optional) |
##### Options
| Option | Type | Default | Description |
| ------------ | ---------------------- | ------------------------------------------ | --------------------------- |
| `windowSize` | `number` | `11` | Size of the Gaussian window |
| `scales` | `number` | `5` | Number of scales to use |
| `weights` | `number[]` | `[0.0448, 0.2856, 0.3001, 0.2363, 0.1333]` | Weights for each scale |
| `method` | `'product'` or `'sum'` | `'product'` | Aggregation method |
##### Returns
`number` - MS-SSIM score between 0 and 1
### `hitchhikersSSIM(image1, image2, output, width, height, options?)`
Compares two images using Hitchhiker's SSIM (fast rectangular-window version).
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------------- | ----------------------------------- |
| `image1` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the first image |
| `image2` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the second image |
| `output` | `Buffer`, `Uint8Array`, `Uint8ClampedArray`, or `undefined` | Optional output buffer for SSIM map |
| `width` | `number` | Width of the images in pixels |
| `height` | `number` | Height of the images in pixels |
| `options` | `HitchhikersSsimOptions` | Comparison options (optional) |
##### Options
| Option | Type | Default | Description |
| -------------- | --------- | ------------ | ------------------------------------------------------ |
| `windowSize` | `number` | `11` | Size of the rectangular window |
| `windowStride` | `number` | `windowSize` | Stride for window sliding (non-overlapping by default) |
| `covPooling` | `boolean` | `true` | Use Coefficient of Variation pooling (recommended) |
| `k1` | `number` | `0.01` | Algorithm parameter for luminance |
| `k2` | `number` | `0.03` | Algorithm parameter for contrast |
| `L` | `number` | `255` | Dynamic range of pixel values |
##### Returns
`number` - SSIM score between 0 and 1
**Performance**: ~4x faster than standard SSIM using integral images for O(1) window computation.
### Score Interpretation
| Score Range | Similarity Level | Description |
| ----------- | ---------------- | ------------------------------------------------------- |
| `1.0` | Identical | Images are identical or perceptually identical |
| `0.99+` | Excellent | Extremely high similarity (minor compression artifacts) |
| `0.95-0.99` | Very Good | High similarity (small compression or noise) |
| `0.90-0.95` | Good | Noticeable but acceptable differences |
| `0.80-0.90` | Fair | Significant but tolerable differences |
| `<0.80` | Poor | Major structural differences |
> **Info:** **Threshold Guidelines:** - Use threshold `>0.99` for strict visual regression testing - Use threshold `>0.95` for standard visual regression testing - Scores below `0.90` indicate substantial visual differences
## Usage Examples
```typescript
import ssim from "@blazediff/ssim/ssim";
// Basic usage
const score = ssim(img1.data, img2.data, undefined, width, height);
// With SSIM map output
const output = new Uint8ClampedArray(width * height * 4);
const score = ssim(img1.data, img2.data, output, width, height);
// With custom options
const score = ssim(img1.data, img2.data, undefined, width, height, {
windowSize: 8,
k1: 0.01,
k2: 0.03,
});
```
```typescript
import msssim from "@blazediff/ssim/msssim";
// Basic usage
const score = msssim(img1.data, img2.data, undefined, width, height);
// With SSIM map at finest scale
const output = new Uint8ClampedArray(width * height * 4);
const score = msssim(img1.data, img2.data, output, width, height);
// With custom scales and weights
const score = msssim(img1.data, img2.data, undefined, width, height, {
scales: 3,
weights: [0.33, 0.33, 0.34],
method: "sum",
});
```
```typescript
import hitchhikersSSIM from "@blazediff/ssim/hitchhikers-ssim";
// Basic usage with CoV pooling (recommended)
const score = hitchhikersSSIM(img1.data, img2.data, undefined, width, height);
// With mean pooling (traditional)
const score = hitchhikersSSIM(img1.data, img2.data, undefined, width, height, {
covPooling: true,
});
// With custom window and stride
const score = hitchhikersSSIM(img1.data, img2.data, undefined, width, height, {
windowSize: 16,
windowStride: 8, // Overlapping windows
covPooling: true,
});
```
## CLI Usage
All three variants are available via the `@blazediff/cli` CLI:
```bash
# Standard SSIM
blazediff-cli ssim image1.png image2.png
# MS-SSIM
blazediff-cli msssim image1.png image2.png
# Hitchhiker's SSIM
blazediff-cli hitchhikers-ssim image1.png image2.png
# With options
blazediff-cli hitchhikers-ssim image1.png image2.png --window-size 16 --no-cov-pooling
```
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/ssim)
- [SSIM Paper](https://ieeexplore.ieee.org/document/1284395) - Wang et al. (2004)
- [MS-SSIM Paper](https://ieeexplore.ieee.org/document/1292216) - Wang et al. (2003)
- [Hitchhiker's SSIM Paper](https://ieeexplore.ieee.org/document/9345560) - Venkataramanan et al. (2021)
- [Examples →](/docs/structural-comparison)
---
# @blazediff/gmsd
Fast single-threaded GMSD (Gradient Magnitude Similarity Deviation) metric for perceptual image quality assessment. Perfect for CI visual testing where you need a similarity score rather than pixel-by-pixel differences.
## Installation
```sh
npm install @blazediff/gmsd
```
## Features
- **Gradient-based** - Measures structural similarity via gradient magnitudes, tolerant to compression artifacts
- **GMS map output** - Optional grayscale visualization of gradient similarity
[Mathematical details (FORMULA.md)](https://github.com/teimurjan/blazediff/blob/main/packages/gmsd/FORMULA.md)
## API Reference
### `gmsd(image1, image2, output, width, height, options?)`
Compares two images using GMSD metric and returns a similarity score.
#### Parameters
| Parameter | Type | Description |
| --------- | -------------------------------------------- | ------------------------------------------------ |
| `image1` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the first image |
| `image2` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the second image |
| `output` | `Buffer`, `Uint8Array`, `Uint8ClampedArray`, or `undefined` | Optional output buffer for GMS map visualization |
| `width` | `number` | Width of the images in pixels |
| `height` | `number` | Height of the images in pixels |
| `options` | `GmsdOptions` | Comparison options (optional) |
##### Options
| Option | Type | Default | Description |
| ------------ | -------- | ------- | ----------------------------------------------------- |
| `downsample` | `0` or `1` | `0` | Downsample factor (0 = no downsampling, 1 = half) |
| `c` | `number` | `170` | Constant for numerical stability (tuned for Prewitt) |
##### Returns
`number` - Difference score between 0 and 1:
- `0.0` - Images are identical or perceptually identical
- `0.0-0.05` - Very low difference (minor compression artifacts)
- `0.05-0.15` - Low difference (noticeable but small changes)
- `0.15-0.35` - Moderate similarity (significant structural differences)
- `>0.35` - High difference (major differences)
> **Info:** **Score Guidelines:** - Use threshold `>0.0` for strict regression testing - Use threshold `>0.15` for loose regression testing with compression - Scores below `0.35` indicate substantial visual differences
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/gmsd)
- [FORMULA.md](https://github.com/teimurjan/blazediff/blob/main/packages/gmsd/FORMULA.md) - Mathematical foundation
- [Original GMSD Paper](http://www4.comp.polyu.edu.hk/~cslzhang/IQA/TIP_IQA_GMSD.pdf) - Xue et al. (2014)
- [Examples →](/docs/structural-comparison)
---
# @blazediff/object
Structural object comparison with path tracking, cycle detection, and CREATE/REMOVE/CHANGE types.
## Installation
```sh
npm install @blazediff/object
```
## Features
- **Path tracking** for nested modifications
- **Handles** primitives, objects, arrays, dates, regex, and circular references
- **Consistent shapes** for V8 optimization (type, path, value, oldValue)
## Quick Start
```javascript
import diff from '@blazediff/object';
const oldObj = {
name: "John",
age: 30,
city: "NYC",
skills: ["JavaScript", "TypeScript"]
};
const newObj = {
name: "John",
age: 31,
city: "San Francisco",
skills: ["JavaScript", "TypeScript", "Go"],
active: true
};
const changes = diff(oldObj, newObj);
console.log(changes);
```
**Output:**
```json
[
{
"type": 2,
"path": ["age"],
"value": 31,
"oldValue": 30
},
{
"type": 2,
"path": ["city"],
"value": "San Francisco",
"oldValue": "NYC"
},
{
"type": 0,
"path": ["skills", 2],
"value": "Go",
"oldValue": undefined
},
{
"type": 0,
"path": ["active"],
"value": true,
"oldValue": undefined
}
]
```
## API Reference
### `diff(oldObj, newObj, options?)`
Compares two objects and returns an array of differences.
#### Parameters
| Parameter | Type | Description |
| --------- | -------- | ------------------------------------- |
| `oldObj` | `any` | The original object to compare from |
| `newObj` | `any` | The new object to compare to |
| `options` | `object` | Configuration options (optional) |
#### Options
| Option | Type | Default | Description |
| --------------- | --------- | ------- | ---------------------------------------------- |
| `detectCycles` | `boolean` | `true` | Enable circular reference detection |
#### Returns
Returns `Difference[]` - Array of difference objects with consistent structure:
```typescript
interface Difference {
type: DifferenceType;
path: (string | number)[];
value: any;
oldValue: any;
}
```
### Difference Types
> **Info:** **Difference Types** are represented as numbers for optimal performance:
| Type | Name | Description |
| ---- | -------- | -------------------------------------- |
| `0` | `CREATE` | Property or array element was added |
| `1` | `REMOVE` | Property or array element was deleted |
| `2` | `CHANGE` | Property or array element was modified |
All difference objects maintain consistent shape with `type`, `path`, `value`, and `oldValue` fields for optimal V8 performance.
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/object)
- [Examples →](/docs/object-comparison)
---
# @blazediff/cli
CLI for image comparison. Wraps the native Rust binary (fastest), JS pixel diff, GMSD, SSIM, MS-SSIM, and Hitchhiker's SSIM.
[View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md)
## Installation
### Global Installation
```sh
npm install -g @blazediff/cli
```
### Local Installation
```sh
npm install --save-dev @blazediff/cli
```
### Using npx
```sh
npx blazediff-cli image1.png image2.png diff.png
```
## Available Commands
### `blazediff-cli core-native` (default)
Native Rust binary with SIMD optimization. The fastest option - **3-4x faster** than odiff on large images.
#### Basic Usage
```bash
# Default command (core-native)
blazediff-cli image1.png image2.png diff.png
# Or explicitly
blazediff-cli core-native image1.png image2.png diff.png
```
#### With Options
```bash
blazediff-cli image1.png image2.png diff.png --threshold 0.05 --antialiasing
```
#### Options
```bash
blazediff-cli core-native <image1> <image2> [output] [options]
Options:
-t, --threshold <n> Color difference threshold (0-1, default: 0.1)
-a, --antialiasing Enable anti-aliasing detection
--diff-mask Output only differences (transparent background)
-c, --compression <n> PNG compression level (0-9, default: 0)
--interpret Run structured interpretation (region detection + classification)
--output-format <fmt> Output format: png (default) or html (interpret report)
-h, --help Display help
```
#### Exit Codes
- `0` - Images are identical
- `1` - Images have differences
- `2` - Error (file not found, invalid format, etc.)
```bash
blazediff-cli image1.png image2.png diff.png
if [ $? -eq 0 ]; then
echo "Images match!"
else
echo "Images differ!"
fi
```
> **Info:** **Why so fast?** Uses a two-pass block-based algorithm with SIMD acceleration (NEON on ARM, SSE4.1 on x86). The cold pass quickly identifies unchanged blocks, then the hot pass only processes changed regions.
#### Interpret Mode
Add `--interpret` to get structured diff analysis - region detection, content-aware classification (Addition, Deletion, Shift, ContentChange, ColorChange, RenderingNoise), severity scoring, and human-readable summaries.
```bash
# JSON output to stdout
blazediff-cli image1.png image2.png --interpret
# With diff image + interpretation
blazediff-cli image1.png image2.png diff.png --interpret
# HTML report
blazediff-cli image1.png image2.png report.html --output-format html
```
##### Example
```bash
$ blazediff-cli image1.png image2.png --interpret
{
"summary": "Moderate visual change detected (1.87% of image, 10 regions).\n...",
"severity": "Medium",
"diffPercentage": 1.87,
"regions": [...]
}
$ blazediff-cli image1.png image2.png report.html --output-format html
# writes report.html with side-by-side images and clickable region rows
```
### `blazediff-cli core`
Pure JavaScript pixel-by-pixel comparison. Slower than `core-native` but offers more customization options like custom diff colors and color spaces.
#### Basic Usage
```bash
blazediff-cli core image1.png image2.png
```
#### Save Diff Image
```bash
blazediff-cli core image1.png image2.png --output diff.png
```
#### Options
```bash
blazediff-cli core <image1> <image2> [options]
Options:
-o, --output <path> Output diff image path
-t, --threshold <n> Matching threshold (0-1, default: 0.1)
-a, --alpha <n> Opacity of original in diff (0-1, default: 0.1)
--diff-color <r,g,b> Color for different pixels (default: 255,0,0)
--aa-color <r,g,b> Color for anti-aliased pixels (default: 255,255,0)
--include-aa Include anti-aliased pixels in diff count
--diff-mask Output diff mask with transparent background
--codec <name> Image codec (pngjs, sharp, jsquash-png)
-h, --help Display help
```
#### Exit Codes
- `0` - Images are identical or within threshold
- `1` - Images have differences beyond threshold
```bash
blazediff-cli core image1.png image2.png
if [ $? -eq 0 ]; then
echo "Images match!"
else
echo "Images differ!"
fi
```
### `blazediff-cli gmsd`
GMSD (Gradient Magnitude Similarity Deviation) perceptual quality assessment. Returns a similarity score from 0-1.
#### Basic Usage
```bash
blazediff-cli gmsd image1.png image2.png
```
#### Save GMS Map
```bash
blazediff-cli gmsd image1.png image2.png --output gms-map.png
```
#### Options
```bash
blazediff-cli gmsd <image1> <image2> [options]
Options:
-o, --output <path> Output GMS map image path
--downsample <n> Downsample factor (0 or 1, default: 0)
-h, --help Display help
```
#### Output
Prints the GMSD score (lower = more similar):
- `0.0` - Images are identical
- `0.0-0.05` - Very low difference
- `0.05-0.15` - Low difference
- `0.15-0.35` - Moderate difference
- `>0.35` - High difference
#### Example
```bash
$ blazediff-cli gmsd reference.png test.png
GMSD: 0.0234
$ blazediff-cli gmsd reference.png test.png --output gms-map.png
GMSD: 0.0234
GMS map saved to: gms-map.png
```
### `blazediff-cli ssim`
Standard SSIM (Structural Similarity Index) with Gaussian weighting. MATLAB-compatible with \<0.01% error.
#### Basic Usage
```bash
blazediff-cli ssim image1.png image2.png
```
#### Save SSIM Map
```bash
blazediff-cli ssim image1.png image2.png --output ssim-map.png
```
#### Options
```bash
blazediff-cli ssim <image1> <image2> [options]
Options:
-o, --output <path> Output SSIM map image path
--window-size <n> Gaussian window size (default: 11)
--k1 <n> Algorithm parameter (default: 0.01)
--k2 <n> Algorithm parameter (default: 0.03)
-h, --help Display help
```
#### Output
Prints the SSIM score (higher = more similar):
- `1.0` - Identical images
- `0.99+` - Excellent similarity
- `0.95-0.99` - Very good similarity
- `0.90-0.95` - Good similarity
- `0.80-0.90` - Fair similarity
- `<0.80` - Poor similarity
#### Example
```bash
$ blazediff-cli ssim reference.png test.png
SSIM: 0.9876
$ blazediff-cli ssim reference.png test.png --output ssim-map.png --window-size 8
SSIM: 0.9823
SSIM map saved to: ssim-map.png
```
### `blazediff-cli msssim`
MS-SSIM (Multi-Scale SSIM) for better perceptual correlation. Analyzes images at multiple scales.
#### Basic Usage
```bash
blazediff-cli msssim image1.png image2.png
```
#### Save MS-SSIM Map
```bash
blazediff-cli msssim image1.png image2.png --output msssim-map.png
```
#### Options
```bash
blazediff-cli msssim <image1> <image2> [options]
Options:
-o, --output <path> Output SSIM map at finest scale
--window-size <n> Gaussian window size (default: 11)
--scales <n> Number of scales (default: 5)
--method <type> Aggregation method: product or sum (default: product)
-h, --help Display help
```
Prints the MS-SSIM score (higher = more similar, same scale as SSIM).
```bash
$ blazediff-cli msssim reference.png test.png
MS-SSIM: 0.9912
```
### `blazediff-cli hitchhikers-ssim`
Hitchhiker's SSIM - fast rectangular-window SSIM using integral images. ~4x faster than standard SSIM.
#### Basic Usage
```bash
blazediff-cli hitchhikers-ssim image1.png image2.png
```
#### Save SSIM Map
```bash
blazediff-cli hitchhikers-ssim image1.png image2.png --output ssim-map.png
```
#### Options
```bash
blazediff-cli hitchhikers-ssim <image1> <image2> [options]
Options:
-o, --output <path> Output SSIM map image path
--window-size <n> Rectangular window size (default: 11)
--window-stride <n> Window stride (default: window-size)
--no-cov-pooling Disable CoV pooling (use mean pooling)
--k1 <n> Algorithm parameter (default: 0.01)
--k2 <n> Algorithm parameter (default: 0.03)
-h, --help Display help
```
Prints the SSIM score (higher = more similar, same scale as SSIM). CoV pooling is enabled by default.
```bash
$ blazediff-cli hitchhikers-ssim reference.png test.png
SSIM: 0.9597
```
## When to Use Each Algorithm
| Algorithm | Best for |
|-----------|----------|
| `core-native` (default) | Maximum speed, CI/CD, large images, `--interpret` analysis |
| `core` | Custom diff colors, color space control, no native deps |
| `gmsd` | Similarity score, compression-tolerant |
| `ssim` | MATLAB-compatible, research |
| `msssim` | Multi-scale, varying resolutions |
| `hitchhikers-ssim` | Fast SSIM (~4x), large batches |
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/cli)
- [Examples →](/docs/pixel-comparison/vanilla-javascript)
---
# @blazediff/agent
Agentic visual regression for BlazeDiff. Auto-discovers routes, captures deterministic screenshots, diffs them against committed baselines with the native BlazeDiff core, and hands ambiguous diffs back to your coding agent (Claude Code, Cursor, Codex) to judge.
The package ships a deterministic CLI (`blazediff-agent`) plus a portable playbook (`SKILL.md`) that any coding agent drives. No embedded LLM call, no API key in the default flow - your coding agent supplies the loop, vision, and context engineering.
[View on GitHub](https://github.com/teimurjan/blazediff/tree/main/packages/agent) · [Landing page](/agent)
## Installation
### Global
```sh
npm install -g @blazediff/agent
```
### Local
```sh
npm install --save-dev @blazediff/agent
```
### Chromium
First run will prompt to install bundled Playwright Chromium - no sudo, no `npx playwright install --with-deps`.
```sh
blazediff-agent browsers install --check --json # check
blazediff-agent browsers install # install if missing
```
## Onboard a coding agent
`blazediff-agent onboard` installs the BlazeDiff playbook into whatever coding-agent stack lives in your project. Run once per project.
```sh
# Auto-detect (Claude Code, Codex, Cursor)
blazediff-agent onboard --json
# Explicit
blazediff-agent onboard --stack codex
blazediff-agent onboard --stack claude,codex
blazediff-agent onboard --stack all
# No coding agent - install the local (Moondream + Qwen) judge
blazediff-agent onboard --stack local
```
Per stack:
| Stack | Target | Scope | Detection signal |
|---|---|---|---|
| Claude Code | `<project>/.claude/skills/blazediff/SKILL.md` | project | `CLAUDE.md` or `.claude/` |
| Codex | `~/.codex/skills/blazediff/SKILL.md` | user-global | `AGENTS.md`, `.codex/`, or `~/.codex/` |
| Cursor | `<project>/.cursor/rules/blazediff.mdc` | project | `.cursor/` or `.cursorrules` |
Codex is user-global because OpenAI's Codex CLI discovers skills under `~/.codex/skills/<name>/SKILL.md` - installing there means `/blazediff` works in every project on your machine. On a TTY with no detection, the command prompts. Pass `--force` to overwrite a hand-edited file.
## Quickstart from a coding agent
Once onboarded, from Claude Code / Codex / Cursor:
```
/blazediff --cwd apps/website
```
The skill detects whether you're authoring (no `.blazediff/manifest.json`) or checking (manifest exists), runs the right flow end-to-end, and stops to ask for confirmation only before destructive operations (rewriting baselines, masking).
## Quickstart from the CLI
### Author baselines
```bash
# 1. Setup: config from your dev script + Chromium + playbook
# (--no-capture: baselines are captured explicitly in step 3 below)
blazediff-agent onboard --no-capture
# 2. Start the configured dev server (waits up to 60s for the port)
blazediff-agent serve-status --detach --json
# 3. Capture baselines in one call - pipe a JSON list of routes
cat <<'EOF' | blazediff-agent capture --stdin --mode baseline --json
[
{"id": "home", "url": "/", "mask": [".timestamp"]},
{"id": "pricing", "url": "/pricing"}
]
EOF
# 4. Stop the dev server (mandatory teardown)
blazediff-agent serve-status --kill --json
```
Commit `.blazediff/` (config + manifest + baselines).
### Check (CI verb)
```bash
blazediff-agent check --judge host --json
```
The CLI starts the dev server automatically when `config.devServer` is set, runs every manifest entry through Playwright, diffs each capture against its baseline, and emits a `CheckReport`:
```json
{
"summaryPath": ".blazediff/summary.md",
"totalEntries": 23,
"passed": 22,
"failed": 0,
"pendingJudgments": 1,
"results": [
{
"id": "agent",
"url": "/agent",
"status": "needs-judgment",
"verdict": {
"label": "ambiguous",
"headline": "5 regions: 4 content-change, 1 addition @ left (0.13%, low)",
"action": "investigate"
}
}
]
}
```
`results[]` lists non-pass entries only. Full per-entry details (regions, paths, rationale) live in `.blazediff/summary.md` (a 5-column markdown table with inline image previews) and `.blazediff/judgments/<id>/request.json`.
### Accept an intentional regression
```bash
# By id
blazediff-agent rewrite home pricing --json
# All failures from the last check
blazediff-agent rewrite --failed --json
# All entries (rare; usually wrong)
blazediff-agent rewrite --all --json
```
`rewrite` preserves the existing manifest entry's `mask` / `viewport` / `waitFor` / `fullPage`; only the baseline PNG is regenerated. Re-run `check` afterwards to confirm clean.
### Wipe and start over
```bash
blazediff-agent reset --yes --json
```
Deletes the entire `.blazediff/` directory (config, manifest, baselines, actual, judgments, summary, pid/log). Tracked dev server is stopped first. Discards committed baselines - confirm explicitly before running.
## Commands
| Command | Purpose |
|---|---|
| `onboard` | Interactive setup: write `.blazediff/config.json` + `.gitignore`, install Chromium, install the playbook into the detected coding-agent stack (Claude Code, Codex, Cursor, or `--stack local`), and optionally capture baselines |
| `discover` | BFS-crawl routes from `baseUrl` (depth 2, ≤50 routes) as a fallback when source-walking fails |
| `capture --stdin` | Read a JSON array of routes from stdin, screenshot each, write baselines/actuals + manifest |
| `check` | Re-capture every manifest entry, diff against baseline, emit `CheckReport`. Uses LangGraph for per-entry parallelism; suspends on ambiguous entries when `--judge host` and resumes via `--apply-judgments` |
| `rewrite <id...>` | Re-baseline existing manifest entries (mask/viewport/waitFor preserved) |
| `diff <id>` | Re-diff one entry against its actual capture without re-screenshotting |
| `manifest` | Inspect / list manifest entries (`add --harness <name>` to attach a harness) |
| `auth init` | Record a login flow via Playwright codegen into `.blazediff/harnesses/auth.js` (fallback for OAuth/SSO/MFA; simple forms are authored directly) |
| `serve-status` | `--detach` / `--kill` / `--status` against the configured dev server |
| `browsers install` | Install bundled Playwright Chromium |
| `reset --yes` | Wipe `.blazediff/` |
All commands accept `--json` for machine-readable output. Pass `-C, --cwd <abs-path>` to operate on a sub-directory (e.g. one app inside a monorepo).
## The judging model
The heuristic verdict pipeline emits one of four labels per failing entry:
| Label | Meaning | Default action |
|---|---|---|
| `regression-likely` | Confident structural change | Investigate; do not rewrite |
| `intentional-likely` | Confident styling/typographic change | Ask user, then rewrite |
| `noise-likely` | Confident non-deterministic source | Ask user; prefer masking over rewriting |
| `ambiguous` | Heuristic couldn't classify | Defer to host judge |
For `ambiguous`, the `--judge host` backend writes a `JudgmentRequest` to `.blazediff/judgments/<id>/request.json` containing:
- `regions[]` - bounding boxes, pixel counts, and change types per detected region
- `paths.locator` (`locator.png`) - a ~400 px overview thumbnail with every region outlined in red
- `paths.tiles` (`regions.png`) - a vertical stack of `[baseline | actual]` pairs, one row per region, at native resolution
- `paths.{baseline,actual,diff}` - full-page PNGs as a fallback
- `heuristicVerdict` and full `manifestEntry` context
> **Info:** **Token discipline.** The region tiles are 10-100x smaller than the full-page PNGs. A well-behaved host agent reads `regions.png` + `locator.png` first and only falls back to the full-page PNGs if a region clearly continues outside its crop.
The host agent writes its verdict to `.blazediff/judgments/<id>/verdict.json`:
```json
{
"id": "agent",
"verdict": {
"label": "intentional-likely",
"headline": "Em-dash replaced with hyphen in copy",
"rationale": ["region tile shows only typographic substitution"],
"action": "rewrite-if-intended"
},
"rationale": "Full paragraph explanation...",
"confidence": 0.95
}
```
Then re-run `blazediff-agent check --apply-judgments --json` to merge verdicts into the report. No re-screenshot.
## Masking unstable regions
When a diff is `noise-likely` - or when a `regression-likely` / `intentional-likely` diff is actually caused by something inherently non-deterministic in the page - the right fix is usually a mask, not a rebaseline. A rebaseline just resets the clock on a flake; a mask removes it.
**Mask whenever the changing region is:**
- An auto-cycling animation: carousels, marquees, demo widgets with `setInterval`, video posters, Lottie loops
- A third-party iframe or embed: Storybook, YouTube, codesandbox, Stripe checkout - anything whose load timing or content you don't control. `networkidle` does not wait for embedded iframe subresources.
- Time-derived: `Date.now()` clocks, "X minutes ago" timestamps, today-highlighted calendars, expiry countdowns
- Per-session randomness: avatars seeded from session id, A/B-test variants, generated IDs, shuffled lists
- Anti-bot / personalization noise: async cookie banners, recommendation strips, geo-derived prices
**Don't mask** real content that just happens to be changing - that's the change you want the test to catch.
### Default attribute
The agent always masks any element matching `[data-blazediff-agent-mask]`. No manifest changes are needed. This is the preferred path whenever you can edit the source.
```tsx
<div data-blazediff-agent-mask>...</div>
// or with a reason inline:
<div data-blazediff-agent-mask="report-carousel">...</div>
```
The attribute value is ignored by the matcher (presence is enough); use it to document intent for future readers. Add the attribute to a shared component (layout, header, footer) and the mask applies on every route automatically.
### Per-entry selector (fallback)
When you can't edit the source (third-party iframe, framework-owned element), fall back to a CSS selector on the manifest entry. Selectors are passed to `document.querySelectorAll`, then painted with a magenta rect over the bounding rect in both baseline and actual.
- For external embeds, target the element type: `iframe`, `video`, `[data-testid="storybook-preview"]`.
- Avoid Tailwind class chains and `nth-child` selectors. They break on the next style tweak.
- Scope matters. Each manifest entry has its own `mask` array, so `iframe` on `/docs/ui-components/vanilla` won't affect `/home`.
Re-capture the affected entries with the new mask list. The mask list **replaces** the existing one. Include every selector you want kept.
```sh
cat <<'EOF' | blazediff-agent capture --stdin --mode baseline --json
[
{"id": "examples-vanilla", "url": "/docs/ui-components/vanilla", "mask": ["iframe"]}
]
EOF
```
Re-run `check` to confirm the entry now passes.
## Configuration
`.blazediff/config.json` is written by `onboard` and committed:
```json
{
"devServer": {
"command": "pnpm dev",
"port": 3000,
"readyTimeoutMs": 60000
},
"framework": "next",
"packageManager": "pnpm",
"baseUrl": "http://127.0.0.1:3000"
}
```
Per-route behavior (login, interactions) lives in **harnesses**, not config - see [Harnesses](#harnesses) below.
Omit `devServer` to point the agent at an already-running URL (set `baseUrl` directly):
```sh
blazediff-agent onboard --url https://staging.example.com --json
```
`.blazediff/manifest.json` is written by `capture` - **never edit it directly**. Each entry holds:
```ts
{
id: string;
url: string;
mask: string[]; // CSS selectors
viewport: { width: number; height: number };
waitFor: ("networkidle" | "fonts" | string)[];
fullPage: boolean;
harnesses?: { name: string; params?: Record<string, unknown> }[];
parent?: string; // set on sub-entries from screenshot(name)
derived?: boolean;
}
```
## Harnesses
A **harness** is a pluggable script in `.blazediff/harnesses/<name>.js`, attached
to an entry via its `harnesses: [{ name, params? }]` list. Login is just one kind
of harness - anything that drives the page before or around a screenshot is the
same concept.
A harness is an ESM module (`.js` / `.mjs` - TypeScript is not auto-transpiled)
that default-exports a `Harness`. Two phases:
- **`setup`** - runs before navigation (establish a session, e.g. login).
- **`interact`** (default) - runs after the base screenshot; drives the page and
may emit extra named screenshots via `screenshot(name)`. Each becomes its own
baseline entry, id `<entry>__<name>`.
```ts
export interface HarnessContext<P = Record<string, unknown>> {
page: import("playwright").Page;
browser: import("playwright").Browser;
context: import("playwright").BrowserContext;
params: P; // e.g. { persona: "default" }
screenshot(name: string): Promise<void>;
}
export interface Harness<P = Record<string, unknown>> {
phase?: "setup" | "interact";
run(ctx: HarnessContext<P>): Promise<void>;
}
```
### Interaction harnesses
For a test that needs the page driven mid-flow (open a menu, switch a tab, then
shoot again), write an interact harness and attach it by name:
```js
// .blazediff/harnesses/weather-menu.js
/** @type {import("@blazediff/agent").Harness} */
export default {
async run({ page, screenshot }) {
await page.getByRole("button", { name: "More options" }).click();
await screenshot("menu"); // -> baseline "weather__menu"
},
};
```
```json
{ "id": "weather", "url": "/weather", "harnesses": ["weather-menu"] }
```
The base shot `weather` fires automatically; every `screenshot("menu")` becomes
its own manifest/baseline/diff entry. To re-baseline a multi-shot entry,
`rewrite <parent-id>` re-runs the harness and regenerates all children.
### Login harness
Routes behind a login flow capture through a `setup` harness. Credentials live
in environment variables - never in the harness file, the manifest, or LLM
context (the harness only references `process.env.BLAZEDIFF_AUTH_*`).
For a plain email/password form the agent **writes the harness directly** - it
identifies the form fields from the login route source or a DOM snapshot and
emits `.blazediff/harnesses/auth.js`:
```js
/** @type {import("@blazediff/agent").Harness<{ persona?: string }>} */
export default {
phase: "setup",
async run({ page, params }) {
const upper = (params.persona ?? "default").toUpperCase().replace(/[^A-Z0-9]/g, "_");
const email = process.env[`BLAZEDIFF_AUTH_${upper}_EMAIL`];
const password = process.env[`BLAZEDIFF_AUTH_${upper}_PASSWORD`];
if (!email || !password) throw new Error(`missing BLAZEDIFF_AUTH_${upper}_EMAIL / _PASSWORD`);
await page.goto("http://127.0.0.1:3000/login");
await page.locator('input[name="email"]').fill(email);
await page.locator('input[name="password"]').fill(password);
await Promise.all([
page.waitForURL((u) => !u.pathname.startsWith("/login")),
page.getByRole("button", { name: /sign in|log in/i }).click(),
]);
},
};
```
For flows that can't be reduced to fill-and-submit - OAuth/SSO, magic links,
MFA, captcha - record it interactively instead:
```sh
blazediff-agent auth init --persona default --login-url http://127.0.0.1:3000/login
```
This opens a Playwright recorder; log in once, and on close the agent swaps the
typed email/password for `process.env.BLAZEDIFF_AUTH_<PERSONA>_*` and writes the
same `.blazediff/harnesses/auth.js`.
**Per-entry.** Add the harness to the entry's `harnesses` list:
```json
{ "id": "dashboard", "url": "/dashboard",
"harnesses": [{ "name": "auth", "params": { "persona": "default" } }] }
```
**Credentials.** The CLI auto-loads env files from `--cwd` -
`.blazediff/.env[.local]` (blazediff-scoped, auto-gitignored) then the
project-root `.env[.local]` - before any harness runs. Real exported env vars
win; `.blazediff/` files beat the root. So just drop them in `.blazediff/.env`:
```sh
printf 'BLAZEDIFF_AUTH_DEFAULT_EMAIL=you@example.com\nBLAZEDIFF_AUTH_DEFAULT_PASSWORD=hunter2\n' \
> .blazediff/.env
blazediff-agent check
```
The harness throws a clear error at capture time if its vars are missing.
**Multiple personas.** Use a different `params.persona` per entry; each maps to
its own `BLAZEDIFF_AUTH_<PERSONA>_*` pair. One harness file serves them all.
**Note.** Every harness-gated capture runs in a fresh browser context
(`storageState` reuse is not yet implemented), so a setup harness re-runs per
entry.
**Working reference.** [`examples/agent-auth-spa-example`](https://github.com/teimurjan/blazediff/tree/main/examples/agent-auth-spa-example)
in the repo is a Vite + React SPA with 2 public and 8 auth-gated routes. It
ships a `.blazediff/harnesses/auth.js` and committed baselines, so you can
clone the repo and run `pnpm --filter @blazediff/agent-auth-spa-example check`
to see the full flow pass 10/10.
## CI
In CI (`CI=1` or no TTY), only `check` is allowed. `onboard` / `capture` / `rewrite` / `reset` are explicitly blocked - authoring belongs at the developer's machine.
### GitHub Actions
```yaml
- run: pnpm install
- run: npx blazediff-agent browsers install
- run: npx blazediff-agent --cwd apps/website check --json
env:
# Only needed if any entry uses a login harness. One pair per persona.
# (In CI, set these as secrets rather than committing .blazediff/.env.)
BLAZEDIFF_AUTH_DEFAULT_EMAIL: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_EMAIL }}
BLAZEDIFF_AUTH_DEFAULT_PASSWORD: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_PASSWORD }}
```
Exit codes:
- `0` - every entry passed
- `1` - at least one regression, intentional, noise, or pending-judgment entry
- non-zero with structured JSON error on infra failures (missing manifest, no chromium, etc.)
## Hard rules
- Never `--mode baseline` an existing manifest entry without explicit user request.
- Never edit `.blazediff/manifest.json` directly.
- In CI (`CI=1` or no TTY), only `check` is allowed.
- A route that times out is logged once in the result array and skipped - never blocks the run.
- Never leave a dev server running after authoring exits. `serve-status --kill` is mandatory teardown.
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff/tree/main/packages/agent)
- [Skill playbook (`SKILL.md`)](https://github.com/teimurjan/blazediff/blob/main/skill/blazediff/SKILL.md)
- [Landing page →](/agent)
---
# @blazediff/core-native
The fastest single-threaded image diff in the world. Native Rust implementation with SIMD optimization, **3-4x faster** and **3x smaller** than [odiff](https://github.com/dmtrKovalenko/odiff).
[View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md)
> **Warning:** This package was previously published as [`@blazediff/bin`](https://www.npmjs.com/package/@blazediff/bin), which is now deprecated. Please use `@blazediff/core-native` instead.
## Installation
```sh
npm install @blazediff/core-native
```
Also available as a Rust crate: [`cargo install blazediff`](https://crates.io/crates/blazediff)
Pre-built binaries are included for all major platforms - no compilation required:
- macOS ARM64 (Apple Silicon) & x64 (Intel)
- Linux ARM64 & x64
- Windows ARM64 & x64
## Features
- **PNG, JPEG & QOI support** - auto-detected by file extension or encoded bytes
- **3-4x faster** than odiff, **3x smaller** binaries (~700KB-900KB vs ~2-3MB)
- **SIMD-accelerated** - NEON on ARM, SSE4.1 on x86
- **Block-based optimization** - skips unchanged regions
- **Interpret mode** - region detection, classification, severity scoring, human-readable summaries
### Vendored Libraries
- [libspng](https://libspng.org/) - Fast PNG decoding/encoding with SIMD
- [libjpeg-turbo](https://libjpeg-turbo.org/) - High-performance JPEG codec with SIMD
- [qoi](https://github.com/aldanor/qoi-rust) - QOI (Quite OK Image) format for fast lossless compression
## API Reference
### `compare(base, comparison, diffOutput, options?)`
Compares two images from file paths or encoded `Buffer`/`Uint8Array` inputs and optionally generates a diff image. Both inputs must use the same input type. Format is auto-detected from the file extension or encoded bytes.
#### Parameters
| Parameter | Type | Description |
| ------------- | ------------------------- | -------------------------------------------- |
| `base` | `string \| Uint8Array` | Base/expected image path or encoded bytes |
| `comparison` | `string \| Uint8Array` | Comparison/actual image path or encoded bytes |
| `diffOutput` | `string` | Path where the diff image will be saved |
| `options` | `BlazeDiffOptions` | Comparison options (optional) |
##### Options
| Option | Type | Default | Description |
| ------------------ | --------- | ------- | -------------------------------------------------------- |
| `threshold` | `number` | `0.1` | Color difference threshold (0.0-1.0). Lower = more strict |
| `antialiasing` | `boolean` | `false` | Enable anti-aliasing detection |
| `diffMask` | `boolean` | `false` | Output only differences with transparent background |
| `diffColorAlt` | `[number, number, number]` | diff color | Alternative RGB color for darkening differences |
| `interpret` | `boolean` | `false` | Generate the diff image and structured interpretation in one pass |
#### Return Types
```typescript
type BlazeDiffResult =
| { match: true; interpretation?: InterpretResult }
| { match: false; reason: "layout-diff" }
| { match: false; reason: "pixel-diff"; diffCount: number; diffPercentage: number; interpretation?: InterpretResult }
| { match: false; reason: "file-not-exists"; file: string };
```
When `interpret: true`, the requested diff output is written and the result includes an `interpretation` field from the same diff pass.
> **Info:** Encoded inputs are passed by reference across the N-API boundary. Rust borrows the existing JavaScript backing memory for the synchronous call. Image decoding still allocates native RGBA pixel buffers.
> **Info:** **Threshold Guidelines:** - `0.0` - Exact match only - `0.05` - Strict comparison - `0.1` - Default balanced comparison - `0.2` - Lenient comparison
## Usage
### Programmatic API
```typescript
import { compare } from '@blazediff/core-native';
const result = await compare('expected.png', 'actual.png', 'diff.png', {
threshold: 0.1,
antialiasing: true,
});
if (result.match) {
console.log('Images are identical!');
} else if (result.reason === 'pixel-diff') {
console.log(`${result.diffCount} pixels differ (${result.diffPercentage.toFixed(2)}%)`);
} else if (result.reason === 'layout-diff') {
console.log('Images have different dimensions');
}
```
### Encoded Buffer Input
```typescript
import { readFile } from "node:fs/promises";
import { compare } from "@blazediff/core-native";
const [expected, actual] = await Promise.all([
readFile("expected.png"),
readFile("actual.png"),
]);
// Reuse these same Buffer objects across comparisons without a JS-to-Rust copy.
const result = await compare(expected, actual, "diff.png");
```
### CLI Usage
```bash
# Compare two PNG images
npx blazediff expected.png actual.png diff.png
# Compare two JPEG images
npx blazediff expected.jpg actual.jpg diff.jpg
# Compare two QOI images
npx blazediff expected.qoi actual.qoi diff.qoi
# Mixed formats (PNG input, QOI output - recommended for smallest diff files)
npx blazediff expected.png actual.png diff.qoi
# With options
npx blazediff expected.png actual.png diff.png --threshold 0.05 --antialiasing
# With higher PNG compression (smaller output file, slower)
npx blazediff expected.png actual.png diff.png -c 6
# With JPEG quality setting
npx blazediff expected.jpg actual.jpg diff.jpg -q 85
# Output as text format
npx blazediff expected.png actual.png diff.png --output-format text
```
### CLI Options
```bash
blazediff [OPTIONS] <IMAGE1> <IMAGE2> [OUTPUT]
Arguments:
<IMAGE1> First image path (PNG, JPEG, or QOI)
<IMAGE2> Second image path (PNG, JPEG, or QOI)
[OUTPUT] Output diff image path (optional, format detected from extension)
Options:
-t, --threshold <THRESHOLD> Color difference threshold (0.0-1.0) [default: 0.1]
-a, --antialiasing Enable anti-aliasing detection
--diff-mask Output only differences (transparent background)
--diff-color-alt <R,G,B> Alternative RGB color for darkening differences
-c, --compression <LEVEL> PNG compression level (0-9, 0=fastest, 9=smallest) [default: 0]
-q, --quality <QUALITY> JPEG quality (1-100) [default: 90]
--interpret Run structured interpretation after diff
--output-format <FORMAT> Output format (json or text) [default: json]
-h, --help Print help
-V, --version Print version
```
### Supported Formats
| Format | Extensions | Notes |
|--------|------------|-------|
| PNG | `.png` | Lossless, supports transparency |
| JPEG | `.jpg`, `.jpeg` | Lossy, smaller file sizes |
| QOI | `.qoi` | Fast lossless, ideal for diff outputs (12x smaller than uncompressed PNG) |
Input images can be mixed formats (e.g., compare PNG to JPEG). Output format is determined by the output file extension.
> **Tip:** **Use QOI for diff outputs:** QOI excels at encoding diff images with large uniform areas, producing files 12x smaller than PNG (level 0) while being faster to encode.
### Exit Codes
- `0` - Images are identical
- `1` - Images differ (includes layout/size mismatch)
- `2` - Error (file not found, invalid format, etc.)
### Interpret
Structured diff analysis for matching path or encoded byte inputs. Returns classified change regions with human-readable summaries.
**Pipeline:** change mask → morph close → connected components → per-region evidence extraction → classify → describe
**Evidence per region:**
- Dual-image gradient comparison (edges in both images + spatial correlation)
- YIQ color delta with uniformity analysis (mean, max, stddev)
- Background distance (changed pixels vs local unchanged pixels)
- Shape statistics (fill ratio, border density, occupancy)
**Six-label decision tree:**
| Type | Signal |
|---|---|
| `RenderingNoise` | Tiny (≤25px) or sparse + low color delta |
| `Addition` | Blends with background in img1, distinct in img2 |
| `Deletion` | Distinct in img1, blends with background in img2 |
| `ColorChange` | Edge structure preserved across both images + uniform color shift |
| `ContentChange` | Fallback - structural change |
| `Shift` | Post-hoc: matched Addition+Deletion pair with similar luminance |
```typescript
import { compare, interpret } from '@blazediff/core-native';
const result = await interpret('expected.png', 'actual.png');
console.log(result.summary);
// "Moderate visual change detected (1.87% of image, 10 regions).
// Content changed: 4 regions (bottom, center).
// Content added: 3 regions (right, bottom, bottom-left)."
// Via compare()
const diff = await compare('expected.png', 'actual.png', 'diff.png', {
interpret: true,
});
```
```bash
npx blazediff expected.png actual.png --interpret
npx blazediff expected.png actual.png report.html --output-format html
```
Severity: Low (<1%), Medium (1-10%), High (>10%). See [Interpret example →](/docs/difference-analysis) for interactive demo.
**Accuracy:** Measured against hand-labeled datasets — classifier-only macro F1 is **0.998** on clean add/delete edits (`addition_deletion`) and **0.440** on inpaint edits that mix recolor and texture replacement (`inpaintcoco`). Full breakdown in [BENCHMARKS.md](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-interpret-verify/BENCHMARKS.md).
## Performance
Benchmarked on Apple M1 Max with 5600×3200 4K images (25 runs, 5 warmup, image IO included):
| Tool | Time | Comparison |
|------|------|------------|
| **blazediff** | ~359ms | - |
| odiff | ~1221ms | 3.4x slower |
Binary sizes (stripped, LTO optimized):
| Platform | blazediff | odiff |
|----------|-----------|-------|
| macOS ARM64 | 702 KB | 2.2 MB |
| Linux x64 | 869 KB | 2.9 MB |
| Windows x64 | 915 KB | 3.0 MB |
> **Info:** **Why so fast?** BlazeDiff uses a two-pass block-based algorithm with SIMD acceleration. The cold pass quickly identifies unchanged 8x8 blocks using 32-bit integer comparison, then the hot pass only processes changed regions with YIQ perceptual color difference.
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/core-native)
- [Rust Crate](https://crates.io/crates/blazediff)
- [Examples →](/docs/pixel-comparison/rust-napi)
---
# @blazediff/core-wasm
WebAssembly build of the BlazeDiff Rust algorithm for browsers, edge runtimes, and any wasm host. Same two-pass block algorithm as [`@blazediff/core-native`](/apis/core-native), compiled to `wasm32` with `v128` SIMD (`+simd128`). ~58% faster than [pixelmatch](https://github.com/mapbox/pixelmatch) on the same RGBA buffers; diff counts agree with pixelmatch to within ~0.05%.
[View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md)
## Installation
```sh
npm install @blazediff/core-wasm
```
Ships ~32 KB of optimized wasm + ~10 KB of JS glue. No native binaries, no postinstall, no platform packages.
## Features
- **Same algorithm as `@blazediff/core-native`**: YIQ perceptual delta + block-based cold/hot pass
- **wasm32 v128 SIMD** (`+simd128`): 4-lane vectorized cold and hot loops; up to ~16× faster than pixelmatch on 4K
- **Buffers-only API**: caller decodes images, hands in `Uint8Array`. No PNG/JPEG codecs bundled
- **Runs anywhere wasm runs**: browsers (`fetch()` of the .wasm), Node (`fs.readFileSync` + bytes), Cloudflare Workers, Deno, Bun
## API Reference
### `initBlazediff(input?)`
Initializes the wasm module. Safe to call multiple times; subsequent calls return the cached promise. The function accepts a `URL`, `Response`, `ArrayBuffer`, `Uint8Array`, or compiled `WebAssembly.Module`. Without `input`, the default `--target web` glue fetches the sibling `blazediff_bg.wasm` via `import.meta.url`, which works in browsers but fails in runtimes whose `fetch()` cannot resolve the resulting URL (Node `file://`, Workers, etc.).
#### Loading the wasm module
Pick the recipe that matches your runtime. All four are equivalent; the wasm itself is the same.
**Browser, plain script tag or ESM**. The default works because the sibling `.wasm` is reachable via `fetch(import.meta.url)`:
```typescript
import { initBlazediff } from '@blazediff/core-wasm';
await initBlazediff();
```
**Universal CDN URL (recommended for Node, Workers, Deno, Bun)**. jsDelivr serves the published `.wasm` over HTTPS, so any `fetch()`-capable runtime can load it. One network round-trip on cold start, cached by the runtime after that:
```typescript
import { initBlazediff } from '@blazediff/core-wasm';
await initBlazediff(
new URL(
'https://cdn.jsdelivr.net/npm/@blazediff/core-wasm@4.2.0/wasm/blazediff_bg.wasm',
),
);
```
Pin the version (`@4.2.0`) for reproducibility. `unpkg.com/@blazediff/core-wasm@4.2.0/wasm/blazediff_bg.wasm` works identically.
**Bundlers (Vite, Webpack 5+, esbuild, Rollup with plugin)**. The `new URL(asset, import.meta.url)` pattern is bundler-aware: the asset is emitted into the build output and the URL is rewritten at build time:
```typescript
import { initBlazediff } from '@blazediff/core-wasm';
const wasmUrl = new URL(
'@blazediff/core-wasm/wasm/blazediff_bg.wasm',
import.meta.url,
);
await initBlazediff(wasmUrl);
```
**Node from the local filesystem** (offline, no CDN dependency). Read the bytes and pass them in. Path resolution depends on your module system:
```typescript
// ESM (Node 20.6+):
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { initBlazediff } from '@blazediff/core-wasm';
const wasmPath = fileURLToPath(
import.meta.resolve('@blazediff/core-wasm/wasm/blazediff_bg.wasm'),
);
await initBlazediff(readFileSync(wasmPath));
```
```typescript
// CommonJS:
const { readFileSync } = require('node:fs');
const { initBlazediff } = require('@blazediff/core-wasm');
const wasmPath = require.resolve(
'@blazediff/core-wasm/wasm/blazediff_bg.wasm',
);
await initBlazediff(readFileSync(wasmPath));
```
### `diff(a, b, width, height, output?, options?)`
Compares two RGBA pixel buffers and returns the number of differing pixels. Set `interpret: true` to return structured interpretation while producing the diff output in the same pass.
#### Parameters
| Parameter | Type | Description |
| --------- | --------------------- | -------------------------------------------------------- |
| `a` | `Uint8Array` | First image in RGBA8 order (`width * height * 4` bytes) |
| `b` | `Uint8Array` | Second image in RGBA8 order (same length) |
| `width` | `number` | Image width in pixels |
| `height` | `number` | Image height in pixels |
| `output` | `Uint8Array \| undefined` | Optional diff visualization output (same length). Written in place |
| `options` | `DiffOptions \| InterpretedDiffOptions` | Comparison options (optional) |
##### Options
| Option | Type | Default | Description |
| ------------ | --------- | ------- | -------------------------------------------------------- |
| `threshold` | `number` | `0.1` | Color difference threshold (0.0-1.0). Lower = more strict |
| `includeAA` | `boolean` | `false` | Count anti-aliased pixels as differences |
| `diffMask` | `boolean` | `false` | Render diff with transparent background instead of grayscale base |
| `diffColorAlt` | `[number, number, number]` | diff color | Alternative RGB color for darkening differences |
| `interpret` | `boolean` | `false` | Return structured interpretation from the same diff pass |
> **Info:** **Threshold Guidelines:** `0.0` exact match · `0.05` strict · `0.1` default · `0.2` lenient
With `interpret: true`, `diff()` returns a `DiffResult` instead of a pixel count:
```typescript
const result = await diff(a, b, width, height, output, {
diffColorAlt: [0, 128, 255],
interpret: true,
});
```
```typescript
type DiffResult =
| { match: true; interpretation: InterpretResult }
| {
match: false;
reason: 'pixel-diff';
diffCount: number;
diffPercentage: number;
interpretation: InterpretResult;
};
```
### `interpret(a, b, width, height, options?)`
Returns structured change regions without retaining a diff visualization. It is a convenience wrapper over `diff()` with `interpret: true`.
## Usage
### Browser
Decode images via `createImageBitmap` + `OffscreenCanvas` (or the `ImageDecoder` API), then pass the RGBA buffer to `diff()`.
```typescript
import { diff, initBlazediff } from '@blazediff/core-wasm';
await initBlazediff();
async function toRgba(url: string) {
const bitmap = await createImageBitmap(await (await fetch(url)).blob());
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext('2d')!;
ctx.drawImage(bitmap, 0, 0);
const { data } = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
return { data: new Uint8Array(data.buffer), width: bitmap.width, height: bitmap.height };
}
const a = await toRgba('/baseline.png');
const b = await toRgba('/current.png');
const out = new Uint8Array(a.width * a.height * 4);
const diffCount = await diff(a.data, b.data, a.width, a.height, out, {
threshold: 0.1,
});
console.log(`${diffCount} pixels differ`);
```
### Node
The wasm can be loaded either from jsDelivr (zero-config, works in Node 18+) or from the local `node_modules` install. The CDN form is shown here; see [Loading the wasm module](#loading-the-wasm-module) for the offline `readFileSync` recipe.
```typescript
import { readFileSync } from 'node:fs';
import { diff, initBlazediff } from '@blazediff/core-wasm';
import { PNG } from 'pngjs';
await initBlazediff(
new URL(
'https://cdn.jsdelivr.net/npm/@blazediff/core-wasm@4.2.0/wasm/blazediff_bg.wasm',
),
);
const a = PNG.sync.read(readFileSync('baseline.png'));
const b = PNG.sync.read(readFileSync('current.png'));
const diffCount = await diff(
new Uint8Array(a.data),
new Uint8Array(b.data),
a.width,
a.height,
);
```
## Performance
vs `pixelmatch` on M1 Max, image I/O excluded (pre-decoded RGBA buffers):
| Fixture | pixelmatch | core-wasm | Improvement |
| ----------------- | ---------- | ---------- | ----------- |
| 4k/1 | 287.72ms | 51.75ms | **82.0%** |
| 4k/3 | 366.81ms | 69.90ms | **80.9%** |
| page/2 | 443.83ms | 109.74ms | **75.3%** |
| blazediff/3 | 14.60ms | 5.52ms | **62.2%** |
| pixelmatch/1 | 0.87ms | 0.13ms | **84.6%** |
Average ~58% faster across the full fixture set. Counts agree with pixelmatch within ~0.05% (e.g. `4k/1`: 69 932 vs 69 912 of 17 920 000 pixels; both use YIQ perceptual delta, residual differences are anti-aliasing edge cases).
> **Info:** **Why so fast?** Block-based two-pass algorithm (cold pass skips unchanged 8×8 blocks via integer compare, hot pass runs YIQ delta only on changed regions) with `v128` SIMD intrinsics: 4-lane vectorized RGBA extraction, YIQ transform, and threshold compare.
## Picking the Right Package
| Use case | Package |
| --------------------------------- | -------------------------------- |
| Browser, edge worker, wasm host | **`@blazediff/core-wasm`** |
| Node CLI / server with native bin | [`@blazediff/core-native`](/apis/core-native) |
| Pure JS / no wasm support | [`@blazediff/core`](/apis/core) |
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/core-wasm)
- [Rust Crate](https://crates.io/crates/blazediff)
---
# @blazediff/matcher
Core snapshot comparison logic that powers [@blazediff/jest](/apis/jest), [@blazediff/vitest](/apis/vitest), and [@blazediff/bun](/apis/bun). Most users should use those framework packages directly.
## Installation
```sh
npm install @blazediff/matcher
```
## Quick Start
```typescript
import { getOrCreateSnapshot } from '@blazediff/matcher';
const result = await getOrCreateSnapshot(
imageBuffer, // or file path
{
method: 'core',
failureThreshold: 0.01,
failureThresholdType: 'percent',
},
{
testPath: '/path/to/test.spec.ts',
testName: 'should render correctly',
}
);
if (result.pass) {
console.log(`✓ Snapshot ${result.snapshotStatus}`);
} else {
console.log(`✗ ${result.diffPercentage}% different`);
}
```
## API Reference
### getOrCreateSnapshot(received, options, testContext)
Main function for snapshot comparison and management.
| Parameter | Type | Description |
|-----------|------|-------------|
| `received` | `ImageInput` | Image to compare (file path or buffer with dimensions) |
| `options` | `MatcherOptions` | Comparison options |
| `testContext` | `TestContext` | Test information (testPath, testName) |
**Returns**: `Promise<ComparisonResult>`
### MatcherOptions
Core comparison options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `method` | `ComparisonMethod` | - | Comparison algorithm: `'core'`, `'core-native'`, `'ssim'`, `'msssim'`, `'hitchhikers-ssim'`, `'gmsd'` |
| `failureThreshold` | `number` | `0` | Number of pixels or percentage difference allowed |
| `failureThresholdType` | `'pixel' \| 'percent'` | `'pixel'` | How to interpret failureThreshold |
| `snapshotsDir` | `string` | `'__snapshots__'` | Directory to store snapshots (relative to test file) |
| `snapshotIdentifier` | `string` | auto-generated | Custom identifier for the snapshot file |
| `updateSnapshots` | `boolean` | `false` | Force update snapshots |
Method-specific options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `threshold` | `number` | `0.1` | Color difference threshold (0-1) for `core`/`core-native` methods |
| `antialiasing` | `boolean` | `false` | Enable anti-aliasing detection (`core-native` method) |
| `includeAA` | `boolean` | `false` | Include anti-aliased pixels in diff count (`core` method) |
| `windowSize` | `number` | `11` | Window size for SSIM variants |
| `k1` | `number` | `0.01` | k1 constant for SSIM |
| `k2` | `number` | `0.03` | k2 constant for SSIM |
| `downsample` | `0 \| 1` | `0` | Downsample factor for GMSD |
| `runInWorker` | `boolean` | `true` | Run image I/O and comparison in a worker thread for better performance |
### ComparisonResult
Result object returned by `getOrCreateSnapshot`:
| Field | Type | Description |
|-------|------|-------------|
| `pass` | `boolean` | Whether the comparison passed |
| `message` | `string` | Human-readable message describing the result |
| `snapshotStatus` | `SnapshotStatus` | Status: `'added'`, `'matched'`, `'updated'`, `'failed'` |
| `diffCount` | `number?` | Number of different pixels (pixel-based methods) |
| `diffPercentage` | `number?` | Percentage of different pixels |
| `score` | `number?` | Similarity score (SSIM: 1 = identical, GMSD: 0 = identical) |
| `baselinePath` | `string?` | Path to baseline snapshot |
| `receivedPath` | `string?` | Path to received image (saved on failure) |
| `diffPath` | `string?` | Path to diff visualization |
### ImageInput
```typescript
type ImageInput =
| string // File path
| {
data: Uint8Array | Uint8ClampedArray | Buffer;
width: number;
height: number;
};
```
## Comparison Methods
Available methods: `core`, `core-native`, `ssim`, `msssim`, `hitchhikers-ssim`, `gmsd`. See [@blazediff/core](/apis/core), [@blazediff/core-native](/apis/core-native), [@blazediff/ssim](/apis/ssim), and [@blazediff/gmsd](/apis/gmsd) for algorithm details.
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/matcher)
- [@blazediff/core](/apis/core) - Core comparison algorithm
- [@blazediff/ssim](/apis/ssim) - SSIM algorithms
- [@blazediff/gmsd](/apis/gmsd) - GMSD algorithm
- [@blazediff/core-native](/apis/core-native) - Rust-native bindings
---
# @blazediff/jest
`toMatchImageSnapshot()` for Jest. Auto-registers on import, tracks snapshot state, supports all comparison methods.
## Installation
```sh
npm install --save-dev @blazediff/jest
```
**Peer dependencies**: Jest >= 27.0.0
## Quick Start
```typescript
import '@blazediff/jest';
describe('Visual Regression Tests', () => {
it('should match screenshot', async () => {
const screenshot = await page.screenshot();
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
});
});
});
```
> **Info:** The matcher auto-registers when you import `@blazediff/jest`. No additional setup required!
## API Reference
### toMatchImageSnapshot(options?)
Jest matcher for image snapshot comparison.
```typescript
await expect(imageInput).toMatchImageSnapshot(options?);
```
#### Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `imageInput` | `ImageInput` | Image to compare (file path or buffer with dimensions) |
| `options` | `Partial<MatcherOptions>` | Optional comparison options |
#### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `method` | `ComparisonMethod` | `'core'` | Comparison algorithm |
| `failureThreshold` | `number` | `0` | Allowed difference (pixels or percentage) |
| `failureThresholdType` | `'pixel' \| 'percent'` | `'pixel'` | Threshold interpretation |
| `snapshotsDir` | `string` | `'__snapshots__'` | Snapshot directory (relative to test) |
| `snapshotIdentifier` | `string` | auto-generated | Custom snapshot filename |
| `updateSnapshots` | `boolean` | `false` | Force snapshot update |
| `threshold` | `number` | `0.1` | Color threshold for `core`/`core-native` (0-1) |
| `runInWorker` | `boolean` | `true` | Run comparison in worker thread for better performance |
See [@blazediff/matcher](/apis/matcher) for all available options.
## Comparison Methods
### `core` - Pure JavaScript (Default)
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core', // Fast, works with buffers and file paths
});
```
### `core-native` - Rust Native (Fastest)
```typescript
await expect('/path/to/image.png').toMatchImageSnapshot({
method: 'core-native', // Requires file paths
});
```
### `ssim` - Perceptual Similarity
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'ssim', // Structural similarity
});
```
### `gmsd` - Gradient-based
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'gmsd', // Detects structural changes
});
```
## Usage Patterns
### Basic Snapshot Test
```typescript
import '@blazediff/jest';
it('renders homepage correctly', async () => {
const screenshot = await browser.screenshot();
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
});
});
```
### Custom Thresholds
Allow small differences while catching regressions:
```typescript
// Allow up to 100 pixels difference
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
failureThreshold: 100,
failureThresholdType: 'pixel',
});
// Allow up to 0.5% difference
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
failureThreshold: 0.5,
failureThresholdType: 'percent',
});
```
> **Note:** Use percentage-based thresholds for responsive images that may change size across different viewports.
### Update Snapshots
```bash
# Update all failing snapshots
jest -u
# Update snapshots for specific test file
jest -u src/components/Button.test.tsx
# Update snapshots matching pattern
jest -u --testNamePattern="renders correctly"
# Using environment variable
JEST_UPDATE_SNAPSHOTS=true jest
```
Programmatically:
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
updateSnapshots: true,
});
```
### Custom Snapshot Directory
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotsDir: '__image_snapshots__', // Custom directory
});
```
### Custom Snapshot Identifier
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'homepage-desktop-1920x1080',
});
```
### Negation
Test that images are intentionally different:
```typescript
const before = await page.screenshot();
await page.click('.toggle-theme');
const after = await page.screenshot();
// Assert that theme toggle changed the UI
await expect(after).not.toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'before-theme-toggle',
});
```
## Configuration
### Global Setup
To avoid importing in every test file, configure Jest:
```javascript
// jest.config.js
module.exports = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
```
```javascript
// jest.setup.js
import '@blazediff/jest';
```
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/jest)
- [@blazediff/matcher](/apis/matcher) - Core matcher logic
- [Jest Documentation](https://jestjs.io/)
---
# @blazediff/vitest
`toMatchImageSnapshot()` for Vitest. Auto-registers on import, tracks snapshot state, supports all comparison methods.
## Installation
```sh
npm install --save-dev @blazediff/vitest
```
**Peer dependencies**: Vitest >= 1.0.0
## Quick Start
```typescript
import { expect, it } from 'vitest';
import '@blazediff/vitest';
it('should match screenshot', async () => {
const screenshot = await page.screenshot();
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
});
});
```
> **Info:** The matcher auto-registers when you import `@blazediff/vitest`. No additional setup required!
## API Reference
### toMatchImageSnapshot(options?)
Vitest matcher for image snapshot comparison.
```typescript
await expect(imageInput).toMatchImageSnapshot(options?);
```
#### Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `imageInput` | `ImageInput` | Image to compare (file path or buffer with dimensions) |
| `options` | `Partial<MatcherOptions>` | Optional comparison options |
#### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `method` | `ComparisonMethod` | `'core'` | Comparison algorithm |
| `failureThreshold` | `number` | `0` | Allowed difference (pixels or percentage) |
| `failureThresholdType` | `'pixel' \| 'percent'` | `'pixel'` | Threshold interpretation |
| `snapshotsDir` | `string` | `'__snapshots__'` | Snapshot directory (relative to test) |
| `snapshotIdentifier` | `string` | auto-generated | Custom snapshot filename |
| `updateSnapshots` | `boolean` | `false` | Force snapshot update |
| `threshold` | `number` | `0.1` | Color threshold for `core`/`core-native` (0-1) |
| `runInWorker` | `boolean` | `true` | Run comparison in worker thread for better performance |
See [@blazediff/matcher](/apis/matcher) for all available options.
## Comparison Methods
### `core` - Pure JavaScript (Default)
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core', // Fast, works with buffers and file paths
});
```
### `core-native` - Rust Native (Fastest)
```typescript
await expect('/path/to/image.png').toMatchImageSnapshot({
method: 'core-native', // Requires file paths
});
```
### `ssim` - Perceptual Similarity
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'ssim', // Structural similarity
});
```
### `gmsd` - Gradient-based
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'gmsd', // Detects structural changes
});
```
## Usage Patterns
### Basic Snapshot Test
```typescript
import { expect, it } from 'vitest';
import '@blazediff/vitest';
it('renders homepage correctly', async () => {
const screenshot = await browser.screenshot();
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
});
});
```
### Custom Thresholds
Allow small differences while catching regressions:
```typescript
// Allow up to 100 pixels difference
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
failureThreshold: 100,
failureThresholdType: 'pixel',
});
// Allow up to 0.5% difference
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
failureThreshold: 0.5,
failureThresholdType: 'percent',
});
```
> **Note:** Use percentage-based thresholds for responsive images that may change size across different viewports.
### Update Snapshots
```bash
# Update all failing snapshots
vitest -u
# Update snapshots for specific test file
vitest -u src/components/Button.test.ts
# Update snapshots matching pattern
vitest -u --testNamePattern="renders correctly"
# Using environment variable
VITEST_UPDATE_SNAPSHOTS=true vitest
```
Programmatically:
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
updateSnapshots: true,
});
```
### Custom Snapshot Directory
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotsDir: '__image_snapshots__', // Custom directory
});
```
### Custom Snapshot Identifier
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'homepage-desktop-1920x1080',
});
```
### Negation
Test that images are intentionally different:
```typescript
const before = await page.screenshot();
await page.click('.toggle-theme');
const after = await page.screenshot();
// Assert that theme toggle changed the UI
await expect(after).not.toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'before-theme-toggle',
});
```
## Configuration
### Global Setup
To avoid importing in every test file, configure Vitest:
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['./vitest.setup.ts'],
},
});
```
```typescript
// vitest.setup.ts
import '@blazediff/vitest';
```
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/vitest)
- [@blazediff/matcher](/apis/matcher) - Core matcher logic
- [Vitest Documentation](https://vitest.dev/)
---
# @blazediff/bun
`toMatchImageSnapshot()` for Bun. Auto-registers on import, supports all comparison methods.
## Installation
```sh
npm install --save-dev @blazediff/bun
```
**Peer dependencies**: Bun >= 1.0.0
## Quick Start
```typescript
import { expect, it } from 'bun:test';
import '@blazediff/bun';
it('should match screenshot', async () => {
const screenshot = await page.screenshot();
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'homepage',
});
});
```
> **Info:** The matcher auto-registers when you import `@blazediff/bun`. No additional setup required!
> **Warning:** Unlike Jest/Vitest, Bun has limited context exposure. Always provide a `snapshotIdentifier` for reliable snapshot management.
## API Reference
### toMatchImageSnapshot(options?)
Bun test matcher for image snapshot comparison.
```typescript
await expect(imageInput).toMatchImageSnapshot(options?);
```
#### Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `imageInput` | `ImageInput` | Image to compare (file path or buffer with dimensions) |
| `options` | `Partial<MatcherOptions>` | Optional comparison options |
#### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `method` | `ComparisonMethod` | `'core'` | Comparison algorithm |
| `snapshotIdentifier` | `string` | `'snapshot'` | **Required**: Snapshot filename identifier |
| `failureThreshold` | `number` | `0` | Allowed difference (pixels or percentage) |
| `failureThresholdType` | `'pixel' \| 'percent'` | `'pixel'` | Threshold interpretation |
| `snapshotsDir` | `string` | `'__snapshots__'` | Snapshot directory (relative to test) |
| `updateSnapshots` | `boolean` | `false` | Force snapshot update |
| `threshold` | `number` | `0.1` | Color threshold for `core`/`core-native` (0-1) |
| `runInWorker` | `boolean` | `true` | Run comparison in worker thread for better performance |
See [@blazediff/matcher](/apis/matcher) for all available options.
## Comparison Methods
### `core` - Pure JavaScript (Default)
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'my-test',
});
```
### `core-native` - Rust Native (Fastest)
```typescript
await expect('/path/to/image.png').toMatchImageSnapshot({
method: 'core-native',
snapshotIdentifier: 'my-test',
});
```
### `ssim` - Perceptual Similarity
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'ssim',
snapshotIdentifier: 'my-test',
});
```
### `gmsd` - Gradient-based
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'gmsd',
snapshotIdentifier: 'my-test',
});
```
## Usage Patterns
### Basic Snapshot Test
```typescript
import { expect, it } from 'bun:test';
import '@blazediff/bun';
it('renders homepage correctly', async () => {
const screenshot = await browser.screenshot();
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'homepage',
});
});
```
### Custom Thresholds
Allow small differences while catching regressions:
```typescript
// Allow up to 100 pixels difference
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'homepage',
failureThreshold: 100,
failureThresholdType: 'pixel',
});
// Allow up to 0.5% difference
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'homepage',
failureThreshold: 0.5,
failureThresholdType: 'percent',
});
```
> **Note:** Use percentage-based thresholds for responsive images that may change size across different viewports.
### Update Snapshots
```bash
# Update all failing snapshots (recommended)
bun test --update-snapshots
# Using environment variable
BUN_UPDATE_SNAPSHOTS=true bun test
```
> **Info:** The Bun's `--update-snapshots` flag is consumed by Bun internally and won't update image snapshots.
Programmatically:
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'homepage',
updateSnapshots: true,
});
```
### Custom Snapshot Directory
```typescript
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'homepage',
snapshotsDir: '__image_snapshots__',
});
```
## Bun-Specific Notes
Unlike Jest and Vitest, Bun has limited test context exposure. Always provide a `snapshotIdentifier`:
```typescript
// Good
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
snapshotIdentifier: 'my-component',
});
// Avoid - falls back to generic "snapshot"
await expect(screenshot).toMatchImageSnapshot({
method: 'core',
});
```
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/bun)
- [@blazediff/matcher](/apis/matcher) - Core matcher logic
- [Bun Documentation](https://bun.sh/)
---
# @blazediff/ui
Headless engine and a framework-agnostic renderer for building image comparison interfaces. Works with any JavaScript framework or vanilla HTML.
## Installation
```sh
npm install @blazediff/ui
```
## Two layers
- **`@blazediff/ui`** — a tiny pure-JS renderer. `mount*` functions build the DOM, wire events, and keep it updated.
- **`@blazediff/ui/engine`** — the headless engine. All state, calculations, and handlers live here with no rendering, so you can drive any framework from it.
> **Info:** Using React? [`@blazediff/react`](/apis/react) renders from this engine for you, with the same modes and props.
## Features
- **Framework agnostic**: drive the engine from React, Vue, Angular, Svelte, or vanilla JS
- **No web components**: plain `mount*` functions and a subscribable engine
- **Multiple modes**: swipe, two-up, onion skin, and difference visualization
- **Functional by default**: the layout each mode needs is built in; classes are only for theming
- **Lightweight**: zero dependencies except `@blazediff/core`
## Quick Start
Every mode is a `mount*(target, options)` function. It appends the UI into `target` and returns a handle with `update(options)` and `destroy()`.
```js
import { mountSwipe } from "@blazediff/ui";
const handle = mountSwipe(document.getElementById("app"), {
src1: "image1.jpg",
src2: "image2.jpg",
alt1: "Original",
alt2: "Modified",
onPositionChange: (position) => console.log(position),
});
// later
handle.update({ src2: "image3.jpg" });
handle.destroy();
```
## Renderer API
### `mountSwipe(target, options)`
Compare two images with a draggable divider.
| Option | Type | Default | Description |
| ------------------ | ---------------------------- | ---------- | ------------------------------------ |
| `src1` | `string` | - | URL of the first image |
| `src2` | `string` | - | URL of the second image |
| `alt1` | `string` | `"Before"` | Alt text for the first image |
| `alt2` | `string` | `"After"` | Alt text for the second image |
| `initialPosition` | `number` | `50` | Initial divider position (0–100) |
| `className` | `string` | - | Class for the root element |
| `containerClassName` | `string` | - | Class for the container |
| `image1ClassName` | `string` | - | Class for the first image |
| `image2ClassName` | `string` | - | Class for the second image |
| `dividerClassName` | `string` | - | Class for the divider |
| `onPositionChange` | `(position: number) => void` | - | Called when the divider moves (0–100)|
### `mountTwoUp(target, options)`
Two images side by side, with dimension-change detection.
| Option | Type | Default | Description |
| ------------------------- | ------------------------------------------------- | ------- | --------------------------------- |
| `src1` | `string` | - | URL of the first image |
| `src2` | `string` | - | URL of the second image |
| `crossOrigin` | `string \| null` | `"anonymous"` | `crossOrigin` for image loads |
| `className` | `string` | - | Class for the root element |
| `containerClassName` | `string` | - | Class for the main container |
| `containerInnerClassName` | `string` | - | Class for the inner container |
| `panelClassName` | `string` | - | Class for each panel |
| `imageClassName` | `string` | - | Class for images |
| `dimensionInfoClassName` | `string` | - | Class for the dimension info text |
| `onImagesLoaded` | `(detail) => void` | - | `{ image1, image2 }` dimensions |
| `onLoadError` | `(error: unknown) => void` | - | Called when loading fails |
### `mountOnionSkin(target, options)`
Overlay two images with an opacity slider.
| Option | Type | Default | Description |
| -------------------------- | --------------------------- | ------------ | -------------------------------- |
| `src1` | `string` | - | URL of the base image |
| `src2` | `string` | - | URL of the overlay image |
| `opacity` | `number` | `50` | Initial opacity (0–100) |
| `crossOrigin` | `string \| null` | `"anonymous"`| `crossOrigin` for image loads |
| `className` | `string` | - | Class for the root element |
| `sliderLabelText` | `string` | `"Opacity:"` | Text for the slider label |
| `containerClassName` | `string` | - | Class for the main container |
| `imageContainerClassName` | `string` | - | Class for the image container |
| `imageClassName` | `string` | - | Class for images |
| `sliderContainerClassName` | `string` | - | Class for the slider container |
| `sliderClassName` | `string` | - | Class for the slider |
| `sliderLabelClassName` | `string` | - | Class for the slider label |
| `onOpacityChange` | `(opacity: number) => void` | - | Called when opacity changes |
| `onImagesLoaded` | `(detail) => void` | - | `{ image1, image2 }` dimensions |
| `onLoadError` | `(error: unknown) => void` | - | Called when loading fails |
### `mountDifference(target, options)`
Show pixel differences using the BlazeDiff algorithm.
| Option | Type | Default | Description |
| -------------------- | -------------------------- | ------- | --------------------------- |
| `src1` | `string` | - | URL of the first image |
| `src2` | `string` | - | URL of the second image |
| `threshold` | `number` | `0.1` | Difference threshold (0–1) |
| `includeAA` | `boolean` | `false` | Include anti-aliased pixels |
| `alpha` | `number` | `0.1` | Opacity of the original |
| `crossOrigin` | `string \| null` | `"anonymous"` | `crossOrigin` for loads |
| `className` | `string` | - | Class for the root element |
| `containerClassName` | `string` | - | Class for the container |
| `canvasClassName` | `string` | - | Class for the canvas |
| `onDiffComplete` | `(detail) => void` | - | `{ diffCount, totalPixels, percentage }` |
| `onDiffError` | `(error: unknown) => void` | - | Called when comparison fails |
## Headless engine
Need another framework, or full control? Drive the engine directly from `@blazediff/ui/engine`. Each factory returns a controller with `getState()`, `subscribe(listener)`, `setConfig(config)`, `actions`, and `destroy()`.
```js
import { createSwipeEngine } from "@blazediff/ui/engine";
const engine = createSwipeEngine(50);
const unsubscribe = engine.subscribe(() => {
const { position, isDragging } = engine.getState();
// render position (0–100) however your framework wants
});
engine.actions.start(40); // pass an already-computed percentage
engine.actions.move(55); // clamped + ignored unless dragging
engine.actions.end();
unsubscribe();
engine.destroy();
```
| Factory | State | Actions |
| --------------------------------------------- | --------------------------------------------------------------------- | -------------------------------- |
| `createDifferenceEngine(config)` | `{ status, diff?: { output, width, height, diffCount, totalPixels, percentage }, error? }` | – |
| `createSwipeEngine(initialPosition = 50)` | `{ position, isDragging }` | `start`, `move`, `end`, `setPosition` |
| `createTwoUpEngine(config)` | `{ status, dims1, dims2, dimensionLabel, changed, error }` | – |
| `createOnionSkinEngine(config, opacity = 50)` | `{ status, opacity, dims1, dims2, error }` | `setOpacity` |
Also exported: `formatDimensionLabel`, `normalizedOpacity`, `loadImageElement`, `getImageData`, `createStore`.
The engine uses browser APIs (`Image`, a throwaway `<canvas>` for pixel extraction) but never touches the surface you render to — that boundary is what keeps it framework-agnostic.
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/ui)
- [Examples →](/docs/ui-components/vanilla)
---
# @blazediff/react
React components for high-performance image comparison. Built with TypeScript and optimized for modern React applications.
## Installation
```sh
npm install @blazediff/react
```
## Features
- **React 16.8+ compatible**: Works with hooks and modern React features
- **TypeScript first**: Full type safety and IntelliSense
- **Engine-powered**: Renders from the [`@blazediff/ui`](/apis/ui) headless engine — all state and logic live there; React just renders
- **Idiomatic React**: Real JSX, no web components or `dangerouslySetInnerHTML`
- **Accessible**: Proper event handling and React patterns
## Quick Start
```jsx
import { SwipeMode } from '@blazediff/react';
function App() {
return (
<SwipeMode
src1="/before.jpg"
src2="/after.jpg"
onPositionChange={(position) => console.log(position)}
/>
);
}
```
## API Reference
### `<SwipeMode />`
Interactive swipe component for comparing two images with a draggable divider.
#### Props
| Prop | Type | Default | Description |
| ------------------ | --------------------------------- | ------- | ------------------------------------ |
| `src1` | `string` (required) | - | URL of the first image |
| `src2` | `string` (required) | - | URL of the second image |
| `alt1` | `string` | - | Alt text for the first image |
| `alt2` | `string` | - | Alt text for the second image |
| `className` | `string` | - | CSS class for the root element |
| `containerClassName` | `string` | - | CSS class for the container |
| `image1ClassName` | `string` | - | CSS class for the first image |
| `image2ClassName` | `string` | - | CSS class for the second image |
| `dividerClassName` | `string` | - | CSS class for the divider |
| `onPositionChange` | `(position: number) => void` | - | Callback when divider position changes |
### `<TwoUpMode />`
Side-by-side comparison component with dimension information.
#### Props
| Prop | Type | Default | Description |
| ------------------------- | ----------------------------------------------------------- | ------- | --------------------------------- |
| `src1` | `string` (required) | - | URL of the first image |
| `src2` | `string` (required) | - | URL of the second image |
| `className` | `string` | - | CSS class for the root element |
| `containerClassName` | `string` | - | CSS class for the main container |
| `containerInnerClassName` | `string` | - | CSS class for the inner container |
| `panelClassName` | `string` | - | CSS class for each panel |
| `imageClassName` | `string` | - | CSS class for images |
| `dimensionInfoClassName` | `string` | - | CSS class for dimension info text |
| `onImagesLoaded` | `(detail: {image1: {width, height}, image2: {width, height}}) => void` | - | Callback when images load |
| `onLoadError` | `(error: unknown) => void` | - | Callback when loading fails |
### `<OnionSkinMode />`
Overlay comparison with opacity control slider.
#### Props
| Prop | Type | Default | Description |
| -------------------------- | ----------------------------------------------------------- | ------- | -------------------------------- |
| `src1` | `string` (required) | - | URL of the base image |
| `src2` | `string` (required) | - | URL of the overlay image |
| `opacity` | `number` | - | Initial opacity (0-100) |
| `className` | `string` | - | CSS class for the root element |
| `containerClassName` | `string` | - | CSS class for the main container |
| `imageContainerClassName` | `string` | - | CSS class for image container |
| `imageClassName` | `string` | - | CSS class for images |
| `sliderContainerClassName` | `string` | - | CSS class for slider container |
| `sliderClassName` | `string` | - | CSS class for the slider |
| `sliderLabelClassName` | `string` | - | CSS class for slider label |
| `sliderLabelText` | `string` | - | Text for the slider label |
| `onOpacityChange` | `(opacity: number) => void` | - | Callback when opacity changes |
| `onImagesLoaded` | `(detail: {image1: {width, height}, image2: {width, height}}) => void` | - | Callback when images load |
| `onLoadError` | `(error: unknown) => void` | - | Callback when loading fails |
### `<DifferenceMode />`
Shows pixel differences using the BlazeDiff comparison algorithm.
#### Props
| Prop | Type | Default | Description |
| ------------------- | ----------------------------------------------------------------- | ------- | ------------------------------- |
| `src1` | `string` (required) | - | URL of the first image |
| `src2` | `string` (required) | - | URL of the second image |
| `threshold` | `number` | - | Difference threshold (0-1) |
| `includeAA` | `boolean` | - | Include anti-aliased pixels |
| `alpha` | `number` | - | Opacity of original image |
| `className` | `string` | - | CSS class for the root element |
| `containerClassName`| `string` | - | CSS class for the container |
| `canvasClassName` | `string` | - | CSS class for the canvas |
| `onDiffComplete` | `(detail: {diffCount, totalPixels, percentage}) => void` | - | Callback on completion |
| `onDiffError` | `(error: unknown) => void` | - | Callback on error |
## Links
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [NPM Package](https://www.npmjs.com/package/@blazediff/react)
- [Examples →](/docs/ui-components/react)
---
# blazediff (Python)
PyO3 bindings to the same Rust core that powers `@blazediff/core-native`. **3-4x faster** than [odiff](https://github.com/dmtrKovalenko/odiff), shipped as `abi3` wheels for CPython ≥ 3.8 across macOS, Linux, and Windows.
[View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md)
## Installation
```sh
pip install blazediff
```
`uv pip install blazediff` and `poetry add blazediff` work identically. Pre-built wheels are published for:
- macOS ARM64 (Apple Silicon) & x86_64 (Intel)
- Linux ARM64 & x86_64 (manylinux 2.17)
- Windows ARM64 & x86_64
A single `abi3-py38` wheel per platform serves every CPython ≥ 3.8 - no separate wheel per minor version.
## Features
- **PNG, JPEG & QOI support** - auto-detected by file extension
- **Path-based** - pass file paths in, blazediff handles decode + compare + encode
- **SIMD-accelerated** - NEON on ARM, SSE4.1 on x86
- **Block-based optimization** - skips unchanged regions
- **Interpret mode** - region detection, classification, severity scoring, human-readable summaries
## API Reference
### `compare(base_path, compare_path, diff_output=None, *, ...)`
Compares two images and optionally writes a diff image. All keyword arguments are optional.
```python
from blazediff import compare
result = compare(
"expected.png",
"actual.png",
"diff.png",
threshold=0.1,
antialiasing=True,
)
```
#### Parameters
| Parameter | Type | Default | Description |
| ---------------- | --------------- | ------- | ---------------------------------------------------------------------- |
| `base_path` | `str` | - | Path to the base/expected image |
| `compare_path` | `str` | - | Path to the comparison/actual image |
| `diff_output` | `str \| None` | `None` | Path where the diff image (or HTML report) will be written |
| `threshold` | `float` | `0.1` | Color difference threshold (0.0-1.0). Lower = stricter |
| `antialiasing` | `bool` | `False` | Enable anti-aliasing detection |
| `diff_mask` | `bool` | `False` | Output only differences with a transparent background |
| `compression` | `int` | `0` | PNG compression level (0-9, 0 = fastest, 9 = smallest) |
| `quality` | `int` | `90` | JPEG quality (1-100) |
| `interpret` | `bool` | `False` | Run structured interpretation (region detection + classification) |
| `output_format` | `"png" \| "html"` | `"png"` | `"html"` writes an interactive interpret report to `diff_output` |
#### Return value
`compare()` returns a `DiffResult` object with:
| Field | Type | Description |
| ---------------- | -------------------------- | ------------------------------------------------------------ |
| `match` | `bool` | `True` when images are identical (within threshold) |
| `reason` | `str \| None` | `"layout-diff"` or `"pixel-diff"` when `match` is `False` |
| `diff_count` | `int \| None` | Number of differing pixels (pixel-diff only) |
| `diff_percentage`| `float \| None` | Percentage of differing pixels (pixel-diff only) |
| `interpretation` | `InterpretResult \| None` | Populated when `interpret=True` or `output_format="html"` |
> **Info:** **Threshold guidelines:** - `0.0` - Exact match only - `0.05` - Strict comparison - `0.1` - Default balanced comparison - `0.2` - Lenient comparison
### `interpret_images(image1_path, image2_path, *, threshold=0.1, antialiasing=False)`
Returns the full `InterpretResult` (regions, severity, summary) without writing any output file. Equivalent to `compare(..., interpret=True)` minus the diff image.
```python
from blazediff import interpret_images
result = interpret_images("expected.png", "actual.png")
print(result.summary)
print(f"{result.total_regions} regions, severity={result.severity}")
```
## Usage
### Basic comparison
```python
from blazediff import compare
result = compare("expected.png", "actual.png", "diff.png", threshold=0.1)
if result.match:
print("Images are identical!")
elif result.reason == "pixel-diff":
print(f"{result.diff_count} pixels differ ({result.diff_percentage:.2f}%)")
elif result.reason == "layout-diff":
print("Images have different dimensions")
```
### pytest assertion
```python
from blazediff import compare
def test_homepage_matches_baseline():
result = compare("baseline.png", "rendered.png", "diff.png", threshold=0.05)
assert result.match, (
f"{result.diff_count} pixels differ ({result.diff_percentage:.2f}%)"
)
```
### Mixed formats
`compare()` infers format from the file extension. Inputs and outputs can mix freely:
```python
# PNG inputs, QOI diff output (12x smaller diff files)
compare("expected.png", "actual.png", "diff.qoi")
# JPEG comparison
compare("expected.jpg", "actual.jpg", "diff.jpg", quality=85)
```
| Format | Extensions | Notes |
| ------ | ---------------- | ------------------------------------------------------------------ |
| PNG | `.png` | Lossless, transparency |
| JPEG | `.jpg`, `.jpeg` | Lossy, smaller files |
| QOI | `.qoi` | Fast lossless, ideal for diff outputs (~12x smaller than PNG) |
### Interpret mode
Structured analysis - change regions, classification (Addition, Deletion, Shift, ContentChange, ColorChange, RenderingNoise), severity scoring, and human-readable summaries.
```python
from blazediff import compare, interpret_images
# Programmatic
result = interpret_images("expected.png", "actual.png")
print(result.summary)
# "Moderate visual change detected (1.87% of image, 10 regions).
# Content changed: 4 regions (bottom, center).
# Content added: 3 regions (right, bottom, bottom-left)."
for region in result.regions:
print(f" {region.region_type} @ {region.position}, severity={region.severity}")
# Diff image + interpretation
result = compare("expected.png", "actual.png", "diff.png", interpret=True)
# Interactive HTML report
compare("expected.png", "actual.png", "report.html", output_format="html")
```
Severity tiers: `Low` (<1%), `Medium` (1-10%), `High` (>10%). See [Interpret example →](/docs/difference-analysis) for the interactive demo.
## Links
- [PyPI Package](https://pypi.org/project/blazediff/)
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [Rust Crate](https://crates.io/crates/blazediff)
- [NPM Package (`@blazediff/core-native`)](https://www.npmjs.com/package/@blazediff/core-native)
---
# blazediff (Rust)
The Rust crate that powers the entire BlazeDiff stack. **3-4x faster** than [odiff](https://github.com/dmtrKovalenko/odiff), **8x faster** than pixelmatch on 4K images. Block-based diff algorithm with SIMD-accelerated YIQ color comparison; the same engine drives `@blazediff/core-native` (Node.js) and `blazediff` (Python).
[View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md)
## Installation
### As a CLI
```sh
cargo install blazediff
```
### As a library
```toml
# Cargo.toml
[dependencies]
blazediff = "*"
```
Pre-built binaries (via `cargo install`) and crate sources are available on [crates.io](https://crates.io/crates/blazediff).
## Features
- **PNG, JPEG & QOI support** with vendored libspng, libjpeg-turbo, and qoi-rust (no system dependencies)
- **SIMD-accelerated** - NEON on ARM, SSE4.1 on x86
- **Block-based optimization** - skips unchanged 8x8 blocks via 32-bit integer compare before perceptual diff
- **Interpret mode** - region detection, classification, severity scoring, human-readable summaries
- **Cargo features** - opt into `napi` (Node.js bindings) or `python` (PyO3 bindings) when building from source
## CLI Usage
```sh
# Basic diff
blazediff image1.png image2.png diff.png
# Custom threshold (0.0 - 1.0)
blazediff image1.png image2.png diff.png -t 0.05
# JPEG comparison
blazediff a.jpg b.jpg diff.jpg -q 85
# QOI diff output (12x smaller than PNG, faster encode)
blazediff a.png b.png diff.qoi
# Structured interpretation (JSON)
blazediff a.png b.png --interpret
# HTML interpret report
blazediff a.png b.png report.html --output-format html
```
### CLI Options
```sh
blazediff [OPTIONS] <IMAGE1> <IMAGE2> [OUTPUT]
Arguments:
<IMAGE1> First image path (PNG, JPEG, or QOI)
<IMAGE2> Second image path (PNG, JPEG, or QOI)
[OUTPUT] Output diff image path (optional, format detected from extension)
Options:
-t, --threshold <THRESHOLD> Color difference threshold (0.0-1.0) [default: 0.1]
-a, --antialiasing Enable anti-aliasing detection
--diff-mask Output only differences (transparent background)
-c, --compression <LEVEL> PNG compression level (0-9) [default: 0]
-q, --quality <QUALITY> JPEG quality (1-100) [default: 90]
--interpret Run structured interpretation after diff
--output-format <FORMAT> Output format (json, text, or html for interpret) [default: json]
-h, --help Print help
-V, --version Print version
```
### Exit Codes
- `0` - Images are identical
- `1` - Images differ (includes layout/size mismatch)
- `2` - Error (file not found, invalid format, etc.)
## Library Usage
The crate exposes `diff()` plus codec helpers for PNG, JPEG, and QOI. Decode once, diff in memory, encode if you want a diff image.
```rust
use blazediff::{diff, load_pngs, save_png, DiffOptions, Image};
let (img1, img2) = load_pngs("expected.png", "actual.png")?;
let options = DiffOptions {
threshold: 0.1,
include_aa: true,
..Default::default()
};
let mut output = Image::new_uninit(img1.width, img1.height);
let result = diff(&img1, &img2, Some(&mut output), &options)?;
println!("{} pixels differ ({:.2}%)", result.diff_count, result.diff_percentage);
if !result.identical {
save_png(&output, "diff.png")?;
}
```
### Types
```rust
pub struct DiffOptions {
pub threshold: f64, // 0.0-1.0, default 0.1
pub include_aa: bool, // count anti-aliased pixels as diffs
pub alpha: f64, // background opacity, default 0.1
pub aa_color: [u8; 3], // default yellow [255, 255, 0]
pub diff_color: [u8; 3], // default red [255, 0, 0]
pub diff_color_alt: Option<[u8; 3]>,
pub diff_mask: bool, // transparent background mode
pub compression: u8, // PNG compression 0-9 (0 = fastest)
}
pub struct DiffResult {
pub diff_count: u32,
pub diff_percentage: f64,
pub identical: bool,
}
```
### Codec helpers
| Function | Format |
| ------------------------------------------------- | ------ |
| `load_png` / `load_pngs` / `save_png` / `encode_png` | PNG |
| `load_jpeg` / `load_jpegs` / `save_jpeg` | JPEG |
| `load_qoi` / `load_qois` / `save_qoi` | QOI |
All decoders return `Image` (RGBA8, tightly packed). Inputs can be mixed formats - decode each side with the matching helper, then call `diff()`.
> **Info:** **Threshold guidelines:** - `0.0` - Exact match only - `0.05` - Strict comparison - `0.1` - Default balanced comparison - `0.2` - Lenient comparison
## Cargo Features
| Feature | Purpose |
| --------- | ------------------------------------------------------- |
| (default) | Pure Rust library + CLI |
| `napi` | N-API bindings used by `@blazediff/core-native` on NPM |
| `python` | PyO3 bindings used by the `blazediff` package on PyPI |
The `napi` and `python` features are mutually exclusive build modes for distributing prebuilt artifacts; you don't need them when consuming the crate as a Rust dependency.
## Interpret
Structured diff analysis. Takes two images, returns classified change regions with human-readable summaries.
**Pipeline:** change mask -> morph close -> connected components -> per-region evidence extraction -> classify -> describe
**Six-label decision tree:**
| Type | Signal |
| ---------------- | ----------------------------------------------------------------------- |
| `RenderingNoise` | Tiny (<=25px) or sparse + low color delta |
| `Addition` | Blends with background in img1, distinct in img2 |
| `Deletion` | Distinct in img1, blends with background in img2 |
| `ColorChange` | Edge structure preserved across both images + uniform color shift |
| `ContentChange` | Fallback - structural change |
| `Shift` | Post-hoc: matched Addition+Deletion pair with similar luminance |
```sh
blazediff a.png b.png --interpret
blazediff a.png b.png report.html --output-format html
```
Severity tiers: `Low` (<1%), `Medium` (1-10%), `High` (>10%). See [Interpret example →](/docs/difference-analysis) for the interactive demo, and [INTERPRET.md](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff/INTERPRET.md) for the full algorithm spec.
## Vendored libraries
- [libspng](https://libspng.org/) - fast PNG decoding/encoding with SIMD
- [libjpeg-turbo](https://libjpeg-turbo.org/) - high-performance JPEG codec with SIMD
- [qoi-rust](https://github.com/aldanor/qoi-rust) - QOI (Quite OK Image) format
No system C dependencies; the crate builds standalone with a recent Rust toolchain.
## Links
- [Crates.io](https://crates.io/crates/blazediff)
- [GitHub Repository](https://github.com/teimurjan/blazediff)
- [Crate README](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff/README.md)
- [INTERPRET.md (algorithm spec)](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff/INTERPRET.md)
- [NPM Package (`@blazediff/core-native`)](https://www.npmjs.com/package/@blazediff/core-native)
- [PyPI Package](https://pypi.org/project/blazediff/)
---
# blazediff-png
A from-scratch PNG codec in Rust: **single-threaded, SIMD-first, with byte-exact decode parity to [libspng](https://libspng.org)** — and faster than spng on every fixture we test, for both encode and decode. It powers PNG I/O in the BlazeDiff Rust crate.
[View per-fixture benchmarks](https://github.com/teimurjan/blazediff/tree/main/crates/blazediff-png-benchmark)
## Installation
```toml
# Cargo.toml
[dependencies]
blazediff-png = "0.0.1"
```
The crate name is `blazediff-png`; the library imports as `blazediff_png`. Crate sources are available on [crates.io](https://crates.io/crates/blazediff-png).
> **Warning:** Experimental. Inside BlazeDiff it is opt-in behind the `BLAZEDIFF_PNG_ENABLED` environment variable while it stabilizes; spng stays the default and the defensive decode fallback.
## Features
- **Decodes everything spng decodes** — bit depths 1/2/4/8/16, all five color types, palette + tRNS, gray/RGB color-key transparency, Adam7 interlacing — to RGBA8, producing the *same bytes* spng produces and *rejecting the same inputs* spng rejects.
- **Targets any pixel format** — `decode_with` reaches any `SPNG_FMT_*` with optional gamma / sBIT transforms; `decode_with_metadata` captures every ancillary chunk.
- **Encodes** all color-type / bit-depth combinations, optional Adam7, and real deflate levels (libdeflate) plus a stored level 0. Lossless by construction: `decode(encode(x)) == x` always holds.
- **SIMD-first, single-threaded** — whole-buffer inflate, in-place defilter, NEON adaptive-filter kernels; the caller parallelizes across images, not the codec.
- **Pluggable deflate backend** — system zlib + libdeflate for spng parity, or pure Rust for C-free builds.
## Performance
Versus spng over the BlazeDiff corpus (34 PNGs, 342.7 MPx, up to 5600×3200), single-threaded on Apple Silicon — **faster on every fixture**:
| Operation | vs spng | How |
| --- | --- | --- |
| Decode | **~1.4×** | whole-buffer libdeflate inflate + SIMD defilter |
| Encode, stored (level 0) | **~2.2×** | uncompressed deflate blocks, copy- and allocation-light pipeline |
| Encode, compressed | **~3.8×** | libdeflate level 6 vs spng zlib 4, at ~94% of spng's file size |
The wins come from doing less, not from threads: a whole-buffer inflate instead of spng's per-scanline gating, in-place sequential defiltering, autovectorizable row expansion, and hand-written NEON kernels for the encode SAD/filter hot path. [See the full per-fixture benchmarks →](/benchmarks/png-codec)
## Library Usage
Decode to RGBA8, work in memory, encode back out.
```rust
use blazediff_png::{decode, encode, EncodeOptions};
let bytes = std::fs::read("image.png")?;
// Decode to RGBA8 — Image { data, width, height }.
let image = decode(&bytes)?;
// Re-encode (Auto picks the smallest lossless color mode; level 4 by default).
let png = encode(&image, &EncodeOptions::default())?;
```
### Types
```rust
pub struct Image {
pub data: Vec<u8>, // RGBA8, 4 bytes per pixel, row-major
pub width: u32,
pub height: u32,
}
pub struct EncodeOptions {
pub color: ColorMode, // Auto = smallest lossless mode
pub compression: u8, // 0 = stored, 1..=12 = libdeflate level (default 4)
pub filter: Filter, // None/Sub/Up/Average/Paeth/Adaptive/Choice
pub interlace: bool, // Adam7
}
```
`ColorMode` covers every PNG color type and bit depth (`Gray1..16`, `GrayAlpha8/16`, `Indexed1..8`, `Rgb8/16`, `Rgba8/16`) plus `Auto`. `Filter::Choice(FilterSet)` restricts the adaptive heuristic to a chosen subset of filters, mirroring spng's `SPNG_IMG_FILTER_CHOICE`.
### API
| Function | Purpose |
| --- | --- |
| `decode` | PNG → RGBA8 `Image`, spng-parity |
| `decode_with` | PNG → any `DecodeFormat`, optional gamma / sBIT |
| `decode_with_metadata` | `decode` + every ancillary chunk |
| `encode` / `encode_ref` | RGBA8 → PNG (`Vec<u8>`); `_ref` borrows the buffer |
| `encode_to` | stream a PNG encode into any `Write` sink |
| `encode16` | true 16-bit `Image16` → PNG |
| `encode_with_metadata` | `encode` + caller-supplied ancillary chunks |
> **Info:** **Compression levels:** - `0` — uncompressed stored blocks (fastest) - `4` — default speed/size knee - `6` — spng's default ratio (~98% of its file size) - `12` — libdeflate maximum
## Cargo Features
The inflate/compress seam is pluggable; everything else is pure Rust.
| Feature | Backend | Use |
| --- | --- | --- |
| `zlib-backend` (default) | system zlib + libdeflate (C) | byte-exact spng parity, incl. accept/reject on malformed streams |
| `rust-backend` | `zune-inflate` + `fdeflate` (pure Rust) | C-free native builds |
The `rust-backend` is correct for every well-formed PNG but is **not** bug-compatible with spng on malformed/adversarial streams; spng's edge-case accept/reject parity is a `zlib-backend`-only guarantee.
## Parity by identity
zlib's *acceptance* of malformed deflate streams isn't portable. Classic zlib (what spng links) tolerates "distance too far back" at scanline boundaries and copies from window memory; zlib-ng/zlib-rs reject those streams; libdeflate insists on complete adler-valid streams; miniz validates ahead of the write gate. Worse, classic zlib's verdict can depend on the exact `avail_out` gating sequence.
So for the malformed-input edge cases the decoder **links the same system zlib spng links** and drives it with spng's exact per-scanline gate sequence — parity by identity, not by reimplementation. libdeflate stays the whole-buffer fast path for well-formed streams. (Parity is verified on system-zlib platforms; on Windows spng bundles miniz, so the boundary semantics differ there.)
## Verified
| Layer | Result |
| --- | --- |
| Exhaustive matrix | every `{depth × color × interlace × filter × tRNS}` at edge sizes, byte-parity with spng |
| PngSuite conformance | 176/176 — 164 decode at parity, 12 corrupt files reject in lockstep |
| Differential fuzzing | 40M+ execs vs spng, **0 unresolved divergences** |
| Encode round-trip fuzzing | 5M+ execs, round-trip + spng cross-decode clean |
| Line coverage | **98.89%** (residual lines are unreachable defensive arms) |
Byte-identical *encode* output to spng is explicitly **not** a goal — both emit valid-but-different streams. The encode contract is lossless round-tripping plus spng cross-decode compatibility.
## Links
- [Crates.io](https://crates.io/crates/blazediff-png)
- [Crate README](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-png/README.md)
- [Benchmarks](https://github.com/teimurjan/blazediff/tree/main/crates/blazediff-png-benchmark)
- [GitHub Repository](https://github.com/teimurjan/blazediff)
---
# Examples
# Pixel-by-pixel Comparison - Vanilla JavaScript
Compare two images pixel by pixel in pure JavaScript - no native binaries, no
WebAssembly. `@blazediff/core` runs anywhere JS runs (browser, Node, Deno, Bun,
edge) and is ~1.5× faster than pixelmatch. Reach for it when you want zero install
friction and portability over raw throughput. Need more speed? See
[🐆🐆 Rust + WASM](/docs/pixel-comparison/rust-wasm) and
[🐆🐆🐆 Rust + N-API](/docs/pixel-comparison/rust-napi).
## Installation
```bash
npm install @blazediff/core
```
## Examples
```ts
import blazediff from "@blazediff/core";
// loadImage can either be a browser function or a server function
const img1 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png"
);
const img2 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png"
);
const output = new Uint8Array(img1.width * img1.height * 4);
const width = img1.width;
const height = img1.height;
const diff = blazediff(img1, img2, output, width, height);
```
```ts
import blazediff from "@blazediff/core";
// loadImage can either be a browser function or a server function
const img1 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png"
);
const img2 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png"
);
const output = new Uint8Array(img1.width * img1.height * 4);
const width = img1.width;
const height = img1.height;
const diff = blazediff(img1, img2, output, width, height, { threshold: 0.5 });
```
```ts
import blazediff from "@blazediff/core";
// loadImage can either be a browser function or a server function
const img1 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png"
);
const img2 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png"
);
const output = new Uint8Array(img1.width * img1.height * 4);
const width = img1.width;
const height = img1.height;
const diff = blazediff(img1, img2, output, width, height, {
aaColor: [255, 0, 0],
diffColor: [0, 255, 0],
});
```
See the [full API reference →](/apis/core).
---
# Pixel-by-pixel Comparison - Rust + WASM in JavaScript
The same diff core, compiled to WebAssembly with SIMD. `@blazediff/core-wasm` runs
in browsers, Workers, Deno, Bun, and edge runtimes - ~58% faster than pixelmatch
from a 32KB binary. Reach for it when you want native-grade speed in the browser
without a native dependency. The API takes pre-decoded RGBA buffers, so you decode
images yourself with `createImageBitmap` + a canvas (or the `ImageDecoder` API).
## Installation
```bash
npm install @blazediff/core-wasm
```
## Decoding images to RGBA
```ts
async function loadRgba(url: string) {
const bitmap = await createImageBitmap(await (await fetch(url)).blob());
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext("2d")!;
ctx.drawImage(bitmap, 0, 0);
const { data, width, height } = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
return { data: new Uint8Array(data.buffer), width, height };
}
```
## Examples
```ts
import { initBlazediff, diff } from "@blazediff/core-wasm";
await initBlazediff(); // loads the sibling .wasm; call once
const a = await loadRgba("3a.png");
const b = await loadRgba("3b.png");
const output = new Uint8Array(a.width * a.height * 4);
const diffCount = await diff(a.data, b.data, a.width, a.height, output);
```
```ts
import { initBlazediff, diff } from "@blazediff/core-wasm";
await initBlazediff();
const a = await loadRgba("3a.png");
const b = await loadRgba("3b.png");
const output = new Uint8Array(a.width * a.height * 4);
const diffCount = await diff(a.data, b.data, a.width, a.height, output, {
threshold: 0.5,
});
```
```ts
import { initBlazediff, diff } from "@blazediff/core-wasm";
await initBlazediff();
const a = await loadRgba("3a.png");
const b = await loadRgba("3b.png");
const output = new Uint8Array(a.width * a.height * 4);
// diffMask renders changes on a transparent background instead of grayscale
const diffCount = await diff(a.data, b.data, a.width, a.height, output, {
diffMask: true,
});
```
Loading the `.wasm` from a CDN, a bundler, or the filesystem? `initBlazediff`
accepts a `URL`, `Response`, or raw bytes - see the
[full API reference →](/apis/core-wasm).
---
# Pixel-by-pixel Comparison - Rust + N-API in Node
The fastest path: native Rust compiled to a Node addon via N-API, ~3-4× faster
than odiff. `@blazediff/core-native` accepts file paths or encoded `Buffer` and
`Uint8Array` inputs. It decodes PNG, JPEG, and QOI itself in parallel and can
write the diff image straight to disk. Reach for it in Node test runners and CI
when you want maximum throughput.
## Installation
```bash
npm install @blazediff/core-native
```
## Examples
```ts
import { compare } from "@blazediff/core-native";
const result = await compare("3a.png", "3b.png");
if (result.match) {
console.log("identical");
} else if (result.reason === "pixel-diff") {
console.log(`${result.diffCount} pixels differ (${result.diffPercentage.toFixed(2)}%)`);
} else if (result.reason === "layout-diff") {
console.log("dimensions differ");
}
```
```ts
import { readFile } from "node:fs/promises";
import { compare } from "@blazediff/core-native";
const [expected, actual] = await Promise.all([
readFile("3a.png"),
readFile("3b.png"),
]);
// The native binding borrows these Buffer allocations without copying them.
const result = await compare(expected, actual);
```
```ts
import { compare } from "@blazediff/core-native";
const result = await compare("3a.png", "3b.png", undefined, {
threshold: 0.5,
antialiasing: true,
});
```
```ts
import { compare } from "@blazediff/core-native";
// Pass an output path to render the diff visualization to disk.
// Format is inferred from the extension (PNG, JPEG, QOI).
const result = await compare("3a.png", "3b.png", "diff.png");
```
Encoded inputs are borrowed directly for each synchronous native call, so the
JavaScript bytes are not copied into Rust. PNG, JPEG, or QOI decoding still
allocates native RGBA pixel buffers.
Want a verdict on *what* changed, not just how many pixels? Pass `interpret: true`
or use the dedicated [Image Difference Analysis →](/docs/difference-analysis)
example. Full options in the [API reference →](/apis/core-native).
---
# Structural Image Comparison
Pixel-by-pixel diffing answers "how many pixels changed". Structural metrics answer
"how *different do these look*" - they tolerate imperceptible noise (compression
artifacts, sub-pixel shifts) and score perceived similarity instead. Reach for them
when an exact pixel match is too strict.
BlazeDiff ships two: **GMSD** (gradient-based, fast) and **SSIM** (the classic
structural index, with a 4× faster Hitchhiker's variant).
## Installation
```bash
npm install @blazediff/gmsd @blazediff/ssim
```
## GMSD (Gradient Magnitude Similarity Deviation)
Scores gradient (edge) similarity. Returns `0` for identical images; **lower is
better**, typically in the `0`–`0.35` range.
```ts
const img1 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png"
);
const img2 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png"
);
const width = img1.width;
const height = img1.height;
const score = gmsd(img1, img2, undefined, width, height);
```
```ts
const img1 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png"
);
const img2 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png"
);
const width = img1.width;
const height = img1.height;
const score = gmsd(img1, img2, undefined, width, height, { c: 100 });
```
See the [GMSD API reference →](/apis/gmsd).
## SSIM (Structural Similarity Index)
Scores luminance, contrast, and structure. Returns `1` for identical images;
**higher is better**, in the `0`–`1` range. The Hitchhiker's variant uses
non-overlapping windows for ~4× the throughput at near-identical accuracy.
```ts
import ssim from "@blazediff/ssim/ssim";
const img1 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png"
);
const img2 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png"
);
const output = new Uint8Array(img1.width * img1.height * 4);
const width = img1.width;
const height = img1.height;
const score = ssim(img1, img2, output, width, height);
```
```ts
import hitchhikersSSIM from "@blazediff/ssim/hitchhikers-ssim";
const img1 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png"
);
const img2 = await loadImage(
"https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png"
);
const output = new Uint8Array(img1.width * img1.height * 4);
const width = img1.width;
const height = img1.height;
const score = hitchhikersSSIM(img1, img2, output, width, height);
```
See the [SSIM API reference →](/apis/ssim).
---
# Image Difference Analysis
Takes a raw pixel diff and tells you _what_ changed, _where_, and _how much_. No ML
models - a deterministic pipeline that runs in the same binary as the diff itself.
Available in both the native Node binding (`@blazediff/core-native`) and the
WebAssembly build (`@blazediff/core-wasm`), so you can run it server-side or in the
browser.
## How it works
1. Pixel diff → binary change mask
2. Morphological close → bridge small gaps
3. Connected components → isolate regions
4. Per-region evidence extraction:
- **Dual-image gradients** - edges in both images + spatial correlation to detect structural preservation
- **Color delta distribution** - mean, max, and stddev of YIQ distance to separate uniform recolors from patchy texture changes
- **Background distance** - how much changed pixels blend with local unchanged pixels in each image
5. Six-label decision tree classifies each region
6. Post-hoc shift detection matches Addition+Deletion pairs
## Demo
## Usage
Both bindings return the same result shape - `summary`, `regions[]` (with
`position`, `changeType`, `percentage`, …), and `severity`. The native binding
reads files; the WASM binding takes pre-decoded RGBA buffers.
```ts
import { interpret } from "@blazediff/core-native";
const result = await interpret("fixtures/3a.png", "fixtures/3b.png");
console.log(result.summary);
for (const region of result.regions) {
console.log(`${region.position}: ${region.changeType} (${region.percentage.toFixed(2)}%)`);
}
```
```ts
import { initBlazediff, interpret } from "@blazediff/core-wasm";
await initBlazediff(); // loads the .wasm; call once
// Decode both images to RGBA Uint8Arrays (see the Rust + WASM example)
const a = await loadRgba("3a.png");
const b = await loadRgba("3b.png");
const result = await interpret(a.data, b.data, a.width, a.height);
console.log(result.summary);
for (const region of result.regions) {
console.log(`${region.position}: ${region.changeType} (${region.percentage.toFixed(2)}%)`);
}
```
### Identical images
When nothing changed, `regions` is empty and `summary` reports no differences -
identical for both bindings.
## Change types
| Type | Meaning |
|---|---|
| `Addition` | Content appeared - blends with background in before image, distinct in after |
| `Deletion` | Content removed - distinct in before, blends with background in after |
| `Shift` | Content moved - matched Addition+Deletion pair with similar luminance |
| `ColorChange` | Recolor - edge structure preserved across both images, uniform color shift |
| `ContentChange` | Structural change - edges differ between images |
| `RenderingNoise` | Sub-pixel artifacts - filtered from output |
## Accuracy
Measured against datasets with hand-labeled change regions (see
[crates/blazediff-interpret-verify/BENCHMARKS.md](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-interpret-verify/BENCHMARKS.md)
for the full breakdown).
| Dataset | What it tests | Classifier-only macro F1 | End-to-end macro F1 |
|---|---|---:|---:|
| `addition_deletion` | Clean object insert / remove on photographs | **0.998** | **0.888** |
| `shift` | Sub-region translations with pixel-perfect ground truth | **0.813** | **0.628** |
| `inpaintcoco` | Inpaint edits that mix recolor and texture replacement | **0.440** | **0.260** |
| `html_color_pairs` | Recolors on rendered Tailwind UI screenshots | **0.993** | **0.786** |
Read this as: on clear add/delete edits the classifier almost never picks
the wrong label (0.998 means 4 mistakes in 904 regions). On synthetic shifts
the post-pass pairs about two thirds of moved-block events with near-perfect
precision. On real inpainted photos it lands the right label in about four
of every nine regions - the `ColorChange` vs `ContentChange` boundary is the
dominant confusion and the focus area for the next iteration. `html_color_pairs`
isolates that same boundary on 100 rendered Tailwind UI screenshots that differ
only in color classes: a dedicated chromatic-recolor branch admits
same-luminance hue swaps that YIQ otherwise scores as low-delta, lifting
classifier F1 to 0.993 with zero false positives.
End-to-end runs the full detector pipeline before classifying, so the score
also reflects detection misses and spurious small regions; classifier-only
isolates labeling quality from detection.
---
# Agentic Visual Testing - Setting Up
`@blazediff/agent` captures deterministic screenshots of your routes, diffs them
against committed baselines with the native BlazeDiff core, and hands ambiguous
diffs back to your coding agent (Claude Code, Cursor, Codex) to judge. This page
gets you from zero to a committed set of baselines.
## 1. Install
```bash
npm install --save-dev @blazediff/agent
```
First run prompts to install bundled Playwright Chromium - no `sudo`, no
`npx playwright install --with-deps`.
```bash
blazediff-agent browsers install --check --json # check
blazediff-agent browsers install # install if missing
```
## 2. Onboard
`onboard` writes `.blazediff/config.json` from your dev script, sets up
`.gitignore`, installs Chromium, and installs the playbook into whatever
coding-agent stack lives in your project.
```bash
# Setup only - baselines are captured explicitly in step 4
blazediff-agent onboard --no-capture
```
It detects your stack automatically (Claude Code, Codex, Cursor); pass
`--stack <name>` to be explicit, or `--stack local` to install the local
(Moondream + Qwen) judge when there's no coding agent.
## 3. Start the dev server
```bash
blazediff-agent serve-status --detach --json # waits up to 60s for the port
```
Already have a running URL (e.g. staging)? Skip the dev server and point the agent
at it: `blazediff-agent onboard --url https://staging.example.com`.
## 4. Capture baselines
Pipe a JSON list of routes - one `capture` call screenshots them all and writes the
manifest plus baseline PNGs.
```bash
cat <<'EOF' | blazediff-agent capture --stdin --mode baseline --json
[
{"id": "home", "url": "/", "mask": [".timestamp"]},
{"id": "pricing", "url": "/pricing"}
]
EOF
```
Then tear down the dev server (mandatory):
```bash
blazediff-agent serve-status --kill --json
```
> **Info:** **Commit `.blazediff/`** - config, manifest, and baselines all live there and are the source of truth for future checks.
## The config
`.blazediff/config.json` is committed and drives every later run:
```json
{
"devServer": { "command": "pnpm dev", "port": 3000, "readyTimeoutMs": 60000 },
"framework": "next",
"packageManager": "pnpm",
"baseUrl": "http://127.0.0.1:3000"
}
```
`.blazediff/manifest.json` is written by `capture` - **never edit it directly**.
Per-route behavior (login, interactions) lives in
[harnesses](/docs/agentic-testing/judging-and-harnesses#harnesses), not config.
Next: [Running in CI →](/docs/agentic-testing/running-in-ci). Full command
reference lives in the [`@blazediff/agent` docs](/apis/agent).
---
# Agentic Visual Testing - Running in CI
With baselines committed (see [Setting Up](/docs/agentic-testing/setting-up)),
CI's job is one verb: `check`. It re-captures every manifest entry, diffs each
against its baseline, and fails the build on any regression.
## The check command
```bash
blazediff-agent check --judge host --json
```
The CLI starts the dev server automatically when `config.devServer` is set, runs
every entry through Playwright, diffs each capture, and emits a `CheckReport`:
```json
{
"summaryPath": ".blazediff/summary.md",
"totalEntries": 23,
"passed": 22,
"failed": 0,
"pendingJudgments": 1,
"results": [
{
"id": "agent",
"url": "/agent",
"status": "needs-judgment",
"verdict": {
"label": "ambiguous",
"headline": "5 regions: 4 content-change, 1 addition @ left (0.13%, low)",
"action": "investigate"
}
}
]
}
```
`results[]` lists non-pass entries only. Full per-entry detail lives in
`.blazediff/summary.md` and `.blazediff/judgments/<id>/request.json`.
> **Warning:** **Check-only in CI.** When `CI=1` or there's no TTY, only `check` runs. `onboard` / `capture` / `rewrite` / `reset` are blocked - authoring belongs on a developer's machine, where baseline changes can be reviewed.
## GitHub Actions
```yaml
- run: pnpm install
- run: npx blazediff-agent browsers install
- run: npx blazediff-agent --cwd apps/website check --json
env:
# Only needed if any entry uses a login harness. One pair per persona.
# In CI, set these as secrets rather than committing .blazediff/.env.
BLAZEDIFF_AUTH_DEFAULT_EMAIL: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_EMAIL }}
BLAZEDIFF_AUTH_DEFAULT_PASSWORD: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_PASSWORD }}
```
Pass `-C, --cwd <abs-path>` to target one app inside a monorepo.
## Exit codes
| Code | Meaning |
|---|---|
| `0` | Every entry passed |
| `1` | At least one regression, intentional, noise, or pending-judgment entry |
| non-zero + JSON | Infra failure (missing manifest, no Chromium, etc.) |
A route that times out is logged once in the result array and skipped - it never
blocks the run.
## When a check fails
A `1` exit usually means a diff needs a verdict. Locally, your coding agent reads
the judgment request and decides; intentional changes are accepted with
`rewrite`. That loop - verdicts, harnesses, and masking flakes - is covered in
[Judging and Harnesses →](/docs/agentic-testing/judging-and-harnesses).
---
# Agentic Visual Testing - Judging and Harnesses
A failing `check` isn't always a regression. This page covers the two things that
turn a raw diff into a decision: **judging** (is this change real, intentional, or
noise?) and **harnesses** (driving the page - login, interactions - so the right
thing gets screenshotted in the first place).
## The judging model
The heuristic verdict pipeline emits one of four labels per failing entry:
| Label | Meaning | Default action |
|---|---|---|
| `regression-likely` | Confident structural change | Investigate; do not rewrite |
| `intentional-likely` | Confident styling/typographic change | Ask user, then rewrite |
| `noise-likely` | Confident non-deterministic source | Ask user; prefer masking |
| `ambiguous` | Heuristic couldn't classify | Defer to host judge |
For `ambiguous`, `--judge host` writes a `JudgmentRequest` to
`.blazediff/judgments/<id>/request.json` with:
- `regions[]` - bounding boxes, pixel counts, and change types per region
- `paths.locator` (`locator.png`) - a ~400px overview with regions outlined in red
- `paths.tiles` (`regions.png`) - a vertical stack of `[baseline | actual]` pairs
- `paths.{baseline,actual,diff}` - full-page PNGs as a fallback
> **Info:** **Token discipline.** The region tiles are 10–100× smaller than the full-page PNGs. A well-behaved host agent reads `regions.png` + `locator.png` first and only falls back to the full-page PNGs if a region clearly continues outside its crop.
The host agent writes its verdict to `.blazediff/judgments/<id>/verdict.json`:
```json
{
"id": "agent",
"verdict": {
"label": "intentional-likely",
"headline": "Em-dash replaced with hyphen in copy",
"rationale": ["region tile shows only typographic substitution"],
"action": "rewrite-if-intended"
},
"rationale": "Full paragraph explanation...",
"confidence": 0.95
}
```
Then merge verdicts into the report - no re-screenshot:
```bash
blazediff-agent check --apply-judgments --json
```
Accept an intentional change by re-baselining the entry (mask/viewport/waitFor are
preserved; only the PNG regenerates):
```bash
blazediff-agent rewrite agent --json # by id
blazediff-agent rewrite --failed --json # all failures from the last check
```
## Harnesses
A **harness** is a pluggable ESM script in `.blazediff/harnesses/<name>.js`,
attached to an entry via its `harnesses: [{ name, params? }]` list. Login is just
one kind - anything that drives the page before or around a screenshot is a
harness. Two phases:
- **`setup`** - runs before navigation (establish a session, e.g. login).
- **`interact`** (default) - runs after the base screenshot; drives the page and
may emit extra named screenshots via `screenshot(name)`, each its own baseline
entry `<entry>__<name>`.
### Interaction harness
```js
// .blazediff/harnesses/weather-menu.js
/** @type {import("@blazediff/agent").Harness} */
export default {
async run({ page, screenshot }) {
await page.getByRole("button", { name: "More options" }).click();
await screenshot("menu"); // -> baseline "weather__menu"
},
};
```
```json
{ "id": "weather", "url": "/weather", "harnesses": ["weather-menu"] }
```
### Login harness
Routes behind a login flow capture through a `setup` harness. Credentials live in
environment variables - never in the harness file, the manifest, or LLM context.
```js
/** @type {import("@blazediff/agent").Harness<{ persona?: string }>} */
export default {
phase: "setup",
async run({ page, params }) {
const upper = (params.persona ?? "default").toUpperCase().replace(/[^A-Z0-9]/g, "_");
const email = process.env[`BLAZEDIFF_AUTH_${upper}_EMAIL`];
const password = process.env[`BLAZEDIFF_AUTH_${upper}_PASSWORD`];
if (!email || !password) throw new Error(`missing BLAZEDIFF_AUTH_${upper}_*`);
await page.goto("http://127.0.0.1:3000/login");
await page.locator('input[name="email"]').fill(email);
await page.locator('input[name="password"]').fill(password);
await Promise.all([
page.waitForURL((u) => !u.pathname.startsWith("/login")),
page.getByRole("button", { name: /sign in|log in/i }).click(),
]);
},
};
```
Attach it per entry, and drop credentials in `.blazediff/.env` (auto-gitignored):
```json
{ "id": "dashboard", "url": "/dashboard",
"harnesses": [{ "name": "auth", "params": { "persona": "default" } }] }
```
For OAuth/SSO, magic links, MFA, or captcha - record interactively instead:
`blazediff-agent auth init --persona default --login-url http://127.0.0.1:3000/login`.
## Masking flaky regions
When a diff is `noise-likely` - or a real-looking diff is actually caused by
something non-deterministic - mask it, don't rebaseline. A rebaseline just resets
the clock on a flake; a mask removes it. Mask auto-cycling animations, third-party
iframes, timestamps, per-session randomness, and personalization noise. **Don't**
mask real content that happens to be changing - that's the change you want caught.
The agent always masks any element matching `[data-blazediff-agent-mask]` - no
manifest change needed. Add it to a shared component and it applies on every route:
```tsx
<div data-blazediff-agent-mask="report-carousel">...</div>
```
When you can't edit the source (third-party embed), fall back to a per-entry CSS
selector. The mask list **replaces** the existing one, so include every selector
you want kept:
```bash
cat <<'EOF' | blazediff-agent capture --stdin --mode baseline --json
[
{"id": "examples-vanilla", "url": "/docs/ui-components/vanilla", "mask": ["iframe"]}
]
EOF
```
Full reference: [`@blazediff/agent` docs →](/apis/agent).
---
# React Components
ReactSwipeDemo,
ReactTwoUpDemo,
ReactOnionSkinDemo,
ReactDifferenceDemo,
} from "../../../../components/demos/react-modes";
Build interactive image comparison components with React. Every example below is live and runs right on this page.
## Installation
```bash
npm install @blazediff/react
```
```tsx
```
```tsx
```
```tsx
```
```tsx
```
All four components render from the [`@blazediff/ui`](/apis/ui) headless engine and accept `*ClassName` props for styling. See the [full API reference →](/apis/react).
---
# Vanilla Components
VanillaSwipeDemo,
VanillaTwoUpDemo,
VanillaOnionSkinDemo,
VanillaDifferenceDemo,
} from "../../../../components/demos/vanilla-modes";
Use the BlazeDiff UI renderer (`mount*` functions) in vanilla JS - or drive the headless engine from any framework. Every example below is live and runs right on this page.
## Installation
```bash
npm install @blazediff/ui
```
```ts
mountSwipe(document.getElementById("app"), {
src1: "before.png",
src2: "after.png",
onPositionChange: (position) => console.log(position),
});
```
```ts
mountTwoUp(document.getElementById("app"), {
src1: "before.png",
src2: "after.png",
onImagesLoaded: ({ image1, image2 }) => console.log(image1, image2),
});
```
```ts
mountOnionSkin(document.getElementById("app"), {
src1: "before.png",
src2: "after.png",
opacity: 50,
onOpacityChange: (opacity) => console.log(opacity),
});
```
```ts
mountDifference(document.getElementById("app"), {
src1: "before.png",
src2: "after.png",
threshold: 0.1,
onDiffComplete: ({ diffCount, percentage }) =>
console.log(diffCount, percentage),
});
```
Each `mount*` call returns a `{ update, destroy }` handle - call `update(options)` to change sources or options in place, and `destroy()` to remove the UI. See the [full API reference →](/apis/ui).
---
# Object Comparison
Learn how to use BlazeDiff for comparing JavaScript objects and detecting changes in complex data structures.
## Installation
```bash
npm install @blazediff/object
```
## Overview
BlazeDiff Object provides fast and efficient comparison of JavaScript objects, detecting additions, removals, and modifications with detailed path information. Try the interactive demos below to see different comparison scenarios in action.
```ts
import diff from "@blazediff/object";
const oldObj = { a: 1, b: 2, c: 3 };
const newObj = { a: 1, b: 20, d: 4 };
const changes = diff(oldObj, newObj);
```
```ts
import diff from "@blazediff/object";
const oldObj = {
user: {
name: "John",
email: "john@old.com",
settings: {
theme: "dark",
notifications: true
}
}
};
const newObj = {
user: {
name: "John Doe",
email: "john@new.com",
settings: {
theme: "light",
notifications: true,
language: "en"
}
}
};
const changes = diff(oldObj, newObj);
```
```ts
import diff from "@blazediff/object";
const oldObj = {
items: [
{ id: 1, name: "Item 1", value: 100 },
{ id: 2, name: "Item 2", value: 200 }
],
total: 300
};
const newObj = {
items: [
{ id: 1, name: "Item 1", value: 150 },
{ id: 2, name: "Item 2", value: 200 },
{ id: 3, name: "Item 3", value: 50 }
],
total: 400
};
const changes = diff(oldObj, newObj);
```
---