blazediff (Python)
PyO3 bindings to the same Rust core that powers @blazediff/core-native. 3-4x faster than odiffΒ , shipped as abi3 wheels for CPython β₯ 3.8 across macOS, Linux, and Windows.
Installation
pip install blazediffuv 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
- Paths or bytes - pass file paths or encoded bytes 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
diff(a, b, output=None, *, ...)
Compares two images and optionally writes a diff image. It has the same options and result as every other BlazeDiff surface, in snake_case. All keyword arguments are optional.
from blazediff import diff
result = diff("expected.png", "actual.png", "diff.png", threshold=0.1)Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
a, b | str | bytes | - | A file path or encoded PNG, JPEG or QOI bytes |
output | str | None | None | Path for the diff image. It is written only when the images differ |
threshold | float | 0.1 | Color difference threshold (0.0-1.0). Lower is stricter |
antialiasing | bool | True | Detect anti-aliased pixels and leave them out of the count |
metric | str | None | "yiq" | Color metric: "yiq" or "oklab" (same counts as pixelmatch 8) |
mask | bytes | None | None | One byte per pixel (width Γ height). Non-zero pixels are skipped |
window | int | None | None | Also report the NΓN window with the most diff pixels |
alpha | float | 0.1 | Opacity of the faded background in the diff image |
diff_color, diff_color_alt, aa_color | tuple[int, int, int] | None | red, diff_color, yellow | Colors of different, darker and anti-aliased pixels |
background | str | None | "faded" | "faded" or "transparent" |
compression | int | 0 | PNG compression level (0-9, 0 = fastest, 9 = smallest) |
quality | int | 90 | JPEG quality (1-100) |
Return value
diff() returns a DiffResult. Its kind decides which fields are set:
kind | Fields |
|---|---|
"compared" | count, percentage, identical, and window (x, y, count) when window is set |
"dimension-mismatch" | a and b, each with width and height |
"io-error" | path (when known) and message |
Invalid options raise ValueError.
Threshold guidelines:
0.0- Exact match only0.05- Strict comparison0.1- Default balanced comparison0.2- Lenient comparison
Usage
Basic comparison
from blazediff import diff
result = diff("expected.png", "actual.png", "diff.png", threshold=0.1)
if result.kind == "compared":
if result.identical:
print("Images are identical!")
else:
print(f"{result.count} pixels differ ({result.percentage:.2f}%)")
elif result.kind == "dimension-mismatch":
print("Images have different dimensions")
else:
print(f"Could not read {result.path}: {result.message}")pytest assertion
from blazediff import diff
def test_homepage_matches_baseline():
result = diff("baseline.png", "rendered.png", "diff.png", threshold=0.05)
assert result.kind == "compared" and result.identical, (
f"{result.count} pixels differ ({result.percentage:.2f}%)"
)Mixed formats
diff() infers format from the file extension. Inputs and outputs can mix freely:
# PNG inputs, QOI diff output (12x smaller diff files)
diff("expected.png", "actual.png", "diff.qoi")
# JPEG comparison
diff("expected.jpg", "actual.jpg", "diff.jpg", quality=85)| Format | Extensions | Notes |
|---|---|---|
| PNG | .png | Lossless, transparency |
| JPEG | .jpg, .jpeg | Lossy, smaller files |
| QOI | .qoi | Fast lossless, ideal for diff outputs (~12x smaller than PNG) |
Interpret mode
Structured analysis β change regions, classification (Addition, Deletion,
Shift, ContentChange, ColorChange, RenderingNoise), severity scoring and
human-readable summaries β is not part of the Python wheel. It lives in the
blazediff-interpret crate, reachable from a
shell with its blazediff-interpret CLI:
blazediff-interpret expected.png actual.png --jsonSee Interpret example β for the interactive demo.