Skip to Content
New: codec-png-native and codec-png-wasm bring the blazediff-png codec to Node and WebAssembly, faster than spng and jSquash. Read more β†’
APIs@blazediff/core-native

@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Β .

View Detailed BenchmarksΒ 

This package was previously published as @blazediff/bin, which is now deprecated. Please use @blazediff/core-native instead.

Installation

npm install @blazediff/core-native

Also available as a Rust crate: cargo install blazediff

Pre-built binaries are included for all major platforms - no compilation required:

  • macOS ARM64 (Apple Silicon) & x64 (Intel)
  • Linux ARM64 & x64
  • Windows ARM64 & x64

Features

  • PNG, JPEG & QOI support - auto-detected by file extension or encoded bytes
  • 4.4-4.9x faster than odiff, 3x smaller binaries (~700KB-900KB vs ~2-3MB)
  • SIMD-accelerated - NEON on ARM, SSE4.1 on x86
  • Block-based optimization - skips unchanged regions
  • Interpret mode - region detection, classification, severity scoring, human-readable summaries

Vendored Libraries

  • libspngΒ  - Fast PNG decoding/encoding with SIMD
  • libjpeg-turboΒ  - High-performance JPEG codec with SIMD
  • qoiΒ  - QOI (Quite OK Image) format for fast lossless compression

API Reference

diff(a, b, options?)

Compares two images and optionally draws the diff. It takes the same options and returns the same result as @blazediff/core, with wider inputs and outputs. Each side is 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
OptionTypeDefaultDescription
thresholdnumber0.1Color difference threshold (0-1). Lower is stricter
antialiasingbooleantrueDetect anti-aliased pixels and leave them out of the count
metric"yiq" | "oklab""yiq"Color metric. "oklab" gives the same counts as pixelmatch 8
maskUint8Array-One byte per pixel (width Γ— height). Non-zero pixels are skipped. Needs the N-API binding
windownumber-Also report the NΓ—N window with the most diff pixels
outputstring | Image | { path, compression?, quality? }-A file path (PNG compression 0-9, JPEG quality 1-100), or an Image filled in place. A file is not written when the images are identical
render{ alpha?, diffColor?, diffColorAlt?, aaColor?, background? }-Output styling, as in @blazediff/core

Result

type NativeDiffResult = | { kind: "compared"; count: number; percentage: number; identical: boolean; 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 with a code, as in @blazediff/core.

Encoded inputs are passed by reference across the N-API boundary. Rust borrows the existing JavaScript backing memory for the synchronous call. Image decoding still allocates native RGBA pixel buffers.

Threshold Guidelines:

  • 0.0 - Exact match only
  • 0.05 - Strict comparison
  • 0.1 - Default balanced comparison
  • 0.2 - Lenient comparison

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

Encoded Buffer Input

import { readFile } from "node:fs/promises"; import { diff } from "@blazediff/core-native"; const [expected, actual] = await Promise.all([ readFile("expected.png"), readFile("actual.png"), ]); // Reuse these same Buffer objects across comparisons without a JS-to-Rust copy. const result = await diff(expected, actual, { output: "diff.png" });

CLI Usage

# Compare two PNG images and write the diff 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 # Stricter threshold, anti-aliased pixels counted npx blazediff expected.png actual.png -o diff.png --threshold 0.05 --no-antialiasing # pixelmatch 8 metric and the busiest 16Γ—16 window, as JSON npx blazediff expected.png actual.png --metric oklab --window 16 --json # With higher PNG compression (smaller output file, slower) npx blazediff expected.png actual.png -o diff.png -c 6

CLI Options

Usage: blazediff [OPTIONS] <A> <B> Options: -o, --output <OUTPUT> Write the diff image here (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) [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

--json prints the result object, for example {"kind":"compared","count":121,"percentage":0.09,"identical":false}.

Supported Formats

FormatExtensionsNotes
PNG.pngLossless, supports transparency
JPEG.jpg, .jpegLossy, smaller file sizes
QOI.qoiFast 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.

Use QOI for diff outputs: QOI excels at encoding diff images with large uniform areas, producing files 12x smaller than PNG (level 0) while being faster to encode.

Exit Codes

  • 0 - Images are identical
  • 1 - Images differ (includes layout/size mismatch)
  • 2 - Error (file not found, invalid format, etc.)

Interpret

Structured region analysis is a separate package. This one answers where pixels differ; @blazediff/interpret-native takes the same pair and describes what changed β€” labelled regions, severity and a human-readable summary β€” and can locate those regions with a pixel diff, an SSIM map, or boxes you already have.

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

See Interpret example β†’ for the interactive demo.

Performance

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

ToolTimeComparison
blazediff (encoded buffer input)~203ms-
blazediff (file paths)~275ms-
odiff~1266ms4.6x slower

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

Binary sizes (stripped, LTO optimized):

Platformblazediffodiff
macOS ARM64702 KB2.2 MB
Linux x64869 KB2.9 MB
Windows x64915 KB3.0 MB

Why so fast? BlazeDiff uses a two-pass block-based algorithm with SIMD acceleration. The cold pass quickly identifies unchanged blocks using 32-bit integer comparison, then the hot pass only processes changed regions with YIQ perceptual color difference. Three refinements keep the common paths cheap: the cold pass folds four SIMD chunks into one wide test, so 16 unchanged pixels cost a single branch; an integer bound derived from the YIQ metric’s largest eigenvalue rejects sub-threshold pixels before any floating-point work; and chunks where nothing crosses the threshold take a vectorized background write instead of falling back to per-pixel handling.

Last updated on