blazediff (Rust)
The Rust crate that powers the entire BlazeDiff stack. 3-4x faster than 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).
Installation
As a CLI
cargo install blazediffAs a library
# Cargo.toml
[dependencies]
blazediff = "*"Pre-built binaries (via cargo install) and crate sources are available on crates.ioΒ .
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) orpython(PyO3 bindings) when building from source
CLI Usage
# Basic diff
blazediff image1.png image2.png -o diff.png
# Custom threshold (0.0 - 1.0)
blazediff image1.png image2.png -o diff.png -t 0.05
# JPEG comparison
blazediff a.jpg b.jpg -o diff.jpg -q 85
# QOI diff output (12x smaller than PNG, faster encode)
blazediff a.png b.png -o diff.qoi
# pixelmatch 8 metric and the busiest 16Γ16 window, as JSON
blazediff a.png b.png --metric oklab --window 16 --json
# Structured interpretation (separate crate and binary)
blazediff-interpret a.png b.png --jsonCLI 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 versionExit Codes
0- Images are identical1- Images differ (includes a dimension 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.
use blazediff::{diff, load_pngs, save_png, DiffOptions, DiffResult, Image};
let (img1, img2) = load_pngs("expected.png", "actual.png")?;
let options = DiffOptions {
threshold: 0.1,
antialiasing: false, // count anti-aliased pixels too
..Default::default()
};
let mut output = Image::new(img1.width, img1.height);
match diff(&img1, &img2, Some(&mut output), &options)? {
DiffResult::Compared { count, percentage, identical, .. } => {
println!("{count} pixels differ ({percentage:.2}%)");
if !identical {
save_png(&output, "diff.png")?;
}
}
DiffResult::DimensionMismatch { a, b } => println!("Sizes differ: {a:?} vs {b:?}"),
}Types
pub struct DiffOptions {
pub threshold: f64, // 0.0-1.0, default 0.1
pub antialiasing: bool, // detect and exclude anti-aliased pixels, default true
pub metric: Metric, // Yiq (default) or Oklab (pixelmatch 8)
pub mask: Option<Vec<u8>>, // one byte per pixel; non-zero is skipped
pub window: Option<u32>, // also report the busiest NΓN window
pub render: RenderOptions,
}
pub struct RenderOptions {
pub alpha: f64, // background opacity, default 0.1
pub diff_color: [u8; 3], // default red [255, 0, 0]
pub diff_color_alt: Option<[u8; 3]>, // pixels that get darker in the second image
pub aa_color: [u8; 3], // default yellow [255, 255, 0]
pub background: Background, // Faded (default) or Transparent
}
pub enum DiffResult {
Compared { count: u32, percentage: f64, identical: bool, window: Option<WindowHit> },
DimensionMismatch { a: Size, b: Size },
}
pub struct WindowHit { pub x: u32, pub y: u32, pub count: u32 } // top-left corner and countdiff returns Err only for buffers that do not match their size and masks of the wrong length. PNG compression is an argument of save_png_with_compression, not a diff option.
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().
Threshold guidelines:
0.0- Exact match only0.05- Strict comparison0.1- Default balanced comparison0.2- Lenient comparison
Cargo Features
| Feature | Purpose |
|---|---|
| (default) | Pure Rust library + CLI |
napi | N-API bindings used by @blazediff/core-native on NPM |
python | PyO3 bindings used by the blazediff package on PyPI |
The napi and python features are mutually exclusive build modes for distributing prebuilt artifacts; you donβt need them when consuming the crate as a Rust dependency.
Interpret
Structured diff analysis. Takes two images, returns classified change regions
with human-readable summaries. It is not part of this crate: it lives in
blazediff-interpret, which consumes what diff
returns and ships its own blazediff-interpret binary.
Pipeline: change mask -> morph close -> connected components -> noise census + fragment merge -> per-region evidence extraction -> classify -> shift pairing -> describe
Six-label rule cascade:
| Type | Signal |
|---|---|
RenderingNoise | Tiny (<=25px) or sparse + low color delta, without an add/delete signature |
Addition | Blends with background in img1, distinct in img2 |
Deletion | Distinct in img1, blends with background in img2 |
ColorChange | Luminance structure preserved under a color shift, or a coherent chroma move (hue rotation, smooth delta field) over regenerated texture |
ContentChange | Fallback - structure replaced, chroma scattered |
Shift | Post-hoc: region pair whose before-crop and after-crop hold the same content, matched by patch correlation |
blazediff-interpret a.png b.png
blazediff-interpret a.png b.png diff.png --jsonSeverity tiers: Low (<1%), Medium (1-10%), High (>10%). See Interpret example β for the interactive demo, and INTERPRET.mdΒ for the full algorithm spec.
Vendored libraries
- libspngΒ - fast PNG decoding/encoding with SIMD
- libjpeg-turboΒ - high-performance JPEG codec with SIMD
- qoi-rustΒ - QOI (Quite OK Image) format
No system C dependencies; the crate builds standalone with a recent Rust toolchain.