@blazediff/core-native
The fastest single-threaded image diff in the world. Native Rust implementation with SIMD optimization, 4.4-4.9x faster and 3x smaller than odiff.
Features:
- PNG, JPEG & QOI support - auto-detected by file extension or encoded bytes
- SIMD-accelerated (NEON on ARM, SSE4.1 on x86)
- Block-based two-pass optimization
- YIQ perceptual color difference
- Anti-aliasing detection
- Cross-platform pre-built binaries (~700KB-900KB, no compilation required)
Vendored Libraries:
- libspng - Fast PNG decoding/encoding with SIMD (default)
- libjpeg-turbo - High-performance JPEG codec with SIMD
- qoi - QOI (Quite OK Image) format for fast lossless compression
Experimental PNG codec: the binaries also bundle an in-house Rust PNG codec (
blazediff-png), faster than spng on every fixture with byte-exact decode parity. It's opt-in: setBLAZEDIFF_PNG_ENABLED=1to route PNG decode and stored-block (level 0) encode through it. spng stays the default.
Note: This package was previously published as
@blazediff/bin, which is now deprecated. Please use@blazediff/core-nativeinstead.
Installation
npm install @blazediff/core-native
Also available as a Rust crate: cargo install blazediff
Pre-built binaries are included via platform-specific packages:
@blazediff/core-native-darwin-arm64- macOS ARM64 (Apple Silicon)@blazediff/core-native-darwin-x64- macOS x64 (Intel)@blazediff/core-native-linux-arm64- Linux ARM64@blazediff/core-native-linux-x64- Linux x64@blazediff/core-native-win32-arm64- Windows ARM64@blazediff/core-native-win32-x64- Windows x64
API
diff(a, b, options?)
Compare two images and optionally draw the diff. options is @blazediff/core's DiffOptions, with a wider output.
| Parameter | Type | Description |
|---|---|---|
a, b |
string | Uint8Array | Image | A file path, encoded PNG, JPEG or QOI bytes (a Buffer works), or raw RGBA pixels { data, width, height }. The two sides can use different kinds |
options |
NativeDiffOptions | Comparison options (optional) |
Returns: Promise<NativeDiffResult>
| Option | Type | Default | Description |
|---|---|---|---|
threshold |
number | 0.1 | Color difference threshold (0-1). Lower is stricter |
antialiasing |
boolean | true | Detect anti-aliased pixels and leave them out of the count |
metric |
"yiq" | "oklab" | "yiq" | Color difference metric. oklab gives the same counts as pixelmatch 8 |
mask |
Uint8Array | - | One byte per pixel (width × height). Non-zero pixels are skipped. Needs the N-API binding |
window |
number | - | Also report the N×N window with the most diff pixels |
output |
string | Image | { path, compression?, quality? } | - | Where to draw the diff: a file (PNG compression 0-9, JPEG quality 1-100), or an Image filled in place. A file is not written when the images are identical. An Image is always filled |
render |
{ alpha?, diffColor?, diffColorAlt?, aaColor?, background? } | - | Output styling. background: "transparent" draws the diff alone |
Result Types
type NativeDiffResult =
| {
kind: "compared";
count: number;
percentage: number;
identical: boolean;
/** Top-left corner and count of the busiest window, when `window` is set. */
window?: { x: number; y: number; count: number };
}
| { kind: "dimension-mismatch"; a: Size; b: Size }
| { kind: "io-error"; path?: string; message: string };
Invalid options or buffers throw a BlazeDiffError (re-exported from @blazediff/core) with a code: invalid-image, invalid-output, invalid-mask or invalid-option.
Encoded Buffer Inputs
import { readFile } from "node:fs/promises";
import { diff } from "@blazediff/core-native";
const [expected, actual] = await Promise.all([
readFile("expected.png"),
readFile("actual.png"),
]);
const result = await diff(expected, actual, { output: "diff.png" });
The native binding borrows the existing Buffer or Uint8Array backing memory for the synchronous call. The encoded bytes are not copied into Rust, so the same JavaScript buffers can be reused across comparisons. Decoding still allocates native RGBA pixel buffers.
CLI fallback
If the N-API binding cannot load, diff runs the bundled blazediff CLI. The fallback takes path inputs and a path output only, and no mask. Anything else throws.
Usage
Programmatic API
import { diff } from '@blazediff/core-native';
const result = await diff('expected.png', 'actual.png', {
output: 'diff.png',
threshold: 0.1,
});
if (result.kind === 'compared') {
if (result.identical) console.log('Images are identical!');
else console.log(`${result.count} pixels differ (${result.percentage.toFixed(2)}%)`);
} else if (result.kind === 'dimension-mismatch') {
console.log('Images have different dimensions');
} else {
console.log(`Could not read ${result.path}: ${result.message}`);
}
Describing What Changed
This package answers where two images differ. For what changed — labelled
regions, change types, severity and a human-readable summary — use
@blazediff/interpret-native,
which classifies the same diff and can also locate regions with an SSIM map or
boxes you already have.
import { interpret } from '@blazediff/interpret-native';
const result = await interpret('expected.png', 'actual.png', 'diff.png');
console.log(result.summary);
console.log(`Severity: ${result.severity}, ${result.diffPercentage.toFixed(2)}% changed`);
CLI Usage
# Compare two PNG images
npx blazediff expected.png actual.png -o diff.png
# Compare two JPEG images
npx blazediff expected.jpg actual.jpg -o diff.jpg
# Mixed formats (PNG input, QOI output - recommended for smallest diff files)
npx blazediff expected.png actual.png -o diff.qoi
# With options
npx blazediff expected.png actual.png -o diff.png --threshold 0.05 --no-antialiasing
# pixelmatch 8 metric, a mask, and the busiest 16×16 window
npx blazediff expected.png actual.png --metric oklab --mask mask.png --window 16
# With higher PNG compression (smaller output file, slower)
npx blazediff expected.png actual.png -o diff.png -c 6
# Output as JSON
npx blazediff expected.png actual.png --json
CLI Options
Usage: blazediff [OPTIONS] <A> <B>
Arguments:
<A> First image path (PNG, JPEG, or QOI)
<B> Second image path (PNG, JPEG, or QOI)
Options:
-o, --output <OUTPUT> Write the diff image (only when the images differ)
-t, --threshold <THRESHOLD> Color difference threshold (0.0-1.0) [default: 0.1]
--antialiasing Detect anti-aliased pixels and leave them out of the count (default)
--no-antialiasing Count anti-aliased pixels as differences
--metric <METRIC> Color difference metric: yiq or oklab (pixelmatch 8) [default: yiq]
--mask <MASK> Image the size of the inputs; its non-black pixels are skipped
--window <WINDOW> Also report the N×N window with the most diff pixels
-a, --alpha <ALPHA> Opacity of the faded background (0.0-1.0) [default: 0.1]
--diff-color <R,G,B> RGB color for differing pixels
--diff-color-alt <R,G,B> RGB color for pixels that get darker in the second image
--aa-color <R,G,B> RGB color for anti-aliased pixels
--background <BACKGROUND> faded or transparent [default: faded]
-c, --compression <COMPRESSION> PNG compression level (0-9, 0=fastest, 9=smallest) [default: 0]
-q, --quality <QUALITY> JPEG quality (1-100) [default: 90]
--json Print the result as JSON
-h, --help Print help
-V, --version Print version
With --json, the result object prints to stdout and an error such as {"kind":"io-error","path":"...","message":"..."} prints to stderr.
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.
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 identical1- Images differ, or their dimensions do not match2- Error (file not found, invalid format, etc.)
Performance
Benchmarked on Apple M1 Max (25 runs, 5 warmup, image IO included) against odiff on 5600×3200 4K PNG images via hyperfine. Full numbers in the root BENCHMARKS.md.
| Tool | 4k/1 | 4k/2 | 4k/3 | vs blazediff |
|---|---|---|---|---|
| blazediff | ~275ms | ~342ms | ~347ms | - |
| blazediff (encoded buffer input) | ~203ms | ~256ms | ~270ms | - |
| odiff | ~1266ms | ~1538ms | ~1799ms | 4.5-5.2x slower |
Binary sizes (stripped, LTO optimized) - ~3x smaller than odiff:
| Platform | blazediff | odiff |
|---|---|---|
| macOS ARM64 | 702 KB | 2.2 MB |
| macOS x64 | 773 KB | 2.6 MB |
| Linux ARM64 | 753 KB | 2.3 MB |
| Linux x64 | 869 KB | 2.9 MB |
| Windows ARM64 | 580 KB | 2.4 MB |
| Windows x64 | 915 KB | 3.0 MB |
Algorithm
BlazeDiff uses a two-pass block-based approach with SIMD acceleration:
- Cold Pass: Scans image in 8x8 blocks using 32-bit integer comparison to identify changed regions
- Hot Pass: Only processes blocks marked as changed, applying YIQ perceptual color difference
- SIMD: Uses NEON (ARM) or SSE4.1 (x86) for parallel pixel processing
- Anti-aliasing: Implements Vysniauskas (2009) algorithm to detect AA artifacts
References
- YIQ Color Space: Kotsarenko & Ramos (2009) - "Measuring perceived color difference using YIQ NTSC transmission color space"
- Anti-Aliasing Detection: Vysniauskas (2009) - "Anti-aliased Pixel and Intensity Slope Detector"
- Inspiration: odiff - Fast image comparison tool written in Zig