# BlazeDiff > BlazeDiff is a high-performance diff ecosystem. Originally built in JavaScript as a pixel-perfect image comparison library that's 1.5x faster than [pixelmatch](https://github.com/mapbox/pixelmatch). Now, BlazeDiff has evolved into a comprehensive suite of blazing-fast diff tools including image comparison, image diff analysis deterministically + agent-in-the-loop verdict, object diffing, perceptual quality metrics, framework-agnostic UI renderers, and React components for visualizing differences. **Install (npm):** `npm install @blazediff/core` **Install (JSR):** `deno add jsr:@blazediff/core` ## Performance - **Native (Rust)**: 3-4x faster than odiff, 8x faster than pixelmatch on 4K images - **WASM**: ~58% faster than pixelmatch, up to ~5x on 4K (browser, edge, any wasm host) - **Image Pixel-by-Pixel (JS)**: ~50% faster than pixelmatch (up to 88% on identical images) - **SSIM**: ~25% faster than ssim.js, ~70% faster with Hitchhiker's SSIM - **Object Diff**: ~55% faster than microdiff (up to 96% on identical arrays) --- # Packages # @blazediff/core High-performance pixel-by-pixel image comparison library. 1.5x faster than [pixelmatch](https://github.com/mapbox/pixelmatch) while maintaining identical accuracy. [View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md) ## Installation ```sh npm install @blazediff/core ``` ## Features - **1.5x faster** than pixelmatch on average, **88% faster** on identical images - **100% API compatible** with pixelmatch - drop-in replacement - **Zero dependencies** ## API Reference ### `blazediff(image1, image2, output, width, height, options?)` Compares two images pixel by pixel and returns the number of different pixels. #### Parameters | Parameter | Type | Description | | --------- | --------------------------------------------------- | ------------------------------------------- | | `image1` | `Buffer \| Uint8Array \| Uint8ClampedArray` | Image data of the first image | | `image2` | `Buffer \| Uint8Array \| Uint8ClampedArray` | Image data of the second image | | `output` | `Buffer \| Uint8Array \| Uint8ClampedArray \| null` | Output buffer for the diff image (optional) | | `width` | `number` | Width of the images in pixels | | `height` | `number` | Height of the images in pixels | | `options` | `Options` | Comparison options (optional) | ##### Options | Option | Type | Default | Description | | ----------------- | --------- | ------------- | ------------------------------------------------ | | `threshold` | `number` | `0.1` | Matching threshold (0-1). Lower = more sensitive | | `includeAA` | `boolean` | `false` | Include anti-aliased pixels in diff count | | `alpha` | `number` | `0.1` | Opacity of original image in diff output | | `aaColor` | `[R,G,B]` | `[255,255,0]` | Color of anti-aliased pixels (yellow) | | `diffColor` | `[R,G,B]` | `[255,0,0]` | Color of different pixels (red) | | `diffColorAlt` | `[R,G,B]` | `null` | Alternative color for dark differences | | `diffMask` | `boolean` | `false` | Draw diff as a mask with transparent background | | `fastBufferCheck` | `boolean` | `true` | Use fast buffer comparison for identical images | > **Info:** **Threshold Guidelines:** - `0.0` - Exact match only - `0.05` - Strict comparison - `0.1` - Default balanced comparison - `0.2` - Lenient comparison ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff) - [NPM Package](https://www.npmjs.com/package/@blazediff/core) - [Examples →](/docs/pixel-comparison/vanilla-javascript) --- # @blazediff/ssim Fast SSIM (Structural Similarity Index) implementations for perceptual image quality assessment. Includes standard SSIM, MS-SSIM (Multi-Scale SSIM), and Hitchhiker's SSIM for various use cases and performance requirements. ## Installation ```sh npm install @blazediff/ssim ``` ## Features - **Three variants** - Standard SSIM, MS-SSIM, and Hitchhiker's SSIM (~4x faster) - **MATLAB-compatible** - Standard SSIM matches reference implementation with \<0.01% error - **SSIM map output** - Optional grayscale visualization of similarity ## API Reference ### `ssim(image1, image2, output, width, height, options?)` Compares two images using standard SSIM metric and returns a similarity score. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------------- | ------------------------------------------------- | | `image1` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the first image | | `image2` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the second image | | `output` | `Buffer`, `Uint8Array`, `Uint8ClampedArray`, or `undefined` | Optional output buffer for SSIM map visualization | | `width` | `number` | Width of the images in pixels | | `height` | `number` | Height of the images in pixels | | `options` | `SsimOptions` | Comparison options (optional) | ##### Options | Option | Type | Default | Description | | ------------ | -------- | ------- | --------------------------------- | | `windowSize` | `number` | `11` | Size of the Gaussian window | | `k1` | `number` | `0.01` | Algorithm parameter for luminance | | `k2` | `number` | `0.03` | Algorithm parameter for contrast | | `L` | `number` | `255` | Dynamic range of pixel values | ##### Returns `number` - SSIM score between 0 and 1 ### `msssim(image1, image2, output, width, height, options?)` Compares two images using MS-SSIM (Multi-Scale SSIM) metric. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------------- | --------------------------------------------------- | | `image1` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the first image | | `image2` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the second image | | `output` | `Buffer`, `Uint8Array`, `Uint8ClampedArray`, or `undefined` | Optional output buffer for SSIM map at finest scale | | `width` | `number` | Width of the images in pixels | | `height` | `number` | Height of the images in pixels | | `options` | `MsssimOptions` | Comparison options (optional) | ##### Options | Option | Type | Default | Description | | ------------ | ---------------------- | ------------------------------------------ | --------------------------- | | `windowSize` | `number` | `11` | Size of the Gaussian window | | `scales` | `number` | `5` | Number of scales to use | | `weights` | `number[]` | `[0.0448, 0.2856, 0.3001, 0.2363, 0.1333]` | Weights for each scale | | `method` | `'product'` or `'sum'` | `'product'` | Aggregation method | ##### Returns `number` - MS-SSIM score between 0 and 1 ### `hitchhikersSSIM(image1, image2, output, width, height, options?)` Compares two images using Hitchhiker's SSIM (fast rectangular-window version). #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------------- | ----------------------------------- | | `image1` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the first image | | `image2` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the second image | | `output` | `Buffer`, `Uint8Array`, `Uint8ClampedArray`, or `undefined` | Optional output buffer for SSIM map | | `width` | `number` | Width of the images in pixels | | `height` | `number` | Height of the images in pixels | | `options` | `HitchhikersSsimOptions` | Comparison options (optional) | ##### Options | Option | Type | Default | Description | | -------------- | --------- | ------------ | ------------------------------------------------------ | | `windowSize` | `number` | `11` | Size of the rectangular window | | `windowStride` | `number` | `windowSize` | Stride for window sliding (non-overlapping by default) | | `covPooling` | `boolean` | `true` | Use Coefficient of Variation pooling (recommended) | | `k1` | `number` | `0.01` | Algorithm parameter for luminance | | `k2` | `number` | `0.03` | Algorithm parameter for contrast | | `L` | `number` | `255` | Dynamic range of pixel values | ##### Returns `number` - SSIM score between 0 and 1 **Performance**: ~4x faster than standard SSIM using integral images for O(1) window computation. ### Score Interpretation | Score Range | Similarity Level | Description | | ----------- | ---------------- | ------------------------------------------------------- | | `1.0` | Identical | Images are identical or perceptually identical | | `0.99+` | Excellent | Extremely high similarity (minor compression artifacts) | | `0.95-0.99` | Very Good | High similarity (small compression or noise) | | `0.90-0.95` | Good | Noticeable but acceptable differences | | `0.80-0.90` | Fair | Significant but tolerable differences | | `<0.80` | Poor | Major structural differences | > **Info:** **Threshold Guidelines:** - Use threshold `>0.99` for strict visual regression testing - Use threshold `>0.95` for standard visual regression testing - Scores below `0.90` indicate substantial visual differences ## Usage Examples ```typescript import ssim from "@blazediff/ssim/ssim"; // Basic usage const score = ssim(img1.data, img2.data, undefined, width, height); // With SSIM map output const output = new Uint8ClampedArray(width * height * 4); const score = ssim(img1.data, img2.data, output, width, height); // With custom options const score = ssim(img1.data, img2.data, undefined, width, height, { windowSize: 8, k1: 0.01, k2: 0.03, }); ``` ```typescript import msssim from "@blazediff/ssim/msssim"; // Basic usage const score = msssim(img1.data, img2.data, undefined, width, height); // With SSIM map at finest scale const output = new Uint8ClampedArray(width * height * 4); const score = msssim(img1.data, img2.data, output, width, height); // With custom scales and weights const score = msssim(img1.data, img2.data, undefined, width, height, { scales: 3, weights: [0.33, 0.33, 0.34], method: "sum", }); ``` ```typescript import hitchhikersSSIM from "@blazediff/ssim/hitchhikers-ssim"; // Basic usage with CoV pooling (recommended) const score = hitchhikersSSIM(img1.data, img2.data, undefined, width, height); // With mean pooling (traditional) const score = hitchhikersSSIM(img1.data, img2.data, undefined, width, height, { covPooling: true, }); // With custom window and stride const score = hitchhikersSSIM(img1.data, img2.data, undefined, width, height, { windowSize: 16, windowStride: 8, // Overlapping windows covPooling: true, }); ``` ## CLI Usage All three variants are available via the `@blazediff/cli` CLI: ```bash # Standard SSIM blazediff-cli ssim image1.png image2.png # MS-SSIM blazediff-cli msssim image1.png image2.png # Hitchhiker's SSIM blazediff-cli hitchhikers-ssim image1.png image2.png # With options blazediff-cli hitchhikers-ssim image1.png image2.png --window-size 16 --no-cov-pooling ``` ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff) - [NPM Package](https://www.npmjs.com/package/@blazediff/ssim) - [SSIM Paper](https://ieeexplore.ieee.org/document/1284395) - Wang et al. (2004) - [MS-SSIM Paper](https://ieeexplore.ieee.org/document/1292216) - Wang et al. (2003) - [Hitchhiker's SSIM Paper](https://ieeexplore.ieee.org/document/9345560) - Venkataramanan et al. (2021) - [Examples →](/docs/structural-comparison) --- # @blazediff/gmsd Fast single-threaded GMSD (Gradient Magnitude Similarity Deviation) metric for perceptual image quality assessment. Perfect for CI visual testing where you need a similarity score rather than pixel-by-pixel differences. ## Installation ```sh npm install @blazediff/gmsd ``` ## Features - **Gradient-based** - Measures structural similarity via gradient magnitudes, tolerant to compression artifacts - **GMS map output** - Optional grayscale visualization of gradient similarity [Mathematical details (FORMULA.md)](https://github.com/teimurjan/blazediff/blob/main/packages/gmsd/FORMULA.md) ## API Reference ### `gmsd(image1, image2, output, width, height, options?)` Compares two images using GMSD metric and returns a similarity score. #### Parameters | Parameter | Type | Description | | --------- | -------------------------------------------- | ------------------------------------------------ | | `image1` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the first image | | `image2` | `Buffer`, `Uint8Array`, or `Uint8ClampedArray` | Image data of the second image | | `output` | `Buffer`, `Uint8Array`, `Uint8ClampedArray`, or `undefined` | Optional output buffer for GMS map visualization | | `width` | `number` | Width of the images in pixels | | `height` | `number` | Height of the images in pixels | | `options` | `GmsdOptions` | Comparison options (optional) | ##### Options | Option | Type | Default | Description | | ------------ | -------- | ------- | ----------------------------------------------------- | | `downsample` | `0` or `1` | `0` | Downsample factor (0 = no downsampling, 1 = half) | | `c` | `number` | `170` | Constant for numerical stability (tuned for Prewitt) | ##### Returns `number` - Difference score between 0 and 1: - `0.0` - Images are identical or perceptually identical - `0.0-0.05` - Very low difference (minor compression artifacts) - `0.05-0.15` - Low difference (noticeable but small changes) - `0.15-0.35` - Moderate similarity (significant structural differences) - `>0.35` - High difference (major differences) > **Info:** **Score Guidelines:** - Use threshold `>0.0` for strict regression testing - Use threshold `>0.15` for loose regression testing with compression - Scores below `0.35` indicate substantial visual differences ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff) - [NPM Package](https://www.npmjs.com/package/@blazediff/gmsd) - [FORMULA.md](https://github.com/teimurjan/blazediff/blob/main/packages/gmsd/FORMULA.md) - Mathematical foundation - [Original GMSD Paper](http://www4.comp.polyu.edu.hk/~cslzhang/IQA/TIP_IQA_GMSD.pdf) - Xue et al. (2014) - [Examples →](/docs/structural-comparison) --- # @blazediff/object Structural object comparison with path tracking, cycle detection, and CREATE/REMOVE/CHANGE types. ## Installation ```sh npm install @blazediff/object ``` ## Features - **Path tracking** for nested modifications - **Handles** primitives, objects, arrays, dates, regex, and circular references - **Consistent shapes** for V8 optimization (type, path, value, oldValue) ## Quick Start ```javascript import diff from '@blazediff/object'; const oldObj = { name: "John", age: 30, city: "NYC", skills: ["JavaScript", "TypeScript"] }; const newObj = { name: "John", age: 31, city: "San Francisco", skills: ["JavaScript", "TypeScript", "Go"], active: true }; const changes = diff(oldObj, newObj); console.log(changes); ``` **Output:** ```json [ { "type": 2, "path": ["age"], "value": 31, "oldValue": 30 }, { "type": 2, "path": ["city"], "value": "San Francisco", "oldValue": "NYC" }, { "type": 0, "path": ["skills", 2], "value": "Go", "oldValue": undefined }, { "type": 0, "path": ["active"], "value": true, "oldValue": undefined } ] ``` ## API Reference ### `diff(oldObj, newObj, options?)` Compares two objects and returns an array of differences. #### Parameters | Parameter | Type | Description | | --------- | -------- | ------------------------------------- | | `oldObj` | `any` | The original object to compare from | | `newObj` | `any` | The new object to compare to | | `options` | `object` | Configuration options (optional) | #### Options | Option | Type | Default | Description | | --------------- | --------- | ------- | ---------------------------------------------- | | `detectCycles` | `boolean` | `true` | Enable circular reference detection | #### Returns Returns `Difference[]` - Array of difference objects with consistent structure: ```typescript interface Difference { type: DifferenceType; path: (string | number)[]; value: any; oldValue: any; } ``` ### Difference Types > **Info:** **Difference Types** are represented as numbers for optimal performance: | Type | Name | Description | | ---- | -------- | -------------------------------------- | | `0` | `CREATE` | Property or array element was added | | `1` | `REMOVE` | Property or array element was deleted | | `2` | `CHANGE` | Property or array element was modified | All difference objects maintain consistent shape with `type`, `path`, `value`, and `oldValue` fields for optimal V8 performance. ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff) - [NPM Package](https://www.npmjs.com/package/@blazediff/object) - [Examples →](/docs/object-comparison) --- # @blazediff/cli CLI for image comparison. Wraps the native Rust binary (fastest), JS pixel diff, GMSD, SSIM, MS-SSIM, and Hitchhiker's SSIM. [View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md) ## Installation ### Global Installation ```sh npm install -g @blazediff/cli ``` ### Local Installation ```sh npm install --save-dev @blazediff/cli ``` ### Using npx ```sh npx blazediff-cli image1.png image2.png diff.png ``` ## Available Commands ### `blazediff-cli core-native` (default) Native Rust binary with SIMD optimization. The fastest option - **3-4x faster** than odiff on large images. #### Basic Usage ```bash # Default command (core-native) blazediff-cli image1.png image2.png diff.png # Or explicitly blazediff-cli core-native image1.png image2.png diff.png ``` #### With Options ```bash blazediff-cli image1.png image2.png diff.png --threshold 0.05 --antialiasing ``` #### Options ```bash blazediff-cli core-native [output] [options] Options: -t, --threshold Color difference threshold (0-1, default: 0.1) -a, --antialiasing Enable anti-aliasing detection --diff-mask Output only differences (transparent background) -c, --compression PNG compression level (0-9, default: 0) --interpret Run structured interpretation (region detection + classification) --output-format Output format: png (default) or html (interpret report) -h, --help Display help ``` #### Exit Codes - `0` - Images are identical - `1` - Images have differences - `2` - Error (file not found, invalid format, etc.) ```bash blazediff-cli image1.png image2.png diff.png if [ $? -eq 0 ]; then echo "Images match!" else echo "Images differ!" fi ``` > **Info:** **Why so fast?** Uses a two-pass block-based algorithm with SIMD acceleration (NEON on ARM, SSE4.1 on x86). The cold pass quickly identifies unchanged blocks, then the hot pass only processes changed regions. #### Interpret Mode Add `--interpret` to get structured diff analysis - region detection, content-aware classification (Addition, Deletion, Shift, ContentChange, ColorChange, RenderingNoise), severity scoring, and human-readable summaries. ```bash # JSON output to stdout blazediff-cli image1.png image2.png --interpret # With diff image + interpretation blazediff-cli image1.png image2.png diff.png --interpret # HTML report blazediff-cli image1.png image2.png report.html --output-format html ``` ##### Example ```bash $ blazediff-cli image1.png image2.png --interpret { "summary": "Moderate visual change detected (1.87% of image, 10 regions).\n...", "severity": "Medium", "diffPercentage": 1.87, "regions": [...] } $ blazediff-cli image1.png image2.png report.html --output-format html # writes report.html with side-by-side images and clickable region rows ``` ### `blazediff-cli core` Pure JavaScript pixel-by-pixel comparison. Slower than `core-native` but offers more customization options like custom diff colors and color spaces. #### Basic Usage ```bash blazediff-cli core image1.png image2.png ``` #### Save Diff Image ```bash blazediff-cli core image1.png image2.png --output diff.png ``` #### Options ```bash blazediff-cli core [options] Options: -o, --output Output diff image path -t, --threshold Matching threshold (0-1, default: 0.1) -a, --alpha Opacity of original in diff (0-1, default: 0.1) --diff-color Color for different pixels (default: 255,0,0) --aa-color 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 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 [options] Options: -o, --output Output GMS map image path --downsample 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 [options] Options: -o, --output Output SSIM map image path --window-size Gaussian window size (default: 11) --k1 Algorithm parameter (default: 0.01) --k2 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 [options] Options: -o, --output Output SSIM map at finest scale --window-size Gaussian window size (default: 11) --scales Number of scales (default: 5) --method 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 [options] Options: -o, --output Output SSIM map image path --window-size Rectangular window size (default: 11) --window-stride Window stride (default: window-size) --no-cov-pooling Disable CoV pooling (use mean pooling) --k1 Algorithm parameter (default: 0.01) --k2 Algorithm parameter (default: 0.03) -h, --help Display help ``` Prints the SSIM score (higher = more similar, same scale as SSIM). CoV pooling is enabled by default. ```bash $ blazediff-cli hitchhikers-ssim reference.png test.png SSIM: 0.9597 ``` ## When to Use Each Algorithm | Algorithm | Best for | |-----------|----------| | `core-native` (default) | Maximum speed, CI/CD, large images, `--interpret` analysis | | `core` | Custom diff colors, color space control, no native deps | | `gmsd` | Similarity score, compression-tolerant | | `ssim` | MATLAB-compatible, research | | `msssim` | Multi-scale, varying resolutions | | `hitchhikers-ssim` | Fast SSIM (~4x), large batches | ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff) - [NPM Package](https://www.npmjs.com/package/@blazediff/cli) - [Examples →](/docs/pixel-comparison/vanilla-javascript) --- # @blazediff/agent Agentic visual regression for BlazeDiff. Auto-discovers routes, captures deterministic screenshots, diffs them against committed baselines with the native BlazeDiff core, and hands ambiguous diffs back to your coding agent (Claude Code, Cursor, Codex) to judge. The package ships a deterministic CLI (`blazediff-agent`) plus a portable playbook (`SKILL.md`) that any coding agent drives. No embedded LLM call, no API key in the default flow - your coding agent supplies the loop, vision, and context engineering. [View on GitHub](https://github.com/teimurjan/blazediff/tree/main/packages/agent) · [Landing page](/agent) ## Installation ### Global ```sh npm install -g @blazediff/agent ``` ### Local ```sh npm install --save-dev @blazediff/agent ``` ### Chromium First run will prompt to install bundled Playwright Chromium - no sudo, no `npx playwright install --with-deps`. ```sh blazediff-agent browsers install --check --json # check blazediff-agent browsers install # install if missing ``` ## Onboard a coding agent `blazediff-agent onboard` installs the BlazeDiff playbook into whatever coding-agent stack lives in your project. Run once per project. ```sh # Auto-detect (Claude Code, Codex, Cursor) blazediff-agent onboard --json # Explicit blazediff-agent onboard --stack codex blazediff-agent onboard --stack claude,codex blazediff-agent onboard --stack all # No coding agent - install the local (Moondream + Qwen) judge blazediff-agent onboard --stack local ``` Per stack: | Stack | Target | Scope | Detection signal | |---|---|---|---| | Claude Code | `/.claude/skills/blazediff/SKILL.md` | project | `CLAUDE.md` or `.claude/` | | Codex | `~/.codex/skills/blazediff/SKILL.md` | user-global | `AGENTS.md`, `.codex/`, or `~/.codex/` | | Cursor | `/.cursor/rules/blazediff.mdc` | project | `.cursor/` or `.cursorrules` | Codex is user-global because OpenAI's Codex CLI discovers skills under `~/.codex/skills//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//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 ` | Re-baseline existing manifest entries (mask/viewport/waitFor preserved) | | `diff ` | Re-diff one entry against its actual capture without re-screenshotting | | `manifest` | Inspect / list manifest entries (`add --harness ` 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 ` 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//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//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
...
// or with a reason inline:
...
``` 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 }[]; parent?: string; // set on sub-entries from screenshot(name) derived?: boolean; } ``` ## Harnesses A **harness** is a pluggable script in `.blazediff/harnesses/.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 `__`. ```ts export interface HarnessContext

> { page: import("playwright").Page; browser: import("playwright").Browser; context: import("playwright").BrowserContext; params: P; // e.g. { persona: "default" } screenshot(name: string): Promise; } export interface Harness

> { phase?: "setup" | "interact"; run(ctx: HarnessContext

): Promise; } ``` ### 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 ` 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__*` 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__*` pair. One harness file serves them all. **Note.** Every harness-gated capture runs in a fresh browser context (`storageState` reuse is not yet implemented), so a setup harness re-runs per entry. **Working reference.** [`examples/agent-auth-spa-example`](https://github.com/teimurjan/blazediff/tree/main/examples/agent-auth-spa-example) in the repo is a Vite + React SPA with 2 public and 8 auth-gated routes. It ships a `.blazediff/harnesses/auth.js` and committed baselines, so you can clone the repo and run `pnpm --filter @blazediff/agent-auth-spa-example check` to see the full flow pass 10/10. ## CI In CI (`CI=1` or no TTY), only `check` is allowed. `onboard` / `capture` / `rewrite` / `reset` are explicitly blocked - authoring belongs at the developer's machine. ### GitHub Actions ```yaml - run: pnpm install - run: npx blazediff-agent browsers install - run: npx blazediff-agent --cwd apps/website check --json env: # Only needed if any entry uses a login harness. One pair per persona. # (In CI, set these as secrets rather than committing .blazediff/.env.) BLAZEDIFF_AUTH_DEFAULT_EMAIL: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_EMAIL }} BLAZEDIFF_AUTH_DEFAULT_PASSWORD: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_PASSWORD }} ``` Exit codes: - `0` - every entry passed - `1` - at least one regression, intentional, noise, or pending-judgment entry - non-zero with structured JSON error on infra failures (missing manifest, no chromium, etc.) ## Hard rules - Never `--mode baseline` an existing manifest entry without explicit user request. - Never edit `.blazediff/manifest.json` directly. - In CI (`CI=1` or no TTY), only `check` is allowed. - A route that times out is logged once in the result array and skipped - never blocks the run. - Never leave a dev server running after authoring exits. `serve-status --kill` is mandatory teardown. ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff/tree/main/packages/agent) - [Skill playbook (`SKILL.md`)](https://github.com/teimurjan/blazediff/blob/main/skill/blazediff/SKILL.md) - [Landing page →](/agent) --- # @blazediff/core-native The fastest single-threaded image diff in the world. Native Rust implementation with SIMD optimization, **3-4x faster** and **3x smaller** than [odiff](https://github.com/dmtrKovalenko/odiff). [View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md) > **Warning:** This package was previously published as [`@blazediff/bin`](https://www.npmjs.com/package/@blazediff/bin), which is now deprecated. Please use `@blazediff/core-native` instead. ## Installation ```sh npm install @blazediff/core-native ``` Also available as a Rust crate: [`cargo install blazediff`](https://crates.io/crates/blazediff) Pre-built binaries are included for all major platforms - no compilation required: - macOS ARM64 (Apple Silicon) & x64 (Intel) - Linux ARM64 & x64 - Windows ARM64 & x64 ## Features - **PNG, JPEG & QOI support** - auto-detected by file extension - **3-4x faster** than odiff, **3x smaller** binaries (~700KB-900KB vs ~2-3MB) - **SIMD-accelerated** - NEON on ARM, SSE4.1 on x86 - **Block-based optimization** - skips unchanged regions - **Interpret mode** - region detection, classification, severity scoring, human-readable summaries ### Vendored Libraries - [libspng](https://libspng.org/) - Fast PNG decoding/encoding with SIMD - [libjpeg-turbo](https://libjpeg-turbo.org/) - High-performance JPEG codec with SIMD - [qoi](https://github.com/aldanor/qoi-rust) - QOI (Quite OK Image) format for fast lossless compression ## API Reference ### `compare(basePath, comparePath, diffOutput, options?)` Compares two images (PNG, JPEG, or QOI) and generates a diff image. Format is auto-detected from file extension. #### Parameters | Parameter | Type | Description | | ------------- | ----------------- | ------------------------------------- | | `basePath` | `string` | Path to the base/expected image | | `comparePath` | `string` | Path to the comparison/actual image | | `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 | | `interpret` | `boolean` | `false` | Run structured interpretation (region detection + classification) | #### Return Types ```typescript type BlazeDiffResult = | { match: true; interpretation?: InterpretResult } | { match: false; reason: "layout-diff" } | { match: false; reason: "pixel-diff"; diffCount: number; diffPercentage: number; interpretation?: InterpretResult } | { match: false; reason: "file-not-exists"; file: string }; ``` When `interpret: true`, the result includes an `interpretation` field with structured analysis. > **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'); } ``` ### 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] [OUTPUT] Arguments: First image path (PNG, JPEG, or QOI) Second image path (PNG, JPEG, or QOI) [OUTPUT] Output diff image path (optional, format detected from extension) Options: -t, --threshold Color difference threshold (0.0-1.0) [default: 0.1] -a, --antialiasing Enable anti-aliasing detection --diff-mask Output only differences (transparent background) -c, --compression PNG compression level (0-9, 0=fastest, 9=smallest) [default: 0] -q, --quality JPEG quality (1-100) [default: 90] --interpret Run structured interpretation after diff --output-format Output format (json or text) [default: json] -h, --help Print help -V, --version Print version ``` ### Supported Formats | Format | Extensions | Notes | |--------|------------|-------| | PNG | `.png` | Lossless, supports transparency | | JPEG | `.jpg`, `.jpeg` | Lossy, smaller file sizes | | QOI | `.qoi` | Fast lossless, ideal for diff outputs (12x smaller than uncompressed PNG) | Input images can be mixed formats (e.g., compare PNG to JPEG). Output format is determined by the output file extension. > **Tip:** **Use QOI for diff outputs:** QOI excels at encoding diff images with large uniform areas, producing files 12x smaller than PNG (level 0) while being faster to encode. ### Exit Codes - `0` - Images are identical - `1` - Images differ (includes layout/size mismatch) - `2` - Error (file not found, invalid format, etc.) ### Interpret Structured diff analysis. Takes two images, returns classified change regions with human-readable summaries. **Pipeline:** change mask → morph close → connected components → per-region evidence extraction → classify → describe **Evidence per region:** - Dual-image gradient comparison (edges in both images + spatial correlation) - YIQ color delta with uniformity analysis (mean, max, stddev) - Background distance (changed pixels vs local unchanged pixels) - Shape statistics (fill ratio, border density, occupancy) **Six-label decision tree:** | Type | Signal | |---|---| | `RenderingNoise` | Tiny (≤25px) or sparse + low color delta | | `Addition` | Blends with background in img1, distinct in img2 | | `Deletion` | Distinct in img1, blends with background in img2 | | `ColorChange` | Edge structure preserved across both images + uniform color shift | | `ContentChange` | Fallback - structural change | | `Shift` | Post-hoc: matched Addition+Deletion pair with similar luminance | ```typescript import { compare, interpret } from '@blazediff/core-native'; const result = await interpret('expected.png', 'actual.png'); console.log(result.summary); // "Moderate visual change detected (1.87% of image, 10 regions). // Content changed: 4 regions (bottom, center). // Content added: 3 regions (right, bottom, bottom-left)." // Via compare() const diff = await compare('expected.png', 'actual.png', 'diff.png', { interpret: true, }); ``` ```bash npx blazediff expected.png actual.png --interpret npx blazediff expected.png actual.png report.html --output-format html ``` Severity: Low (<1%), Medium (1-10%), High (>10%). See [Interpret example →](/docs/difference-analysis) for interactive demo. **Accuracy:** Measured against hand-labeled datasets — classifier-only macro F1 is **0.998** on clean add/delete edits (`addition_deletion`) and **0.440** on inpaint edits that mix recolor and texture replacement (`inpaintcoco`). Full breakdown in [BENCHMARKS.md](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-interpret-verify/BENCHMARKS.md). ## Performance Benchmarked on Apple M1 Max with 5600×3200 4K images (25 runs, 5 warmup, image IO included): | Tool | Time | Comparison | |------|------|------------| | **blazediff** | ~359ms | - | | odiff | ~1221ms | 3.4x slower | Binary sizes (stripped, LTO optimized): | Platform | blazediff | odiff | |----------|-----------|-------| | macOS ARM64 | 702 KB | 2.2 MB | | Linux x64 | 869 KB | 2.9 MB | | Windows x64 | 915 KB | 3.0 MB | > **Info:** **Why so fast?** BlazeDiff uses a two-pass block-based algorithm with SIMD acceleration. The cold pass quickly identifies unchanged 8x8 blocks using 32-bit integer comparison, then the hot pass only processes changed regions with YIQ perceptual color difference. ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff) - [NPM Package](https://www.npmjs.com/package/@blazediff/core-native) - [Rust Crate](https://crates.io/crates/blazediff) - [Examples →](/docs/pixel-comparison/rust-napi) --- # @blazediff/core-wasm WebAssembly build of the BlazeDiff Rust algorithm for browsers, edge runtimes, and any wasm host. Same two-pass block algorithm as [`@blazediff/core-native`](/apis/core-native), compiled to `wasm32` with `v128` SIMD (`+simd128`). ~58% faster than [pixelmatch](https://github.com/mapbox/pixelmatch) on the same RGBA buffers; diff counts agree with pixelmatch to within ~0.05%. [View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md) ## Installation ```sh npm install @blazediff/core-wasm ``` Ships ~32 KB of optimized wasm + ~10 KB of JS glue. No native binaries, no postinstall, no platform packages. ## Features - **Same algorithm as `@blazediff/core-native`**: YIQ perceptual delta + block-based cold/hot pass - **wasm32 v128 SIMD** (`+simd128`): 4-lane vectorized cold and hot loops; up to ~16× faster than pixelmatch on 4K - **Buffers-only API**: caller decodes images, hands in `Uint8Array`. No PNG/JPEG codecs bundled - **Runs anywhere wasm runs**: browsers (`fetch()` of the .wasm), Node (`fs.readFileSync` + bytes), Cloudflare Workers, Deno, Bun ## API Reference ### `initBlazediff(input?)` Initializes the wasm module. Safe to call multiple times; subsequent calls return the cached promise. The function accepts a `URL`, `Response`, `ArrayBuffer`, `Uint8Array`, or compiled `WebAssembly.Module`. Without `input`, the default `--target web` glue fetches the sibling `blazediff_bg.wasm` via `import.meta.url`, which works in browsers but fails in runtimes whose `fetch()` cannot resolve the resulting URL (Node `file://`, Workers, etc.). #### Loading the wasm module Pick the recipe that matches your runtime. All four are equivalent; the wasm itself is the same. **Browser, plain script tag or ESM**. The default works because the sibling `.wasm` is reachable via `fetch(import.meta.url)`: ```typescript import { initBlazediff } from '@blazediff/core-wasm'; await initBlazediff(); ``` **Universal CDN URL (recommended for Node, Workers, Deno, Bun)**. jsDelivr serves the published `.wasm` over HTTPS, so any `fetch()`-capable runtime can load it. One network round-trip on cold start, cached by the runtime after that: ```typescript import { initBlazediff } from '@blazediff/core-wasm'; await initBlazediff( new URL( 'https://cdn.jsdelivr.net/npm/@blazediff/core-wasm@4.2.0/wasm/blazediff_bg.wasm', ), ); ``` Pin the version (`@4.2.0`) for reproducibility. `unpkg.com/@blazediff/core-wasm@4.2.0/wasm/blazediff_bg.wasm` works identically. **Bundlers (Vite, Webpack 5+, esbuild, Rollup with plugin)**. The `new URL(asset, import.meta.url)` pattern is bundler-aware: the asset is emitted into the build output and the URL is rewritten at build time: ```typescript import { initBlazediff } from '@blazediff/core-wasm'; const wasmUrl = new URL( '@blazediff/core-wasm/wasm/blazediff_bg.wasm', import.meta.url, ); await initBlazediff(wasmUrl); ``` **Node from the local filesystem** (offline, no CDN dependency). Read the bytes and pass them in. Path resolution depends on your module system: ```typescript // ESM (Node 20.6+): import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { initBlazediff } from '@blazediff/core-wasm'; const wasmPath = fileURLToPath( import.meta.resolve('@blazediff/core-wasm/wasm/blazediff_bg.wasm'), ); await initBlazediff(readFileSync(wasmPath)); ``` ```typescript // CommonJS: const { readFileSync } = require('node:fs'); const { initBlazediff } = require('@blazediff/core-wasm'); const wasmPath = require.resolve( '@blazediff/core-wasm/wasm/blazediff_bg.wasm', ); await initBlazediff(readFileSync(wasmPath)); ``` ### `diff(a, b, width, height, output?, options?)` Compares two RGBA pixel buffers and returns the number of differing pixels. #### 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 | > **Info:** **Threshold Guidelines:** `0.0` exact match · `0.05` strict · `0.1` default · `0.2` lenient ## Usage ### Browser Decode images via `createImageBitmap` + `OffscreenCanvas` (or the `ImageDecoder` API), then pass the RGBA buffer to `diff()`. ```typescript import { diff, initBlazediff } from '@blazediff/core-wasm'; await initBlazediff(); async function toRgba(url: string) { const bitmap = await createImageBitmap(await (await fetch(url)).blob()); const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); const ctx = canvas.getContext('2d')!; ctx.drawImage(bitmap, 0, 0); const { data } = ctx.getImageData(0, 0, bitmap.width, bitmap.height); return { data: new Uint8Array(data.buffer), width: bitmap.width, height: bitmap.height }; } const a = await toRgba('/baseline.png'); const b = await toRgba('/current.png'); const out = new Uint8Array(a.width * a.height * 4); const diffCount = await diff(a.data, b.data, a.width, a.height, out, { threshold: 0.1, }); console.log(`${diffCount} pixels differ`); ``` ### Node The wasm can be loaded either from jsDelivr (zero-config, works in Node 18+) or from the local `node_modules` install. The CDN form is shown here; see [Loading the wasm module](#loading-the-wasm-module) for the offline `readFileSync` recipe. ```typescript import { readFileSync } from 'node:fs'; import { diff, initBlazediff } from '@blazediff/core-wasm'; import { PNG } from 'pngjs'; await initBlazediff( new URL( 'https://cdn.jsdelivr.net/npm/@blazediff/core-wasm@4.2.0/wasm/blazediff_bg.wasm', ), ); const a = PNG.sync.read(readFileSync('baseline.png')); const b = PNG.sync.read(readFileSync('current.png')); const diffCount = await diff( new Uint8Array(a.data), new Uint8Array(b.data), a.width, a.height, ); ``` ## Performance vs `pixelmatch` on M1 Max, image I/O excluded (pre-decoded RGBA buffers): | Fixture | pixelmatch | core-wasm | Improvement | | ----------------- | ---------- | ---------- | ----------- | | 4k/1 | 287.72ms | 51.75ms | **82.0%** | | 4k/3 | 366.81ms | 69.90ms | **80.9%** | | page/2 | 443.83ms | 109.74ms | **75.3%** | | blazediff/3 | 14.60ms | 5.52ms | **62.2%** | | pixelmatch/1 | 0.87ms | 0.13ms | **84.6%** | Average ~58% faster across the full fixture set. Counts agree with pixelmatch within ~0.05% (e.g. `4k/1`: 69 932 vs 69 912 of 17 920 000 pixels; both use YIQ perceptual delta, residual differences are anti-aliasing edge cases). > **Info:** **Why so fast?** Block-based two-pass algorithm (cold pass skips unchanged 8×8 blocks via integer compare, hot pass runs YIQ delta only on changed regions) with `v128` SIMD intrinsics: 4-lane vectorized RGBA extraction, YIQ transform, and threshold compare. ## Picking the Right Package | Use case | Package | | --------------------------------- | -------------------------------- | | Browser, edge worker, wasm host | **`@blazediff/core-wasm`** | | Node CLI / server with native bin | [`@blazediff/core-native`](/apis/core-native) | | Pure JS / no wasm support | [`@blazediff/core`](/apis/core) | ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff) - [NPM Package](https://www.npmjs.com/package/@blazediff/core-wasm) - [Rust Crate](https://crates.io/crates/blazediff) --- # @blazediff/matcher Core snapshot comparison logic that powers [@blazediff/jest](/apis/jest), [@blazediff/vitest](/apis/vitest), and [@blazediff/bun](/apis/bun). Most users should use those framework packages directly. ## Installation ```sh npm install @blazediff/matcher ``` ## Quick Start ```typescript import { getOrCreateSnapshot } from '@blazediff/matcher'; const result = await getOrCreateSnapshot( imageBuffer, // or file path { method: 'core', failureThreshold: 0.01, failureThresholdType: 'percent', }, { testPath: '/path/to/test.spec.ts', testName: 'should render correctly', } ); if (result.pass) { console.log(`✓ Snapshot ${result.snapshotStatus}`); } else { console.log(`✗ ${result.diffPercentage}% different`); } ``` ## API Reference ### getOrCreateSnapshot(received, options, testContext) Main function for snapshot comparison and management. | Parameter | Type | Description | |-----------|------|-------------| | `received` | `ImageInput` | Image to compare (file path or buffer with dimensions) | | `options` | `MatcherOptions` | Comparison options | | `testContext` | `TestContext` | Test information (testPath, testName) | **Returns**: `Promise` ### 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` | 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: ['/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` | 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` | Optional comparison options | #### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `method` | `ComparisonMethod` | `'core'` | Comparison algorithm | | `snapshotIdentifier` | `string` | `'snapshot'` | **Required**: Snapshot filename identifier | | `failureThreshold` | `number` | `0` | Allowed difference (pixels or percentage) | | `failureThresholdType` | `'pixel' \| 'percent'` | `'pixel'` | Threshold interpretation | | `snapshotsDir` | `string` | `'__snapshots__'` | Snapshot directory (relative to test) | | `updateSnapshots` | `boolean` | `false` | Force snapshot update | | `threshold` | `number` | `0.1` | Color threshold for `core`/`core-native` (0-1) | | `runInWorker` | `boolean` | `true` | Run comparison in worker thread for better performance | See [@blazediff/matcher](/apis/matcher) for all available options. ## Comparison Methods ### `core` - Pure JavaScript (Default) ```typescript await expect(screenshot).toMatchImageSnapshot({ method: 'core', snapshotIdentifier: 'my-test', }); ``` ### `core-native` - Rust Native (Fastest) ```typescript await expect('/path/to/image.png').toMatchImageSnapshot({ method: 'core-native', snapshotIdentifier: 'my-test', }); ``` ### `ssim` - Perceptual Similarity ```typescript await expect(screenshot).toMatchImageSnapshot({ method: 'ssim', snapshotIdentifier: 'my-test', }); ``` ### `gmsd` - Gradient-based ```typescript await expect(screenshot).toMatchImageSnapshot({ method: 'gmsd', snapshotIdentifier: 'my-test', }); ``` ## Usage Patterns ### Basic Snapshot Test ```typescript import { expect, it } from 'bun:test'; import '@blazediff/bun'; it('renders homepage correctly', async () => { const screenshot = await browser.screenshot(); await expect(screenshot).toMatchImageSnapshot({ method: 'core', snapshotIdentifier: 'homepage', }); }); ``` ### Custom Thresholds Allow small differences while catching regressions: ```typescript // Allow up to 100 pixels difference await expect(screenshot).toMatchImageSnapshot({ method: 'core', snapshotIdentifier: 'homepage', failureThreshold: 100, failureThresholdType: 'pixel', }); // Allow up to 0.5% difference await expect(screenshot).toMatchImageSnapshot({ method: 'core', snapshotIdentifier: 'homepage', failureThreshold: 0.5, failureThresholdType: 'percent', }); ``` > **Note:** Use percentage-based thresholds for responsive images that may change size across different viewports. ### Update Snapshots ```bash # Update all failing snapshots (recommended) bun test --update-snapshots # Using environment variable BUN_UPDATE_SNAPSHOTS=true bun test ``` > **Info:** The Bun's `--update-snapshots` flag is consumed by Bun internally and won't update image snapshots. Programmatically: ```typescript await expect(screenshot).toMatchImageSnapshot({ method: 'core', snapshotIdentifier: 'homepage', updateSnapshots: true, }); ``` ### Custom Snapshot Directory ```typescript await expect(screenshot).toMatchImageSnapshot({ method: 'core', snapshotIdentifier: 'homepage', snapshotsDir: '__image_snapshots__', }); ``` ## Bun-Specific Notes Unlike Jest and Vitest, Bun has limited test context exposure. Always provide a `snapshotIdentifier`: ```typescript // Good await expect(screenshot).toMatchImageSnapshot({ method: 'core', snapshotIdentifier: 'my-component', }); // Avoid - falls back to generic "snapshot" await expect(screenshot).toMatchImageSnapshot({ method: 'core', }); ``` ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff) - [NPM Package](https://www.npmjs.com/package/@blazediff/bun) - [@blazediff/matcher](/apis/matcher) - Core matcher logic - [Bun Documentation](https://bun.sh/) --- # @blazediff/ui Headless engine and a framework-agnostic renderer for building image comparison interfaces. Works with any JavaScript framework or vanilla HTML. ## Installation ```sh npm install @blazediff/ui ``` ## Two layers - **`@blazediff/ui`** — a tiny pure-JS renderer. `mount*` functions build the DOM, wire events, and keep it updated. - **`@blazediff/ui/engine`** — the headless engine. All state, calculations, and handlers live here with no rendering, so you can drive any framework from it. > **Info:** Using React? [`@blazediff/react`](/apis/react) renders from this engine for you, with the same modes and props. ## Features - **Framework agnostic**: drive the engine from React, Vue, Angular, Svelte, or vanilla JS - **No web components**: plain `mount*` functions and a subscribable engine - **Multiple modes**: swipe, two-up, onion skin, and difference visualization - **Functional by default**: the layout each mode needs is built in; classes are only for theming - **Lightweight**: zero dependencies except `@blazediff/core` ## Quick Start Every mode is a `mount*(target, options)` function. It appends the UI into `target` and returns a handle with `update(options)` and `destroy()`. ```js import { mountSwipe } from "@blazediff/ui"; const handle = mountSwipe(document.getElementById("app"), { src1: "image1.jpg", src2: "image2.jpg", alt1: "Original", alt2: "Modified", onPositionChange: (position) => console.log(position), }); // later handle.update({ src2: "image3.jpg" }); handle.destroy(); ``` ## Renderer API ### `mountSwipe(target, options)` Compare two images with a draggable divider. | Option | Type | Default | Description | | ------------------ | ---------------------------- | ---------- | ------------------------------------ | | `src1` | `string` | - | URL of the first image | | `src2` | `string` | - | URL of the second image | | `alt1` | `string` | `"Before"` | Alt text for the first image | | `alt2` | `string` | `"After"` | Alt text for the second image | | `initialPosition` | `number` | `50` | Initial divider position (0–100) | | `className` | `string` | - | Class for the root element | | `containerClassName` | `string` | - | Class for the container | | `image1ClassName` | `string` | - | Class for the first image | | `image2ClassName` | `string` | - | Class for the second image | | `dividerClassName` | `string` | - | Class for the divider | | `onPositionChange` | `(position: number) => void` | - | Called when the divider moves (0–100)| ### `mountTwoUp(target, options)` Two images side by side, with dimension-change detection. | Option | Type | Default | Description | | ------------------------- | ------------------------------------------------- | ------- | --------------------------------- | | `src1` | `string` | - | URL of the first image | | `src2` | `string` | - | URL of the second image | | `crossOrigin` | `string \| null` | `"anonymous"` | `crossOrigin` for image loads | | `className` | `string` | - | Class for the root element | | `containerClassName` | `string` | - | Class for the main container | | `containerInnerClassName` | `string` | - | Class for the inner container | | `panelClassName` | `string` | - | Class for each panel | | `imageClassName` | `string` | - | Class for images | | `dimensionInfoClassName` | `string` | - | Class for the dimension info text | | `onImagesLoaded` | `(detail) => void` | - | `{ image1, image2 }` dimensions | | `onLoadError` | `(error: unknown) => void` | - | Called when loading fails | ### `mountOnionSkin(target, options)` Overlay two images with an opacity slider. | Option | Type | Default | Description | | -------------------------- | --------------------------- | ------------ | -------------------------------- | | `src1` | `string` | - | URL of the base image | | `src2` | `string` | - | URL of the overlay image | | `opacity` | `number` | `50` | Initial opacity (0–100) | | `crossOrigin` | `string \| null` | `"anonymous"`| `crossOrigin` for image loads | | `className` | `string` | - | Class for the root element | | `sliderLabelText` | `string` | `"Opacity:"` | Text for the slider label | | `containerClassName` | `string` | - | Class for the main container | | `imageContainerClassName` | `string` | - | Class for the image container | | `imageClassName` | `string` | - | Class for images | | `sliderContainerClassName` | `string` | - | Class for the slider container | | `sliderClassName` | `string` | - | Class for the slider | | `sliderLabelClassName` | `string` | - | Class for the slider label | | `onOpacityChange` | `(opacity: number) => void` | - | Called when opacity changes | | `onImagesLoaded` | `(detail) => void` | - | `{ image1, image2 }` dimensions | | `onLoadError` | `(error: unknown) => void` | - | Called when loading fails | ### `mountDifference(target, options)` Show pixel differences using the BlazeDiff algorithm. | Option | Type | Default | Description | | -------------------- | -------------------------- | ------- | --------------------------- | | `src1` | `string` | - | URL of the first image | | `src2` | `string` | - | URL of the second image | | `threshold` | `number` | `0.1` | Difference threshold (0–1) | | `includeAA` | `boolean` | `false` | Include anti-aliased pixels | | `alpha` | `number` | `0.1` | Opacity of the original | | `crossOrigin` | `string \| null` | `"anonymous"` | `crossOrigin` for loads | | `className` | `string` | - | Class for the root element | | `containerClassName` | `string` | - | Class for the container | | `canvasClassName` | `string` | - | Class for the canvas | | `onDiffComplete` | `(detail) => void` | - | `{ diffCount, totalPixels, percentage }` | | `onDiffError` | `(error: unknown) => void` | - | Called when comparison fails | ## Headless engine Need another framework, or full control? Drive the engine directly from `@blazediff/ui/engine`. Each factory returns a controller with `getState()`, `subscribe(listener)`, `setConfig(config)`, `actions`, and `destroy()`. ```js import { createSwipeEngine } from "@blazediff/ui/engine"; const engine = createSwipeEngine(50); const unsubscribe = engine.subscribe(() => { const { position, isDragging } = engine.getState(); // render position (0–100) however your framework wants }); engine.actions.start(40); // pass an already-computed percentage engine.actions.move(55); // clamped + ignored unless dragging engine.actions.end(); unsubscribe(); engine.destroy(); ``` | Factory | State | Actions | | --------------------------------------------- | --------------------------------------------------------------------- | -------------------------------- | | `createDifferenceEngine(config)` | `{ status, diff?: { output, width, height, diffCount, totalPixels, percentage }, error? }` | – | | `createSwipeEngine(initialPosition = 50)` | `{ position, isDragging }` | `start`, `move`, `end`, `setPosition` | | `createTwoUpEngine(config)` | `{ status, dims1, dims2, dimensionLabel, changed, error }` | – | | `createOnionSkinEngine(config, opacity = 50)` | `{ status, opacity, dims1, dims2, error }` | `setOpacity` | Also exported: `formatDimensionLabel`, `normalizedOpacity`, `loadImageElement`, `getImageData`, `createStore`. The engine uses browser APIs (`Image`, a throwaway `` 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 ( console.log(position)} /> ); } ``` ## API Reference ### `` Interactive swipe component for comparing two images with a draggable divider. #### Props | Prop | Type | Default | Description | | ------------------ | --------------------------------- | ------- | ------------------------------------ | | `src1` | `string` (required) | - | URL of the first image | | `src2` | `string` (required) | - | URL of the second image | | `alt1` | `string` | - | Alt text for the first image | | `alt2` | `string` | - | Alt text for the second image | | `className` | `string` | - | CSS class for the root element | | `containerClassName` | `string` | - | CSS class for the container | | `image1ClassName` | `string` | - | CSS class for the first image | | `image2ClassName` | `string` | - | CSS class for the second image | | `dividerClassName` | `string` | - | CSS class for the divider | | `onPositionChange` | `(position: number) => void` | - | Callback when divider position changes | ### `` Side-by-side comparison component with dimension information. #### Props | Prop | Type | Default | Description | | ------------------------- | ----------------------------------------------------------- | ------- | --------------------------------- | | `src1` | `string` (required) | - | URL of the first image | | `src2` | `string` (required) | - | URL of the second image | | `className` | `string` | - | CSS class for the root element | | `containerClassName` | `string` | - | CSS class for the main container | | `containerInnerClassName` | `string` | - | CSS class for the inner container | | `panelClassName` | `string` | - | CSS class for each panel | | `imageClassName` | `string` | - | CSS class for images | | `dimensionInfoClassName` | `string` | - | CSS class for dimension info text | | `onImagesLoaded` | `(detail: {image1: {width, height}, image2: {width, height}}) => void` | - | Callback when images load | | `onLoadError` | `(error: unknown) => void` | - | Callback when loading fails | ### `` Overlay comparison with opacity control slider. #### Props | Prop | Type | Default | Description | | -------------------------- | ----------------------------------------------------------- | ------- | -------------------------------- | | `src1` | `string` (required) | - | URL of the base image | | `src2` | `string` (required) | - | URL of the overlay image | | `opacity` | `number` | - | Initial opacity (0-100) | | `className` | `string` | - | CSS class for the root element | | `containerClassName` | `string` | - | CSS class for the main container | | `imageContainerClassName` | `string` | - | CSS class for image container | | `imageClassName` | `string` | - | CSS class for images | | `sliderContainerClassName` | `string` | - | CSS class for slider container | | `sliderClassName` | `string` | - | CSS class for the slider | | `sliderLabelClassName` | `string` | - | CSS class for slider label | | `sliderLabelText` | `string` | - | Text for the slider label | | `onOpacityChange` | `(opacity: number) => void` | - | Callback when opacity changes | | `onImagesLoaded` | `(detail: {image1: {width, height}, image2: {width, height}}) => void` | - | Callback when images load | | `onLoadError` | `(error: unknown) => void` | - | Callback when loading fails | ### `` Shows pixel differences using the BlazeDiff comparison algorithm. #### Props | Prop | Type | Default | Description | | ------------------- | ----------------------------------------------------------------- | ------- | ------------------------------- | | `src1` | `string` (required) | - | URL of the first image | | `src2` | `string` (required) | - | URL of the second image | | `threshold` | `number` | - | Difference threshold (0-1) | | `includeAA` | `boolean` | - | Include anti-aliased pixels | | `alpha` | `number` | - | Opacity of original image | | `className` | `string` | - | CSS class for the root element | | `containerClassName`| `string` | - | CSS class for the container | | `canvasClassName` | `string` | - | CSS class for the canvas | | `onDiffComplete` | `(detail: {diffCount, totalPixels, percentage}) => void` | - | Callback on completion | | `onDiffError` | `(error: unknown) => void` | - | Callback on error | ## Links - [GitHub Repository](https://github.com/teimurjan/blazediff) - [NPM Package](https://www.npmjs.com/package/@blazediff/react) - [Examples →](/docs/ui-components/react) --- # blazediff (Python) PyO3 bindings to the same Rust core that powers `@blazediff/core-native`. **3-4x faster** than [odiff](https://github.com/dmtrKovalenko/odiff), shipped as `abi3` wheels for CPython ≥ 3.8 across macOS, Linux, and Windows. [View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md) ## Installation ```sh pip install blazediff ``` `uv pip install blazediff` and `poetry add blazediff` work identically. Pre-built wheels are published for: - macOS ARM64 (Apple Silicon) & x86_64 (Intel) - Linux ARM64 & x86_64 (manylinux 2.17) - Windows ARM64 & x86_64 A single `abi3-py38` wheel per platform serves every CPython ≥ 3.8 - no separate wheel per minor version. ## Features - **PNG, JPEG & QOI support** - auto-detected by file extension - **Path-based** - pass file paths in, blazediff handles decode + compare + encode - **SIMD-accelerated** - NEON on ARM, SSE4.1 on x86 - **Block-based optimization** - skips unchanged regions - **Interpret mode** - region detection, classification, severity scoring, human-readable summaries ## API Reference ### `compare(base_path, compare_path, diff_output=None, *, ...)` Compares two images and optionally writes a diff image. All keyword arguments are optional. ```python from blazediff import compare result = compare( "expected.png", "actual.png", "diff.png", threshold=0.1, antialiasing=True, ) ``` #### Parameters | Parameter | Type | Default | Description | | ---------------- | --------------- | ------- | ---------------------------------------------------------------------- | | `base_path` | `str` | - | Path to the base/expected image | | `compare_path` | `str` | - | Path to the comparison/actual image | | `diff_output` | `str \| None` | `None` | Path where the diff image (or HTML report) will be written | | `threshold` | `float` | `0.1` | Color difference threshold (0.0-1.0). Lower = stricter | | `antialiasing` | `bool` | `False` | Enable anti-aliasing detection | | `diff_mask` | `bool` | `False` | Output only differences with a transparent background | | `compression` | `int` | `0` | PNG compression level (0-9, 0 = fastest, 9 = smallest) | | `quality` | `int` | `90` | JPEG quality (1-100) | | `interpret` | `bool` | `False` | Run structured interpretation (region detection + classification) | | `output_format` | `"png" \| "html"` | `"png"` | `"html"` writes an interactive interpret report to `diff_output` | #### Return value `compare()` returns a `DiffResult` object with: | Field | Type | Description | | ---------------- | -------------------------- | ------------------------------------------------------------ | | `match` | `bool` | `True` when images are identical (within threshold) | | `reason` | `str \| None` | `"layout-diff"` or `"pixel-diff"` when `match` is `False` | | `diff_count` | `int \| None` | Number of differing pixels (pixel-diff only) | | `diff_percentage`| `float \| None` | Percentage of differing pixels (pixel-diff only) | | `interpretation` | `InterpretResult \| None` | Populated when `interpret=True` or `output_format="html"` | > **Info:** **Threshold guidelines:** - `0.0` - Exact match only - `0.05` - Strict comparison - `0.1` - Default balanced comparison - `0.2` - Lenient comparison ### `interpret_images(image1_path, image2_path, *, threshold=0.1, antialiasing=False)` Returns the full `InterpretResult` (regions, severity, summary) without writing any output file. Equivalent to `compare(..., interpret=True)` minus the diff image. ```python from blazediff import interpret_images result = interpret_images("expected.png", "actual.png") print(result.summary) print(f"{result.total_regions} regions, severity={result.severity}") ``` ## Usage ### Basic comparison ```python from blazediff import compare result = compare("expected.png", "actual.png", "diff.png", threshold=0.1) if result.match: print("Images are identical!") elif result.reason == "pixel-diff": print(f"{result.diff_count} pixels differ ({result.diff_percentage:.2f}%)") elif result.reason == "layout-diff": print("Images have different dimensions") ``` ### pytest assertion ```python from blazediff import compare def test_homepage_matches_baseline(): result = compare("baseline.png", "rendered.png", "diff.png", threshold=0.05) assert result.match, ( f"{result.diff_count} pixels differ ({result.diff_percentage:.2f}%)" ) ``` ### Mixed formats `compare()` infers format from the file extension. Inputs and outputs can mix freely: ```python # PNG inputs, QOI diff output (12x smaller diff files) compare("expected.png", "actual.png", "diff.qoi") # JPEG comparison compare("expected.jpg", "actual.jpg", "diff.jpg", quality=85) ``` | Format | Extensions | Notes | | ------ | ---------------- | ------------------------------------------------------------------ | | PNG | `.png` | Lossless, transparency | | JPEG | `.jpg`, `.jpeg` | Lossy, smaller files | | QOI | `.qoi` | Fast lossless, ideal for diff outputs (~12x smaller than PNG) | ### Interpret mode Structured analysis - change regions, classification (Addition, Deletion, Shift, ContentChange, ColorChange, RenderingNoise), severity scoring, and human-readable summaries. ```python from blazediff import compare, interpret_images # Programmatic result = interpret_images("expected.png", "actual.png") print(result.summary) # "Moderate visual change detected (1.87% of image, 10 regions). # Content changed: 4 regions (bottom, center). # Content added: 3 regions (right, bottom, bottom-left)." for region in result.regions: print(f" {region.region_type} @ {region.position}, severity={region.severity}") # Diff image + interpretation result = compare("expected.png", "actual.png", "diff.png", interpret=True) # Interactive HTML report compare("expected.png", "actual.png", "report.html", output_format="html") ``` Severity tiers: `Low` (<1%), `Medium` (1-10%), `High` (>10%). See [Interpret example →](/docs/difference-analysis) for the interactive demo. ## Links - [PyPI Package](https://pypi.org/project/blazediff/) - [GitHub Repository](https://github.com/teimurjan/blazediff) - [Rust Crate](https://crates.io/crates/blazediff) - [NPM Package (`@blazediff/core-native`)](https://www.npmjs.com/package/@blazediff/core-native) --- # blazediff (Rust) The Rust crate that powers the entire BlazeDiff stack. **3-4x faster** than [odiff](https://github.com/dmtrKovalenko/odiff), **8x faster** than pixelmatch on 4K images. Block-based diff algorithm with SIMD-accelerated YIQ color comparison; the same engine drives `@blazediff/core-native` (Node.js) and `blazediff` (Python). [View Detailed Benchmarks](https://github.com/teimurjan/blazediff/blob/main/BENCHMARKS.md) ## Installation ### As a CLI ```sh cargo install blazediff ``` ### As a library ```toml # Cargo.toml [dependencies] blazediff = "*" ``` Pre-built binaries (via `cargo install`) and crate sources are available on [crates.io](https://crates.io/crates/blazediff). ## Features - **PNG, JPEG & QOI support** with vendored libspng, libjpeg-turbo, and qoi-rust (no system dependencies) - **SIMD-accelerated** - NEON on ARM, SSE4.1 on x86 - **Block-based optimization** - skips unchanged 8x8 blocks via 32-bit integer compare before perceptual diff - **Interpret mode** - region detection, classification, severity scoring, human-readable summaries - **Cargo features** - opt into `napi` (Node.js bindings) or `python` (PyO3 bindings) when building from source ## CLI Usage ```sh # Basic diff blazediff image1.png image2.png diff.png # Custom threshold (0.0 - 1.0) blazediff image1.png image2.png diff.png -t 0.05 # JPEG comparison blazediff a.jpg b.jpg diff.jpg -q 85 # QOI diff output (12x smaller than PNG, faster encode) blazediff a.png b.png diff.qoi # Structured interpretation (JSON) blazediff a.png b.png --interpret # HTML interpret report blazediff a.png b.png report.html --output-format html ``` ### CLI Options ```sh blazediff [OPTIONS] [OUTPUT] Arguments: First image path (PNG, JPEG, or QOI) Second image path (PNG, JPEG, or QOI) [OUTPUT] Output diff image path (optional, format detected from extension) Options: -t, --threshold Color difference threshold (0.0-1.0) [default: 0.1] -a, --antialiasing Enable anti-aliasing detection --diff-mask Output only differences (transparent background) -c, --compression PNG compression level (0-9) [default: 0] -q, --quality JPEG quality (1-100) [default: 90] --interpret Run structured interpretation after diff --output-format Output format (json, text, or html for interpret) [default: json] -h, --help Print help -V, --version Print version ``` ### Exit Codes - `0` - Images are identical - `1` - Images differ (includes layout/size mismatch) - `2` - Error (file not found, invalid format, etc.) ## Library Usage The crate exposes `diff()` plus codec helpers for PNG, JPEG, and QOI. Decode once, diff in memory, encode if you want a diff image. ```rust use blazediff::{diff, load_pngs, save_png, DiffOptions, Image}; let (img1, img2) = load_pngs("expected.png", "actual.png")?; let options = DiffOptions { threshold: 0.1, include_aa: true, ..Default::default() }; let mut output = Image::new_uninit(img1.width, img1.height); let result = diff(&img1, &img2, Some(&mut output), &options)?; println!("{} pixels differ ({:.2}%)", result.diff_count, result.diff_percentage); if !result.identical { save_png(&output, "diff.png")?; } ``` ### Types ```rust pub struct DiffOptions { pub threshold: f64, // 0.0-1.0, default 0.1 pub include_aa: bool, // count anti-aliased pixels as diffs pub alpha: f64, // background opacity, default 0.1 pub aa_color: [u8; 3], // default yellow [255, 255, 0] pub diff_color: [u8; 3], // default red [255, 0, 0] pub diff_color_alt: Option<[u8; 3]>, pub diff_mask: bool, // transparent background mode pub compression: u8, // PNG compression 0-9 (0 = fastest) } pub struct DiffResult { pub diff_count: u32, pub diff_percentage: f64, pub identical: bool, } ``` ### Codec helpers | Function | Format | | ------------------------------------------------- | ------ | | `load_png` / `load_pngs` / `save_png` / `encode_png` | PNG | | `load_jpeg` / `load_jpegs` / `save_jpeg` | JPEG | | `load_qoi` / `load_qois` / `save_qoi` | QOI | All decoders return `Image` (RGBA8, tightly packed). Inputs can be mixed formats - decode each side with the matching helper, then call `diff()`. > **Info:** **Threshold guidelines:** - `0.0` - Exact match only - `0.05` - Strict comparison - `0.1` - Default balanced comparison - `0.2` - Lenient comparison ## Cargo Features | Feature | Purpose | | --------- | ------------------------------------------------------- | | (default) | Pure Rust library + CLI | | `napi` | N-API bindings used by `@blazediff/core-native` on NPM | | `python` | PyO3 bindings used by the `blazediff` package on PyPI | The `napi` and `python` features are mutually exclusive build modes for distributing prebuilt artifacts; you don't need them when consuming the crate as a Rust dependency. ## Interpret Structured diff analysis. Takes two images, returns classified change regions with human-readable summaries. **Pipeline:** change mask -> morph close -> connected components -> per-region evidence extraction -> classify -> describe **Six-label decision tree:** | Type | Signal | | ---------------- | ----------------------------------------------------------------------- | | `RenderingNoise` | Tiny (<=25px) or sparse + low color delta | | `Addition` | Blends with background in img1, distinct in img2 | | `Deletion` | Distinct in img1, blends with background in img2 | | `ColorChange` | Edge structure preserved across both images + uniform color shift | | `ContentChange` | Fallback - structural change | | `Shift` | Post-hoc: matched Addition+Deletion pair with similar luminance | ```sh blazediff a.png b.png --interpret blazediff a.png b.png report.html --output-format html ``` Severity tiers: `Low` (<1%), `Medium` (1-10%), `High` (>10%). See [Interpret example →](/docs/difference-analysis) for the interactive demo, and [INTERPRET.md](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff/INTERPRET.md) for the full algorithm spec. ## Vendored libraries - [libspng](https://libspng.org/) - fast PNG decoding/encoding with SIMD - [libjpeg-turbo](https://libjpeg-turbo.org/) - high-performance JPEG codec with SIMD - [qoi-rust](https://github.com/aldanor/qoi-rust) - QOI (Quite OK Image) format No system C dependencies; the crate builds standalone with a recent Rust toolchain. ## Links - [Crates.io](https://crates.io/crates/blazediff) - [GitHub Repository](https://github.com/teimurjan/blazediff) - [Crate README](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff/README.md) - [INTERPRET.md (algorithm spec)](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff/INTERPRET.md) - [NPM Package (`@blazediff/core-native`)](https://www.npmjs.com/package/@blazediff/core-native) - [PyPI Package](https://pypi.org/project/blazediff/) --- # blazediff-png A from-scratch PNG codec in Rust: **single-threaded, SIMD-first, with byte-exact decode parity to [libspng](https://libspng.org)** — and faster than spng on every fixture we test, for both encode and decode. It powers PNG I/O in the BlazeDiff Rust crate. [View per-fixture benchmarks](https://github.com/teimurjan/blazediff/tree/main/crates/blazediff-png-benchmark) ## Installation ```toml # Cargo.toml [dependencies] blazediff-png = "0.0.1" ``` The crate name is `blazediff-png`; the library imports as `blazediff_png`. Crate sources are available on [crates.io](https://crates.io/crates/blazediff-png). > **Warning:** Experimental. Inside BlazeDiff it is opt-in behind the `BLAZEDIFF_PNG_ENABLED` environment variable while it stabilizes; spng stays the default and the defensive decode fallback. ## Features - **Decodes everything spng decodes** — bit depths 1/2/4/8/16, all five color types, palette + tRNS, gray/RGB color-key transparency, Adam7 interlacing — to RGBA8, producing the *same bytes* spng produces and *rejecting the same inputs* spng rejects. - **Targets any pixel format** — `decode_with` reaches any `SPNG_FMT_*` with optional gamma / sBIT transforms; `decode_with_metadata` captures every ancillary chunk. - **Encodes** all color-type / bit-depth combinations, optional Adam7, and real deflate levels (libdeflate) plus a stored level 0. Lossless by construction: `decode(encode(x)) == x` always holds. - **SIMD-first, single-threaded** — whole-buffer inflate, in-place defilter, NEON adaptive-filter kernels; the caller parallelizes across images, not the codec. - **Pluggable deflate backend** — system zlib + libdeflate for spng parity, or pure Rust for C-free builds. ## Performance Versus spng over the BlazeDiff corpus (34 PNGs, 342.7 MPx, up to 5600×3200), single-threaded on Apple Silicon — **faster on every fixture**: | Operation | vs spng | How | | --- | --- | --- | | Decode | **~1.4×** | whole-buffer libdeflate inflate + SIMD defilter | | Encode, stored (level 0) | **~2.2×** | uncompressed deflate blocks, copy- and allocation-light pipeline | | Encode, compressed | **~3.8×** | libdeflate level 6 vs spng zlib 4, at ~94% of spng's file size | The wins come from doing less, not from threads: a whole-buffer inflate instead of spng's per-scanline gating, in-place sequential defiltering, autovectorizable row expansion, and hand-written NEON kernels for the encode SAD/filter hot path. [See the full per-fixture benchmarks →](/benchmarks/png-codec) ## Library Usage Decode to RGBA8, work in memory, encode back out. ```rust use blazediff_png::{decode, encode, EncodeOptions}; let bytes = std::fs::read("image.png")?; // Decode to RGBA8 — Image { data, width, height }. let image = decode(&bytes)?; // Re-encode (Auto picks the smallest lossless color mode; level 4 by default). let png = encode(&image, &EncodeOptions::default())?; ``` ### Types ```rust pub struct Image { pub data: Vec, // 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`); `_ref` borrows the buffer | | `encode_to` | stream a PNG encode into any `Write` sink | | `encode16` | true 16-bit `Image16` → PNG | | `encode_with_metadata` | `encode` + caller-supplied ancillary chunks | > **Info:** **Compression levels:** - `0` — uncompressed stored blocks (fastest) - `4` — default speed/size knee - `6` — spng's default ratio (~98% of its file size) - `12` — libdeflate maximum ## Cargo Features The inflate/compress seam is pluggable; everything else is pure Rust. | Feature | Backend | Use | | --- | --- | --- | | `zlib-backend` (default) | system zlib + libdeflate (C) | byte-exact spng parity, incl. accept/reject on malformed streams | | `rust-backend` | `zune-inflate` + `fdeflate` (pure Rust) | C-free native builds | The `rust-backend` is correct for every well-formed PNG but is **not** bug-compatible with spng on malformed/adversarial streams; spng's edge-case accept/reject parity is a `zlib-backend`-only guarantee. ## Parity by identity zlib's *acceptance* of malformed deflate streams isn't portable. Classic zlib (what spng links) tolerates "distance too far back" at scanline boundaries and copies from window memory; zlib-ng/zlib-rs reject those streams; libdeflate insists on complete adler-valid streams; miniz validates ahead of the write gate. Worse, classic zlib's verdict can depend on the exact `avail_out` gating sequence. So for the malformed-input edge cases the decoder **links the same system zlib spng links** and drives it with spng's exact per-scanline gate sequence — parity by identity, not by reimplementation. libdeflate stays the whole-buffer fast path for well-formed streams. (Parity is verified on system-zlib platforms; on Windows spng bundles miniz, so the boundary semantics differ there.) ## Verified | Layer | Result | | --- | --- | | Exhaustive matrix | every `{depth × color × interlace × filter × tRNS}` at edge sizes, byte-parity with spng | | PngSuite conformance | 176/176 — 164 decode at parity, 12 corrupt files reject in lockstep | | Differential fuzzing | 40M+ execs vs spng, **0 unresolved divergences** | | Encode round-trip fuzzing | 5M+ execs, round-trip + spng cross-decode clean | | Line coverage | **98.89%** (residual lines are unreachable defensive arms) | Byte-identical *encode* output to spng is explicitly **not** a goal — both emit valid-but-different streams. The encode contract is lossless round-tripping plus spng cross-decode compatibility. ## Links - [Crates.io](https://crates.io/crates/blazediff-png) - [Crate README](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-png/README.md) - [Benchmarks](https://github.com/teimurjan/blazediff/tree/main/crates/blazediff-png-benchmark) - [GitHub Repository](https://github.com/teimurjan/blazediff) --- # Examples # Pixel-by-pixel Comparison - Vanilla JavaScript Compare two images pixel by pixel in pure JavaScript - no native binaries, no WebAssembly. `@blazediff/core` runs anywhere JS runs (browser, Node, Deno, Bun, edge) and is ~1.5× faster than pixelmatch. Reach for it when you want zero install friction and portability over raw throughput. Need more speed? See [🐆🐆 Rust + WASM](/docs/pixel-comparison/rust-wasm) and [🐆🐆🐆 Rust + N-API](/docs/pixel-comparison/rust-napi). ## Installation ```bash npm install @blazediff/core ``` ## Examples ```ts import blazediff from "@blazediff/core"; // loadImage can either be a browser function or a server function const img1 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png" ); const img2 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png" ); const output = new Uint8Array(img1.width * img1.height * 4); const width = img1.width; const height = img1.height; const diff = blazediff(img1, img2, output, width, height); ``` ```ts import blazediff from "@blazediff/core"; // loadImage can either be a browser function or a server function const img1 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png" ); const img2 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png" ); const output = new Uint8Array(img1.width * img1.height * 4); const width = img1.width; const height = img1.height; const diff = blazediff(img1, img2, output, width, height, { threshold: 0.5 }); ``` ```ts import blazediff from "@blazediff/core"; // loadImage can either be a browser function or a server function const img1 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png" ); const img2 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png" ); const output = new Uint8Array(img1.width * img1.height * 4); const width = img1.width; const height = img1.height; const diff = blazediff(img1, img2, output, width, height, { aaColor: [255, 0, 0], diffColor: [0, 255, 0], }); ``` See the [full API reference →](/apis/core). --- # Pixel-by-pixel Comparison - Rust + WASM in JavaScript The same diff core, compiled to WebAssembly with SIMD. `@blazediff/core-wasm` runs in browsers, Workers, Deno, Bun, and edge runtimes - ~58% faster than pixelmatch from a 32KB binary. Reach for it when you want native-grade speed in the browser without a native dependency. The API takes pre-decoded RGBA buffers, so you decode images yourself with `createImageBitmap` + a canvas (or the `ImageDecoder` API). ## Installation ```bash npm install @blazediff/core-wasm ``` ## Decoding images to RGBA ```ts async function loadRgba(url: string) { const bitmap = await createImageBitmap(await (await fetch(url)).blob()); const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); const ctx = canvas.getContext("2d")!; ctx.drawImage(bitmap, 0, 0); const { data, width, height } = ctx.getImageData(0, 0, bitmap.width, bitmap.height); return { data: new Uint8Array(data.buffer), width, height }; } ``` ## Examples ```ts import { initBlazediff, diff } from "@blazediff/core-wasm"; await initBlazediff(); // loads the sibling .wasm; call once const a = await loadRgba("3a.png"); const b = await loadRgba("3b.png"); const output = new Uint8Array(a.width * a.height * 4); const diffCount = await diff(a.data, b.data, a.width, a.height, output); ``` ```ts import { initBlazediff, diff } from "@blazediff/core-wasm"; await initBlazediff(); const a = await loadRgba("3a.png"); const b = await loadRgba("3b.png"); const output = new Uint8Array(a.width * a.height * 4); const diffCount = await diff(a.data, b.data, a.width, a.height, output, { threshold: 0.5, }); ``` ```ts import { initBlazediff, diff } from "@blazediff/core-wasm"; await initBlazediff(); const a = await loadRgba("3a.png"); const b = await loadRgba("3b.png"); const output = new Uint8Array(a.width * a.height * 4); // diffMask renders changes on a transparent background instead of grayscale const diffCount = await diff(a.data, b.data, a.width, a.height, output, { diffMask: true, }); ``` Loading the `.wasm` from a CDN, a bundler, or the filesystem? `initBlazediff` accepts a `URL`, `Response`, or raw bytes - see the [full API reference →](/apis/core-wasm). --- # Pixel-by-pixel Comparison - Rust + N-API in Node The fastest path: native Rust compiled to a Node addon via N-API, ~3-4× faster than odiff. `@blazediff/core-native` works on **file paths** - it decodes PNG, JPEG, and QOI itself (in parallel) and can write the diff image straight to disk. Reach for it in Node test runners and CI where you have files and want maximum throughput. ## Installation ```bash npm install @blazediff/core-native ``` ## Examples ```ts import { compare } from "@blazediff/core-native"; const result = await compare("3a.png", "3b.png"); if (result.match) { console.log("identical"); } else if (result.reason === "pixel-diff") { console.log(`${result.diffCount} pixels differ (${result.diffPercentage.toFixed(2)}%)`); } else if (result.reason === "layout-diff") { console.log("dimensions differ"); } ``` ```ts import { compare } from "@blazediff/core-native"; const result = await compare("3a.png", "3b.png", undefined, { threshold: 0.5, antialiasing: true, }); ``` ```ts import { compare } from "@blazediff/core-native"; // Pass an output path to render the diff visualization to disk. // Format is inferred from the extension (PNG, JPEG, QOI). const result = await compare("3a.png", "3b.png", "diff.png"); ``` Want a verdict on *what* changed, not just how many pixels? Pass `interpret: true` or use the dedicated [Image Difference Analysis →](/docs/difference-analysis) example. Full options in the [API reference →](/apis/core-native). --- # Structural Image Comparison Pixel-by-pixel diffing answers "how many pixels changed". Structural metrics answer "how *different do these look*" - they tolerate imperceptible noise (compression artifacts, sub-pixel shifts) and score perceived similarity instead. Reach for them when an exact pixel match is too strict. BlazeDiff ships two: **GMSD** (gradient-based, fast) and **SSIM** (the classic structural index, with a 4× faster Hitchhiker's variant). ## Installation ```bash npm install @blazediff/gmsd @blazediff/ssim ``` ## GMSD (Gradient Magnitude Similarity Deviation) Scores gradient (edge) similarity. Returns `0` for identical images; **lower is better**, typically in the `0`–`0.35` range. ```ts const img1 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png" ); const img2 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png" ); const width = img1.width; const height = img1.height; const score = gmsd(img1, img2, undefined, width, height); ``` ```ts const img1 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png" ); const img2 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png" ); const width = img1.width; const height = img1.height; const score = gmsd(img1, img2, undefined, width, height, { c: 100 }); ``` See the [GMSD API reference →](/apis/gmsd). ## SSIM (Structural Similarity Index) Scores luminance, contrast, and structure. Returns `1` for identical images; **higher is better**, in the `0`–`1` range. The Hitchhiker's variant uses non-overlapping windows for ~4× the throughput at near-identical accuracy. ```ts import ssim from "@blazediff/ssim/ssim"; const img1 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png" ); const img2 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png" ); const output = new Uint8Array(img1.width * img1.height * 4); const width = img1.width; const height = img1.height; const score = ssim(img1, img2, output, width, height); ``` ```ts import hitchhikersSSIM from "@blazediff/ssim/hitchhikers-ssim"; const img1 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3a.png" ); const img2 = await loadImage( "https://raw.githubusercontent.com/teimurjan/blazediff/refs/heads/main/fixtures/blazediff/3b.png" ); const output = new Uint8Array(img1.width * img1.height * 4); const width = img1.width; const height = img1.height; const score = hitchhikersSSIM(img1, img2, output, width, height); ``` See the [SSIM API reference →](/apis/ssim). --- # Image Difference Analysis Takes a raw pixel diff and tells you _what_ changed, _where_, and _how much_. No ML models - a deterministic pipeline that runs in the same binary as the diff itself. Available in both the native Node binding (`@blazediff/core-native`) and the WebAssembly build (`@blazediff/core-wasm`), so you can run it server-side or in the browser. ## How it works 1. Pixel diff → binary change mask 2. Morphological close → bridge small gaps 3. Connected components → isolate regions 4. Per-region evidence extraction: - **Dual-image gradients** - edges in both images + spatial correlation to detect structural preservation - **Color delta distribution** - mean, max, and stddev of YIQ distance to separate uniform recolors from patchy texture changes - **Background distance** - how much changed pixels blend with local unchanged pixels in each image 5. Six-label decision tree classifies each region 6. Post-hoc shift detection matches Addition+Deletion pairs ## Demo ## Usage Both bindings return the same result shape - `summary`, `regions[]` (with `position`, `changeType`, `percentage`, …), and `severity`. The native binding reads files; the WASM binding takes pre-decoded RGBA buffers. ```ts import { interpret } from "@blazediff/core-native"; const result = await interpret("fixtures/3a.png", "fixtures/3b.png"); console.log(result.summary); for (const region of result.regions) { console.log(`${region.position}: ${region.changeType} (${region.percentage.toFixed(2)}%)`); } ``` ```ts import { initBlazediff, interpret } from "@blazediff/core-wasm"; await initBlazediff(); // loads the .wasm; call once // Decode both images to RGBA Uint8Arrays (see the Rust + WASM example) const a = await loadRgba("3a.png"); const b = await loadRgba("3b.png"); const result = await interpret(a.data, b.data, a.width, a.height); console.log(result.summary); for (const region of result.regions) { console.log(`${region.position}: ${region.changeType} (${region.percentage.toFixed(2)}%)`); } ``` ### Identical images When nothing changed, `regions` is empty and `summary` reports no differences - identical for both bindings. ## Change types | Type | Meaning | |---|---| | `Addition` | Content appeared - blends with background in before image, distinct in after | | `Deletion` | Content removed - distinct in before, blends with background in after | | `Shift` | Content moved - matched Addition+Deletion pair with similar luminance | | `ColorChange` | Recolor - edge structure preserved across both images, uniform color shift | | `ContentChange` | Structural change - edges differ between images | | `RenderingNoise` | Sub-pixel artifacts - filtered from output | ## Accuracy Measured against datasets with hand-labeled change regions (see [crates/blazediff-interpret-verify/BENCHMARKS.md](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff-interpret-verify/BENCHMARKS.md) for the full breakdown). | Dataset | What it tests | Classifier-only macro F1 | End-to-end macro F1 | |---|---|---:|---:| | `addition_deletion` | Clean object insert / remove on photographs | **0.998** | **0.888** | | `shift` | Sub-region translations with pixel-perfect ground truth | **0.813** | **0.628** | | `inpaintcoco` | Inpaint edits that mix recolor and texture replacement | **0.440** | **0.260** | | `html_color_pairs` | Recolors on rendered Tailwind UI screenshots | **0.993** | **0.786** | Read this as: on clear add/delete edits the classifier almost never picks the wrong label (0.998 means 4 mistakes in 904 regions). On synthetic shifts the post-pass pairs about two thirds of moved-block events with near-perfect precision. On real inpainted photos it lands the right label in about four of every nine regions - the `ColorChange` vs `ContentChange` boundary is the dominant confusion and the focus area for the next iteration. `html_color_pairs` isolates that same boundary on 100 rendered Tailwind UI screenshots that differ only in color classes: a dedicated chromatic-recolor branch admits same-luminance hue swaps that YIQ otherwise scores as low-delta, lifting classifier F1 to 0.993 with zero false positives. End-to-end runs the full detector pipeline before classifying, so the score also reflects detection misses and spurious small regions; classifier-only isolates labeling quality from detection. --- # Agentic Visual Testing - Setting Up `@blazediff/agent` captures deterministic screenshots of your routes, diffs them against committed baselines with the native BlazeDiff core, and hands ambiguous diffs back to your coding agent (Claude Code, Cursor, Codex) to judge. This page gets you from zero to a committed set of baselines. ## 1. Install ```bash npm install --save-dev @blazediff/agent ``` First run prompts to install bundled Playwright Chromium - no `sudo`, no `npx playwright install --with-deps`. ```bash blazediff-agent browsers install --check --json # check blazediff-agent browsers install # install if missing ``` ## 2. Onboard `onboard` writes `.blazediff/config.json` from your dev script, sets up `.gitignore`, installs Chromium, and installs the playbook into whatever coding-agent stack lives in your project. ```bash # Setup only - baselines are captured explicitly in step 4 blazediff-agent onboard --no-capture ``` It detects your stack automatically (Claude Code, Codex, Cursor); pass `--stack ` to be explicit, or `--stack local` to install the local (Moondream + Qwen) judge when there's no coding agent. ## 3. Start the dev server ```bash blazediff-agent serve-status --detach --json # waits up to 60s for the port ``` Already have a running URL (e.g. staging)? Skip the dev server and point the agent at it: `blazediff-agent onboard --url https://staging.example.com`. ## 4. Capture baselines Pipe a JSON list of routes - one `capture` call screenshots them all and writes the manifest plus baseline PNGs. ```bash cat <<'EOF' | blazediff-agent capture --stdin --mode baseline --json [ {"id": "home", "url": "/", "mask": [".timestamp"]}, {"id": "pricing", "url": "/pricing"} ] EOF ``` Then tear down the dev server (mandatory): ```bash blazediff-agent serve-status --kill --json ``` > **Info:** **Commit `.blazediff/`** - config, manifest, and baselines all live there and are the source of truth for future checks. ## The config `.blazediff/config.json` is committed and drives every later run: ```json { "devServer": { "command": "pnpm dev", "port": 3000, "readyTimeoutMs": 60000 }, "framework": "next", "packageManager": "pnpm", "baseUrl": "http://127.0.0.1:3000" } ``` `.blazediff/manifest.json` is written by `capture` - **never edit it directly**. Per-route behavior (login, interactions) lives in [harnesses](/docs/agentic-testing/judging-and-harnesses#harnesses), not config. Next: [Running in CI →](/docs/agentic-testing/running-in-ci). Full command reference lives in the [`@blazediff/agent` docs](/apis/agent). --- # Agentic Visual Testing - Running in CI With baselines committed (see [Setting Up](/docs/agentic-testing/setting-up)), CI's job is one verb: `check`. It re-captures every manifest entry, diffs each against its baseline, and fails the build on any regression. ## The check command ```bash blazediff-agent check --judge host --json ``` The CLI starts the dev server automatically when `config.devServer` is set, runs every entry through Playwright, diffs each capture, and emits a `CheckReport`: ```json { "summaryPath": ".blazediff/summary.md", "totalEntries": 23, "passed": 22, "failed": 0, "pendingJudgments": 1, "results": [ { "id": "agent", "url": "/agent", "status": "needs-judgment", "verdict": { "label": "ambiguous", "headline": "5 regions: 4 content-change, 1 addition @ left (0.13%, low)", "action": "investigate" } } ] } ``` `results[]` lists non-pass entries only. Full per-entry detail lives in `.blazediff/summary.md` and `.blazediff/judgments//request.json`. > **Warning:** **Check-only in CI.** When `CI=1` or there's no TTY, only `check` runs. `onboard` / `capture` / `rewrite` / `reset` are blocked - authoring belongs on a developer's machine, where baseline changes can be reviewed. ## GitHub Actions ```yaml - run: pnpm install - run: npx blazediff-agent browsers install - run: npx blazediff-agent --cwd apps/website check --json env: # Only needed if any entry uses a login harness. One pair per persona. # In CI, set these as secrets rather than committing .blazediff/.env. BLAZEDIFF_AUTH_DEFAULT_EMAIL: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_EMAIL }} BLAZEDIFF_AUTH_DEFAULT_PASSWORD: ${{ secrets.BLAZEDIFF_AUTH_DEFAULT_PASSWORD }} ``` Pass `-C, --cwd ` to target one app inside a monorepo. ## Exit codes | Code | Meaning | |---|---| | `0` | Every entry passed | | `1` | At least one regression, intentional, noise, or pending-judgment entry | | non-zero + JSON | Infra failure (missing manifest, no Chromium, etc.) | A route that times out is logged once in the result array and skipped - it never blocks the run. ## When a check fails A `1` exit usually means a diff needs a verdict. Locally, your coding agent reads the judgment request and decides; intentional changes are accepted with `rewrite`. That loop - verdicts, harnesses, and masking flakes - is covered in [Judging and Harnesses →](/docs/agentic-testing/judging-and-harnesses). --- # Agentic Visual Testing - Judging and Harnesses A failing `check` isn't always a regression. This page covers the two things that turn a raw diff into a decision: **judging** (is this change real, intentional, or noise?) and **harnesses** (driving the page - login, interactions - so the right thing gets screenshotted in the first place). ## The judging model The heuristic verdict pipeline emits one of four labels per failing entry: | Label | Meaning | Default action | |---|---|---| | `regression-likely` | Confident structural change | Investigate; do not rewrite | | `intentional-likely` | Confident styling/typographic change | Ask user, then rewrite | | `noise-likely` | Confident non-deterministic source | Ask user; prefer masking | | `ambiguous` | Heuristic couldn't classify | Defer to host judge | For `ambiguous`, `--judge host` writes a `JudgmentRequest` to `.blazediff/judgments//request.json` with: - `regions[]` - bounding boxes, pixel counts, and change types per region - `paths.locator` (`locator.png`) - a ~400px overview with regions outlined in red - `paths.tiles` (`regions.png`) - a vertical stack of `[baseline | actual]` pairs - `paths.{baseline,actual,diff}` - full-page PNGs as a fallback > **Info:** **Token discipline.** The region tiles are 10–100× smaller than the full-page PNGs. A well-behaved host agent reads `regions.png` + `locator.png` first and only falls back to the full-page PNGs if a region clearly continues outside its crop. The host agent writes its verdict to `.blazediff/judgments//verdict.json`: ```json { "id": "agent", "verdict": { "label": "intentional-likely", "headline": "Em-dash replaced with hyphen in copy", "rationale": ["region tile shows only typographic substitution"], "action": "rewrite-if-intended" }, "rationale": "Full paragraph explanation...", "confidence": 0.95 } ``` Then merge verdicts into the report - no re-screenshot: ```bash blazediff-agent check --apply-judgments --json ``` Accept an intentional change by re-baselining the entry (mask/viewport/waitFor are preserved; only the PNG regenerates): ```bash blazediff-agent rewrite agent --json # by id blazediff-agent rewrite --failed --json # all failures from the last check ``` ## Harnesses A **harness** is a pluggable ESM script in `.blazediff/harnesses/.js`, attached to an entry via its `harnesses: [{ name, params? }]` list. Login is just one kind - anything that drives the page before or around a screenshot is a harness. Two phases: - **`setup`** - runs before navigation (establish a session, e.g. login). - **`interact`** (default) - runs after the base screenshot; drives the page and may emit extra named screenshots via `screenshot(name)`, each its own baseline entry `__`. ### Interaction harness ```js // .blazediff/harnesses/weather-menu.js /** @type {import("@blazediff/agent").Harness} */ export default { async run({ page, screenshot }) { await page.getByRole("button", { name: "More options" }).click(); await screenshot("menu"); // -> baseline "weather__menu" }, }; ``` ```json { "id": "weather", "url": "/weather", "harnesses": ["weather-menu"] } ``` ### Login harness Routes behind a login flow capture through a `setup` harness. Credentials live in environment variables - never in the harness file, the manifest, or LLM context. ```js /** @type {import("@blazediff/agent").Harness<{ persona?: string }>} */ export default { phase: "setup", async run({ page, params }) { const upper = (params.persona ?? "default").toUpperCase().replace(/[^A-Z0-9]/g, "_"); const email = process.env[`BLAZEDIFF_AUTH_${upper}_EMAIL`]; const password = process.env[`BLAZEDIFF_AUTH_${upper}_PASSWORD`]; if (!email || !password) throw new Error(`missing BLAZEDIFF_AUTH_${upper}_*`); await page.goto("http://127.0.0.1:3000/login"); await page.locator('input[name="email"]').fill(email); await page.locator('input[name="password"]').fill(password); await Promise.all([ page.waitForURL((u) => !u.pathname.startsWith("/login")), page.getByRole("button", { name: /sign in|log in/i }).click(), ]); }, }; ``` Attach it per entry, and drop credentials in `.blazediff/.env` (auto-gitignored): ```json { "id": "dashboard", "url": "/dashboard", "harnesses": [{ "name": "auth", "params": { "persona": "default" } }] } ``` For OAuth/SSO, magic links, MFA, or captcha - record interactively instead: `blazediff-agent auth init --persona default --login-url http://127.0.0.1:3000/login`. ## Masking flaky regions When a diff is `noise-likely` - or a real-looking diff is actually caused by something non-deterministic - mask it, don't rebaseline. A rebaseline just resets the clock on a flake; a mask removes it. Mask auto-cycling animations, third-party iframes, timestamps, per-session randomness, and personalization noise. **Don't** mask real content that happens to be changing - that's the change you want caught. The agent always masks any element matching `[data-blazediff-agent-mask]` - no manifest change needed. Add it to a shared component and it applies on every route: ```tsx

...
``` When you can't edit the source (third-party embed), fall back to a per-entry CSS selector. The mask list **replaces** the existing one, so include every selector you want kept: ```bash cat <<'EOF' | blazediff-agent capture --stdin --mode baseline --json [ {"id": "examples-vanilla", "url": "/docs/ui-components/vanilla", "mask": ["iframe"]} ] EOF ``` Full reference: [`@blazediff/agent` docs →](/apis/agent). --- # React Components ReactSwipeDemo, ReactTwoUpDemo, ReactOnionSkinDemo, ReactDifferenceDemo, } from "../../../../components/demos/react-modes"; Build interactive image comparison components with React. Every example below is live and runs right on this page. ## Installation ```bash npm install @blazediff/react ``` ```tsx ``` ```tsx ``` ```tsx ``` ```tsx ``` All four components render from the [`@blazediff/ui`](/apis/ui) headless engine and accept `*ClassName` props for styling. See the [full API reference →](/apis/react). --- # Vanilla Components VanillaSwipeDemo, VanillaTwoUpDemo, VanillaOnionSkinDemo, VanillaDifferenceDemo, } from "../../../../components/demos/vanilla-modes"; Use the BlazeDiff UI renderer (`mount*` functions) in vanilla JS - or drive the headless engine from any framework. Every example below is live and runs right on this page. ## Installation ```bash npm install @blazediff/ui ``` ```ts mountSwipe(document.getElementById("app"), { src1: "before.png", src2: "after.png", onPositionChange: (position) => console.log(position), }); ``` ```ts mountTwoUp(document.getElementById("app"), { src1: "before.png", src2: "after.png", onImagesLoaded: ({ image1, image2 }) => console.log(image1, image2), }); ``` ```ts mountOnionSkin(document.getElementById("app"), { src1: "before.png", src2: "after.png", opacity: 50, onOpacityChange: (opacity) => console.log(opacity), }); ``` ```ts mountDifference(document.getElementById("app"), { src1: "before.png", src2: "after.png", threshold: 0.1, onDiffComplete: ({ diffCount, percentage }) => console.log(diffCount, percentage), }); ``` Each `mount*` call returns a `{ update, destroy }` handle - call `update(options)` to change sources or options in place, and `destroy()` to remove the UI. See the [full API reference →](/apis/ui). --- # Object Comparison Learn how to use BlazeDiff for comparing JavaScript objects and detecting changes in complex data structures. ## Installation ```bash npm install @blazediff/object ``` ## Overview BlazeDiff Object provides fast and efficient comparison of JavaScript objects, detecting additions, removals, and modifications with detailed path information. Try the interactive demos below to see different comparison scenarios in action. ```ts import diff from "@blazediff/object"; const oldObj = { a: 1, b: 2, c: 3 }; const newObj = { a: 1, b: 20, d: 4 }; const changes = diff(oldObj, newObj); ``` ```ts import diff from "@blazediff/object"; const oldObj = { user: { name: "John", email: "john@old.com", settings: { theme: "dark", notifications: true } } }; const newObj = { user: { name: "John Doe", email: "john@new.com", settings: { theme: "light", notifications: true, language: "en" } } }; const changes = diff(oldObj, newObj); ``` ```ts import diff from "@blazediff/object"; const oldObj = { items: [ { id: 1, name: "Item 1", value: 100 }, { id: 2, name: "Item 2", value: 200 } ], total: 300 }; const newObj = { items: [ { id: 1, name: "Item 1", value: 150 }, { id: 2, name: "Item 2", value: 200 }, { id: 3, name: "Item 3", value: 50 } ], total: 400 }; const changes = diff(oldObj, newObj); ``` ---