| Title: | Fast Pixel-by-Pixel Image Comparison Using 'odiff' |
|---|---|
| Description: | R bindings to 'odiff', a blazing-fast pixel-by-pixel image comparison tool <https://github.com/dmtrKovalenko/odiff>. Supports PNG, JPEG, WEBP, and TIFF with configurable thresholds, antialiasing detection, and region ignoring. Requires system installation of 'odiff'. Ideal for visual regression testing in automated workflows. |
| Authors: | Ben Wolstenholme [aut, cre] |
| Maintainer: | Ben Wolstenholme <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.5.1 |
| Built: | 2026-05-31 09:12:20 UTC |
| Source: | https://github.com/benwolst/odiffr |
Creates a standalone HTML report summarizing batch image comparison results. Includes pass/fail statistics, failure reasons, diff statistics, and thumbnails of the worst offenders.
batch_report( object, output_file = NULL, title = "odiffr Comparison Report", embed = FALSE, relative_paths = FALSE, n_worst = 10, show_all = FALSE, ... )batch_report( object, output_file = NULL, title = "odiffr Comparison Report", embed = FALSE, relative_paths = FALSE, n_worst = 10, show_all = FALSE, ... )
object |
An |
output_file |
Path to write the HTML file. If NULL, returns HTML as a character string. |
title |
Report title. Default: "odiffr Comparison Report". |
embed |
If TRUE, embed diff images as base64 data URIs for a fully self-contained file. If FALSE (default), link to image files on disk. |
relative_paths |
If TRUE and |
n_worst |
Number of worst offenders to display. Default: 10. |
show_all |
If TRUE, include a table of all comparisons. Default: FALSE. |
... |
Additional arguments passed to |
Diff image thumbnails (or embedded images when embed = TRUE) are only
shown for comparisons where a diff_output file was created. This requires
using diff_dir in compare_images_batch() or compare_image_dirs().
Comparisons without diff images will show "No diff" in the preview column.
If output_file is NULL, returns the HTML as a character string
(invisibly). If output_file is specified, writes the file and returns
the file path (invisibly).
compare_images_batch(), compare_image_dirs(),
summary.odiffr_batch()
## Not run: results <- compare_image_dirs("baseline/", "current/", diff_dir = "diffs/") # Generate report file batch_report(results, output_file = "report.html") # Self-contained report with embedded images batch_report(results, output_file = "report.html", embed = TRUE) # Get HTML as string html <- batch_report(results) ## End(Not run)## Not run: results <- compare_image_dirs("baseline/", "current/", diff_dir = "diffs/") # Generate report file batch_report(results, output_file = "report.html") # Self-contained report with embedded images batch_report(results, output_file = "report.html", embed = TRUE) # Get HTML as string html <- batch_report(results) ## End(Not run)
Convenience function that compares all images in two directories and generates an HTML report in one step.
compare_dirs_report( baseline_dir, current_dir, diff_dir = "diffs", output_file = file.path(diff_dir, "report.html"), parallel = FALSE, title = "odiffr Comparison Report", embed = FALSE, relative_paths = FALSE, n_worst = 10, show_all = FALSE, ... )compare_dirs_report( baseline_dir, current_dir, diff_dir = "diffs", output_file = file.path(diff_dir, "report.html"), parallel = FALSE, title = "odiffr Comparison Report", embed = FALSE, relative_paths = FALSE, n_worst = 10, show_all = FALSE, ... )
baseline_dir |
Path to the directory containing baseline images. |
current_dir |
Path to the directory containing current images to compare against baseline. |
diff_dir |
Directory to save diff images. If |
output_file |
Path for the HTML report. Defaults to
|
parallel |
Logical; if |
title |
Title for the HTML report. |
embed |
Logical; if |
relative_paths |
Logical; if |
n_worst |
Number of worst offenders to display in the report. |
show_all |
Logical; if |
... |
Additional arguments passed to |
The odiffr_batch results (invisibly). The HTML report is written
to output_file as a side effect.
compare_image_dirs(), batch_report()
## Not run: # One-liner for QA workflow compare_dirs_report("baseline/", "current/") # -> Creates diffs/ directory with diff images and report.html # With parallel processing and embedded images compare_dirs_report("baseline/", "current/", parallel = TRUE, embed = TRUE) # Pass comparison options via ... compare_dirs_report("baseline/", "current/", threshold = 0.1, antialiasing = TRUE) ## End(Not run)## Not run: # One-liner for QA workflow compare_dirs_report("baseline/", "current/") # -> Creates diffs/ directory with diff images and report.html # With parallel processing and embedded images compare_dirs_report("baseline/", "current/", parallel = TRUE, embed = TRUE) # Pass comparison options via ... compare_dirs_report("baseline/", "current/", threshold = 0.1, antialiasing = TRUE) ## End(Not run)
Compare all images in a baseline directory against corresponding images in a
current directory. Files are matched by relative path (including
subdirectories when recursive = TRUE).
compare_image_dirs( baseline_dir, current_dir, pattern = "\\.(png|jpe?g|webp|tiff?)$", recursive = FALSE, diff_dir = NULL, parallel = FALSE, ... )compare_image_dirs( baseline_dir, current_dir, pattern = "\\.(png|jpe?g|webp|tiff?)$", recursive = FALSE, diff_dir = NULL, parallel = FALSE, ... )
baseline_dir |
Path to the directory containing baseline images. |
current_dir |
Path to the directory containing current images to compare against baseline. |
pattern |
Regular expression pattern to match image files. Default matches common image formats (PNG, JPEG, WEBP, TIFF). |
recursive |
Logical; if |
diff_dir |
Directory to save diff images. If |
parallel |
Logical; if |
... |
Additional arguments passed to |
The baseline directory is the source of truth. For each image found in
baseline_dir matching pattern:
If a corresponding file exists in current_dir (same relative
path), it is included in the comparison.
If the file is missing from current_dir, a warning is issued and
the file is excluded from results.
Files that exist only in current_dir (not in baseline_dir) are not
compared, but a message is emitted noting how many such files were found.
A tibble (if available) or data.frame with one row per comparison,
containing all columns from compare_images() plus a pair_id column.
compare_images_batch() for comparing explicit pairs,
compare_images() for single comparisons.
## Not run: # Compare all images in two directories results <- compare_image_dirs("baseline/", "current/") # Only compare PNG files results <- compare_image_dirs("baseline/", "current/", pattern = "\\.png$") # Include subdirectories and save diff images results <- compare_image_dirs( "baseline/", "current/", recursive = TRUE, diff_dir = "diffs/" ) # Check which comparisons failed results[!results$match, ] ## End(Not run)## Not run: # Compare all images in two directories results <- compare_image_dirs("baseline/", "current/") # Only compare PNG files results <- compare_image_dirs("baseline/", "current/", pattern = "\\.png$") # Include subdirectories and save diff images results <- compare_image_dirs( "baseline/", "current/", recursive = TRUE, diff_dir = "diffs/" ) # Check which comparisons failed results[!results$match, ] ## End(Not run)
High-level function for comparing images with convenient output. Returns a tibble if the tibble package is available, otherwise a data.frame. Accepts file paths or magick-image objects.
compare_images( img1, img2, diff_output = NULL, threshold = 0.1, antialiasing = FALSE, fail_on_layout = FALSE, ignore_regions = NULL, ... )compare_images( img1, img2, diff_output = NULL, threshold = 0.1, antialiasing = FALSE, fail_on_layout = FALSE, ignore_regions = NULL, ... )
img1 |
Path to the first image, or a magick-image object. |
img2 |
Path to the second image, or a magick-image object. |
diff_output |
Path for the diff output image (PNG only). Use |
threshold |
Numeric; color difference threshold between 0.0 and 1.0. Default is 0.1. |
antialiasing |
Logical; if |
fail_on_layout |
Logical; if |
ignore_regions |
List of regions to ignore during comparison.
Use |
... |
Additional arguments passed to |
A tibble (if available) or data.frame with columns:
Logical; TRUE if images match.
Character; comparison result reason.
Integer; number of different pixels.
Numeric; percentage of different pixels.
Character; path to diff image, or NA.
Character; path to first image.
Character; path to second image.
odiff_run() for the low-level interface,
ignore_region() for creating ignore regions.
## Not run: # Compare two image files result <- compare_images("baseline.png", "current.png") result$match # With diff output result <- compare_images("baseline.png", "current.png", diff_output = TRUE) result$diff_output # Compare magick-image objects (requires magick package) library(magick) img1 <- image_read("baseline.png") img2 <- image_read("current.png") result <- compare_images(img1, img2) # Ignore specific regions result <- compare_images("baseline.png", "current.png", ignore_regions = list( ignore_region(0, 0, 100, 50), # Header ignore_region(0, 500, 800, 600) # Footer )) ## End(Not run)## Not run: # Compare two image files result <- compare_images("baseline.png", "current.png") result$match # With diff output result <- compare_images("baseline.png", "current.png", diff_output = TRUE) result$diff_output # Compare magick-image objects (requires magick package) library(magick) img1 <- image_read("baseline.png") img2 <- image_read("current.png") result <- compare_images(img1, img2) # Ignore specific regions result <- compare_images("baseline.png", "current.png", ignore_regions = list( ignore_region(0, 0, 100, 50), # Header ignore_region(0, 500, 800, 600) # Footer )) ## End(Not run)
Compare multiple pairs of images in batch. Useful for visual regression testing across many screenshots.
compare_images_batch(pairs, diff_dir = NULL, parallel = FALSE, ...)compare_images_batch(pairs, diff_dir = NULL, parallel = FALSE, ...)
pairs |
A data.frame with columns |
diff_dir |
Directory to save diff images. If |
parallel |
Logical; if |
... |
Additional arguments passed to |
A tibble (if available) or data.frame with class odiffr_batch,
containing one row per comparison with all columns from compare_images()
plus a pair_id column. Use summary() to get aggregate statistics.
summary.odiffr_batch() for summarizing batch results,
compare_image_dirs() for directory-based comparison.
## Not run: # Create a data frame of image pairs pairs <- data.frame( img1 = c("baseline/page1.png", "baseline/page2.png"), img2 = c("current/page1.png", "current/page2.png") ) # Compare all pairs results <- compare_images_batch(pairs, diff_dir = "diffs/") # Compare in parallel (Unix only) results <- compare_images_batch(pairs, parallel = TRUE) # Check which comparisons failed results[!results$match, ] ## End(Not run)## Not run: # Create a data frame of image pairs pairs <- data.frame( img1 = c("baseline/page1.png", "baseline/page2.png"), img2 = c("current/page1.png", "current/page2.png") ) # Compare all pairs results <- compare_images_batch(pairs, diff_dir = "diffs/") # Compare in parallel (Unix only) results <- compare_images_batch(pairs, parallel = TRUE) # Check which comparisons failed results[!results$match, ] ## End(Not run)
Assert that images match or differ using odiff. These expectations are designed for visual regression testing in testthat test suites.
expect_images_match( actual, expected, threshold = 0.1, antialiasing = FALSE, fail_on_layout = TRUE, ignore_regions = NULL, ..., info = NULL, label = NULL ) expect_images_differ( img1, img2, threshold = 0.1, antialiasing = FALSE, ..., info = NULL, label = NULL )expect_images_match( actual, expected, threshold = 0.1, antialiasing = FALSE, fail_on_layout = TRUE, ignore_regions = NULL, ..., info = NULL, label = NULL ) expect_images_differ( img1, img2, threshold = 0.1, antialiasing = FALSE, ..., info = NULL, label = NULL )
actual |
Path to the actual/current image, or a magick-image object. |
expected |
Path to the expected/baseline image, or a magick-image object. |
threshold |
Numeric; color difference threshold between 0.0 and 1.0. Default is 0.1. |
antialiasing |
Logical; if |
fail_on_layout |
Logical; if |
ignore_regions |
List of regions to ignore during comparison.
Use |
... |
Additional arguments passed to |
info |
Extra information to be included in the failure message (useful for providing context about what was being tested). |
label |
Optional custom label for the actual image in failure messages. If not provided, uses the deparsed expression. |
img1, img2
|
Paths to images being compared (for |
expect_images_match() asserts that two images are visually identical
(within the specified threshold). On failure, a diff image is saved to
tests/testthat/_odiffr/ by default, which can be controlled via
options(odiffr.save_diff = FALSE) or options(odiffr.diff_dir = "path").
expect_images_differ() asserts that two images are visually different.
No diff image is saved since there's nothing to debug when images match
unexpectedly.
Both expectations will skip (not fail) if the odiff binary is not available, making tests portable across environments.
Invisibly returns the comparison result (a data.frame/tibble with match, reason, diff_count, diff_percentage, etc.), allowing further inspection if needed.
odiffr expectations are designed for pixel-based comparison of screenshots, rendered images, and bitmap files. For SVG-based comparison of ggplot2 and grid graphics, consider using the vdiffr package instead. The two approaches are complementary.
compare_images() for the underlying comparison function,
ignore_region() for excluding regions from comparison.
## Not run: # Basic visual regression test test_that("login page renders correctly", { skip_if_no_odiff() expect_images_match( "screenshots/login_current.png", "screenshots/login_baseline.png" ) }) # With tolerance for minor differences test_that("chart renders correctly", { skip_if_no_odiff() expect_images_match( "actual_chart.png", "expected_chart.png", threshold = 0.2, antialiasing = TRUE, ignore_regions = list( ignore_region(0, 0, 100, 30) # Ignore timestamp ) ) }) # Assert images are different test_that("button changes on hover", { skip_if_no_odiff() expect_images_differ( "button_normal.png", "button_hover.png" ) }) ## End(Not run)## Not run: # Basic visual regression test test_that("login page renders correctly", { skip_if_no_odiff() expect_images_match( "screenshots/login_current.png", "screenshots/login_baseline.png" ) }) # With tolerance for minor differences test_that("chart renders correctly", { skip_if_no_odiff() expect_images_match( "actual_chart.png", "expected_chart.png", threshold = 0.2, antialiasing = TRUE, ignore_regions = list( ignore_region(0, 0, 100, 30) # Ignore timestamp ) ) }) # Assert images are different test_that("button changes on hover", { skip_if_no_odiff() expect_images_differ( "button_normal.png", "button_hover.png" ) }) ## End(Not run)
Extract only the failed (non-matching) comparisons from batch results.
failed_pairs(object)failed_pairs(object)
object |
An |
A tibble or data.frame containing only rows where match is FALSE.
compare_images_batch(), compare_image_dirs(), passed_pairs()
## Not run: results <- compare_image_dirs("baseline/", "current/") failed <- failed_pairs(results) nrow(failed) # Number of failures ## End(Not run)## Not run: results <- compare_image_dirs("baseline/", "current/") failed <- failed_pairs(results) nrow(failed) # Number of failures ## End(Not run)
Locates the odiff executable using a priority-based search:
User-specified path via options(odiffr.path = "...")
System PATH (Sys.which("odiff"))
Cached binary from odiffr_update()
find_odiff()find_odiff()
Character string with the absolute path to the odiff executable.
## Not run: find_odiff() ## End(Not run)## Not run: find_odiff() ## End(Not run)
Helper function to create a region specification for use with
odiff_run() and compare_images().
ignore_region(x1, y1, x2, y2)ignore_region(x1, y1, x2, y2)
x1 |
Integer; x-coordinate of the top-left corner. |
y1 |
Integer; y-coordinate of the top-left corner. |
x2 |
Integer; x-coordinate of the bottom-right corner. |
y2 |
Integer; y-coordinate of the bottom-right corner. |
A list with components x1, y1, x2, y2.
# Create a region to ignore region <- ignore_region(10, 10, 100, 50) # Use with odiff_run ## Not run: result <- odiff_run("img1.png", "img2.png", ignore_regions = list(region)) ## End(Not run)# Create a region to ignore region <- ignore_region(10, 10, 100, 50) # Use with odiff_run ## Not run: result <- odiff_run("img1.png", "img2.png", ignore_regions = list(region)) ## End(Not run)
Check if odiff is Available
odiff_available()odiff_available()
Logical TRUE if odiff is found and executable, FALSE otherwise.
odiff_available()odiff_available()
Display odiff Configuration Information
odiff_info()odiff_info()
A list with components:
Operating system (darwin, linux, windows)
Architecture (arm64, x64)
Path to the odiff binary
odiff version string
Source of the binary (option, system, cached)
## Not run: odiff_info() ## End(Not run)## Not run: odiff_info() ## End(Not run)
Direct wrapper around the odiff CLI with zero external dependencies. Returns a structured list with comparison results.
odiff_run( img1, img2, diff_output = NULL, threshold = 0.1, antialiasing = FALSE, fail_on_layout = FALSE, diff_mask = FALSE, diff_overlay = NULL, diff_color = NULL, diff_lines = FALSE, reduce_ram = FALSE, ignore_regions = NULL, timeout = 60 )odiff_run( img1, img2, diff_output = NULL, threshold = 0.1, antialiasing = FALSE, fail_on_layout = FALSE, diff_mask = FALSE, diff_overlay = NULL, diff_color = NULL, diff_lines = FALSE, reduce_ram = FALSE, ignore_regions = NULL, timeout = 60 )
img1 |
Character; path to the first (baseline) image file. |
img2 |
Character; path to the second (comparison) image file. |
diff_output |
Character or |
threshold |
Numeric; color difference threshold between 0.0 and 1.0. Lower values are more precise. Default is 0.1. |
antialiasing |
Logical; if |
fail_on_layout |
Logical; if |
diff_mask |
Logical; if |
diff_overlay |
Logical or numeric; if |
diff_color |
Character; hex color for highlighting differences
(e.g., |
diff_lines |
Logical; if |
reduce_ram |
Logical; if |
ignore_regions |
A list of regions to ignore during comparison. Each
region should be a list with |
timeout |
Numeric; timeout in seconds for the odiff process. Default is 60. |
A list with the following components:
Logical; TRUE if images match, FALSE otherwise.
Character; one of "match", "pixel-diff",
"layout-diff", or "error".
Integer; number of different pixels, or NA.
Numeric; percentage of different pixels, or NA.
Integer vector of line numbers with differences,
or NULL.
Integer; odiff exit code (0 = match, 21 = layout diff, 22 = pixel diff).
Character; raw stdout output.
Character; raw stderr output.
Character; path to first image.
Character; path to second image.
Character or NULL; path to diff image if created.
Numeric; time elapsed in seconds.
compare_images() for a higher-level interface,
ignore_region() for creating ignore regions.
## Not run: # Basic comparison result <- odiff_run("baseline.png", "current.png") result$match # With diff output result <- odiff_run("baseline.png", "current.png", "diff.png") # With threshold and antialiasing result <- odiff_run("baseline.png", "current.png", threshold = 0.05, antialiasing = TRUE) # Ignoring specific regions result <- odiff_run("baseline.png", "current.png", ignore_regions = list( ignore_region(10, 10, 100, 50), ignore_region(200, 200, 300, 300) )) ## End(Not run)## Not run: # Basic comparison result <- odiff_run("baseline.png", "current.png") result$match # With diff output result <- odiff_run("baseline.png", "current.png", "diff.png") # With threshold and antialiasing result <- odiff_run("baseline.png", "current.png", threshold = 0.05, antialiasing = TRUE) # Ignoring specific regions result <- odiff_run("baseline.png", "current.png", ignore_regions = list( ignore_region(10, 10, 100, 50), ignore_region(200, 200, 300, 300) )) ## End(Not run)
Get odiff Version
odiff_version()odiff_version()
Character string with the odiff version, or NA_character_ if
unavailable.
## Not run: odiff_version() ## End(Not run)## Not run: odiff_version() ## End(Not run)
Returns the path to the odiffr cache directory where downloaded binaries are stored.
odiffr_cache_path()odiffr_cache_path()
Character string with the path to the cache directory.
odiffr_cache_path()odiffr_cache_path()
Removes all cached binaries downloaded by odiffr_update().
odiffr_clear_cache()odiffr_clear_cache()
Invisibly returns TRUE if successful, FALSE otherwise.
## Not run: odiffr_clear_cache() ## End(Not run)## Not run: odiffr_clear_cache() ## End(Not run)
Downloads the odiff binary from GitHub releases to the user's cache
directory. The downloaded binary will be used by find_odiff() if no
system-wide installation or user-specified path is found.
odiffr_update(version = "latest", force = FALSE)odiffr_update(version = "latest", force = FALSE)
version |
Character string specifying the version to download.
Use |
force |
Logical; if |
Character string with the path to the downloaded binary.
## Not run: # Download latest version odiffr_update() # Download specific version odiffr_update(version = "v4.1.2") # Force re-download odiffr_update(force = TRUE) ## End(Not run)## Not run: # Download latest version odiffr_update() # Download specific version odiffr_update(version = "v4.1.2") # Force re-download odiffr_update(force = TRUE) ## End(Not run)
Extract only the passed (matching) comparisons from batch results.
passed_pairs(object)passed_pairs(object)
object |
An |
A tibble or data.frame containing only rows where match is TRUE.
compare_images_batch(), compare_image_dirs(), failed_pairs()
## Not run: results <- compare_image_dirs("baseline/", "current/") passed <- passed_pairs(results) nrow(passed) # Number of passing comparisons ## End(Not run)## Not run: results <- compare_image_dirs("baseline/", "current/") passed <- passed_pairs(results) nrow(passed) # Number of passing comparisons ## End(Not run)
Generate a summary of batch image comparison results, including pass/fail statistics, failure reasons, and worst offenders.
## S3 method for class 'odiffr_batch' summary(object, n_worst = 5, ...) ## S3 method for class 'odiffr_batch_summary' print(x, ...)## S3 method for class 'odiffr_batch' summary(object, n_worst = 5, ...) ## S3 method for class 'odiffr_batch_summary' print(x, ...)
object |
An |
n_worst |
Integer; number of worst offenders to include in the summary. Default is 5. |
... |
Additional arguments (currently unused). |
x |
An |
The summary method expects the standard output of compare_images_batch(),
which includes columns: match, reason, diff_percentage, diff_count,
pair_id, and img2.
An odiffr_batch_summary object with the following components:
Total number of comparisons.
Number of matching image pairs.
Number of non-matching image pairs.
Proportion of passing comparisons (0 to 1).
Table of failure reasons (NULL if no failures).
List with min, median, mean, max diff percentages (NULL if no failures with diff data).
Data frame of worst offenders by diff percentage (NULL if no failures).
compare_images_batch(), compare_image_dirs()
## Not run: # Compare image pairs and summarize pairs <- data.frame( img1 = c("baseline/a.png", "baseline/b.png", "baseline/c.png"), img2 = c("current/a.png", "current/b.png", "current/c.png") ) results <- compare_images_batch(pairs) summary(results) # Get summary with more worst offenders summary(results, n_worst = 10) ## End(Not run)## Not run: # Compare image pairs and summarize pairs <- data.frame( img1 = c("baseline/a.png", "baseline/b.png", "baseline/c.png"), img2 = c("current/a.png", "current/b.png", "current/c.png") ) results <- compare_images_batch(pairs) summary(results) # Get summary with more worst offenders summary(results, n_worst = 10) ## End(Not run)