The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Fast pixel-by-pixel image comparison for R, powered by odiff.
expect_snapshot_image(), and a compare function for
shinytest2 screenshotsInstall odiffr:
install.packages("odiffr")
# Or the development version from GitHub
# install.packages("pak")
pak::pak("BenWolst/odiffr")Odiffr requires the odiff binary (>= 4.1.1). The easiest way to get it is from R, which downloads the binary for your platform to your user cache (no Node.js needed):
odiffr::install_odiff()In interactive sessions, odiffr also offers to do this the first time odiff is needed; it never downloads anything without asking. Alternatively, install odiff system-wide:
# npm (cross-platform)
npm install -g odiff-bin
# Or download binaries from https://github.com/dmtrKovalenko/odiff/releaseslibrary(odiffr)
result <- compare_images("baseline.png", "current.png", diff_output = "diff.png")
result$match
#> [1] FALSE
result$diff_percentage
#> [1] 2.45# Adjust sensitivity (0-1, lower = stricter) and ignore antialiased pixels
compare_images("img1.png", "img2.png", threshold = 0.05, antialiasing = TRUE)
# Fail immediately if dimensions differ
compare_images("img1.png", "img2.png", fail_on_layout = TRUE)
# Ignore areas with dynamic content, e.g. timestamps
compare_images("img1.png", "img2.png",
ignore_regions = list(
ignore_region(0, 0, 200, 50), # header
ignore_region(0, 500, 800, 600) # footer
)
)
# magick-image objects and plots work too
compare_images(magick::image_read("baseline.png"), "current.png")
# Low-level interface with every odiff option
odiff_run("img1.png", "img2.png", diff_lines = TRUE)When a comparison fails, plot(odiff_run(...)) shows the
baseline, current and diff images side by side, and
diff_image() returns the diff as a magick image or
raster.
test_that("dashboard renders correctly", {
expect_images_match("screenshots/current.png", "screenshots/baseline.png")
})
test_that("plot matches its baseline", {
p <- ggplot2::ggplot(mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point()
expect_images_match(p, test_path("baselines/scatter.png"),
plot_options = plot_options(width = 6, height = 4))
})Plots (ggplot objects, functions that draw a plot, and recorded
plots) are rendered to PNG with ragg if installed. On failure, a diff
image is saved to tests/testthat/_odiffr/.
expect_snapshot_image() lets testthat manage the
baselines in tests/testthat/_snaps/, while odiff does the
comparison, so differences below the threshold don’t fail:
test_that("plots are stable", {
p <- ggplot2::ggplot(mtcars, ggplot2::aes(wt, mpg)) + ggplot2::geom_point()
expect_snapshot_image(p)
expect_snapshot_image(function() hist(mtcars$mpg), name = "mpg-hist",
preset = "screenshot")
})
# After an intended change
testthat::snapshot_review()
testthat::snapshot_accept()Like other file snapshots, these are skipped on CRAN.
odiff_preset() gives calibrated settings
("strict", "default",
"screenshot", "cross_platform"). On CI,
snapshot_report() writes an HTML, Markdown or JUnit report
of all changed snapshots.
Use odiff for shinytest2 screenshots to tolerate browser antialiasing noise and get a diff image when a screenshot changes:
app$expect_screenshot(compare = compare_file_odiff(preset = "screenshot"))See vignette("shinytest2", package = "odiffr"), and
vignette("web-pages", package = "odiffr") for web pages and
htmlwidgets.
# Compare two directories (matched by relative path)
results <- compare_image_dirs("baseline/", "current/", recursive = TRUE,
diff_dir = "diffs/")
summary(results)
failed_pairs(results)
# HTML report with baseline, current and diff thumbnails
batch_report(results, "diffs/report.html", images = "all", embed = TRUE)
# Or both steps in one call
compare_dirs_report("baseline/", "current/")
# Accept intended changes as the new baselines
approve_changes(results, dry_run = TRUE)
approve_changes(results)Files missing from current/ are reported as failing
"missing" rows, and a pair that can’t be compared becomes
an "error" row with the message in the error
column. compare_images_batch() compares an explicit list of
pairs, optionally in parallel.
res <- compare_pdfs("before/report.pdf", "after/report.pdf", dpi = 150,
diff_dir = "pdf-diffs")
failed_pairs(res)[, c("page", "reason", "diff_percentage")]
# Every PDF in two directories, e.g. outputs before/after an R upgrade
res <- compare_pdf_dirs("outputs-old/", "outputs-new/", diff_dir = "pdf-diffs")Requires the pdftools package. See
vignette("pdf-outputs", package = "odiffr").
use_odiffr_ci() adds a ready-to-use GitHub Actions
workflow to your package (.github/workflows/odiffr.yaml)
that installs odiff with install_odiff(), runs your tests,
summarises changed image snapshots with snapshot_report()
on the job summary page and uploads the new snapshots and diff images
when tests fail:
odiffr::use_odiffr_ci()Batch results can be written in formats CI systems display natively:
- name: Compare images
run: |
library(odiffr)
results <- compare_image_dirs("baseline/", "current/", diff_dir = "diffs/")
batch_markdown(results) # GitHub job summary
batch_junit(results, "odiffr-junit.xml") # test report
batch_report(results, "diffs/report.html", images = "all", embed = TRUE)
if (any(!results$match)) stop("Visual regression detected!")
shell: Rscript {0}
- name: Upload diffs
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-diffs
path: diffs/odiff_available() # is odiff installed?
odiff_info() # path, version and source
install_odiff() # download odiff to the user cache
odiffr_update() # same, lower-level (e.g. to update between releases)
# Use a specific binary
options(odiffr.path = "/path/to/odiff")odiff is found via options(odiffr.path), then the system
PATH, then the binary downloaded by install_odiff(). For
npm installs (odiff >= 4.4), odiffr calls the native binary directly
rather than the Node.js launcher on the PATH, which makes each
comparison around 5x faster; set
options(odiffr.resolve_npm = FALSE) to disable this.
| Type | Formats |
|---|---|
| Input | PNG, JPEG, WEBP, TIFF (.tiff), BMP; PDF via
compare_pdfs() |
| Output | PNG only |
Cross-format comparison is supported (e.g. JPEG against PNG). odiff
does not accept the .tif extension.
options(odiffr.path = ...)audit_record() writes a
JSON or CSV record of comparisons, with input and output file hashes,
the odiff version and binary hash, parameters, platform and a UTC
timestampoptions(odiffr.path = "/validated/bin/odiff-4.5.0")
result <- odiff_run("baseline.png", "current.png", "diff.png", threshold = 0.05)
audit_record(result, file = "audit.json")odiff is approximately 6x faster than ImageMagick for pixel
comparison, thanks to SIMD optimisations. On x86_64 systems with
AVX-512, pass enable_asm = TRUE to odiff_run()
for ~12% faster comparisons.
MIT
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.