# 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)**: 4.4-4.9x faster than odiff on 4K images (5.7-6.7x from encoded buffers)
- **WASM**: ~51% faster than pixelmatch, up to ~10x on 4K (browser, edge, any wasm host)
- **Image Pixel-by-Pixel (JS)**: ~50% faster than pixelmatch (up to 88% on identical images)
- **SSIM (JS)**: ~30% faster than ssim.js, ~75% faster with Hitchhiker's SSIM
- **SSIM (Rust)**: ~15x faster again than the JS port (~4x for Hitchhiker's SSIM), from the same decoded buffers
- **Object Diff**: ~55% faster than microdiff (up to 96% on identical arrays)

---

# API Reference

# @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)
  -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.

### `blazediff-cli interpret`

Describes *what* changed rather than only where: region detection,
content-aware classification (Addition, Deletion, Shift, ContentChange,
ColorChange, RenderingNoise), severity scoring and a human-readable summary.

#### Basic Usage

```bash
blazediff-cli interpret <image1> <image2> [output] [options]

Options:
  --source <name>           How to locate regions: pixel (default), ssim,
                            ms-ssim, hitchhikers-ssim
  -t, --threshold <num>     Color difference threshold (0-1, default: 0.1). pixel only
  -a, --antialiasing        Exclude anti-aliased pixels. pixel only
  -c, --compression <num>   PNG compression level (0-9, default: 0)
  --window-size <num>       Local window size for the metric sources (default: 11)
  --region-floor <num>      Window score at or below which it counts as changed (default: 0.99)
  --regions <json>          Skip the search and classify these boxes instead
  --json                    Print the full result as JSON
  -h, --help                Display help
```

`output` is written by the `pixel` source only — the metric sources and
`--regions` have no diff visualization to write.

#### Example

```bash
$ blazediff-cli interpret image1.png image2.png --json
{
  "summary": "Moderate visual change detected (1.87% of image, 4 regions).\n...",
  "severity": "Medium",
  "diffPercentage": 1.87,
  "regions": [...]
}
```

#### Exit Codes

- `0` - Nothing actionable changed
- `1` - Change detected
- `2` - Error

### `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 |
| `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, **4.4-4.9x 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
- **4.4-4.9x 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 |

#### Return Types

```typescript
type BlazeDiffResult =
  | { match: true }
  | { match: false; reason: "layout-diff" }
  | { match: false; reason: "pixel-diff"; diffCount: number; diffPercentage: number }
  | { match: false; reason: "file-not-exists"; file: string };
```

> **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]
      --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 region analysis is a separate package. This one answers *where*
pixels differ; [`@blazediff/interpret-native`](/apis/interpret-native) takes the
same pair and describes *what* changed — labelled regions, severity and a
human-readable summary — and can locate those regions with a pixel diff, an SSIM
map, or boxes you already have.

```typescript
import { interpret } from '@blazediff/interpret-native';

const result = await interpret('expected.png', 'actual.png');
console.log(result.summary);
// "Moderate visual change detected (1.87% of image, 4 regions).
//  Content changed: 1 region (bottom).
//  Content added: 2 regions (right, bottom-left)."
```

See [Interpret example →](/docs/difference-analysis) for the interactive demo.

## Performance

Benchmarked on Apple M1 Max with 5600×3200 4K images (25 runs, 5 warmup, image IO included):

| Tool | Time | Comparison |
|------|------|------------|
| **blazediff** (encoded buffer input) | ~203ms | - |
| **blazediff** (file paths) | ~275ms | - |
| odiff | ~1266ms | 4.6x slower |

These are end-to-end times, so PNG decode and encode dominate them. The diff kernel
itself is 27–37% faster than the previous release on 4K buffers; that gain is diluted
here because IO is the larger share of the total.

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 blocks using 32-bit integer comparison, then the hot pass only processes changed regions with YIQ perceptual color difference. Three refinements keep the common paths cheap: the cold pass folds four SIMD chunks into one wide test, so 16 unchanged pixels cost a single branch; an integer bound derived from the YIQ metric's largest eigenvalue rejects sub-threshold pixels before any floating-point work; and chunks where nothing crosses the threshold take a vectorized background write instead of falling back to per-pixel handling.

## 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`). ~51% faster than [pixelmatch](https://github.com/mapbox/pixelmatch) on the same RGBA buffers (up to ~90% on 4K); 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.

#### 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`         | 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 |

> **Info:** **Threshold Guidelines:** `0.0` exact match · `0.05` strict · `0.1` default · `0.2` lenient

```typescript
const diffCount = await diff(a, b, width, height, output, {
  diffColorAlt: [0, 128, 255],
});
```

> **Info:** Region analysis — *what* changed rather than where — is not part of this package. It lives in [`@blazediff/interpret-native`](/apis/interpret-native), a Node N-API package with no wasm build.

## 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              | 338.53ms   | 34.42ms    | **89.8%**   |
| 4k/3              | 415.01ms   | 45.34ms    | **89.1%**   |
| page/2            | 511.88ms   | 70.89ms    | **86.2%**   |
| blazediff/3       | 12.08ms    | 5.76ms     | **52.3%**   |
| pixelmatch/1      | 0.57ms     | 0.11ms     | **80.5%**   |

Average ~51% faster across the full fixture set (~60% across differing pairs). 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 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. The cold pass folds four chunks into a single wide test so 16 unchanged pixels cost one branch, and an integer bound derived from the YIQ metric's largest eigenvalue rejects sub-threshold pixels before any float work happens.

## 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/interpret-native

Native Rust **structured interpretation** of image diffs: what changed, where, and how much — not just which pixels differ.

## Installation

```sh
npm install @blazediff/interpret-native
```

The platform binary installs as an optional dependency; there is no compile step.

## Usage

```ts
import { interpret } from "@blazediff/interpret-native";

const result = await interpret("expected.png", "actual.png");
console.log(result.summary);
// "Low-impact visual change detected (0.18% of image, 2 regions).
//  Content added: 2 regions (bottom-right, center)."

for (const region of result.regions) {
  console.log(`${region.position}: ${region.changeType} (${region.pixelCount}px)`);
}
```

## Choosing how regions are found

The classifier is independent of whatever locates the change, so the locator is a parameter:

| `source` | How it finds regions | When |
| --- | --- | --- |
| `pixel` (default) | connected components over a per-pixel diff | exact boxes; the usual choice |
| `ssim`, `ms-ssim`, `hitchhikers-ssim` | thresholding a structural-similarity map | tolerant of imperceptible noise |

```ts
const loose = await interpret("expected.png", "actual.png", undefined, {
  source: "ms-ssim",
});
```

> **Info:** A metric's map is far coarser than a pixel, so its **boxes are blocky**. Its **numbers are not**: every box is refined against the source pixels before anything is measured, so `pixelCount` and `diffCount` count actually-changed pixels on every source, never map windows. On a real fixture pair the pixel diff reports 776 changed pixels and MS-SSIM-located regions report 792.

## Regions you already have

If something else already knows where to look — DOM rectangles from a layout pass, a crop list — skip the search entirely:

```ts
import { interpretRegions } from "@blazediff/interpret-native";

const result = await interpretRegions("expected.png", "actual.png", [
  { x: 16, y: 16, width: 32, height: 32 },
]);
```

Boxes may be coarse; they are refined the same way. A box outside the image is rejected rather than read past.

## Result

```ts
interface InterpretResult {
  summary: string;        // human-readable
  diffCount: number;      // actually-changed pixels
  totalRegions: number;
  regions: ChangeRegion[];
  severity: string;
  diffPercentage: number;
  width: number;
  height: number;
}
```

Each `ChangeRegion` carries a `changeType`, `shape`, `position`, `confidence`, and the statistics behind them — colour delta, gradient/edge correlation, chroma-plane movement (`chroma`: hue rotation, saturation, delta smoothness), fill ratios, and the classifier's signals.

## Options

```ts
{
  source?: "pixel" | "ssim" | "ms-ssim" | "hitchhikers-ssim",

  // pixel source
  threshold?: number,     // Default: 0.1
  antialiasing?: boolean, // exclude AA pixels. Default: false
  compression?: number,   // PNG level for a written diff. Default: 0
  quality?: number,       // JPEG quality for a written diff. Default: 90

  // metric sources
  windowSize?: number,    // Default: 11
  regionFloor?: number,   // window score at/below which it counts as changed. Default: 0.99
}
```

Passing a third argument to `interpret` writes the diff visualization to that path (pixel source only). Encoded PNG/JPEG/QOI buffers work in place of paths on the pixel source; the metric sources need paths.

## Relationship to the other packages

[`@blazediff/core-native`](/apis/core-native) answers *where* pixels differ. [`@blazediff/ssim-native`](/apis/ssim-native) answers *how alike* two images look. This package answers *what changed*, and is the only one of the three that depends on the other two — they know nothing about interpretation.

## Platforms

macOS (arm64, x64), Linux (arm64, x64), Windows (arm64, x64). The binding is required; there is no JS fallback.

## Links

- [npm](https://www.npmjs.com/package/@blazediff/interpret-native)
- [The Rust crate behind it](/apis/blazediff-interpret)
- [GitHub Repository](https://github.com/teimurjan/blazediff)

---

# @blazediff/ssim-native

Native Rust structural-similarity metrics for Node.js: **SSIM, MS-SSIM, Hitchhiker's SSIM and perceptual SSIM**, through N-API. Decodes PNG, JPEG and QOI. It is the [`blazediff-ssim`](/apis/blazediff-ssim) crate with nothing held back.

## Installation

```sh
npm install @blazediff/ssim-native
```

The platform binary installs as an optional dependency; there is no compile step.

## Why a separate package

[`@blazediff/core-native`](/apis/core-native) is a pixel diff: it answers *where* two images differ. This package answers *how alike* they look — a different question, so it is a different package rather than a flag on that one.

It exposes the whole crate: every stability constant, both pooling methods, Hitchhiker's stride, all of `perceptual-ssim`, and the local score map itself. The two are independent and share no code but the decoders; installing one does not pull in the other.

## Usage

```ts
import { compare } from "@blazediff/ssim-native";

const result = await compare("expected.png", "actual.png", "map.png", {
  metric: "ms-ssim",
  minScore: 0.99,
});

if (result.match) {
  console.log(`close enough: ${result.score}`);
} else if (result.reason === "score-below-threshold") {
  console.log(`scored ${result.score}, ${result.belowCount} windows below the floor`);
}
```

`compare` takes two file paths or two encoded buffers (Node `Buffer` works directly). A third argument renders the local score map to that path as grayscale, dark where the score is low.

### Result

`SsimResult` is a discriminated union:

| `match` | `reason` | Carries |
| --- | --- | --- |
| `true` | — | `score`, `metric`, `mapWidth`, `mapHeight` |
| `false` | `"score-below-threshold"` | the above plus `belowCount`, `belowPercentage` |
| `false` | `"layout-diff"` | — the images are different sizes |
| `false` | `"file-not-exists"` | `file` |

`score` is pooled similarity in `0..=1`, where 1 is identical. `belowCount` counts map windows scoring under `minScore` — window counts, not pixel counts.

## Metrics

| Metric | What it does |
| --- | --- |
| `ssim` (default) | Gaussian-windowed single-scale SSIM, with the automatic downsample to ~256px on the short edge that MATLAB's `ssim.m` does |
| `ms-ssim` | SSIM pooled across a 5-octave dyadic pyramid, per `msssim.m`. Needs ≥176px on the short edge |
| `hitchhikers-ssim` | Box windows over five integral images, pooled by coefficient of variation (Venkataramanan et al. 2021). Every window sum is an O(1) summed-area-table lookup instead of ten 11-tap convolutions |
| `perceptual-ssim` | The tunable variant: CIE L\*a\*b\*, chroma weighting, chroma subsampling and MAD pooling, each an independent knob. At its defaults it reduces *bit-identically* to `ms-ssim`, which is what makes it usable as an ablation study rather than a second opinion |

## Raw RGBA

If you already have decoded pixels, skip the codec. These are synchronous:

```ts
import { msSsim, renderMap } from "@blazediff/ssim-native";

const result = msSsim(rgba1, rgba2, width, height, { returnMap: true });
const grayscale = renderMap(result.map!, result.mapWidth, result.mapHeight, width, height);
```

`ssim`, `msSsim`, `hitchhikersSsim` and `perceptualSsim` all take `(base, comparison, width, height, options?)`. Both buffers are read at the same dimensions, so there is no layout-difference case here — a buffer too short for `width * height` throws.

## Options

```ts
interface CompareOptions {
  metric?: "ssim" | "ms-ssim" | "hitchhikers-ssim" | "perceptual-ssim";
  minScore?: number;   // identical at or above this. Default: 1
  returnMap?: boolean; // include the Float32Array map. Default: false

  // shared by every metric
  windowSize?: number; // Default: 11
  k1?: number;         // Default: 0.01
  k2?: number;         // Default: 0.03
  bitDepth?: number;   // Default: 8, so L = 255

  msSsim?: { weights?: number[]; method?: "product" | "weighted-sum" };
  hitchhikers?: { windowStride?: number; covPooling?: boolean };
  perceptual?: {
    weights?: number[]; method?: "product" | "weighted-sum";
    color?: "gamma-luma" | "lab"; chromaWeight?: number; chromaSubsample?: number;
    pooling?: "mean" | "mad"; deviationWeight?: number;
  };

  compression?: number; // PNG level for a rendered map. Default: 0
  quality?: number;     // JPEG quality for a rendered map. Default: 90
}
```

> **Info:** The map is withheld unless `returnMap` is set — it is one float per window and costs a copy across the binding.

## Faster PNG decoding

Decoding is shared with [`@blazediff/core-native`](/apis/core-native) — both sit on the [`blazediff-shared`](/apis/blazediff-shared) crate — so the same opt-in applies here. Setting `BLAZEDIFF_PNG_ENABLED=1` routes PNG decode through the in-house [`blazediff-png`](/apis/blazediff-png) codec instead of libspng:

```bash
BLAZEDIFF_PNG_ENABLED=1 node compare.mjs
```

Worth roughly 15% off a 4K `compare()` call, with byte-identical decoded pixels and therefore an unchanged score. It is read once per process, and only affects the path and buffer APIs — the raw RGBA entry points never decode anything.

## Accuracy

The Rust implementation's tap-by-tap accumulation order is frozen to [`@blazediff/ssim`](/apis/ssim), the TypeScript port whose MATLAB agreement was measured, so this package *inherits* that agreement instead of drifting from it by an unmeasured amount. Both sides carry tests pinning the two ports to within 5e-6; SSIM lands within 0.03% of MATLAB across the fixture set.

> **Warning:** `ms-ssim` with the default `"product"` pooling returns `NaN` for globally anticorrelated content (an inverted image). Both references degenerate the same way — the JS gives `NaN`, MATLAB gives a complex number. `"weighted-sum"` stays finite throughout.

## Caveats

All three shipped metrics reduce to luma, so a change carried entirely by chroma or by alpha is invisible to them. `perceptual-ssim` with `color: "lab"` and a non-zero `chromaWeight` sees colour.

Scores are pooled over a local map, so these say *how much* two images differ, not *where* beyond the map's resolution. For exact locations, use [`@blazediff/core-native`](/apis/core-native).

Unlike `@blazediff/core-native` there is no CLI to fall back to — this package ships only the `.node`, so an unsupported platform throws rather than degrading.

## Platforms

macOS (arm64, x64), Linux (arm64, x64), Windows (arm64, x64).

## Links

- [npm](https://www.npmjs.com/package/@blazediff/ssim-native)
- [The Rust crate behind it](/apis/blazediff-ssim)
- [GitHub Repository](https://github.com/teimurjan/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()`.

`src1` and `src2` accept either image URLs or `Uint8Array` values containing encoded image bytes in every mode.

```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 \| Uint8Array`        | -          | URL or encoded bytes for the first image |
| `src2`             | `string \| Uint8Array`        | -          | URL or encoded bytes for 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 \| Uint8Array`                            | -       | URL or encoded bytes for the first image |
| `src2`                    | `string \| Uint8Array`                            | -       | URL or encoded bytes for 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 \| Uint8Array`       | -            | URL or encoded bytes for the base image |
| `src2`                     | `string \| Uint8Array`       | -            | URL or encoded bytes for 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, or render a precomputed diff without running BlazeDiff in the browser.

| Option               | Type                       | Default | Description                 |
| -------------------- | -------------------------- | ------- | --------------------------- |
| `src1`               | `string \| Uint8Array`      | -       | URL or encoded bytes for the first image |
| `src2`               | `string \| Uint8Array`      | -       | URL or encoded bytes for the second image |
| `diff`               | `Uint8Array`                | -       | Encoded precomputed diff image bytes |
| `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)}
    />
  );
}
```

All components accept image URLs or `Uint8Array` values containing encoded image bytes through `src1` and `src2`.

## API Reference

### `<SwipeMode />`

Interactive swipe component for comparing two images with a draggable divider.

#### Props

| Prop               | Type                              | Default | Description                          |
| ------------------ | --------------------------------- | ------- | ------------------------------------ |
| `src1`             | `string \| Uint8Array` (required) | -       | URL or encoded bytes for the first image |
| `src2`             | `string \| Uint8Array` (required) | -       | URL or encoded bytes for 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 \| Uint8Array` (required)                     | -       | URL or encoded bytes for the first image |
| `src2`                    | `string \| Uint8Array` (required)                     | -       | URL or encoded bytes for 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 \| Uint8Array` (required)                     | -       | URL or encoded bytes for the base image |
| `src2`                     | `string \| Uint8Array` (required)                     | -       | URL or encoded bytes for 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. Pass encoded diff image bytes to display a precomputed result without running BlazeDiff in the browser.

#### Props

| Prop                | Type                                                              | Default | Description                     |
| ------------------- | ----------------------------------------------------------------- | ------- | ------------------------------- |
| `src1`              | `string \| Uint8Array` (required)                                | -       | URL or encoded bytes for the first image |
| `src2`              | `string \| Uint8Array` (required)                                | -       | URL or encoded bytes for the second image |
| `diff`              | `Uint8Array`                                                     | -       | Encoded precomputed diff image bytes |
| `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)                                                   |

#### 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)             |

> **Info:** **Threshold guidelines:** - `0.0` - Exact match only - `0.05` - Strict comparison - `0.1` - Default balanced comparison - `0.2` - Lenient comparison

## 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 — is not part of the Python wheel. It lives in the
[`blazediff-interpret`](/apis/blazediff-interpret) crate, reachable from a
shell with its `blazediff-interpret` CLI:

```bash
blazediff-interpret expected.png actual.png --json
```

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 (separate crate and binary)
blazediff-interpret a.png b.png --json
```

### 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)
      --diff-color-alt <R,G,B> Alternative RGB color for darkening differences
  -c, --compression <LEVEL>    PNG compression level (0-9) [default: 0]
  -q, --quality <QUALITY>      JPEG quality (1-100) [default: 90]
      --output-format <FORMAT> Output format (json or text) [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. It is not part of this crate: it lives in
[`blazediff-interpret`](/apis/blazediff-interpret), which consumes what `diff`
returns and ships its own `blazediff-interpret` binary.

**Pipeline:** change mask -> morph close -> connected components -> noise census + fragment merge -> per-region evidence extraction -> classify -> shift pairing -> describe

**Six-label rule cascade:**

| Type             | Signal                                                                     |
| ---------------- | -------------------------------------------------------------------------- |
| `RenderingNoise` | Tiny (&lt;=25px) or sparse + low color delta, without an add/delete signature |
| `Addition`       | Blends with background in img1, distinct in img2                           |
| `Deletion`       | Distinct in img1, blends with background in img2                           |
| `ColorChange`    | Luminance structure preserved under a color shift, or a coherent chroma move (hue rotation, smooth delta field) over regenerated texture |
| `ContentChange`  | Fallback - structure replaced, chroma scattered                            |
| `Shift`          | Post-hoc: region pair whose before-crop and after-crop hold the same content, matched by patch correlation |

```sh
blazediff-interpret a.png b.png
blazediff-interpret a.png b.png diff.png --json
```

Severity tiers: `Low` (&lt;1%), `Medium` (1-10%), `High` (&gt;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-interpret

Structured region analysis for image diffs. Given two images and a set of changed regions, it says **what** changed in each one — not just where.

## Installation

```toml
# Cargo.toml
[dependencies]
blazediff-interpret = "5.4.0"
```

The crate name is `blazediff-interpret`; the library imports as `blazediff_interpret`.

## Why it's a separate crate

The classifier is deliberately independent of whatever *found* the regions. Three producers feed it:

| Producer | `ChangeSource` | How it finds regions |
| --- | --- | --- |
| [`blazediff`](/apis/rust) | `Diff` | connected components over a pixel-diff mask |
| [`blazediff-ssim`](/apis/blazediff-ssim) | `ScoreMap` | thresholding a local SSIM score map |
| your code | `Regions` | DOM rectangles, a JS-side diff, a crop list — anything |

All three call the same function and get identical treatment; only the description of *where* differs. `blazediff` and `blazediff-ssim` are independent of each other, so a classifier living in either would be unreachable from the other. It sits below both, on [`blazediff-shared`](/apis/blazediff-shared).

## Usage

```rust
use blazediff_interpret::{interpret, ChangeSource};

// From a pixel diff — what `blazediff` does.
let result = interpret(&expected, &actual, ChangeSource::Diff {
    output: &diff_image.data,
    diff_count: diff.diff_count,
    diff_percentage: diff.diff_percentage,
})?;

// From a similarity map — what `blazediff-ssim` does.
let result = interpret(&expected, &actual, ChangeSource::ScoreMap {
    map: &outcome.map,
    width: outcome.map_width,
    height: outcome.map_height,
    floor: 0.99,
})?;

// From boxes you already have.
let result = interpret(&expected, &actual, ChangeSource::Regions(&boxes))?;

println!("{}", result.summary);
for region in &result.regions {
    println!("{:?} at {} ({:.2}%)", region.change_type, region.position, region.percentage);
}
```

## Coarse regions are fine

A producer only has to know roughly where something changed. Before any statistic is computed, each supplied box is refined against the source pixels — every pixel whose YIQ delta falls below the noise floor is dropped — so shape, colour and gradient analysis stay per-pixel no matter how blocky the input was. (If a claimed box refines to nothing but the content does differ — a sub-threshold edit such as a subtle uniform recolor — the box is kept as-is so the region still gets meaningful statistics.)

```rust
// An 8x8 change, described exactly and then quantized to a 16px grid.
let exact  = interpret(&a, &b, ChangeSource::Regions(&[BoundingBox { x: 16, y: 16, width: 8,  height: 8  }]))?;
let coarse = interpret(&a, &b, ChangeSource::Regions(&[BoundingBox { x: 16, y: 16, width: 16, height: 16 }]))?;
assert_eq!(coarse.diff_count, exact.diff_count); // both 64
```

That is what makes an SSIM window map a usable region source: its grid is coarse, but the statistics derived from it are not.

> **Info:** `diff_count` therefore means the same thing on every path — actually-changed pixels, never windows. On a real fixture pair, SSIM-located regions report 792 changed pixels where the exact pixel diff reports 776.

## API

| Item | Purpose |
| --- | --- |
| `interpret` | the entry point: a `ChangeSource` in, a full `InterpretResult` out |
| `ChangeSource` | `Diff` (a pixel diff's output + counts), `ScoreMap` (a similarity map), or `Regions` |
| `regions_from_score_map` | threshold a lower-resolution score map into image-space regions |
| `classify_region` / `classify_regions` | classify against a mask you already hold |
| `detect_regions` | connected components over a boolean mask |
| `merge_overlapping_components` | fuse fragmented components whose bboxes overlap or nearly touch |
| `extract_change_mask` | recover a mask from an RGBA diff visualization |
| `detect_shifts` | the shift-relabeling pass, for producers holding an exact mask |
| `classify_severity`, `build_summary` | the pooling steps, for custom pipelines |

> **Warning:** Regions arriving from a caller are validated: a box outside the image is an `InterpretError::RegionOutOfBounds`, not an out-of-bounds panic. That matters now that regions cross the wasm and N-API boundaries.

## From JavaScript

One package wraps all of it — [`@blazediff/interpret-native`](https://www.npmjs.com/package/@blazediff/interpret-native):

```ts
import { interpret, interpretRegions } from "@blazediff/interpret-native";

// Regions from a pixel diff (default), or from a similarity map.
const exact = await interpret("expected.png", "actual.png");
const loose = await interpret("expected.png", "actual.png", undefined, { source: "ms-ssim" });

// Regions you already know about.
const given = await interpretRegions("expected.png", "actual.png", [
  { x: 16, y: 16, width: 32, height: 32 },
]);
```

## What it classifies

Each region gets a change type, a shape, a position, a confidence, and the statistics behind them — colour delta, gradient/edge correlation, luminance correlation, chroma-plane movement (hue rotation, saturation, delta smoothness), fill ratios, and the signals the classifier used. See [INTERPRET.md](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff/INTERPRET.md) for the full algorithm.

## Links

- [Crates.io](https://crates.io/crates/blazediff-interpret)
- [Crate README](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-interpret/README.md)
- [GitHub Repository](https://github.com/teimurjan/blazediff)

---

# blazediff-shared

The primitives every BlazeDiff crate sits on: the RGBA8 `Image` buffer, **YIQ color math**, and **PNG, JPEG and QOI** decode and encode — normalized to one representation so the compute crates never see a codec.

## Installation

```toml
# Cargo.toml
[dependencies]
blazediff-shared = "5.4.0"
```

The crate name is `blazediff-shared`; the library imports as `blazediff_shared`.

## Why it exists

The crates above it form a chain — [`blazediff`](/apis/rust) depends on [`blazediff-ssim`](/apis/blazediff-ssim), which depends on `blazediff-interpret` — so anything two of them share has to live below all of them. Two things qualify: **image I/O**, because everyone needs pixels and nobody wants to be a codec, and **YIQ color math**, because both the pixel diff and the region classifier measure perceptual distance and now sit in different crates.

Keeping I/O here also collapses the format dispatch to one copy. It used to be pasted separately into the CLI, the N-API binding and the Python extension — three places to forget when adding a format.

## Usage

```rust
use blazediff_shared::{load_image_pair, save_image, Image, ImageFormat};

let (a, b) = load_image_pair("expected.png", "actual.jpg")?;
println!("{}x{}", a.width, a.height);

let out = Image::new(a.width, a.height);
save_image(&out, "diff.png", /* compression */ 0, /* quality */ 90)?;
```

Format comes from the file extension for paths and from the magic bytes for buffers:

```rust
use blazediff_shared::{decode_image, ImageFormat};

assert_eq!(ImageFormat::from_path("a.JPEG"), Some(ImageFormat::Jpeg));
assert_eq!(ImageFormat::from_bytes(b"qoif...."), Some(ImageFormat::Qoi));

let image = decode_image(&encoded_bytes)?;
```

## API

| Item | Purpose |
| --- | --- |
| `Image` | RGBA8 buffer plus dimensions, with `as_u32` / `get_pixel` / `set_pixel` helpers |
| `ImageError` | `Io`, `Png`, `Jpeg`, `Qoi`, `UnsupportedFormat` |
| `ImageFormat` | `from_path`, `from_bytes`, `as_str` |
| `load_image`, `load_image_pair` | path in, format auto-detected; the pair loads in parallel |
| `decode_image`, `decode_image_pair` | encoded bytes in, format sniffed from magic bytes |
| `save_image` | format from the output extension |
| `load_png` … `save_qoi` | the per-codec entry points, when you already know the format |
| `yiq::color_delta` | squared YIQ distance between two packed pixels — the perceptual metric behind both the diff and the region classifier |
| `yiq::{unpack_pixel, pack_pixel, is_opaque}` | packed-`u32` pixel helpers |

The per-codec modules (`png_io`, `jpeg_io`, `qoi_io`) are public too, for callers that want to skip detection.

## Codecs

- **PNG** — vendored [libspng](https://github.com/randy408/libspng), compiled with its SIMD paths. Setting `BLAZEDIFF_PNG_ENABLED` to a truthy value routes decode and level-0 encode through the in-house [`blazediff-png`](/apis/blazediff-png) codec instead, with spng staying as a defensive fallback.
- **JPEG** — vendored [libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo) via the TurboJPEG API.
- **QOI** — [`qoi-rust`](https://crates.io/crates/qoi), pure Rust.

> **Info:** Adler-32 verification stays on for PNG decode. These entry points read arbitrary, possibly untrusted files, so a corrupt zlib stream must error rather than hand back wrong pixels.

## Features

- **`codecs`** (default) — everything above. Needs a C toolchain and cmake.
- Without it the crate is pure Rust and compiles to `wasm32`, leaving only `Image`, `ImageError` and `ImageFormat`. That is what the wasm build of `blazediff` links.
- **`fuzzing`** — internal only; exposes the spng reference decoder for `blazediff-png`'s differential tests.

## Error messages are contract

`ImageError`'s `Display` strings are surfaced verbatim by the CLI, the N-API binding, the Python extension and the JS wrappers, and `@blazediff/core-native` pattern-matches on them to tell a missing file from a malformed one. Changing their wording is a breaking change.

## Links

- [Crates.io](https://crates.io/crates/blazediff-shared)
- [Crate README](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-shared/README.md)
- [GitHub Repository](https://github.com/teimurjan/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 |
| Real-image corpus | Urban100 · BSD100 · Set14 · Set5 + PngSuite (~395 files) decode byte-identically to spng at RGBA8 and every `SPNG_FMT_*`; every accepted image encode-round-trips — run by the `Benchmark PNG` workflow (`corpus_differential`) |
| 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)

---

# blazediff-ssim

Structural-similarity metrics in Rust: **SSIM, MS-SSIM and Hitchhiker's SSIM, vectorised through one lane-generic SIMD layer and held to the reference MATLAB scripts through Octave**. No dependencies, no threads, no runtime dispatch. It powers the `--metric` family in the BlazeDiff Rust crate.

[View the dssim comparison and quality harness](https://github.com/teimurjan/blazediff/tree/main/crates/blazediff-ssim-benchmark)

## Installation

```toml
# Cargo.toml
[dependencies]
blazediff-ssim = "5.4.0"
```

The crate name is `blazediff-ssim`; the library imports as `blazediff_ssim`. Crate sources are available on [crates.io](https://crates.io/crates/blazediff-ssim).

## Why a separate crate

A per-pixel diff answers "which pixels changed", which is the right question until anti-aliasing, font hinting or a codec's rounding moves a few thousand pixels by one unit and the run goes red for no reason a human would call a change. SSIM answers the other question: how alike do these look.

The catch is that "SSIM" names a family whose members disagree by more than their published formulas suggest, because the reference implementations differ in window placement, boundary handling and downsampling. The contract here is not "we implemented the paper", it is **land where MATLAB lands, to a stated tolerance, and keep landing there**.

## Features

- **`ssim`**: Gaussian-windowed single-scale SSIM in `'valid'` mode, with the automatic downsampling to ~256px on the short edge that MATLAB's `ssim.m` does.
- **`ms_ssim`**: SSIM pooled across a 5-octave dyadic pyramid, per `msssim.m`. Product or weighted-sum pooling. Needs at least 176px on the short edge for the default five scales.
- **`hitchhikers_ssim`**: box windows over five integral images, pooled by coefficient of variation (Venkataramanan et al. 2021). Every window sum is an O(1) summed-area-table lookup instead of ten 11-tap convolutions.
- **`perceptual_ssim`**: the tunable variant. CIE L\*a\*b\*, chroma weighting, chroma subsampling and mean-absolute-deviation pooling, each an independent knob. With `PerceptualOptions::default()` it reduces *bit-identically* to `ms_ssim`, which is what makes it usable as an ablation study rather than a second opinion.
- **Zero dependencies**: std only, so it compiles for every target `blazediff` does, wasm32 included, with no feature negotiation.

## Performance

Wall-clock on a 4K pair, decode included (decode is ~200 ms of each):

| Metric | 4K pair | Why |
| --- | --- | --- |
| `ssim` | 320 ms | MATLAB's automatic downsample shrinks the plane to ~256px before any convolution runs |
| `hitchhikers-ssim` | 380 ms | full resolution, but O(1) window sums |
| `ms-ssim` | 480 ms | full resolution at the finest of five scales |

Against single-threaded [dssim](https://github.com/kornelski/dssim), `ms_ssim` runs about **1.9×** faster. Two things bought that, neither of them threads:

**One fused statistics pass.** A scale needs five moments (µ1, µ2, σ1², σ2², σ12), which the textbook pipeline computes as eleven full-size intermediates. The streaming kernel computes them in a single pass through a row ring buffer instead, bit-identically to the unfused path by construction.

**Compile-time lane selection.** Five kernel shapes carry nearly all the time, so each is written once against a `SimdF32` trait and instantiated per ISA: NEON on aarch64, SSE2 on x86_64, simd128 on wasm32, a scalar fallback elsewhere. All are baseline for their target, so nothing dispatches inside a hot loop.

## Library Usage

```rust
use blazediff_ssim::{ms_ssim, MsSsimOptions, Plane, Rgba8, SsimOptions};

let plane1 = Plane::from_rgba8(Rgba8::new(&rgba1, width, height))?;
let plane2 = Plane::from_rgba8(Rgba8::new(&rgba2, width, height))?;

let outcome = ms_ssim(
    &plane1,
    &plane2,
    &SsimOptions::default(),
    &MsSsimOptions::default(),
)?;
println!("{:.6}", outcome.score); // 1.0 means identical
```

`Rgba8` is a borrowed view, so nothing is copied to call in. Decoding is the caller's problem: the crate takes RGBA8 bytes and has no I/O.

### Types

```rust
pub struct Rgba8<'a> {
    pub data: &'a [u8],   // RGBA8, 4 bytes per pixel, row-major
    pub width: usize,
    pub height: usize,
}

pub struct Plane {
    pub samples: Vec<f32>, // luma, MATLAB rgb2gray weights
    pub width: usize,
    pub height: usize,
}

pub struct SsimOptions {
    pub window_size: usize, // default 11
    pub k1: f64,            // default 0.01
    pub k2: f64,            // default 0.03
    pub bit_depth: u32,     // default 8, sets L = 2^bit_depth - 1
}

pub struct SsimOutcome {
    pub score: f64,        // pooled, 1.0 = identical
    pub map: Vec<f32>,     // per-window scores, row-major
    pub map_width: usize,
    pub map_height: usize,
}
```

### API

| Function | Purpose |
| --- | --- |
| `ssim` | single-scale SSIM with MATLAB's automatic downsampling |
| `ms_ssim` | SSIM pooled over a 5-octave pyramid |
| `hitchhikers_ssim` | box windows over integral images, CoV pooling |
| `perceptual_ssim` | tunable Lab / chroma / MAD variant, takes `Rgba8` |
| `render_map` | paint a local map into an RGBA8 buffer as grayscale |

> **Info:** Every metric returns `Err(SsimError)` rather than panicking on a mismatched or truncated pair. `SsimError` carries the same messages the BlazeDiff CLI, N-API, Python and wasm front-ends print.

## Bit-exactness is the constraint, not an outcome

Tap-by-tap accumulation order is frozen to the [`@blazediff/ssim`](/apis/ssim) TypeScript port. That is a deliberate handcuff: the JS port is the one whose MATLAB agreement was measured, so matching its order means this crate *inherits* that agreement instead of drifting away from it by an unmeasured amount. Anything that would reassociate the sums, including some obvious-looking vectorisations, is out of bounds even when it is faster.

Two consequences worth knowing about: `cube_root` replaces `cbrtf` in the Lab conversion and is checked against libm across the whole L\*a\*b\* domain; and FMA is used inside the vector body but deliberately *not* in the scalar tail, because the reference does not fuse either.

> **Warning:** `MsSsimMethod::Product` returns `NaN` when a scale's mean contrast-structure term goes negative. That takes globally anticorrelated content (an inverted image) rather than ordinary degradation, and both references degenerate the same way: the JS gives `NaN`, MATLAB gives a complex number. `MsSsimMethod::WeightedSum` stays finite throughout.

## Verified

| Layer | Result |
| --- | --- |
| MATLAB `ssim.m` | within **0.01%** on three fixture pairs, **0.05%** on the one where downsampling by 5 costs the most precision |
| MATLAB `msssim.m` | within **0.05** absolute. The reference pools `'valid'` statistics where both ports pool symmetric `'same'`, so the gap is algorithmic, not numerical |
| TypeScript port | all three metrics agree to within **5e-6**, the only cross-port pin for `hitchhikers-ssim`, which has no MATLAB reference |
| Fused statistics | bit-identical to the unfused eleven-buffer pipeline |
| `cube_root` | exhaustive over ~67M f32 values across the Lab domain |
| Unit + integration tests | 55 + 4 |

The MATLAB half shells out to Octave and reports a skip when Octave is missing, so the default `cargo test` needs no toolchain beyond Rust. Set `BLAZEDIFF_REQUIRE_OCTAVE=1` to turn a missing Octave into a failure so parity cannot pass vacuously.

## Caveats

All three shipped metrics reduce to luma, so a change carried entirely by chroma or by alpha is invisible to them. `perceptual_ssim` with `ColorSpace::Lab` and a non-zero `chroma_weight` sees colour.

Scores are pooled over a local map, so these metrics say *how much* two images differ, not *where* beyond the resolution of that map. For exact locations, use a pixel diff.

## Links

- [Crates.io](https://crates.io/crates/blazediff-ssim)
- [Crate README](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-ssim/README.md)
- [dssim comparison and quality harness](https://github.com/teimurjan/blazediff/tree/main/crates/blazediff-ssim-benchmark)
- [GitHub Repository](https://github.com/teimurjan/blazediff)

---

# Docs

# Introduction

**BlazeDiff compares two images and tells you what changed, fast enough to run on
every test.** It is a set of MIT-licensed libraries, not a service. No account, no
API key, no per-snapshot billing, and no screenshot leaves the machine that took
it.

Most visual regression setups stop at one number, "N pixels differ", and leave a
person to decide whether that number matters. BlazeDiff goes two steps past that.
It classifies each changed region (something appeared, something moved, something
was recolored), and when the classification is not confident it hands the cropped
regions to a coding agent you already run, which answers regression or intentional
with a reason.

## How the pieces fit

Four layers. Each one is useful on its own, and each one feeds the next.

| Layer         | Question it answers                | Packages                                       |
| ------------- | ---------------------------------- | ---------------------------------------------- |
| **Compare**   | Which pixels differ?               | `core`, `core-wasm`, `core-native`              |
| **Score**     | How different does it look?        | `ssim`, `gmsd`, `ssim-native`                   |
| **Interpret** | What changed, where, and how badly?| `interpret-native`                              |
| **Judge**     | Regression or intentional?         | `agent`                                         |

Around those sit the surfaces you actually touch: matchers for Jest, Vitest and
Bun, a CLI, and React and vanilla components for showing a diff to a person.

`@blazediff/object` diffs JavaScript objects rather than images. It shares the
name and the performance work, not the pipeline. It lives in
[Object Comparison](/docs/object-comparison).

## Which package do I install?

| You want to                                     | Install                                   | Runs in                            |
| ------------------------------------------------ | ----------------------------------------- | ---------------------------------- |
| Count changed pixels in a Node test or CI job     | `@blazediff/core-native`                  | Node                               |
| Count changed pixels in a browser or edge runtime | `@blazediff/core-wasm`                    | Browsers, Workers, Deno, Bun, edge |
| Avoid native binaries and wasm entirely           | `@blazediff/core`                         | Anywhere JS runs                   |
| Score perceived similarity instead of pixels      | `@blazediff/ssim`, `@blazediff/gmsd`      | Anywhere JS runs                   |
| Know what changed and where, not just how much    | `@blazediff/interpret-native`             | Node                               |
| Assert a screenshot inside a test                 | `@blazediff/jest`, `/vitest`, `/bun`      | Your test runner                   |
| Run visual regression over your routes            | `@blazediff/agent`                        | Node plus bundled Chromium         |
| Show a before/after diff in a UI                  | `@blazediff/react`, `@blazediff/ui`       | Browsers                           |
| Diff two JavaScript objects                       | `@blazediff/object`                       | Anywhere JS runs                   |

Not sure between an exact pixel count and a similarity score? Start with pixels
and read [Choosing a Metric](/docs/metrics/choosing-a-metric) when exact matching
turns out to be too strict.

## Your first diff

```bash
npm install @blazediff/core-native
```

```ts
import { compare } from "@blazediff/core-native";

const result = await compare("baseline.png", "current.png", "diff.png");

if (result.match) {
  console.log("identical");
} else if (result.reason === "layout-diff") {
  console.log("dimensions differ");
} else {
  console.log(`${result.diffCount} pixels differ (${result.diffPercentage.toFixed(2)}%)`);
}
```

`compare` takes file paths or encoded buffers, decodes PNG, JPEG and QOI itself,
and writes the diff image when you pass an output path. The same call in the
browser is [WebAssembly](/docs/pixel-comparison/rust-wasm); the same call with no
binary at all is [Vanilla JavaScript](/docs/pixel-comparison/vanilla-javascript).

### Inside a test

Where the PNG came from does not matter. Playwright, Puppeteer, Cypress, a
headless renderer, or a file already on disk all work, because the input is just
pixels.

```ts
import { expect, it } from "vitest";
import "@blazediff/vitest"; // or @blazediff/jest, @blazediff/bun

it("renders the pricing page", async () => {
  // core-native is the fastest method and reads file paths, so write the
  // screenshot to disk first.
  const shot = "screenshots/pricing.png";
  await page.screenshot({ path: shot });

  await expect(shot).toMatchImageSnapshot({
    method: "core-native",
    failureThreshold: 0.1,
    failureThresholdType: "percent",
  });
});
```

The matcher registers itself on import. `method` picks the algorithm, so the same
assertion can run a pixel diff, `ssim`, `msssim`, `hitchhikers-ssim` or `gmsd`.
Every method except `core-native` also accepts an in-memory buffer, which is what
`core` (the default) is for.

## How fast, exactly

Every number on this site comes from the benchmark suite in the repo, run on an
Apple M1 Max with Node 22. The 4K rows below are the three `4k/*` fixtures, so
they are ranges rather than single numbers.

| Comparison                           | Method                   | Baseline          | BlazeDiff       |
| ------------------------------------ | ------------------------ | ----------------- | --------------- |
| `@blazediff/core` vs pixelmatch      | 4K pair, decode excluded | 201.60-253.77ms   | 96.52-135.89ms  |
| `@blazediff/core-wasm` vs pixelmatch | 4K pair, decode excluded | 332.26-423.14ms   | 33.18-68.37ms   |
| `@blazediff/core-native` vs odiff    | 4K pair, decode included | 1157.12-1677.13ms | 288.01-349.43ms |

Averaged over the full fixture set rather than the 4K pairs: `@blazediff/ssim` is
~25% faster than ssim.js and the Hitchhiker's variant ~70% faster, and
`@blazediff/object` is ~55% faster than microdiff.

Per-fixture tables, iteration counts and methodology notes:
[Benchmarks](/benchmarks/pixel-by-pixel).

> **Info:** Identical images are the common case in a passing suite, and every core has a fast path for them. That is where the largest wins are: the pure-JS core is 7x to 9x faster than pixelmatch on an unchanged 4K pair.

## Where to go next

**Comparing images**

- [Pixel-by-pixel comparison](/docs/pixel-comparison/vanilla-javascript) in JS,
  WebAssembly, or native Node
- [Structural comparison](/docs/structural-comparison) with SSIM and GMSD
- [Choosing a metric](/docs/metrics/choosing-a-metric) when you are not sure which

**Going past a pixel count**

- [Image difference analysis](/docs/difference-analysis): regions, change types,
  severity
- [Agentic visual testing](/docs/agentic-testing/setting-up): capture, check, and
  let a coding agent judge what the thresholds could not

**Everything else**

- [UI components](/docs/ui-components/react) for showing a diff
- [Object comparison](/docs/object-comparison)
- [Guides](/guides/claude-code-visual-review) for task-shaped walkthroughs
- [API reference](/apis/core) for exact signatures and options

---

# Pixel-by-pixel Comparison in Vanilla JavaScript

**`@blazediff/core` counts the pixels that differ between two images, in pure
JavaScript.** No native binary, no WebAssembly, no install step beyond `npm
install`. It runs anywhere JS runs and it is API-compatible with
[pixelmatch](https://github.com/mapbox/pixelmatch), so switching is a one-line

Reach for it when portability matters more than raw throughput: a browser bundle,
a Deno script, a serverless function where you would rather not ship a binary.
When throughput is what matters, the same algorithm is available compiled:
[WebAssembly](/docs/pixel-comparison/rust-wasm) for the browser and the edge,
[Node native](/docs/pixel-comparison/rust-napi) for CI.

## Installation

```bash
npm install @blazediff/core
```

## Examples

You supply decoded RGBA data. `loadImage` below is whatever decoder your runtime
already has: a canvas in the browser, `@blazediff/codec-pngjs` or `sharp` on the
server.

```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;

// Returns the number of differing pixels and fills `output` with the diff image.
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;

// threshold is a per-pixel color distance from 0 (exact) to 1 (anything goes).
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;

// Recolor the diff output: anti-aliased pixels red, real differences green.
const diff = blazediff(img1, img2, output, width, height, {
  aaColor: [255, 0, 0],
  diffColor: [0, 255, 0],
});
```

Pass `null` instead of an output buffer when you only need the count. That skips
writing the diff image, which is the more expensive half of the work.

## What it costs

Measured against pixelmatch on the same fixtures, decode excluded, M1 Max, Node 22,
50 iterations:

| Case                          | pixelmatch      | `@blazediff/core` |
| ----------------------------- | --------------- | ----------------- |
| 4K pair, count only           | 201.60-253.77ms | 96.52-135.89ms    |
| 4K pair, identical            | 23.79-28.04ms   | 2.50-3.77ms       |
| 4K pair, writing a diff image | 280.79-336.93ms | 151.72-205.21ms   |

Averaged over the whole fixture set that is ~62% faster when counting and ~28%
faster when also rendering a diff image. Small images narrow the gap further: the
win comes from skipping unchanged blocks, and there are fewer of them to skip.
[Full tables](/benchmarks/pixel-by-pixel).

## Next

- Same algorithm, 5x to 10x faster on 4K:
  [WebAssembly](/docs/pixel-comparison/rust-wasm) or
  [Node native](/docs/pixel-comparison/rust-napi)
- Exact matching too strict? [Structural comparison](/docs/structural-comparison)
- Every option and its default: [`@blazediff/core` reference](/apis/core)

---

# Pixel-by-pixel Comparison with WebAssembly

**`@blazediff/core-wasm` is the Rust diff core compiled to `wasm32` with `v128`
SIMD, in about 32KB.** It runs in browsers, Web Workers, Deno, Bun, Cloudflare
Workers, and any other wasm host, with no native dependency and no network call.

Reach for it when you want compiled-code speed on the client or at the edge. In
Node, [the native binding](/docs/pixel-comparison/rust-napi) is faster still and
decodes images for you. In an environment where you cannot ship a wasm file at
all, use [the pure-JS core](/docs/pixel-comparison/vanilla-javascript).

## Installation

```bash
npm install @blazediff/core-wasm
```

## Decoding images to RGBA

The wasm module takes pre-decoded RGBA and does not bundle an image decoder, so
the browser's own decoder does that half:

```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,
});
```

`initBlazediff` resolves the sibling `.wasm` by default. Loading it from a CDN, a
bundler, or the filesystem instead? It also accepts a `URL`, a `Response`, or raw
bytes.

## What it costs

Against pixelmatch on the same fixtures, decode excluded, M1 Max, Node 22, 25
iterations:

| Case               | pixelmatch      | `@blazediff/core-wasm` |
| ------------------ | --------------- | ---------------------- |
| 4K pair            | 332.26-423.14ms | 33.18-68.37ms          |
| 4K pair, identical | 19.86-30.53ms   | 17.47-22.17ms          |

So 5x to 10x on a changed 4K pair, and ~51% faster averaged over the whole
fixture set. Identical images are the weak spot: `wasm32-unknown-unknown` has no
libc, so the byte-equality shortcut the other cores use lowers to a scalar memcmp
and is skipped here. The block scan reaches the same answer with `v128` compares,
which is why that row is close rather than behind.
[Full tables and the history of that fix](/benchmarks/pixel-by-pixel).

## Next

- Region-level classification runs in wasm too:
  [Image difference analysis](/docs/difference-analysis)
- In Node, prefer [the native binding](/docs/pixel-comparison/rust-napi)
- Every option and its default:
  [`@blazediff/core-wasm` reference](/apis/core-wasm)

---

# Pixel-by-pixel Comparison with Node Native

**`@blazediff/core-native` is the fastest way to diff two images from Node.** It
is the Rust core compiled to a Node addon through N-API, with SIMD in both passes.
Unlike the JS and wasm cores it also handles the file layer: it accepts paths or
encoded buffers, decodes PNG, JPEG and QOI itself, and writes the diff image to
disk for you.

Reach for it in test runners and CI, which is where the decode cost and the
per-run overhead actually add up. It needs a prebuilt binary for the platform, so
it is not an option in browsers or on the edge. That is what
[the wasm build](/docs/pixel-comparison/rust-wasm) is for.

## Installation

```bash
npm install @blazediff/core-native
```

Platform binaries ship as optional dependencies for macOS, Linux and Windows on
x64 and arm64. There is no build step and no `node-gyp`.

## 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";

// Third argument is the diff output path; pass undefined to skip writing one.
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 native call, so the JavaScript bytes
are never copied into Rust. Decoding still allocates native RGBA buffers, and on a
4K pair that decode is the larger share of the total.

## What it costs

Against [odiff](https://github.com/dmtrKovalenko/odiff) on the same fixtures with
image IO included, measured with hyperfine on an M1 Max, 25 runs:

| Case               | odiff             | `@blazediff/core-native` |
| ------------------ | ----------------- | ------------------------ |
| 4K pair            | 1157.12-1677.13ms | 288.01-349.43ms          |
| 4K pair, identical | 269.36-366.79ms   | 183.94-230.29ms          |

That is 4x to 4.8x on a changed 4K pair. Because decode dominates end-to-end time,
passing already-decoded buffers moves the number further than any change to the
diff kernel would. [Full tables](/benchmarks/pixel-by-pixel).

## Next

- Want a verdict on *what* changed rather than how many pixels? Reach for
  [`@blazediff/interpret-native`](/apis/interpret-native) or read
  [Image difference analysis](/docs/difference-analysis)
- Wiring this into Jest, Vitest or Bun:
  [`@blazediff/vitest` reference](/apis/vitest)
- Every option and its default:
  [`@blazediff/core-native` reference](/apis/core-native)

---

# Structural Image Comparison

**Pixel diffing answers "how many pixels changed". Structural metrics answer "how
different does this look".** They score a neighborhood rather than a pixel, so
compression artifacts and sub-pixel rendering noise barely move them while a real
change does.

Reach for one when exact matching is too strict: screenshots that get re-encoded
somewhere in the pipeline, or the same page rendered by two different machines.
Keep a pixel diff in the pipeline too, because both metrics work on luminance and
are close to blind to color-only changes.

BlazeDiff ships two: **GMSD**, which compares edges and is the cheaper of the two,
and **SSIM**, the classic structural index, with a faster Hitchhiker's variant.

New to these? Read [what SSIM measures](/docs/metrics/ssim),
[how GMSD works](/docs/metrics/gmsd), or
[which one to pick](/docs/metrics/choosing-a-metric).

## 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` to `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;

    // Raising c makes the metric more forgiving in low-contrast areas.
    const score = gmsd(img1, img2, undefined, width, height, { c: 100 });
    ```

Pass an output buffer as the third argument to get the similarity map back as a
grayscale image, which shows which edges disagreed.
[GMSD reference](/apis/gmsd).

## SSIM (Structural Similarity Index)

Scores luminance, contrast and structure. Returns `1` for identical images.
**Higher is better**, in the `0` to `1` range. The Hitchhiker's variant swaps the
Gaussian window for non-overlapping rectangular windows over integral images,
which runs 2x to 5x faster depending on the image 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);
```

The output buffer here receives the SSIM map: dark areas are where the two images
disagree. `@blazediff/ssim` also ships MS-SSIM, which scores at five scales for
images that get viewed at more than one size.
[SSIM reference](/apis/ssim).

## Picking a threshold

Do not copy a number from a table. Run your own baselines twice with no code
change, see what score the noise alone produces, then set the gate above it. As
starting points: `0.05` for GMSD when you control the render, `0.15` when
screenshots pass through lossy compression, and `0.98` for SSIM on full-page
shots.

## Next

- [Choosing a metric](/docs/metrics/choosing-a-metric) if you are still deciding
- [What SSIM measures](/docs/metrics/ssim) and
  [how GMSD works](/docs/metrics/gmsd) for the formulas
- [Pixel-by-pixel comparison](/docs/pixel-comparison/vanilla-javascript) when you
  want the exact count instead

---

# Choosing a Metric

**Start with pixel diffing. Move to a structural metric only when exact matching
is too strict.** Pixel diffing is the fastest and the only one that tells you
exactly what changed. SSIM and GMSD trade that detail for tolerance: they score
how different two images *look*, so they shrug off compression artifacts and
rendering noise that pixel counting flags as failures.

## The five options

| Metric                | Answers                        | Output              | Direction         | Relative cost | Best for                                    |
| --------------------- | ------------------------------ | ------------------- | ----------------- | ------------- | ------------------------------------------- |
| **Pixel diff**        | How many pixels changed        | Count and percent   | Lower is better   | Fastest       | The default. CI gates, exact rendering       |
| **GMSD**              | Did the edges change           | Score, `0` to `~1`  | Lower is better   | Low           | Layout and shape changes, compressed images  |
| **SSIM**              | Does it look different         | Score, `0` to `1`   | Higher is better  | Medium        | Perceived quality, matching published results |
| **MS-SSIM**           | Does it look different at any size | Score, `0` to `1` | Higher is better | High          | Images viewed at several resolutions         |
| **Hitchhiker's SSIM** | Same as SSIM, faster           | Score, `0` to `1`   | Higher is better  | Low           | Large batches where SSIM is the bottleneck   |

Read the details: [what SSIM measures](/docs/metrics/ssim) ·
[how GMSD works](/docs/metrics/gmsd).

## Pick by problem

| Your problem                                                | Use                                              |
| ----------------------------------------------------------- | ------------------------------------------------ |
| Standard CI screenshot gate                                  | Pixel diff (`core-native`) with `threshold: 0.1`  |
| Failures from anti-aliasing on text and edges                | Pixel diff with `antialiasing: true` first, then GMSD |
| Screenshots re-encoded as JPEG somewhere in the pipeline     | GMSD, gate around `0.15`                          |
| Same page rendered on macOS locally and Linux in CI          | Pixel diff with a percentage threshold, then GMSD |
| A "did the design get worse" score for a report              | SSIM or MS-SSIM                                   |
| Thousands of images per run and SSIM is too slow             | Hitchhiker's SSIM                                 |
| A brand color changed                                        | Pixel diff. SSIM and GMSD are near-blind to it    |
| You need to know *what* changed, not just how much           | [Interpret mode](/docs/difference-analysis)       |

> **Warning:** **Both structural metrics are weak on color.** SSIM and GMSD work on luminance. Two colors with the same brightness can swap and barely move either score. If color correctness matters, keep a pixel diff in the pipeline.

## Structural metrics instead of RGB thresholds

A raw RGB threshold asks one question per pixel: is this channel more than N away
from the baseline. That has no idea whether the pixel sits on the edge of a
letter, inside a gradient, or in the middle of a flat background. So the noise you
want to ignore and the change you want to catch look the same to it, and the only
knob you have is "be less strict everywhere".

Structural metrics score a neighborhood instead of a pixel. SSIM compares the
brightness, contrast, and structure of a window; GMSD compares the edge strength
around each pixel. Both stay stable when a render is subtly noisy and both react
when the shape of something actually changes - which is the distinction an RGB
threshold cannot express.

The practical setup for a noisy pipeline:

```ts
import { compare } from "@blazediff/core-native";
import gmsd from "@blazediff/gmsd";

// 1. Cheap exact gate. Most runs stop here.
const result = await compare("baseline.png", "current.png", "diff.png", {
  threshold: 0.1,
  antialiasing: true,
});
if (result.match) return "pass";

// 2. Something changed. Is it perceptible?
const score = gmsd(baselinePixels, currentPixels, undefined, width, height);
if (score < 0.05) return "pass-with-noise";

return "fail";
```

## Setting a threshold

Do not copy a number from a table. Run your own baselines twice with no code
change and see what score the noise alone produces, then set the gate above it.

| Comparison           | Knob                                             | Sane starting point         |
| -------------------- | ------------------------------------------------ | --------------------------- |
| Pixel diff           | `threshold` (per-pixel color distance, `0`-`1`)  | `0.1`                       |
| Pixel diff           | `failureThreshold` + `failureThresholdType`      | `0.1` with `'percent'`      |
| GMSD                 | Gate on the returned score                       | `0.05`, or `0.15` if compressed |
| SSIM / MS-SSIM       | Gate on the returned score                       | `0.98` for full-page shots  |

Percentage thresholds travel better than pixel counts, because a pixel budget
tuned on a 1280px viewport becomes far too strict at 4K.

## Using them from a test

Every metric is available through the same matcher:

```ts
import "@blazediff/vitest"; // or @blazediff/jest, @blazediff/bun

// screenshot is a buffer or a file path. core-native takes file paths only.
await expect(screenshotPath).toMatchImageSnapshot({ method: "core-native" });
await expect(screenshot).toMatchImageSnapshot({ method: "gmsd" });
await expect(screenshot).toMatchImageSnapshot({ method: "ssim" });
await expect(screenshot).toMatchImageSnapshot({ method: "hitchhikers-ssim" });
```

And from the CLI:

```bash
blazediff-cli baseline.png current.png diff.png   # core-native, the default
blazediff-cli gmsd baseline.png current.png
blazediff-cli ssim baseline.png current.png
blazediff-cli hitchhikers-ssim baseline.png current.png
```

## Measured cost

Numbers from the [pixel benchmarks](/benchmarks/pixel-by-pixel) and
[structural benchmarks](/benchmarks/structural), M1 Max, Node 22, across the three
4K fixtures:

- Pixel diff, native Rust, image IO included: **288-349ms**
- Pixel diff, pure JS, IO excluded: **97-136ms**
- Hitchhiker's SSIM: **2x to 5x faster** than standard SSIM, depending on the image
- GMSD: a single pass with two 3x3 convolutions, cheaper than SSIM

Next: [What is SSIM →](/docs/metrics/ssim) ·
[How GMSD works →](/docs/metrics/gmsd) ·
[Structural comparison examples →](/docs/structural-comparison)

---

# What Is SSIM?

**SSIM (Structural Similarity Index) scores how similar two images look, on a
scale from 0 to 1.** Instead of counting pixels that changed, it slides a window
over both images and compares three things inside each window: brightness,
contrast, and structure. `1` means identical. Pixel diffing answers "how many
pixels changed". SSIM answers "would a person see a difference".

## SSIM vs pixel-by-pixel comparison

|                            | Pixel-by-pixel                          | SSIM                                  |
| -------------------------- | --------------------------------------- | ------------------------------------- |
| What it measures           | How many pixels changed                 | How different the images look         |
| Output                     | Pixel count and percentage              | One score, 0 to 1, higher is better   |
| Unit of work               | One pixel at a time                     | A window of pixels (11x11 by default) |
| Compression artifacts      | Counted as real changes                 | Mostly ignored                        |
| Anti-aliasing on edges     | Counted unless you filter it            | Mostly ignored                        |
| A block of text moved 1px  | Every pixel in and around it counts     | Scored as one local structure change  |
| Shows you where            | Yes, a diff image                       | Yes, an SSIM map                      |
| Speed                      | Fastest                                 | Slower, more math per pixel           |

Pixel diffing is exact and cheap, so it stays the right default. Reach for SSIM
when exact matching is too strict: JPEG artifacts, font and GPU rendering that
differs between machines, or screenshots that get re-encoded somewhere in the
pipeline.

## How the score is computed

For each window, SSIM takes the mean, the variance, and the covariance of the two
images:

```
mu_x  = mean(x)      mu_y  = mean(y)
var_x = var(x)       var_y = var(y)
cov   = cov(x, y)
```

Those feed three terms - luminance, contrast, and structure - which are
multiplied together:

```
SSIM(x,y) = l(x,y) * c(x,y) * s(x,y)

l(x,y) = (2 * mu_x * mu_y + C1) / (mu_x^2 + mu_y^2 + C1)
c(x,y) = (2 * sd_x * sd_y + C2) / (var_x + var_y + C2)
s(x,y) = (cov + C2/2) / (sd_x * sd_y + C2/2)
```

Which collapses to the form usually quoted:

```
SSIM(x,y) = ((2*mu_x*mu_y + C1) * (2*cov + C2))
          / ((mu_x^2 + mu_y^2 + C1) * (var_x + var_y + C2))
```

`C1` and `C2` only exist to stop the fractions blowing up when a window is flat
(a solid background, where the means and variances are near zero):

```
C1 = (K1 * L)^2      K1 = 0.01
C2 = (K2 * L)^2      K2 = 0.03
L  = 255             dynamic range for 8-bit images
```

The default window is 11x11 Gaussian with sigma 1.5, so pixels near the middle of
the window count more than pixels at its edge. The final score is the mean of
every window's SSIM.

`@blazediff/ssim` matches the reference MATLAB implementation to within 0.01%.

## Reading the score

| Score       | Meaning                    |
| ----------- | -------------------------- |
| `1.00`      | Identical                  |
| `0.95-1.00` | Excellent, safe to pass    |
| `0.85-0.95` | Good, small visible change |
| `0.70-0.85` | Fair, clearly different    |
| `< 0.70`    | Poor, large change         |

For UI screenshots the useful band is narrow. A real regression on a page that is
mostly whitespace can still score above `0.98`, so pick your threshold from your
own baselines rather than from this table.

## Three variants

| Variant             | Import                              | What changes                                                    | Use it for                       |
| ------------------- | ----------------------------------- | --------------------------------------------------------------- | -------------------------------- |
| SSIM                | `@blazediff/ssim/ssim`              | Gaussian windows, one scale                                      | Matching published/MATLAB results |
| MS-SSIM             | `@blazediff/ssim/msssim`            | 5 scales, downsampled 2x each step, weighted geometric mean       | Images seen at different sizes   |
| Hitchhiker's SSIM   | `@blazediff/ssim/hitchhikers-ssim`  | Rectangular non-overlapping windows via integral images, 2x to 5x faster | Large batches in CI       |

MS-SSIM computes contrast and structure at every scale but luminance only at the
coarsest one, then combines them with the default weights
`[0.0448, 0.2856, 0.3001, 0.2363, 0.1333]`. It correlates better with human
judgement when the image will be viewed at more than one size.

Hitchhiker's SSIM swaps the Gaussian window for a rectangular one and uses
integral images, which makes each window O(1) instead of O(window size). Windows
do not overlap by default (`windowStride` defaults to `windowSize`). It pools with
coefficient of variation rather than a plain mean.

## Run it

```bash
npm install @blazediff/ssim
```

```ts
import ssim from "@blazediff/ssim/ssim";

const score = ssim(image1, image2, undefined, width, height);
if (score < 0.98) throw new Error(`too different: ${score}`);
```

Pass an output buffer as the third argument to get the SSIM map back as a
grayscale image - dark areas are where the two images disagree.

From the CLI:

```bash
blazediff-cli ssim baseline.png current.png
blazediff-cli ssim baseline.png current.png --output ssim-map.png
```

In a test:

```ts
await expect(screenshot).toMatchImageSnapshot({ method: "ssim" });
```

> **Info:** Options: `windowSize` (default `11`), `k1` (`0.01`), `k2` (`0.03`), `L` (`255`). Full signature in the [`@blazediff/ssim` reference](/apis/ssim).

## When SSIM is the wrong tool

- **You need to know exactly what changed.** SSIM gives one number. Use
  [pixel diffing](/docs/pixel-comparison/vanilla-javascript) or
  [interpret mode](/docs/difference-analysis) for regions and change types.
- **The change is tiny but important.** A single wrong character in a heading
  barely moves the score on a full-page screenshot.
- **Only the colors changed.** SSIM works on luminance, so a red button turning
  green can score near `1.0`. Pixel diffing catches that; SSIM does not.
- **You need speed above all.** The native pixel core is faster by a wide margin.

## Reference

Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P. (2004). "Image quality
assessment: from error visibility to structural similarity." *IEEE Transactions on
Image Processing*, 13(4), 600-612.

Next: [How GMSD works →](/docs/metrics/gmsd) ·
[Choosing a metric →](/docs/metrics/choosing-a-metric)

---

# How GMSD Works

**GMSD (Gradient Magnitude Similarity Deviation) scores how different two images
are by comparing their edges.** It runs an edge filter over both images, measures
how well the edge strengths agree at every pixel, then returns the standard
deviation of those agreements. `0` means identical, and lower is better - the
opposite direction from SSIM.

The idea behind it: what people notice in a broken render is distorted structure -
shifted text, a missing border, a control that changed shape. Edges are where
structure lives, so comparing edges catches those changes and ignores flat areas
where nothing interesting happens.

## The four steps

### 1. Downsample (optional)

Average each 2x2 block, then keep every second pixel. This halves each dimension
and removes high-frequency noise before anything else runs. Off by default
(`downsample: 0`).

```
aveKernel = [0.25  0.25]
            [0.25  0.25]
```

### 2. Find the edges with a Prewitt filter

Both images are converted to luminance and convolved with two 3x3 kernels, one for
horizontal change and one for vertical:

```
dx = [ 1  0  -1]  / 3       dy = [ 1   1   1]  / 3
     [ 1  0  -1]                 [ 0   0   0]
     [ 1  0  -1]                 [-1  -1  -1]
```

The gradient magnitude at each pixel is how strong the edge is there, in any
direction:

```
gradient = sqrt(Ix^2 + Iy^2)
```

You now have two edge maps, one per image.

### 3. Compare the two edge maps

For every pixel, gradient magnitude similarity (GMS) compares the two edge
strengths:

```
GMS = (2 * g1 * g2 + C) / (g1^2 + g2^2 + C)
```

`C = 170` is a stability constant tuned for the Prewitt operator on 8-bit images.
It stops the fraction from being noisy in flat regions where both gradients are
near zero. GMS lands between 0 and 1, where 1 means the two images have the same
edge strength at that pixel.

### 4. Take the standard deviation

```
GMSD = std(GMS)
```

This is the part that surprises people: GMSD reports the *spread* of the
similarity map, not its average. That is deliberate. A page where one component
is badly broken and everything else is fine has a high spread, and that is what a
reviewer would flag. A mean would average that damage away against the thousands
of pixels that are fine.

## Reading the score

| Score       | Meaning                    |
| ----------- | -------------------------- |
| `0.00`      | Identical                  |
| `0.00-0.05` | Very low, likely artifacts |
| `0.05-0.15` | Low but visible            |
| `0.15-0.35` | Moderate, clearly changed  |
| `> 0.35`    | Large structural change    |

For visual regression, treat anything above `0.0` as worth looking at when you
control the render, and raise the gate to about `0.15` if screenshots pass through
lossy compression.

## GMSD vs SSIM

|                   | GMSD                            | SSIM                                   |
| ----------------- | ------------------------------- | -------------------------------------- |
| Direction         | Lower is better, `0` = identical | Higher is better, `1` = identical      |
| Looks at          | Edge strength                   | Brightness, contrast, structure        |
| Aggregation       | Standard deviation of the map   | Mean of the map                        |
| Cost              | One pass, two 3x3 convolutions  | Windowed statistics over the whole image |
| Strong at         | Layout and shape changes        | General perceived quality              |
| Blind to          | Color-only changes, uniform shifts in brightness | Color-only changes |

They disagree in a useful way. GMSD reacts to a control that moved; SSIM reacts to
a section that got blurrier. Running both is cheap and the pair is more honest
than either alone.

## Run it

```bash
npm install @blazediff/gmsd
```

```ts
import gmsd from "@blazediff/gmsd";

const score = gmsd(image1, image2, undefined, width, height);
if (score > 0.05) throw new Error(`too different: ${score}`);
```

Pass an output buffer as the third argument to get the GMS map back as a grayscale
image, which shows exactly which edges disagreed.

From the CLI:

```bash
blazediff-cli gmsd baseline.png current.png
blazediff-cli gmsd baseline.png current.png --output gms-map.png
```

In a test:

```ts
await expect(screenshot).toMatchImageSnapshot({ method: "gmsd" });
```

> **Info:** Options: `downsample` (`0` or `1`, default `0`) and `c` (default `170`). Raising `c` makes the metric more forgiving in low-contrast areas. Full signature in the [`@blazediff/gmsd` reference](/apis/gmsd).

## When GMSD is the wrong tool

- **Color-only changes.** GMSD works on luminance gradients. Swapping a brand
  color for another of the same brightness barely registers.
- **You need to know where and what.** One number will not tell you a button
  moved. Use [interpret mode](/docs/difference-analysis) for regions and change
  types.
- **Exact matching is the requirement.** For byte-level correctness, use
  [pixel diffing](/docs/pixel-comparison/vanilla-javascript).

## Reference

Xue, W., Zhang, L., Mou, X., & Bovik, A. C. (2013). "Gradient Magnitude Similarity
Deviation: A Highly Efficient Perceptual Image Quality Index." *IEEE Transactions
on Image Processing*, 22(2), 684-695.

Next: [What is SSIM →](/docs/metrics/ssim) ·
[Choosing a metric →](/docs/metrics/choosing-a-metric)

---

# Image Difference Analysis

**Interpret mode takes a raw pixel diff and tells you what changed, where, and how
much.** Instead of one number, you get a list of regions, each with a bounding
box, a change type, a percentage, and a severity.

It is deterministic. No model, no weights, no network call. It lives in
`@blazediff/interpret-native`, a Node binding over the `blazediff-interpret`
crate, and sits above whatever located the change: a pixel diff, an SSIM map, or
boxes you already have.

This is what makes agent review tractable. A coding agent asked to compare two
full-page screenshots invents differences; an agent handed three cropped regions
that were measured deterministically is answering a much easier question. See
[Agentic visual testing](/docs/agentic-testing/setting-up).

## How it works

1. Pixel diff produces a binary change mask.
2. A morphological close bridges small gaps in that mask.
3. Connected components isolate the mask into fragments.
4. Fragments are assembled into regions: a **noise census** counts speck-sized
   fragments to measure how noisy the pair is, nearby bounding boxes are merged
   (an inpainted object shattered into patches, the words of one recolored text
   run) — but only when the box that would enclose them is mostly _touched_
   below the diff threshold, so two distinct edits with untouched background
   between them stay separate instead of collapsing into one region — and a
   noise floor that scales with the census drops the rest, so a clean UI render
   keeps its smallest real regions while a recompressed photo sheds hundreds of
   ringing fragments.
5. Evidence is extracted per region:
   - **Dual-image gradients and luminance correlation.** Edges in both images
     plus spatial correlation, to detect whether structure was preserved.
   - **Color delta distribution.** Mean, max and standard deviation of YIQ
     distance, which separates a uniform recolor from a patchy texture change.
   - **Chroma-plane movement.** How the color mass moved: hue-rotation cosine,
     saturation on both sides, and the smoothness of the chroma delta field.
     A recolor moves chroma coherently; a replacement scatters it. This is what
     separates the two on photographic edits, where regenerated texture makes
     luminance correlation useless.
   - **Background distance.** How far changed pixels sit from the local unchanged
     pixels, in each image separately.
6. A six-label rule cascade classifies each region.
7. A post-pass finds region pairs where the content that left one location is
   the content that appeared at another — by directly correlating the two image
   crops, scored best-first — and relabels both halves as a Shift.

Pick a fixture pair. The analysis runs in your browser, in a Web Worker, on the
same Rust classifier compiled to wasm — nothing here is precomputed.

## Usage

Every entry point returns the same shape: a `summary` string, a `regions[]` array
with `position`, `changeType` and `percentage` on each entry, and an overall
`severity`.

```ts
import { interpret } from "@blazediff/interpret-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)}%)`);
}
```

Pass a third argument to keep the diff visualization, and `source` to locate the
regions with a similarity map instead of a pixel diff:

```ts
await interpret("3a.png", "3b.png", "diff.png");
await interpret("3a.png", "3b.png", undefined, { source: "ms-ssim" });
```

When something else already knows where to look — DOM rectangles from a layout
pass, say — `interpretRegions` skips the search and classifies those boxes:

```ts
import { interpretRegions } from "@blazediff/interpret-native";

await interpretRegions("3a.png", "3b.png", [{ x: 0, y: 0, width: 64, height: 64 }]);
```

```bash
blazediff-cli interpret 3a.png 3b.png
blazediff-cli interpret 3a.png 3b.png diff.png
blazediff-cli interpret 3a.png 3b.png --source ms-ssim --json
```

Exits 0 when nothing actionable changed, 1 when it did, 2 on error.

### Identical images

When nothing changed, `regions` is empty and `summary` says so.

## Change types

| Type | Meaning |
|---|---|
| `Addition` | Content appeared. Blends with the background in the before image, distinct in the after image. |
| `Deletion` | Content was removed. Distinct before, blends with the background after. |
| `Shift` | Content moved. Two regions whose before-crop and after-crop hold the same content, paired by patch correlation. |
| `ColorChange` | A recolor. Either luminance structure is preserved under a color shift (UI recolors), or the chroma moved coherently over regenerated texture (photographic recolors). |
| `ContentChange` | A structural change. Structure replaced and the chroma scattered rather than rotated. |
| `RenderingNoise` | Sub-pixel artifacts. Filtered out of the output. |

## Accuracy

Measured against datasets with hand-labeled change regions. Full breakdown in
[crates/blazediff-interpret-verify/BENCHMARKS.md](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-interpret-verify/BENCHMARKS.md).

| Dataset | What it tests | Classifier-only macro F1 | End-to-end macro F1 |
|---|---|---:|---:|
| `addition_deletion` | Clean object insert and remove on photographs | **1.000** | **0.958** |
| `shift` | Sub-region translations with pixel-perfect ground truth | **1.000** | **0.799** |
| `inpaintcoco` | Inpaint edits that mix recolor and texture replacement | **0.718** | **0.488** |
| `html_color_pairs` | Recolors on rendered Tailwind UI screenshots | **1.000** | **0.874** |

Read the two columns as different questions. Classifier-only assumes the regions
were found correctly and asks whether the label is right. End-to-end runs the full
detector first, so it also pays for missed regions and spurious small ones.

What the numbers say, plainly:

- On the three clean-ground-truth datasets — object insert/remove, moved blocks,
  and UI recolors — the classifier labels every known region correctly. On
  `shift` the patch-correlation matcher pairs all 388 moved-block events with no
  false pairs.
- On real inpainted photographs it lands the right label roughly five times in
  seven. This is the honest ceiling for per-region pixel statistics: a diffusion
  inpaint regenerates the texture whether the semantic edit was a recolor or a
  replacement, so the chroma-coherence evidence carries the whole distinction.
- End to end, the census-scaled noise floor is what makes the photographic
  datasets tractable at all — on `inpaintcoco` it cuts spurious detections from
  tens of thousands to a few hundred. The remaining `html_color_pairs` misses
  are recolors whose pixel delta never crosses the diff threshold, which
  end-to-end detection cannot see by construction.

## Next

- Put this to work in a test loop:
  [Agentic visual testing](/docs/agentic-testing/setting-up)
- Full result type and options:
  [`@blazediff/interpret-native` reference](/apis/interpret-native)

---

# Agentic Visual Testing: Setting Up

**`@blazediff/agent` screenshots your routes, diffs them against baselines
committed to your repo, and hands the diffs it cannot classify to your coding
agent for a verdict.** Claude Code, Cursor and Codex are supported out of the box.
There is no hosted service, no API key in the default flow, and no screenshot
leaves your machine.

The workflow splits in two, on purpose:

- **Authoring happens on your machine.** You capture baselines, add masks, write
  harnesses, and accept intentional changes. All of it is blocked in CI.
- **Checking happens in CI.** One command re-captures, diffs, and fails the build
  on a regression.

This page covers authoring, from nothing to a committed set of baselines. Then
read [Running in CI](/docs/agentic-testing/running-in-ci).

## 1. Install

```bash
npm install --save-dev @blazediff/agent
```

The first run offers to install a 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 whichever
coding-agent stack lives in your project.

```bash
# Setup only. Baselines are captured explicitly in step 4.
blazediff-agent onboard --no-capture
```

It detects the stack automatically (Claude Code, Codex, Cursor). Pass
`--stack <name>` to be explicit, or `--stack local` to install the local
Moondream and Qwen judge when there is no coding agent in the project.

## 3. Start the dev server

```bash
blazediff-agent serve-status --detach --json   # waits up to 60s for the port
```

Already have a running URL, such as staging? Skip the dev server and point the
agent at it: `blazediff-agent onboard --url https://staging.example.com`.

## 4. Capture baselines

Pipe in a JSON list of routes. One `capture` call screenshots them all and writes
the manifest plus the baseline PNGs.

```bash
cat <<'EOF' | blazediff-agent capture --stdin --mode baseline --json
[
  {"id": "home", "url": "/", "mask": [".timestamp"]},
  {"id": "pricing", "url": "/pricing"}
]
EOF
```

Then tear the dev server down. This step is mandatory:

```bash
blazediff-agent serve-status --kill --json
```

> **Info:** **Commit `.blazediff/`.** Config, manifest and baselines all live there, and they are the source of truth for every later check.

## What ends up in `.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"
}
```

`manifest.json` is written by `capture`. Never edit it by hand. Per-route
behavior, such as logging in or clicking through a flow, belongs in a
[harness](/docs/agentic-testing/judging-and-harnesses#harnesses) rather than in
config.

## Next

- [Running in CI](/docs/agentic-testing/running-in-ci): the one command CI runs,
  and what its exit codes mean
- [Judging and harnesses](/docs/agentic-testing/judging-and-harnesses): what to do
  when a check fails
- Every command and flag: [`@blazediff/agent` reference](/apis/agent)

---

# Agentic Visual Testing: Running in CI

**With baselines committed, CI runs one verb: `check`.** It re-captures every
entry in the manifest, diffs each against its baseline, classifies what changed,
and fails the build on a regression.

If you have not captured baselines yet, start with
[Setting Up](/docs/agentic-testing/setting-up).

## The check command

```bash
blazediff-agent check --judge host --json
```

It starts the dev server 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-passing 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 is no TTY, only `check` runs. `onboard`, `capture`, `rewrite` and `reset` are blocked. Authoring belongs on a developer's machine, where a baseline change gets reviewed like any other diff.

## 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. Each app keeps
its own `.blazediff/` directory, so there is nothing central to keep in sync.

## Exit codes

| Code | Meaning |
|---|---|
| `0` | Every entry passed |
| `1` | At least one regression, intentional change, noise, or pending judgment |
| non-zero, with JSON | Infrastructure failure: missing manifest, no Chromium, and so on |

A route that times out is logged once in the result array and skipped. It never
blocks the run.

## When a check fails

Exit code `1` usually means a diff needs a verdict rather than a fix. Locally,
your coding agent reads the judgment request and decides; intentional changes are
accepted with `rewrite`.

## Next

- [Judging and harnesses](/docs/agentic-testing/judging-and-harnesses): verdicts,
  driving the page before a screenshot, and masking flaky regions
- [Agent-judged screenshots in GitHub Actions](/guides/github-actions-agent-judged-screenshots):
  a full workflow, including judging inside the job
- Every command and flag: [`@blazediff/agent` reference](/apis/agent)

---

# Agentic Visual Testing: Judging and Harnesses

**A failing `check` is not always a regression.** This page covers the two things
that turn a raw diff into a decision: judging, meaning is this change real,
intentional, or noise, and harnesses, meaning driving the page so the right thing
gets screenshotted in the first place.

## The judging model

The heuristic pipeline puts one of four labels on every failing entry:

| Label | Meaning | Default action |
|---|---|---|
| `regression-likely` | Confident structural change | Investigate. Do not rewrite. |
| `intentional-likely` | Confident styling or typographic change | Ask the user, then rewrite |
| `noise-likely` | Confident non-deterministic source | Ask the user. Prefer masking. |
| `ambiguous` | The heuristic could not classify it | Defer to the host judge |

Only `ambiguous` reaches an agent. That is the point: asking a model to rule on
every diff, including the obvious ones, is how you get verdicts that contradict
each other between runs.

For `ambiguous`, `--judge host` writes a `JudgmentRequest` to
`.blazediff/judgments/<id>/request.json` containing:

- `regions[]`, with 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}`, the full-page PNGs, as a fallback

> **Info:** **Token discipline.** The region tiles are 10x to 100x smaller than the full-page PNGs. A well-behaved host agent reads `regions.png` and `locator.png` first, and falls back to the full-page PNGs only when 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
}
```

Merge verdicts into the report without re-screenshotting anything:

```bash
blazediff-agent check --apply-judgments --json
```

Accept an intentional change by re-baselining the entry. Mask, viewport and
`waitFor` are preserved; only the PNG is regenerated:

```bash
blazediff-agent rewrite agent --json       # by id
blazediff-agent rewrite --failed --json    # all failures from the last check
```

## Why agents fail at visual review

Handing a coding agent two screenshots and asking "what changed" fails reliably.
The failures have specific causes, and the judgment request is shaped around each
one.

| Failure | Cause | What BlazeDiff does |
| --- | --- | --- |
| Invents differences that are not there | Asked to compare two images in one pass, the model narrates a plausible diff instead of reading one | Regions are found deterministically first. The agent only labels changes that were measured. |
| Misses a real change | On a full-page 4K PNG the change is a fraction of a percent of the pixels | `regions.png` crops each changed area into a `[baseline \| actual]` pair, so it fills the frame |
| Cannot say where on the page something is | Crops carry no context | `locator.png` is a ~400px overview with every region outlined in red |
| Verdicts contradict each other between runs | Every diff gets asked, including the obvious ones | A heuristic settles the confident cases. Only `ambiguous` reaches the agent. |
| Burns tokens or times out | Full-page PNGs, re-sent on every retry | Tiles are 10x to 100x smaller, and the run is checkpointed so it resumes rather than restarts |
| "Fixes" the failure by re-baselining | Judging and accepting are the same action | Verdicts are advisory. `rewrite` is a separate command, blocked in CI. |

The rule underneath all of it: **describe each side separately, then diff the
descriptions.** A model asked to compare two images in one step fills gaps with
what it expects to see. A model asked what a single crop contains is answering a
much easier question, and the comparison afterwards is arithmetic. The
`--judge local` backend follows the same shape: Moondream describes, a
deterministic word diff runs, then Qwen classifies the result.

If your agent is still returning bad verdicts, check in this order:

1. Is it reading `regions.png` and `locator.png`, or reaching straight for the
   full-page PNGs? The full pages are a fallback, not the input.
2. Is the entry actually `ambiguous`? A `regression-likely` entry does not need a
   verdict, it needs a look.
3. Is the diff real, or is the region non-deterministic? Masking a flake is
   correct. Asking an agent to rule on a spinner is not.

## Harnesses

A **harness** is a pluggable ESM script in `.blazediff/harnesses/<name>.js`,
attached to an entry through its `harnesses: [{ name, params? }]` list. Login is
one kind of harness. Anything that drives the page before or around a screenshot
is a harness. There are two phases:

- **`setup`** runs before navigation, to establish a session such as a login.
- **`interact`**, the default, runs after the base screenshot. It drives the page
  and can emit extra named screenshots through `screenshot(name)`, each becoming
  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 capture through a `setup` harness. Credentials live in
environment variables, never in the harness file, the manifest, or an LLM's
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 put the credentials in `.blazediff/.env`, which is
gitignored automatically:

```json
{ "id": "dashboard", "url": "/dashboard",
  "harnesses": [{ "name": "auth", "params": { "persona": "default" } }] }
```

For OAuth, SSO, magic links, MFA or captcha, record the session 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 turns out to come from
something non-deterministic, mask it rather than re-baselining. A re-baseline 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. Do **not** mask real content that happens to
change often. That is the change you want caught.

The agent always masks any element matching `[data-blazediff-agent-mask]`, with no
manifest change needed. Put it on a shared component and it applies on every
route:

```tsx
<div data-blazediff-agent-mask="report-carousel">...</div>
```

When you cannot edit the source, such as a third-party embed, fall back to a
per-entry CSS selector. The mask list **replaces** the existing one, so include
every selector you want to keep:

```bash
cat <<'EOF' | blazediff-agent capture --stdin --mode baseline --json
[
  {"id": "examples-vanilla", "url": "/docs/ui-components/vanilla", "mask": ["iframe"]}
]
EOF
```

## Next

- [Cross-OS false positives](/guides/cross-os-false-positives) when the same page
  renders differently on macOS and Linux CI
- [Anti-aliasing and 1px shifts](/guides/anti-aliasing-false-positives) when text
  edges are the source of the churn
- Every command and flag: [`@blazediff/agent` reference](/apis/agent)

---

# React Components
  ReactSwipeDemo,
  ReactTwoUpDemo,
  ReactOnionSkinDemo,
  ReactDifferenceDemo,
} from "../../../../components/demos/react-modes";

**`@blazediff/react` renders a before/after comparison four ways: swipe, two-up,
onion skin, and a live pixel diff.** Every demo below is the real component,
running in this page.

The components carry no styling of their own. They take `*ClassName` props and
render from the [`@blazediff/ui`](/apis/ui) headless engine, so you can restyle
them freely or drop to the engine and render your own markup.

## Installation

```bash
npm install @blazediff/react
```

    ```tsx

    // A draggable divider over two stacked images. position is 0 to 100.

    ```

    ```tsx

    // Side by side, with a label when the two images differ in size.

    ```

    ```tsx

    // Blends the two images. Good for spotting small position shifts.

    ```

    ```tsx

    // Runs an actual pixel diff in the browser and paints the result.

    ```

## Next

- Not using React? The same four modes ship as
  [vanilla mount functions](/docs/ui-components/vanilla)
- Every prop: [`@blazediff/react` reference](/apis/react)
- The engine underneath: [`@blazediff/ui` reference](/apis/ui)

---

# Vanilla Components
  VanillaSwipeDemo,
  VanillaTwoUpDemo,
  VanillaOnionSkinDemo,
  VanillaDifferenceDemo,
} from "../../../../components/demos/vanilla-modes";

**`@blazediff/ui` gives you the same four comparison modes as
[the React package](/docs/ui-components/react), mounted into any DOM node.** Each
demo below is the real thing, running in this page.

Every `mount*` call returns a `{ update, destroy }` handle: `update(options)`
swaps sources or options in place, `destroy()` tears the UI down. Underneath sits
a headless engine you can drive directly from Vue, Svelte, Solid or your own
renderer, which is what the React package does.

## Installation

```bash
npm install @blazediff/ui
```

    ```ts

    // A draggable divider over two stacked images. position is 0 to 100.
    const handle = mountSwipe(document.getElementById("app"), {
      src1: "before.png",
      src2: "after.png",
      onPositionChange: (position) => console.log(position),
    });
    ```

    ```ts

    // Side by side, with a label when the two images differ in size.
    const handle = mountTwoUp(document.getElementById("app"), {
      src1: "before.png",
      src2: "after.png",
      onImagesLoaded: ({ image1, image2 }) => console.log(image1, image2),
    });
    ```

    ```ts

    // Blends the two images. Good for spotting small position shifts.
    const handle = mountOnionSkin(document.getElementById("app"), {
      src1: "before.png",
      src2: "after.png",
      opacity: 50,
      onOpacityChange: (opacity) => console.log(opacity),
    });
    ```

    ```ts

    // Runs an actual pixel diff in the browser and paints the result.
    const handle = mountDifference(document.getElementById("app"), {
      src1: "before.png",
      src2: "after.png",
      threshold: 0.1,
      onDiffComplete: ({ diffCount, percentage }) =>
        console.log(diffCount, percentage),
    });
    ```

## Next

- Using React? [The React components](/docs/ui-components/react) wrap these
- Every option and the engine API: [`@blazediff/ui` reference](/apis/ui)

---

# Object Comparison

**`@blazediff/object` takes two JavaScript values and returns a flat list of what
changed, with the path to each change.** It handles nested objects, arrays, dates,
regexes and circular references, and it is about 55% faster than
[microdiff](https://github.com/AsyncBanana/microdiff) across the benchmark
fixtures.

This is the one part of BlazeDiff that has nothing to do with images. It shares
the name and the performance work, not the pipeline. Use it for audit logs,
undo stacks, state-change assertions in tests, or anywhere you would otherwise
`JSON.stringify` two objects and compare strings.

## Installation

```bash
npm install @blazediff/object
```

## What you get back

One entry per change, always the same shape, so V8 keeps a single hidden class for
the whole array:

```ts
interface Difference {
  type: 0 | 1 | 2;            // CREATE | REMOVE | CHANGE
  path: (string | number)[];  // e.g. ["user", "settings", "theme"]
  value: unknown;             // the new value
  oldValue: unknown;          // the previous value
}
```

Types are numbers rather than strings on purpose: they get compared in the hot
loop. `0` is CREATE, `1` is REMOVE, `2` is CHANGE.

## Examples

```ts
import diff from "@blazediff/object";

const oldObj = { a: 1, b: 2, c: 3 };
const newObj = { a: 1, b: 20, d: 4 };

// b changed, c was removed, d was created. a is not reported.
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"
    }
  }
};

// Paths go all the way down, e.g. ["user", "settings", "theme"].
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
};

// Arrays are compared by index, so a prepend reports every element as changed.
const changes = diff(oldObj, newObj);
```

## Things worth knowing

- **Arrays are compared by index, not by identity.** Inserting at the front
  reports every following element as a CHANGE. If you need move detection, key the
  array yourself before diffing.
- **Cycles are handled**, and the check costs something. If you know your input is
  a tree, pass `{ detectCycles: false }`.
- **Unchanged values are not reported.** An empty array means the two inputs are
  structurally equal.

## What it costs

Against microdiff, M1 Max, Node 22, 10,000 iterations:

| Fixture                | microdiff | `@blazediff/object` |
| ---------------------- | --------- | ------------------- |
| Large nested object    | 3.3318ms  | 1.4536ms            |
| Large array            | 0.5859ms  | 0.2391ms            |
| Large identical arrays | 0.0919ms  | 0.0031ms            |
| Simple object          | 0.0003ms  | 0.0002ms            |

About 55% faster on average, and ~97% faster when the two inputs are identical,
which is the common case when you are diffing state on every update.
[Full table](/benchmarks/object).

## Next

- Every option and the full `Difference` type:
  [`@blazediff/object` reference](/apis/object)
- Comparing images instead: [Introduction](/docs)

---

# Guides

# Let Claude Code Review Your Visual Diffs

**`@blazediff/agent` installs a skill into your repo that lets Claude Code look at
failing screenshot diffs and decide whether they are real regressions.** BlazeDiff
does the deterministic part - capture, diff, classify. Claude Code only sees the
cases the thresholds could not settle, and answers pass or fail with a reason. No
API key, no hosted vision service, and no screenshot leaves your machine.

## Set it up

```bash
npm install --save-dev @blazediff/agent
```

```bash
blazediff-agent onboard --stack claude
```

That writes three things:

| Path                                    | What it is                                  |
| --------------------------------------- | ------------------------------------------- |
| `.claude/skills/blazediff/SKILL.md`     | The skill Claude Code loads                 |
| `.blazediff/config.json`                | Dev server command, port, base URL          |
| `.blazediff/`                           | Baselines and manifest, committed to git     |

Onboarding auto-detects the stack when it finds `.claude/`, `CLAUDE.md`, or
`AGENTS.md`, so `--stack claude` is only needed when you want to be explicit. Pass
`--stack all` to install for Claude Code, Codex, and Cursor at once.

## Use it

In Claude Code:

```
/blazediff
```

The skill picks its own mode. No `.blazediff/manifest.json` yet means authoring:
it discovers routes, captures baselines, and commits them. A manifest already
there means checking: re-capture, diff, and report.

Under the hood it is running the same CLI you would run by hand:

```bash
blazediff-agent check --judge host --json
```

## What Claude Code actually sees

This is the part that decides whether the review is any good. BlazeDiff does not
hand over two full-page PNGs and ask "spot the difference" - that is the setup
where agents hallucinate. Instead, for each ambiguous entry it writes a judgment
request to `.blazediff/judgments/<id>/request.json` containing:

- `regions[]` - a bounding box, pixel count, and change type for each changed area
- `locator.png` - a ~400px overview with those regions outlined in red
- `regions.png` - a vertical stack of `[baseline | actual]` crops, one per region
- the full-page PNGs, as a fallback only

The region tiles are 10 to 100x smaller than the full-page images. Claude Code
reads the crops, not the page, so the decision is cheap and the relevant pixels
fill the frame.

> **Info:** Most diffs never reach Claude Code. A heuristic pass labels each failure `regression-likely`, `intentional-likely`, `noise-likely`, or `ambiguous`. Only `ambiguous` is handed over.

## The verdict loop

Claude Code writes its answer to `.blazediff/judgments/<id>/verdict.json`:

```json
{
  "id": "pricing",
  "verdict": {
    "label": "intentional-likely",
    "headline": "Button padding increased, no layout break",
    "action": "rewrite-if-intended"
  },
  "confidence": 0.92
}
```

Then the run resumes without re-screenshotting anything:

```bash
blazediff-agent check --apply-judgments --json
```

If the change was intended, accept it:

```bash
blazediff-agent rewrite pricing --json     # one entry
blazediff-agent rewrite --failed --json    # everything that failed
```

`rewrite` regenerates only the PNG. Mask, viewport, and wait conditions are
preserved.

The check itself is suspendable. It runs as a graph that pauses on the first
ambiguous entry and resumes from an on-disk checkpoint, so a long suite does not
restart from zero every time a verdict comes in.

## Why not just paste screenshots into the chat

| Pasting screenshots            | This                                              |
| ------------------------------ | ------------------------------------------------- |
| Agent compares whole pages     | Agent compares one cropped region at a time        |
| No record of the decision      | Verdict, reason, and confidence written to disk    |
| Re-run means re-explaining     | Checkpointed, resumes where it stopped             |
| Agent judges every diff        | Thresholds settle most of them first               |
| Costs scale with page size     | Tiles are 10 to 100x smaller than full pages       |

## Keeping it honest

The agent decides; it does not get to quietly change the baseline. `rewrite` is a
separate command, blocked in CI, and every baseline change lands in git as a PNG
diff a human can look at in the pull request.

> **Warning:** In CI (`CI=1` or no TTY) only `check` runs. `onboard`, `capture`, `rewrite`, and `reset` are blocked, so baselines only change on a developer machine where the change can be reviewed.

## Next

- [Setting up →](/docs/agentic-testing/setting-up)
- [Judging and harnesses →](/docs/agentic-testing/judging-and-harnesses)
- [Running it in GitHub Actions →](/guides/github-actions-agent-judged-screenshots)
- [Same loop in Cursor →](/guides/cursor-visual-qa) ·
  [in Codex →](/guides/codex-visual-review)

---

# Use Cursor as a Visual QA Agent

**`@blazediff/agent` installs a Cursor rule that lets the agent act as a visual QA
reviewer on your local screenshots.** BlazeDiff captures the pages, diffs them
against committed baselines, and classifies what changed. Cursor only gets the
diffs the thresholds could not settle, as cropped before/after tiles, and returns
a pass or fail with a reason. Everything stays on disk in your repo.

## Set it up

```bash
npm install --save-dev @blazediff/agent
```

```bash
blazediff-agent onboard --stack cursor
```

This writes `.cursor/rules/blazediff.mdc` alongside `.blazediff/config.json` and
your baselines. Detection triggers on `.cursor/` or `.cursorrules`, so plain
`onboard` usually picks Cursor on its own.

The rule ships with `alwaysApply: false`. It loads when you mention visual tests,
screenshot regressions, or type `/blazediff` - it does not sit in your context on
every unrelated request.

## Use it

Ask in the Cursor chat:

```
run the visual tests and tell me if anything actually broke
```

Or trigger it directly with `/blazediff`. The rule handles both jobs: authoring
baselines when `.blazediff/manifest.json` does not exist yet, and checking when it
does.

## What Cursor reviews

Not full-page screenshots. For each ambiguous entry, BlazeDiff writes
`.blazediff/judgments/<id>/request.json` with:

- `regions[]` - bounding box, pixel count, and change type per changed area
- `locator.png` - a ~400px overview with the regions outlined in red
- `regions.png` - a vertical stack of `[baseline | actual]` crops
- the full-page PNGs, as fallback

Cursor reads the crops first. They are 10 to 100x smaller than the full pages, so
the changed pixels fill the frame instead of being three percent of a 4K
screenshot.

> **Info:** A heuristic pass labels every failure `regression-likely`, `intentional-likely`, `noise-likely`, or `ambiguous` before the agent is involved. Only `ambiguous` entries are handed over.

## The loop

1. `blazediff-agent check --judge host --json` runs and suspends on the first
   ambiguous entry.
2. Cursor reads the request, looks at the tiles, and writes
   `.blazediff/judgments/<id>/verdict.json`.
3. `blazediff-agent check --apply-judgments --json` merges verdicts in. Nothing is
   re-screenshotted.
4. Intentional changes get accepted with `blazediff-agent rewrite <id>`.

The run is checkpointed, so step 3 picks up where step 1 stopped instead of
starting the suite over.

## Reviewing by hand instead

The agent is not required. To look at the diffs yourself:

```bash
blazediff-agent review
```

That serves a local approve/reject webapp on `127.0.0.1`. Same report, no agent,
nothing uploaded.

## What this costs

Nothing beyond the Cursor subscription you already pay for. There is no BlazeDiff
API key, no per-snapshot pricing, and no vision service in the loop. Screenshots
are files in your repo.

## Next

- [Setting up →](/docs/agentic-testing/setting-up)
- [Judging and harnesses →](/docs/agentic-testing/judging-and-harnesses)
- [Same loop in Claude Code →](/guides/claude-code-visual-review) ·
  [in Codex →](/guides/codex-visual-review)
- [Running it in CI →](/guides/github-actions-agent-judged-screenshots)

---

# Codex Visual Review

**`@blazediff/agent` installs a Codex skill that judges failing screenshot diffs
from the terminal.** Every command is a plain CLI verb with `--json` output, so
the same loop works inside Codex, inside a shell script, or inside CI. BlazeDiff
handles capture, diff, and classification; Codex only decides the cases the
thresholds could not.

## Set it up

```bash
npm install --save-dev @blazediff/agent
```

```bash
blazediff-agent onboard --stack codex
```

> **Warning:** The Codex skill installs at **user scope**, to `~/.codex/skills/blazediff/SKILL.md`, not into the repo. It applies to every project on the machine. Restart Codex after installing so it picks the skill up.

Detection triggers on `AGENTS.md`, `.codex/`, or `~/.codex`. Project files -
`.blazediff/config.json`, the manifest, and baselines - still live in the repo and
still get committed.

## The terminal loop

Every step is a command, and every command takes `--json`:

```bash
blazediff-agent check --judge host --json      # suspends on the first ambiguous diff
# ... agent writes .blazediff/judgments/<id>/verdict.json ...
blazediff-agent check --apply-judgments --json # resume, no re-screenshot
blazediff-agent rewrite <id> --json            # accept an intentional change
```

`check --json` returns a slim payload on purpose - `summaryPath`, `totalEntries`,
`passed`, `failed`, `pendingJudgments`, and a `results` array that lists non-pass
entries only. Full per-entry detail stays on disk in `.blazediff/summary.md` and
`.blazediff/judgments/<id>/request.json`, so a terminal agent parses a few fields
instead of swallowing a report.

## Hooking in your own inspection logic

Three extension points, in order of how deep they go.

### 1. Harnesses - drive the page before the screenshot

A harness is an ESM module in `.blazediff/harnesses/<name>.js` that gets the
Playwright `page`. Use it to log in, open a menu, seed state, or take extra named
screenshots.

```js
// .blazediff/harnesses/dark-mode.js
/** @type {import("@blazediff/agent").Harness} */
export default {
  async run({ page, screenshot }) {
    await page.emulateMedia({ colorScheme: "dark" });
    await screenshot("dark"); // becomes its own baseline entry
  },
};
```

Attach per entry:

```json
{ "id": "home", "url": "/", "harnesses": ["dark-mode"] }
```

`phase: "setup"` runs before navigation (for auth); the default `interact` phase
runs after the base screenshot.

### 2. Judge backend - who decides ambiguous diffs

```bash
blazediff-agent check --judge host    # your coding agent decides
blazediff-agent check --judge local   # local models decide, no host round-trip
blazediff-agent check --judge none    # no judgment, ambiguous entries just fail
```

`--judge local` runs Moondream to describe each region and Qwen to classify it,
entirely on your machine. Useful for headless runs with no agent attached.

### 3. Your own logic on top of the JSON

The report is a file. Anything that reads JSON can gate on it:

```bash
blazediff-agent check --judge none --json > report.json
node ./scripts/my-inspection.mjs report.json
```

Region data, change types, severity, and file paths for baseline, actual, and diff
are all in `.blazediff/judgments/<id>/request.json`.

## Exit codes

| Code             | Meaning                                                     |
| ---------------- | ----------------------------------------------------------- |
| `0`              | Every entry passed                                           |
| `1`              | A regression, intentional, noise, or pending-judgment entry  |
| non-zero + JSON  | Infra failure (missing manifest, no Chromium)                |

A route that times out is logged once and skipped. It never blocks the run.

## Next

- [Judging and harnesses →](/docs/agentic-testing/judging-and-harnesses)
- [Running it in GitHub Actions →](/guides/github-actions-agent-judged-screenshots)
- [Same loop in Claude Code →](/guides/claude-code-visual-review) ·
  [in Cursor →](/guides/cursor-visual-qa)

---

# Agent-Judged Screenshots in GitHub Actions

**CI detects, an agent judges.** `blazediff-agent check` re-captures every route,
diffs it against the committed baseline, and classifies each failure. Ambiguous
diffs go to a judge - either local models running inside the job, or the
developer's coding agent on their own machine. Nothing is uploaded to a vision
service and there is no API key.

## Pick where judgment happens

| Backend         | Runs where             | Use it when                                            |
| --------------- | ---------------------- | ------------------------------------------------------ |
| `--judge none`  | Nowhere                | Default CI gate. Ambiguous entries fail; a human looks  |
| `--judge local` | Inside the CI job      | You want a verdict without a human, no keys, no network |
| `--judge host`  | Developer's machine    | The coding agent reviews the diff during the PR         |

Most teams run `none` in CI and `host` locally. `local` is the option when the job
itself has to decide.

## The workflow

```yaml
name: visual
on: pull_request

jobs:
  visual:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: pnpm install

      - run: npx blazediff-agent browsers install

      - run: npx blazediff-agent check --json --junit visual.xml
        env:
          # Only if an entry uses a login harness. One pair per persona.
          BLAZEDIFF_AUTH_DEFAULT_EMAIL: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_EMAIL }}
          BLAZEDIFF_AUTH_DEFAULT_PASSWORD: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_PASSWORD }}

      - if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: blazediff
          path: .blazediff/
```

`browsers install` pulls the bundled Chromium - no `sudo`, no
`playwright install --with-deps`. In a monorepo, point the run at one app with
`--cwd`:

```yaml
- run: npx blazediff-agent --cwd apps/website check --json
```

> **Warning:** **CI is check-only.** When `CI=1` or there is no TTY, `onboard`, `capture`, `rewrite`, and `reset` are blocked. Baselines can only change on a developer machine, where the change shows up as a PNG diff in the pull request.

## Judging inside the job

To get a verdict without a human, run the local judge:

```yaml
- run: npx blazediff-agent check --judge local --json
```

Moondream describes each changed region, Qwen classifies it, both on the runner.
No API key, no network call, nothing leaves the job. It is slower than `--judge
none` because the models have to load, so use it on the visual job rather than on
every job.

## Judging from the pull request

The other pattern keeps CI dumb and puts the agent where the developer is.

1. CI runs `check --judge none` and fails. The `.blazediff/` artifact holds the
   judgment requests, region tiles, and diffs.
2. The developer pulls the branch and runs their agent - `/blazediff` in Claude
   Code, Cursor, or Codex.
3. The agent reads `.blazediff/judgments/<id>/request.json`, looks at the cropped
   `[baseline | actual]` tiles, and writes a verdict.
4. `blazediff-agent check --apply-judgments` merges the verdicts. Intentional
   changes get `blazediff-agent rewrite <id>`, which updates the baseline PNG.
5. The updated baseline is committed, and the next CI run is green.

The agent never has to re-run the browser. Step 4 works purely from files on disk.

## Making the failure readable

```bash
blazediff-agent check --junit visual.xml --json
```

`--junit` writes JUnit XML that GitHub's test reporters and most CI dashboards
render natively. The human-readable version is `.blazediff/summary.md`, which is
in the uploaded artifact.

For local review of a downloaded artifact:

```bash
blazediff-agent review
```

That serves an approve/reject webapp on `127.0.0.1` - the report, the diffs, and
nothing on the network.

## Keeping the job fast and stable

| Problem                             | Fix                                                        |
| ----------------------------------- | ----------------------------------------------------------- |
| Job is slow                         | `--concurrency <n>`, defaults to CPU count capped at 8      |
| Diff PNGs not needed on green runs  | `--no-diff-png`                                             |
| Fonts render differently than local | Capture baselines in the same Linux container CI uses        |
| A widget flickers between runs      | Mask it, do not re-baseline it                              |
| Slow route blocks everything        | Timeouts are logged once and skipped, never fatal            |

Rendering differences between a developer's macOS and CI's Linux are the most
common source of noise here. That has its own guide:
[cross-OS false positives](/guides/cross-os-false-positives).

## Next

- [Running in CI →](/docs/agentic-testing/running-in-ci)
- [Judging and harnesses →](/docs/agentic-testing/judging-and-harnesses)
- [Claude Code →](/guides/claude-code-visual-review) ·
  [Cursor →](/guides/cursor-visual-qa) ·
  [Codex →](/guides/codex-visual-review)

---

# Fix Cross-OS False Positives

**The same page rendered on macOS and on Linux is not the same image, and no
threshold makes that go away cleanly.** Fonts are hinted differently, text is
anti-aliased differently, and color profiles differ. The fix, in order: capture
baselines on the platform CI uses, harden the browser so rendering is
deterministic, mask what is genuinely non-deterministic, and only then loosen
thresholds.

## Why it happens

| Cause                     | What it looks like                                        |
| ------------------------- | ---------------------------------------------------------- |
| Font hinting              | Every glyph shifts a fraction of a pixel. Whole page differs |
| Subpixel anti-aliasing    | Colored fringes on text edges (LCD text)                   |
| Missing fonts             | A fallback font renders. Large, obvious diff                |
| Color profile             | Every color off by one or two. 100% of pixels differ        |
| GPU vs software raster    | Gradients, shadows, and blurs differ slightly               |
| Scrollbars                | A scrollbar appears on one OS and shifts the layout         |
| Device pixel ratio        | The whole screenshot is a different size                    |

Note the pattern: most of these change *every* pixel a little, not a few pixels a
lot. That is why a pixel-count threshold is the wrong tool - the diff is 100% of
the page at very low intensity.

## 1. Capture baselines where CI runs

This is the actual fix. Everything below reduces leftover noise; this removes the
category.

```bash
docker run --rm -v "$PWD":/app -w /app node:22-bookworm \
  npx blazediff-agent onboard --yes
```

Use the same image CI uses. Commit `.blazediff/` from that run. Developers on
macOS then run `check` against Linux baselines - which will fail locally, and
should. Local runs are for authoring; the verdict that matters is CI's.

> **Info:** If developers need green local runs too, capture two baseline sets in separate directories and point `--cwd` at the right one per environment. It costs a second set of PNGs in git and removes the argument entirely.

## 2. Pin the browser

`@blazediff/agent` bundles its own Chromium:

```bash
blazediff-agent browsers install
```

One binary, same version everywhere. A system Chrome that auto-updates will change
your renders under you.

## 3. Harden the render

`@blazediff/agent` already launches Chromium with the flags that remove
OS-dependent rendering:

| Flag                          | Removes                                     |
| ----------------------------- | -------------------------------------------- |
| `--font-render-hinting=none`  | OS font hinting differences                  |
| `--disable-lcd-text`          | Subpixel anti-aliasing colored fringes       |
| `--force-color-profile=srgb`  | Display color profile differences            |
| `--disable-skia-runtime-opts` | CPU-feature-dependent rasterization paths    |
| `--hide-scrollbars`           | Scrollbar-induced layout shift               |

Rolling your own Playwright setup? Copy that list. It is most of the battle.

## 4. Freeze everything that moves

Layout shift and animation cause the other half of nightly flakes. The agent
injects CSS that zeroes every `animation-duration` and `transition-duration`,
pins `animation-iteration-count` to 1, makes the text caret transparent, and
disables smooth scrolling. Screenshots are also taken with Playwright's
`animations: "disabled"`.

It also freezes the sources of per-run randomness before any page script runs:

| Source             | Replaced with                        |
| ------------------ | ------------------------------------ |
| `Date.now()`       | A fixed timestamp                    |
| `performance.now()`| A counter ticking 16.6667ms per call |
| `Math.random()`    | A seeded generator                    |
| `crypto.randomUUID()` | A counter                         |

That kills "posted 3 minutes ago", randomized placeholder content, and
animation timing that depends on wall-clock time.

## 5. Wait for the page to settle

Set `waitFor` per entry. It accepts `"networkidle"`, `"fonts"`, or a selector:

```json
{
  "id": "dashboard",
  "url": "/dashboard",
  "waitFor": ["fonts", "networkidle", { "selector": "[data-loaded]" }]
}
```

`"fonts"` is the one people forget. A screenshot taken before webfonts load
captures the fallback font, and that is a full-page diff.

## 6. Mask what is genuinely non-deterministic

Do not re-baseline a flake - that just resets the clock. Mask it.

```tsx
<div data-blazediff-agent-mask="live-feed">...</div>
```

Any element with `data-blazediff-agent-mask` is masked on every route with no
manifest change. For third-party embeds you cannot edit, use a per-entry selector:

```json
{ "id": "home", "url": "/", "mask": ["iframe", ".ticker"] }
```

Mask carousels, live data, third-party iframes, and personalization. Do not mask
real content that happens to change - that is the change you want caught.

## 7. Only now, loosen the gate

If residue remains, use a percentage threshold rather than a pixel count. A pixel
budget tuned at 1280px is far too strict at 4K.

```ts
await expect(screenshot).toMatchImageSnapshot({
  method: "core-native",
  failureThreshold: 0.1,
  failureThresholdType: "percent",
});
```

For the low-intensity, everywhere-at-once diff that cross-OS rendering produces, a
structural metric works better than any pixel threshold:

```bash
blazediff-cli gmsd baseline.png current.png   # gate around 0.05
```

[GMSD](/docs/metrics/gmsd) scores edge structure, so uniform sub-pixel differences
across the whole page barely move it while a component that actually moved does.

## 8. Send what is left to an agent

After all of the above, a handful of diffs per run will still be genuinely
ambiguous. Those are the ones worth a judgment rather than a threshold:

```bash
blazediff-agent check --judge host --json
```

The agent gets cropped `[baseline | actual]` region tiles and answers regression
or intentional, with a reason.
[How that works →](/guides/claude-code-visual-review)

## Order of operations

1. Capture baselines in CI's container.
2. Use the bundled Chromium.
3. Keep the hardening flags.
4. Add `waitFor: ["fonts", "networkidle"]`.
5. Mask non-deterministic regions.
6. Switch to a percentage threshold.
7. Add GMSD for low-intensity full-page noise.
8. Route the remainder to an agent.

Steps 1 through 5 remove causes. Steps 6 and 7 only hide symptoms, so do them
last, or you will hide a real regression along with the noise.

## Next

- [Anti-aliasing and 1px shifts →](/guides/anti-aliasing-false-positives)
- [Running in CI →](/docs/agentic-testing/running-in-ci)
- [Choosing a metric →](/docs/metrics/choosing-a-metric)

---

# Stop Anti-Aliasing and 1px Shifts From Failing CI

**Turn on anti-aliasing detection first.** It finds pixels that differ only
because an edge was smoothed differently and excludes them from the diff count,
without loosening anything else. That alone clears most of the churn teams get
after a CSS framework upgrade. What is left - a block of text that actually moved
one pixel - is a different problem, and thresholds are the wrong fix for it.

## The defaults are inverted between cores

This trips people up, so check which one you are using:

| Package                                | Option          | Default | Anti-aliased pixels are |
| -------------------------------------- | --------------- | ------- | ----------------------- |
| `@blazediff/core` (JS)                 | `includeAA`     | `false` | **ignored**             |
| `@blazediff/core-native` (Rust, N-API) | `antialiasing`  | `false` | **counted**             |
| `blazediff-cli core-native`            | `-a, --antialiasing` | off | **counted**             |
| `blazediff` (Python)                   | `antialiasing`  | `False` | **counted**             |
| `@blazediff/agent`                     | -               | on      | **ignored**             |

The two options are named for opposite things. `includeAA: true` means "count
them". `antialiasing: true` means "detect them, so they can be skipped". Both
default to `false`, which is why the JS core ignores AA out of the box and the
native core does not.

```ts
// JS core - already ignoring AA
import diff from "@blazediff/core";
const changed = diff(img1, img2, output, width, height);

// Native core - opt in
import { compare } from "@blazediff/core-native";
const result = await compare("baseline.png", "current.png", "diff.png", {
  antialiasing: true,
});
```

```bash
blazediff-cli baseline.png current.png diff.png --antialiasing
```

## What detection actually does

For each differing pixel, BlazeDiff looks at its eight neighbors and asks whether
the pixel sits on a smoothed edge:

1. **Count neighbors with no brightness difference.** More than two and this is a
   flat region, not an edge. Not anti-aliasing.
2. **Find the darkest and the brightest neighbor.** If there is no gradient at
   all, it is not anti-aliasing.
3. **Check that one end of that gradient is solid color.** The pixel counts as
   anti-aliased if either the darkest or the brightest neighbor has three or more
   identical neighbors of its own, **in both images**.

That last condition is what makes it safe. An anti-aliased pixel is a blend
between two solid regions, so at least one end of its gradient must be solid in
the baseline *and* the current image. A pixel where the surrounding content
genuinely changed fails that test and still counts.

The check runs in both directions, baseline against current and current against
baseline. The algorithm is Vysniauskas's anti-aliased pixel and intensity slope
detector (2009), the same one pixelmatch uses.

A pixel that passes counts as `0` and is painted yellow in the diff image. A pixel
that fails counts as `1` and is painted red.

So the diff image still shows you every AA pixel. They just do not fail the build.
Open a diff and look at the color: **yellow is ignored anti-aliasing, red is a
counted difference.**

> **Info:** Detection costs time - it inspects the neighborhood of every differing pixel. On a clean run with no differences, that cost is zero, because there are no differing pixels to inspect.

## After a CSS framework upgrade

The specific case where teams lose hours: a Tailwind or design-system bump changes
line height or letter spacing by a fraction, every text block re-flows by a
sub-pixel, and hundreds of snapshots fail with nothing visibly wrong.

Work through it in this order:

1. **Turn on anti-aliasing detection.** Removes edge-smoothing noise.
2. **Look at one diff image.** If it is mostly yellow, step 1 solved it. If it is
   red text outlines, the text genuinely moved.
3. **Decide once, not per snapshot.** If the shift is real and intended, it is one
   intentional change across the whole suite, not 300 reviews:

   ```bash
   blazediff-agent check --judge host --json   # confirm what changed
   blazediff-agent rewrite --failed --json     # accept all of it
   ```

4. **Check the diff in the PR.** Baselines are PNGs in git, so the re-baseline is
   reviewable as a single commit.

## When the content actually shifted one pixel

Anti-aliasing detection will not help here, because the pixels really are
different. Three options, worst to best:

**Raise the threshold.** Fast, blunt, and hides real regressions in the same band.
If you do it, use a percentage so it scales with viewport size:

```ts
await expect(screenshot).toMatchImageSnapshot({
  method: "core-native",
  failureThreshold: 0.1,
  failureThresholdType: "percent",
});
```

**Use a structural metric.** [GMSD](/docs/metrics/gmsd) scores edge structure
rather than counting pixels, so a uniform sub-pixel shift barely moves it while a
component that changed shape does:

```bash
blazediff-cli gmsd baseline.png current.png    # gate around 0.05
```

**Let an agent decide.** A one-pixel shift in a footer and a one-pixel shift that
breaks a button's alignment are the same number of pixels and completely
different problems. That distinction needs judgment, not a threshold:

```bash
blazediff-agent check --judge host --json
```

The heuristic pass classifies each failure as `regression-likely`,
`intentional-likely`, `noise-likely`, or `ambiguous`, and only `ambiguous` reaches
the agent. Most runs hand over nothing.

## Tuning the color threshold

`threshold` is a different knob from anti-aliasing. It sets how far apart two
pixels must be, in perceptual color distance, before they count as different at
all.

| Value  | Behavior                                  |
| ------ | ----------------------------------------- |
| `0.0`  | Exact match only                          |
| `0.05` | Strict                                    |
| `0.1`  | Default, balanced                         |
| `0.2`  | Lenient                                   |

Raising `threshold` makes every pixel more forgiving, including the ones you care
about. Prefer anti-aliasing detection, which is targeted, over a higher threshold,
which is not.

## Quick reference

| Symptom                                        | Fix                                        |
| ---------------------------------------------- | ------------------------------------------ |
| Diff image is mostly yellow                    | Already handled. Anti-aliasing is ignored  |
| Diff image is red text outlines everywhere     | Text moved. Re-baseline once, deliberately |
| A few red pixels on curved edges               | Turn on anti-aliasing detection            |
| Every pixel differs slightly                   | Cross-OS rendering, not anti-aliasing      |
| Fails on 4K, passes on 1280px                  | Use a percentage threshold, not a count    |

## Next

- [Cross-OS false positives →](/guides/cross-os-false-positives)
- [Choosing a metric →](/docs/metrics/choosing-a-metric)
- [Judging and harnesses →](/docs/agentic-testing/judging-and-harnesses)

---

# Self-Hosted Visual Regression

**Baselines are PNGs in your git repo, the diff runs on your CI machine, and
review happens in the pull request or a local webapp.** There is no account, no
API key, and no per-snapshot charge, so adding a new screen state costs you disk
space instead of a line item. BlazeDiff is MIT licensed, including commercial and
closed-source use.

## How the pieces sit

| Concern         | Hosted platform                      | Self-hosted with BlazeDiff              |
| --------------- | ------------------------------------- | ---------------------------------------- |
| Baseline storage | Vendor's object storage              | `.blazediff/` in your repo, in git       |
| Diff compute     | Vendor's workers                     | Your CI runner                           |
| Review UI        | Vendor's dashboard                   | PR diff, or `blazediff-agent review` locally |
| Approval record  | Vendor's database                    | A git commit                             |
| Cost per snapshot| Metered                              | Zero marginal cost                       |
| Concurrency limit| Plan tier                            | Your runner's CPU count                  |
| Data location    | Vendor's cloud                       | Your machines                            |

## The cost shape is different

Hosted visual testing bills per snapshot, and a snapshot is one screenshot in one
browser at one viewport. So cost grows with the product of routes, viewports,
browsers, and pull requests. Teams notice this when they add a breakpoint or a
theme, because that multiplies the whole suite at once.

Self-hosting moves the cost into CI minutes, which you were already paying for and
which grow linearly with routes rather than multiplicatively. A 4K pair diffs in
215-269ms with image IO included on the native core, so the diff itself is not
where the time goes - the browser is.

> **Info:** The honest version: you are trading a metered bill for storage and maintenance you own. For a small suite that is clearly better. For 10,000 baselines it means git LFS and a retention policy. Neither is hard, but neither is free of work.

## Set it up

```bash
npm install --save-dev @blazediff/agent
```

```bash
blazediff-agent onboard
blazediff-agent check --json
```

`onboard` writes `.blazediff/config.json`, installs the bundled Chromium, and
captures baselines. Commit `.blazediff/` - config, manifest, and baseline PNGs are
the source of truth.

In CI, one verb:

```yaml
- run: npx blazediff-agent browsers install
- run: npx blazediff-agent check --json --junit visual.xml
```

Or skip the agent entirely and diff files you already have:

```bash
blazediff-cli baseline.png current.png diff.png --threshold 0.1
```

Exit code `0` means match, `1` means differences, `2` means error. That is enough
to gate any pipeline.

## Review without a dashboard

Two options, both local.

**In the pull request.** Baselines are PNGs in git, so an accepted change shows up
as an image diff in the PR. The approval record is the commit and its reviewer.

**In a local webapp.**

```bash
blazediff-agent review
```

Serves an approve/reject UI on `127.0.0.1` reading the report on disk. Nothing is
uploaded. Useful for triaging a downloaded CI artifact.

For machine-readable output, `check --json` returns a slim payload and `--junit`
writes JUnit XML that CI dashboards render natively.

## Who decides on ambiguous diffs

This is the part self-hosting usually loses, because the hosted platforms bundle
a review workflow. BlazeDiff's answer is a heuristic pass plus your own coding
agent:

```bash
blazediff-agent check --judge host --json
```

Confident cases are labelled automatically. Genuinely ambiguous ones get handed to
Claude Code, Cursor, or Codex as cropped `[baseline | actual]` tiles, and the
agent returns a verdict with a reason. No vision service, no API key - it uses the
agent you already run.
[How that works →](/guides/claude-code-visual-review)

## Keeping the repo from bloating

Baseline PNGs are the one real cost.

- Capture at one viewport per breakpoint you actually support, not per device.
- Use `mask` instead of extra entries for regions that vary.
- Turn on git LFS once you pass a few hundred baselines.
- `blazediff-agent reset` clears generated artifacts; `actual/`, `judgments/`, and
  `report.json` are regenerated and do not belong in git.

## What you give up

Worth knowing before you migrate:

- **No hosted history.** Trend charts over months come from your git log, not a
  dashboard.
- **No cross-browser cloud grid.** You run the browsers you install. The agent
  bundles Chromium.
- **You own storage.** Large suites need git LFS.
- **No built-in team workflow.** Approval is your PR review process.

If your team's requirement is a hosted approval dashboard with named reviewers and
retention policies, a SaaS platform genuinely does that better. If your
requirement is a build gate that does not bill per screenshot, self-hosting is
less work than it looks.

## Next

- [Offline with zero API keys →](/guides/offline-visual-regression)
- [Monorepo setup →](/guides/monorepo-visual-regression)
- [Running in CI →](/docs/agentic-testing/running-in-ci)

---

# Offline Visual Regression, Zero API Keys

**BlazeDiff has no account, no API key, and no telemetry.** The comparison
libraries never open a socket. The agent talks only to the app you point it at.
The one thing that can leave your machine is a diff you explicitly hand to a
coding agent, and that is an opt-in backend you can turn off. This page is the
exact network inventory, so a security review can check it rather than trust it.

## What touches the network

| Step                             | Network                              | When            |
| -------------------------------- | ------------------------------------ | --------------- |
| `npm install @blazediff/*`        | Your npm registry                    | Install only    |
| `blazediff-agent browsers install`| Chromium download                    | Install only    |
| `blazediff-cli` / core / ssim / gmsd | **None**                          | Never           |
| `blazediff-agent capture` / `check` | Your own base URL only            | Every run       |
| `blazediff-agent review`          | Binds `127.0.0.1`                    | Local only      |
| `check --judge none`              | **None**                             | Never           |
| `check --judge local`             | Model download on first use, then none | First run only |
| `check --judge host`              | **Your coding agent's provider**     | Per ambiguous diff |

> **Warning:** **Read this before choosing a judge backend.** `--judge host` hands cropped diff regions to Claude Code, Cursor, or Codex, which sends them to whatever model provider that tool uses. BlazeDiff does not upload anything, but your agent does. For an air-gapped or restricted environment, use `--judge none` or `--judge local`.

## The diff libraries are fully offline

`@blazediff/core`, `@blazediff/core-native`, `@blazediff/core-wasm`,
`@blazediff/ssim`, `@blazediff/gmsd`, `@blazediff/cli`, and the Rust and Python
builds do no network I/O at all. They take pixels in and return numbers. There is
no key to configure because there is nothing to authenticate to.

```bash
blazediff-cli baseline.png current.png diff.png --threshold 0.1
```

If your requirement is only "compare two 4K screenshots fast without sending them
anywhere", that command is the whole answer, and the native core does it in
215-269ms with image IO included.

## Air-gapped install

Two artifacts need to cross the boundary once.

**1. Packages.** Mirror them into your internal registry, or pack them:

```bash
npm pack @blazediff/agent @blazediff/core-native @blazediff/cli
# move the tarballs across, then
npm install ./blazediff-agent-*.tgz
```

**2. Chromium.** Playwright's browser download honors a cache directory. Fetch it
on a connected machine, then move the cache:

```bash
# connected machine
PLAYWRIGHT_BROWSERS_PATH=./pw-browsers npx blazediff-agent browsers install
# air-gapped machine, after copying pw-browsers across
export PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers
blazediff-agent browsers install --check --json
```

Then confirm nothing else is needed:

```bash
blazediff-agent check --judge none --json
```

## Judging without leaving the machine

Three levels, pick by policy.

### `--judge none`

Ambiguous diffs simply fail. A human looks at `.blazediff/summary.md` or runs
`blazediff-agent review` for a local approve/reject UI on `127.0.0.1`. No model
involved anywhere. This is the default and the right choice for most restricted
environments.

### `--judge local`

Verdicts from models that run on your hardware:

```bash
blazediff-agent onboard --stack local
blazediff-agent check --judge local --json
```

Moondream describes each changed region, then a deterministic word diff runs, then
Qwen classifies the result. No host round-trip, no key.

The models are ONNX builds fetched from Hugging Face on first use and cached
afterwards. On an air-gapped machine, warm that cache on a connected machine first
and copy it across, the same way as the browser. After that, `--judge local` is
fully offline.

### `--judge host`

Your coding agent reviews. Fastest and most accurate, and the one option where
image crops leave the machine. Use it where policy allows.

## Where the data sits

- **Baselines**: `.blazediff/baselines/`, committed to your repo.
- **Current captures and diffs**: `.blazediff/actual/<id>.png` and
  `.blazediff/actual/<id>.diff.png`, regenerated each run, not committed.
- **Judgment requests**: `.blazediff/judgments/<id>/`, local files.
- **Checkpoints**: `.blazediff/checkpoints/`, so a suspended run can resume.
- **Report**: `.blazediff/report.json` and `.blazediff/summary.md`.

Everything is a file under your repo. Deleting the directory deletes the data.
There is no server-side copy because there is no server.

## Credentials for authenticated routes

Routes behind a login use a harness that reads credentials from environment
variables. Credentials never enter the manifest, the harness file, or any model's
context:

```bash
BLAZEDIFF_AUTH_DEFAULT_EMAIL=...
BLAZEDIFF_AUTH_DEFAULT_PASSWORD=...
```

In CI, set them as secrets. Locally they go in `.blazediff/.env`, which is
gitignored automatically.

## For the security review

- MIT licensed, source on GitHub, no obfuscated binaries beyond compiled Rust
  cores you can build yourself with `cargo build`.
- No analytics, no crash reporting, no license check, no phone-home.
- No API key exists in the product, so there is no credential to rotate or leak.
- Baseline changes are git commits, giving you an audit trail for free.
- CI is check-only: with `CI=1` or no TTY, `onboard`, `capture`, `rewrite`, and
  `reset` are blocked, so a pipeline cannot silently rewrite a baseline.

## Next

- [Self-hosted setup →](/guides/self-hosted-visual-regression)
- [Running in CI →](/docs/agentic-testing/running-in-ci)
- [Judging and harnesses →](/docs/agentic-testing/judging-and-harnesses)

---

# Visual Regression in a Monorepo

**Each app gets its own `.blazediff/` directory, and every command is scoped with
`--cwd`.** There is no central config to keep in sync and no per-app subscription,
because the whole thing is a CLI reading files in a directory.

## Scope every command

```bash
TARGET="$(cd apps/website && pwd -P)"

blazediff-agent --cwd "$TARGET" onboard
blazediff-agent --cwd "$TARGET" check --json
```

> **Warning:** **Always pass an absolute path.** A relative `--cwd` resolves against the current directory, which produces `apps/website/apps/website` the second time you run it from inside the app. The CLI catches that specific case, but absolute paths avoid the class of bug. Do not `cd` into the target either - use `--cwd`.

Each app ends up with its own committed state:

```
apps/
  website/.blazediff/{config.json,manifest.json,baselines/}
  admin/.blazediff/{config.json,manifest.json,baselines/}
packages/
  ui/.blazediff/{config.json,manifest.json,baselines/}
```

Config is per app because dev server command, port, and framework are per app.

## CI, one job per app

```yaml
jobs:
  visual:
    strategy:
      fail-fast: false
      matrix:
        app: [website, admin]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: pnpm install
      - run: npx blazediff-agent browsers install
      - run: npx blazediff-agent --cwd "$PWD/apps/${{ matrix.app }}" check --json
      - if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: blazediff-${{ matrix.app }}
          path: apps/${{ matrix.app }}/.blazediff/
```

`fail-fast: false` matters - you want every app's result, not just the first
failure.

## Only test what changed

Wire it into your task runner so untouched apps are skipped. As a package script:

```json
{
  "scripts": {
    "test:visual": "blazediff-agent check --json"
  }
}
```

```bash
pnpm --filter ...[origin/main] test:visual   # changed packages and dependents
turbo run test:visual --filter='...[origin/main]'
```

Because state lives in each package directory, the filter is doing all the work.
No orchestration layer needed.

## Comparing images without the agent

For files you already have - a design-token render, a chart snapshot, a canvas

```bash
blazediff-cli baseline.png current.png diff.png --threshold 0.05 --antialiasing
```

| Exit code | Meaning        |
| --------- | -------------- |
| `0`       | Identical      |
| `1`       | Differences    |
| `2`       | Error          |

## Choosing a threshold algorithm

One CLI, several algorithms. Pick per package rather than forcing one on the repo:

| Command                          | Output                       | Good for                            |
| -------------------------------- | ---------------------------- | ----------------------------------- |
| `blazediff-cli` (`core-native`)  | Changed pixel count          | The default. Fastest, exact          |
| `blazediff-cli core`             | Changed pixel count          | Custom diff colors, no native binary |
| `blazediff-cli gmsd`             | Score, lower is better       | Compressed or noisy renders          |
| `blazediff-cli ssim`             | Score, higher is better      | Perceived quality                    |
| `blazediff-cli hitchhikers-ssim` | Score, higher is better      | Large batches, ~4x faster than SSIM  |

Knobs on the pixel path are `--threshold` (per-pixel color distance, `0`-`1`) and
`--antialiasing` (ignore smoothed-edge pixels). Details in
[choosing a metric](/docs/metrics/choosing-a-metric).

## In component tests

Packages that render components rather than pages can skip the browser layer
entirely and use the matcher:

```ts
import "@blazediff/vitest"; // or @blazediff/jest, @blazediff/bun

await expect(pngBuffer).toMatchImageSnapshot({
  method: "core-native",
  failureThreshold: 0.1,
  failureThresholdType: "percent",
});
```

Snapshots land in `__snapshots__` next to the test. Mixing this with the agent in
the same repo is normal: the matcher for components, the agent for full routes.

## Shared masks across apps

Anything matching `data-blazediff-agent-mask` is masked on every route, with no
manifest change. Put it in a shared component in `packages/ui` and every app that
renders it inherits the mask:

```tsx
<div data-blazediff-agent-mask="live-metrics">...</div>
```

That is usually better than maintaining selector lists per app.

## Next

- [Self-hosted setup →](/guides/self-hosted-visual-regression)
- [Running in CI →](/docs/agentic-testing/running-in-ci)
- [CLI reference →](/apis/cli)

---

# Speed Up 4K Screenshot Diffs

**Most of the time in a "slow image diff" is not the diff.** It is PNG decoding,
the browser, and writing diff images you never look at. Fix those three and the
comparison itself drops to a couple of hundred milliseconds per 4K pair, running
entirely on your own machine.

## Where the time actually goes

Measured on an M1 Max, from the [pixel benchmarks](/benchmarks/pixel-by-pixel):

| Work                                          | 4K pair       |
| --------------------------------------------- | ------------- |
| Native Rust diff, **image IO included**       | 215-269ms     |
| Pure JS diff, image IO **excluded**           | 97-136ms      |
| WebAssembly diff, image IO excluded           | 33-68ms       |
| pixelmatch, image IO excluded                 | 202-423ms     |
| odiff, image IO included                      | 1157-1677ms   |

Compare rows 1 and 3: the same algorithm takes 33ms on decoded pixels and 215ms
when it has to decode two PNGs first. **Decoding is the majority of the wall
clock.** Optimizing the comparison while re-decoding the same baseline on every
test is the usual mistake.

## 1. Pick the right core

| Package                  | Runs on            | Input                     | Use when                                  |
| ------------------------ | ------------------- | ------------------------- | ----------------------------------------- |
| `@blazediff/core-native` | Node, native binary | File paths or encoded bytes | CI. Fastest end-to-end, decode included |
| `@blazediff/core-wasm`   | Anywhere wasm runs  | Decoded RGBA              | You already have pixels in memory          |
| `@blazediff/core`        | Node, browser       | Decoded RGBA              | No native dependency allowed               |

If you are on `pixelmatch` today, `@blazediff/core` is a drop-in replacement with
an identical API, about 1.5x faster. `core-native` is the bigger jump but takes
file paths.

```ts
import { compare } from "@blazediff/core-native";

const result = await compare("baseline.png", "current.png", "diff.png", {
  threshold: 0.1,
  antialiasing: true,
});
```

## 2. Stop writing diff images you do not read

This is the single biggest easy win in JS. Passing an output buffer forces the
diff to paint every pixel, not just count them:

| Mode                  | Average improvement over pixelmatch |
| --------------------- | ----------------------------------- |
| No output buffer      | ~62%                                |
| With output buffer    | ~28%                                |

So pass `undefined` when a green run does not need a picture:

```ts
const changed = diff(img1, img2, undefined, width, height);
```

With the agent, same idea:

```bash
blazediff-agent check --no-diff-png --json
```

Generate the diff image only on failure, in a second pass.

## 3. Let identical pairs short-circuit

Most snapshots in a healthy suite do not change. `@blazediff/core` has
`fastBufferCheck` on by default, which detects byte-identical inputs before doing
any per-pixel work. On identical 4K pairs that is 2.5-3.8ms against pixelmatch's
24-28ms - roughly 88% faster.

Do not disable it unless you are benchmarking.

## 4. Parallelize, but cap it

The agent captures in parallel, defaulting to CPU count capped at 8:

```bash
blazediff-agent check --concurrency 4 --json
```

Higher is not always faster. Every parallel browser context costs memory, and a CI
runner that starts swapping is far slower than one running four at a time. If your
job is memory-constrained, see
[reducing snapshot memory](/guides/reduce-snapshot-memory).

In Jest and Vitest, the matcher runs comparisons in a worker thread by default
(`runInWorker: true`), keeping the diff off the test thread.

## 5. Do not re-encode at level 9

The CLI writes diff PNGs at compression level `0` by default, which is the right
call - a diff image is a temporary artifact, not something to optimize for size.
Raising `-c` trades real CPU for disk you do not care about.

## Why it is fast

Two passes over the image. The cold pass compares blocks and skips regions where
nothing changed, using SIMD - NEON on ARM, SSE4.1 on x86, `v128` in WebAssembly.
The hot pass does per-pixel work only inside blocks that actually differ. On a
screenshot where 2% of the page changed, 98% of the image is settled by wide
vector compares.

Scan strategy is chosen at runtime from a density probe, so a sparse diff and a
dense one take different paths.

## The 20-minute test suite

If a Node suite spends 20 minutes on high-resolution canvas output, walk through
this order:

1. **Are you decoding the baseline once per test?** Decode once, reuse the RGBA
   buffer. This is usually the whole problem.
2. **Are you writing a diff image every run?** Drop the output buffer on the pass
   path.
3. **Still on pixelmatch?** Swap in `@blazediff/core` - same API, ~1.5x.
4. **Have pixels already in memory?** `@blazediff/core-wasm` runs a 4K pair in
   33-68ms.
5. **Working from files?** `@blazediff/core-native` handles decode in Rust.
6. **Is the browser the bottleneck?** Time capture separately from diff before
   optimizing further. It usually is.

> **Info:** Everything here runs locally. There is no upload step, so 4K screenshots of a private app never leave the machine, and speed does not depend on someone else's queue.

## Next

- [Reduce snapshot memory →](/guides/reduce-snapshot-memory)
- [Browser and edge diffing →](/guides/browser-and-edge-image-diffing)
- [Full benchmarks →](/benchmarks/pixel-by-pixel)

---

# Reduce Snapshot Memory

**A screenshot suite runs out of memory for two reasons: decoded pixel buffers and
live browser contexts.** A PNG on disk is small; the same image decoded is
`width x height x 4` bytes, and a comparison holds at least two of them. Cap
concurrency, skip the output buffer, and keep decoding out of the JS heap.

## Do the arithmetic first

Decoded RGBA is four bytes per pixel, uncompressed:

| Image                     | Pixels  | Decoded size |
| ------------------------- | ------- | ------------ |
| 1280 x 720                | 0.9M    | ~3.7 MB      |
| 1920 x 1080               | 2.1M    | ~8.3 MB      |
| 3840 x 2160               | 8.3M    | ~33 MB       |
| Full-page 1440 x 8000     | 11.5M   | ~46 MB       |

A single 4K comparison holding baseline, current, and an output buffer is about
**100 MB**, and full-page screenshots of a long page are worse than 4K. Run eight
of those in parallel and you are at 800 MB before counting the browser.

## 1. Skip the output buffer

The diff image is often the largest single allocation and it is optional. Pass
`undefined` and you get the changed-pixel count without allocating a third
full-size buffer:

```ts
const changed = diff(img1, img2, undefined, width, height);
```

With the agent:

```bash
blazediff-agent check --no-diff-png --json
```

Write diff images only for entries that actually failed.

## 2. Keep decode out of the JS heap

`@blazediff/core-native` takes file paths or encoded bytes and decodes in Rust.
The pixel buffers live in native memory, not on the V8 heap, so they are not
competing with your test runner for the same budget and are freed as soon as the
call returns.

```ts
import { compare } from "@blazediff/core-native";
await compare("baseline.png", "current.png", "diff.png");
```

> **Info:** Encoded buffers are passed across the N-API boundary by reference - Rust borrows the JavaScript backing memory instead of copying it. Decoding still allocates native RGBA buffers, so the saving is the copy and the heap pressure, not the pixels themselves.

## 3. Cap concurrency

Peak memory is roughly `per-comparison cost x concurrency`. The agent defaults to
CPU count capped at 8, which is too many on a small runner:

```bash
blazediff-agent check --concurrency 3 --json
```

A GitHub-hosted runner with 7 GB doing full-page 4K captures wants 2 to 4, not 8.
Lowering concurrency often makes the job *faster*, because it stops the runner
swapping.

In Vitest and Jest, cap the runner too - `maxWorkers` multiplies against whatever
each worker holds.

## 4. Do not hold buffers across tests

The common leak in a snapshot suite:

```ts
// leaks: every screenshot stays reachable for the whole file
const shots = [];
for (const route of routes) {
  shots.push(await page.screenshot());
}
```

Compare inside the loop and let each buffer go:

```ts
for (const route of routes) {
  const shot = await page.screenshot();
  await expect(shot).toMatchImageSnapshot({ snapshotIdentifier: route });
}
```

Same for module-level caches of decoded baselines. Caching one shared baseline is
fine; caching every baseline in a 300-route suite is 10 GB.

## 5. The browser is usually the bigger half

Chromium typically outweighs the diff. Two things help most:

- **Reuse contexts instead of launching browsers.** The agent keeps one browser
  and pools contexts per viewport.
- **Avoid `fullPage` where you do not need it.** A full-page screenshot of an
  infinite-scroll page can be 20x the viewport, and it is both the memory spike
  and the flakiness source. Capture the viewport, or split the page into entries.

Measure the split before optimizing the wrong half:

```bash
/usr/bin/time -v npx blazediff-agent check --json
```

## 6. Review with tiles, not full pages

When a diff needs a human or an agent, the region tiles are 10 to 100x smaller
than the full-page PNGs. `regions.png` is a stack of cropped `[baseline | actual]`
pairs and `locator.png` is a ~400px overview. Loading those instead of two 33 MB
images is the difference between a review that fits in memory and one that does
not.

## Quick reference

| Symptom                                     | Fix                                        |
| ------------------------------------------- | ------------------------------------------ |
| OOM at high parallelism                     | Lower `--concurrency`                       |
| Memory climbs across a test file            | Do not collect screenshots in an array      |
| Spike only on some routes                   | Those are `fullPage` on long pages          |
| Steady high baseline before any test runs   | Module-level cache of decoded baselines     |
| Node heap OOM specifically                  | Move to `core-native`, decode outside V8    |

## Next

- [Speed up 4K diffs →](/guides/speed-up-4k-screenshot-diffs)
- [Running in CI →](/docs/agentic-testing/running-in-ci)

---

# Browser and Edge Image Diffing

**`@blazediff/core-wasm` is a ~32KB WebAssembly build of the same Rust diff core,
compiled to `wasm32` with `v128` SIMD.** It runs in browsers, Web Workers, Deno,
Bun, Cloudflare Workers, and any other wasm host, with no native dependency and no
network call. On a 4K pair it is about 90% faster than pixelmatch.

## Measured speed

From the [pixel benchmarks](/benchmarks/pixel-by-pixel), M1 Max, image IO
excluded, ~50.8% faster than pixelmatch on average across the fixture set:

| Fixture | pixelmatch | `core-wasm` | Improvement |
| ------- | ---------- | ----------- | ----------- |
| 4k/1    | 332.26ms   | 33.18ms     | 90.0%       |
| 4k/2    | 333.33ms   | 68.37ms     | 79.5%       |
| 4k/3    | 423.14ms   | 44.65ms     | 89.4%       |
| page/2  | 513.06ms   | 73.42ms     | 85.7%       |

Counts agree with pixelmatch to within about 0.05% - both use a YIQ-style
perceptual delta, so they classify the same pixels apart from a handful of edge
cases.

> **Warning:** This is CPU SIMD (`v128`), not GPU acceleration. There is no WebGPU path. The speed comes from 128-bit vector compares and a block-based scan that skips unchanged regions, so it needs a host with SIMD enabled - every current browser and edge runtime qualifies.

## You decode, it compares

The API takes pre-decoded RGBA buffers. It does not ship a PNG decoder, which is
most of why the module is 32KB. In a browser, the platform already has one:

```ts
async function toRGBA(blob: Blob) {
  const bitmap = await createImageBitmap(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(baselineBlob);
const b = await toRGBA(currentBlob);
const output = new Uint8Array(a.data.length);

const changed = diff(a.data, b.data, output, a.width, a.height, {
  threshold: 0.1,
});
```

`getImageData` is the slow part of that snippet, not the diff. The `ImageDecoder`
API avoids the canvas round-trip where it is available.

Full setup in [Rust + WASM in JavaScript →](/docs/pixel-comparison/rust-wasm).

## Runtime support

| Runtime                | Package                  | Notes                                  |
| ---------------------- | ------------------------- | -------------------------------------- |
| Browser main thread    | `@blazediff/core-wasm`    | Blocks the thread. Prefer a Worker     |
| Web Worker             | `@blazediff/core-wasm`    | Recommended for anything above 1080p   |
| Cloudflare Workers     | `@blazediff/core-wasm`    | No filesystem, pass buffers            |
| Deno                   | `@blazediff/core-wasm`    | Also on JSR                            |
| Bun                    | `@blazediff/core-wasm`    | npm or JSR                             |
| Node server            | `@blazediff/core-native`  | Faster still, handles decode itself     |
| Pure JS, no wasm       | `@blazediff/core`         | Drop-in for pixelmatch                 |

> **Info:** On the main thread a 4K diff is 30-70ms, which is several dropped frames. Put it in a Worker and post the result back.

## Edge constraints worth knowing

- **Bundle size.** ~32KB of wasm fits comfortably inside a Workers bundle.
- **No filesystem.** Everything is buffers. That is already the API.
- **CPU time limits.** A 4K diff at 33-68ms is fine for most edge budgets. Diffing
  a batch in one request is not - fan out instead.
- **Cold start.** The module instantiates once per isolate. Instantiate at module
  scope, not per request.
- **Memory.** Decoded RGBA is `width x height x 4`. A 4K pair plus an output buffer
  is around 100MB, which exceeds some edge memory limits. Skip the output buffer
  when you only need the count.

## The rest of the ecosystem

Same algorithm, different hosts:

| Package / crate           | Runtime            | Registry     |
| ------------------------- | ------------------ | ------------ |
| `@blazediff/core`         | Node, browser      | npm, JSR     |
| `@blazediff/core-wasm`    | Browser, edge, any wasm host | npm, JSR |
| `@blazediff/core-native`  | Node, Bun          | npm          |
| `blazediff` (crate)       | Rust               | crates.io    |
| `blazediff` (PyPI)        | Python             | PyPI         |
| `@blazediff/ssim`, `@blazediff/gmsd` | Node, browser | npm, JSR |
| `@blazediff/react`, `@blazediff/ui`  | Browser        | npm          |

All MIT licensed. The JS packages are dual-published to npm and JSR, so Deno and
Bun can resolve them natively.

## Showing the result

`@blazediff/ui` is a framework-agnostic renderer for image-diff views and
`@blazediff/react` wraps it for React: `<SwipeMode />` for a drag slider,
`<TwoUpMode />` for side by side, plus `<OnionSkinMode />` and
`<DifferenceMode />`. Useful when the client-side diff needs to be shown, not
just counted.

[React components →](/docs/ui-components/react) ·
[Vanilla →](/docs/ui-components/vanilla)

## Next

- [Rust + WASM in JavaScript →](/docs/pixel-comparison/rust-wasm)
- [Speed up 4K diffs →](/guides/speed-up-4k-screenshot-diffs)
- [Full benchmarks →](/benchmarks/pixel-by-pixel)

---
