diff --git a/Cargo.toml b/Cargo.toml
index c550fd2..a18c32d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ description = "A flexible rule-based file and folder comparison tool and crate i
repository = "https://github.com/VolumeGraphics/havocompare"
homepage = "https://github.com/VolumeGraphics/havocompare"
documentation = "https://docs.rs/havocompare"
-version = "0.5.4"
+version = "0.6.0"
edition = "2021"
license = "MIT"
authors = ["Volume Graphics GmbH"]
@@ -19,7 +19,7 @@ path = "src/print_args.rs"
[dependencies]
-clap = { version = "4.4", features = ["derive"] }
+clap = { version = "4.5", features = ["derive"] }
chrono = "0.4"
serde = "1.0"
serde_yaml = "0.9"
@@ -27,8 +27,8 @@ schemars = "0.8"
schemars_derive = "0.8"
thiserror = "1.0"
regex = "1.10"
-image = "0.24"
-image-compare = "0.3"
+image = "0.25"
+image-compare = "0.4"
tracing = "0.1"
tracing-subscriber = "0.3"
serde_json = "1.0"
@@ -38,18 +38,17 @@ strsim = "0.11"
itertools = "0.12"
tera = "1.19"
sha2 = "0.10"
-data-encoding = "2.5"
+data-encoding = "2.6"
permutation = "0.4"
pdf-extract = "0.7"
vg_errortools = "0.1"
-rayon = "1.8.0"
+rayon = "1.10.0"
enable-ansi-support = "0.2"
-tempfile = "3.8"
+tempfile = "3.10"
fs_extra = "1.3"
-opener = "0.6"
+opener = "0.7"
anyhow = "1.0"
-json_diff_ng = { version = "0.4" }
-
+json_diff_ng = { version = "0.5" }
[dev-dependencies]
env_logger = "0.11"
diff --git a/README.md b/README.md
index 48af352..f6336f1 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# Havocompare - a folder comparison utility
+
[![Crates.io](https://img.shields.io/crates/d/havocompare?style=flat)](https://crates.io/crates/havocompare)
[![Documentation](https://docs.rs/havocompare/badge.svg)](https://docs.rs/havocompare)
![CI](https://github.com/VolumeGraphics/havocompare/actions/workflows/rust.yml/badge.svg?branch=main "CI")
@@ -6,6 +7,7 @@
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
## Contributors:
+
@@ -13,6 +15,7 @@
## Quickstart
### 0. Install havocompare
+
You have rust? cool! try:
`cargo install havocompare`
@@ -20,28 +23,35 @@ You just want a binary:
Check our binary downloads on github-pages
### 1. Create a config file
-Havocompare was developed with a few design goals in mind. We wanted a human-readable and easily composable configuration file format.
-After a few tries we ended up with the current format, which is a list of rules inside a yaml file.
+
+Havocompare was developed with a few design goals in mind. We wanted a human-readable and easily composable
+configuration file format.
+After a few tries we ended up with the current format, which is a list of rules inside a yaml file.
See the following example `config.yaml`:
+
```yaml
rules:
- name: "Numerical results csv"
# you can have multiple includes and excludes
- pattern_include:
+ pattern_include:
- "**/export_*.csv"
# excludes are optional
- pattern_exclude:
+ pattern_exclude:
- "**/export_1337.csv"
CSV:
comparison_modes:
- Relative: 0.1
- Absolute: 1.0
```
-It creates a new rule named rule including all files matching "export_*.csv" in all sub-folders but exclude "export_1337.csv".
-String cells will be checked for perfect identity, numbers (including numbers with units) will be checked for a relative deviation smaller than `0.1`
+
+It creates a new rule named rule including all files matching "export_*.csv" in all sub-folders but exclude "
+export_1337.csv".
+String cells will be checked for perfect identity, numbers (including numbers with units) will be checked for a relative
+deviation smaller than `0.1`
AND absolute deviation smaller than `1.0`.
__Comparison rules__
+
- Relative means validity is checked like: `|nominal - actual| / |nominal| < tolerance`
- Absolute means validity is checked like: `|nominal - actual| < tolerance`
- "nan" and "nan" is equal
@@ -51,43 +61,56 @@ __Comparison rules__
Running the comparison is super easy, just supply nominal, actual and the config:
`./havocompare compare nominal_dir actual_dir config.yaml`
-The report of the comparison will be written inside the `./report` folder. Differences will also be printed to the terminal.
-Furthermore, if differences are found, the return code will be `1`, if no differences are found, it will be `0` making integration of
+The report of the comparison will be written inside the `./report` folder. Differences will also be printed to the
+terminal.
+Furthermore, if differences are found, the return code will be `1`, if no differences are found, it will be `0` making
+integration of
havocompare into a CI system rather easy.
## Details on the config
+
### Validation Scheme
-Writing a valid configuration file can be error-prone without auto-completion. We suggest using json schema to validate your yaml
+
+Writing a valid configuration file can be error-prone without auto-completion. We suggest using json schema to validate
+your yaml
and even enable auto-completion in IDEs like pycharm. To generate the schema you can call:
`./havocompare schema > config_scheme.json` and import the resulting scheme into your IDE.
### Comparison options
+
#### CSV
-The `comparison_modes` option is required and of type 'list'. It can comprise either a relative numerical ('Relative') maximum deviation or a maximum
-deviation ('Absolute').
-You can specify the decimal separator and the field separator. If you don't specify, havocompare will try to guess it from each csv file.
-Note: If delimiters are not specified, even different delimiters between nominal and actual are accepted as long as all deviations are in bounds.
+
+The `comparison_modes` option is required and of type 'list'. It can comprise either a relative numerical ('Relative')
+maximum deviation or a maximum
+deviation ('Absolute').
+You can specify the decimal separator and the field separator. If you don't specify, havocompare will try to guess it
+from each csv file.
+Note: If delimiters are not specified, even different delimiters between nominal and actual are accepted as long as all
+deviations are in bounds.
To ignore specific cells, you can specify an exclusion regex.
-The preprocessing steps are done after the file is parsed using the given delimiters (or guessing) but before anything else. Processing order is as written in the list.
-In the below example, headers will be extracted from the csv-input file, then a column with the title "Column to delete" will be deleted.
+The preprocessing steps are done after the file is parsed using the given delimiters (or guessing) but before anything
+else. Processing order is as written in the list.
+In the below example, headers will be extracted from the csv-input file, then a column with the title "Column to delete"
+will be deleted.
If any of the preprocessing steps fail, havocompare will exit with an error immediately so use them carefully.
See the following example with all optional parameters set:
+
```yaml
rules:
- name: "CSV - Demo all options"
# what files to include - use as many as make sense to reduce duplication in your rules
- pattern_include:
+ pattern_include:
- "**/*.csv"
# optional: of all included files, remove the ones matching any exclude pattern
- pattern_exclude:
+ pattern_exclude:
- "**/ignored.csv"
CSV:
# delimiters are optional, if not given, they will be auto-detected.
# auto-detection allows different delimiters for nominal and actual
decimal_separator: '.'
- field_delimiter: ';'
+ field_delimiter: ';'
# can have Absolute or Relative or both
comparison_modes:
- Absolute: 1.0
@@ -121,30 +144,36 @@ rules:
```
#### Image comparison
-Image comparison is done using the `image compare` crate's hybrid comparison which does MSSIM on the luma and RMS on the color information.
-Only a threshold can be specified:
+
+Image comparison is done using the `image compare` crate.
+Specify loads of options here and then filter on threshold.
+
```yaml
rules:
- name: "JPG comparison"
- pattern_include:
+ pattern_include:
- "**/*.jpg"
# exclude can of course also be specified!
Image:
- # threshold is between 0.0 for total difference, 0.5 for very dissimilar and 1.0 for perfect mach
- # Usually you want to test with values between 0.90 and 0.97
+ # Compare images in RGBA-mode, can also be RGB and Gray
+ # Comparison mode set to Hybrid means we want MSSIM on the Y channel and 2 dim vec diff on UV for color information
+ RGBA: Hybrid
threshold: 0.9
```
#### Plain text comparison
-For plain text comparison the file is read and compared line by line. For each line the normalized Damerau-Levenshtein distance from the `strsim`
-crate is used. You can ignore single lines which you know are different by specifying an arbitrary number of ignored lines:
+
+For plain text comparison the file is read and compared line by line. For each line the normalized Damerau-Levenshtein
+distance from the `strsim`
+crate is used. You can ignore single lines which you know are different by specifying an arbitrary number of ignored
+lines:
```yaml
rules:
- name: "HTML-Compare strict"
- pattern_exclude:
+ pattern_exclude:
- "**/*_changed.html"
- pattern_include:
+ pattern_include:
- "**/*.html"
PlainText:
# Normalized Damerau-Levenshtein distance
@@ -157,14 +186,16 @@ rules:
```
#### PDF text comparison
-For PDF text comparison the text will be extracted and written to temporary files. The files will then be compared using the Plain text comparison:
+
+For PDF text comparison the text will be extracted and written to temporary files. The files will then be compared using
+the Plain text comparison:
```yaml
rules:
- name: "PDF-Text-Compare"
- pattern_exclude:
+ pattern_exclude:
- "**/*_changed.pdf"
- pattern_include:
+ pattern_include:
- "**/*.pdf"
PDFText:
# Normalized Damerau-Levenshtein distance
@@ -176,15 +207,15 @@ rules:
- "[A-Z]*[0-9]"
```
-
#### Hash comparison
+
For binary files which cannot otherwise be checked we can also do a simple hash comparison.
Currently, we only support SHA-256 but more checks can be added easily.
```yaml
rules:
- name: "Hash comparison strict"
- pattern_exclude:
+ pattern_exclude:
- "**/*.bin"
Hash:
# Currently we only have Sha256
@@ -192,12 +223,13 @@ rules:
```
#### File metadata comparison
+
For the cases where the pure existence or some metadata are already enough.
```yaml
rules:
- name: "Metadata comparison"
- pattern_exclude:
+ pattern_exclude:
- "**/*.bin"
FileProperties:
# nom/act file paths must not contain whitespace
@@ -209,8 +241,8 @@ rules:
```
#### Run external comparison tool
-In case you want to run an external comparison tool, you can use this option
+In case you want to run an external comparison tool, you can use this option
```yaml
rules:
@@ -226,24 +258,25 @@ rules:
- "--only-images"
```
-
#### JSON comparison
+
Compares JSON files for different keys in both files and mismatches in values.
-ignore_keys elements will be ignored, full regex matching on only the key names / paths is supported.
+`ignore_keys` is a list of regexes that are matched against the individual key names, the key value pair is excluded from the comparison if a regex matches.
The values are not affected by this.
```yaml
rules:
-- name: "Compare JSON files"
- pattern_include:
- - "**/*.json"
- Json:
- ignore_keys:
- # drop "ignore_this_key" and "ignore_this_keys" with this regex :)
- - "ignore_this_key(s?)"
+ - name: "Compare JSON files"
+ pattern_include:
+ - "**/*.json"
+ Json:
+ ignore_keys:
+ # drop "ignore_this_key" and "ignore_this_keys" with this regex :)
+ - "ignore_this_key(s?)"
```
### Use HavoCompare in your unit-tests
+
1. Add havocompare to your dev-dependencies:
```toml
[dev-dependencies]
@@ -272,38 +305,51 @@ rules:
## Changelog
+### 0.6.0
+
+- Add new options for image compare module (a lot of options!)
+- Bump json-compare to new version fixing bugs in regex field excludes and sorting
+
### 0.5.4
+
- Add option to run single file mode from CLI
### 0.5.3
+
- Add option to sort json arrays (including deep sorting)
- Make single file comparison function to public api
- Update dependencies, fix broken pdf import regarding whitespaces
-
### 0.5.2
+
- Preserve white spaces in CSV and PDF report instead of being collapsed by HTML
- Add + and - symbols to the report index
- Display combined file names in report index if the compared file names are different
### 0.5.1
+
- Fix potential crash in JSON checking
### 0.5.0
+
- Add JSON checking
### 0.4.0
+
- Separate reporting logic from comparison logic
- Implement a machine-readable JSON reporting
### 0.3.2
+
- Allow direct opening of reports after comparison with `--open`
- Parsing failures when running `compare` are now propagated to terminal
### 0.3.1
+
- Fix swapped actual and nominal for hash and text-compares
### 0.3.0
+
- Allow RGBA image comparison
- Add file metadata comparison
- Add external checking
@@ -312,33 +358,40 @@ rules:
- Added config yaml validation command to check whether file can be loaded or not
### 0.2.4
+
- add check for row lines of both compared csv files, and throw error if they are unequal
- Add deletion by cell
-- Simplify report sub-folders creation: sub-folders are now created temporarily in the temp folder instead of in the current working folder
+- Simplify report sub-folders creation: sub-folders are now created temporarily in the temp folder instead of in the
+ current working folder
- Change report row numbering to always start with 0, so row deletion is more understandable
- fix floating point value comparison of non-displayable diff values
### 0.2.3
-- bump pdf-extract crate to 0.6.4 to fix "'attempted to leave type `linked_hash_map::Node, object::Object>` uninitialized'"
+
+- bump pdf-extract crate to 0.6.4 to fix "'attempted to leave
+ type `linked_hash_map::Node, object::Object>` uninitialized'"
### 0.2.2
+
- Include files which has error and can't be compared to the report
- Fixed a bug which caused the program exited early out of rules-loop, and not processing all
### 0.2.0
+
- Deletion of columns will no longer really delete them but replace every value with "DELETED"
- Expose config struct to library API
- Fixed a bug regarding wrong handling of multiple empty lines
-- Reworked CSV reporting to have an interleaved and more compact view
- - Display the relative path of compared files instead of file name in the report index.html
- - Made header-extraction fallible but uncritical - can now always be enabled
+- Reworked CSV reporting to have an interleaved and more compact view
+ - Display the relative path of compared files instead of file name in the report index.html
+ - Made header-extraction fallible but uncritical - can now always be enabled
- Wrote a completely new csv parser:
- - Respects escaping with '\'
- - Allows string-literals containing unescaped field separators (field1, "field2, but as literal", field3)
- - Allows multi-line string literals with quotes
+ - Respects escaping with '\'
+ - Allows string-literals containing unescaped field separators (field1, "field2, but as literal", field3)
+ - Allows multi-line string literals with quotes
- CSVs with non-rectangular format will now fail
### 0.1.4
+
- Add multiple includes and excludes - warning, this will break rules-files from 0.1.3 and earlier
- Remove all `unwrap` and `expect` in the library code in favor of correct error propagation
- Add preprocessing options for CSV files
@@ -347,14 +400,17 @@ rules:
- Add PDF-Text compare
### 0.1.3:
+
- Add optional cli argument to configure the folder to store the report
### 0.1.2:
+
- Add SHA-256 comparison mode
- Fix BOM on windows for CSV comparison
### 0.1.1:
- - Better error message on folder not found
- - Better test coverage
- - Fix colors on Windows terminal
- - Extend CI to Windows and mac
+
+- Better error message on folder not found
+- Better test coverage
+- Fix colors on Windows terminal
+- Extend CI to Windows and mac
diff --git a/config_scheme.json b/config_scheme.json
index 3670404..18238b0 100644
--- a/config_scheme.json
+++ b/config_scheme.json
@@ -87,6 +87,50 @@
}
}
},
+ "GrayCompareMode": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "Structure"
+ ],
+ "properties": {
+ "Structure": {
+ "$ref": "#/definitions/GrayStructureAlgorithm"
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "required": [
+ "Histogram"
+ ],
+ "properties": {
+ "Histogram": {
+ "$ref": "#/definitions/GrayHistogramCompareMetric"
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
+ },
+ "GrayHistogramCompareMetric": {
+ "type": "string",
+ "enum": [
+ "Correlation",
+ "ChiSquare",
+ "Intersection",
+ "Hellinger"
+ ]
+ },
+ "GrayStructureAlgorithm": {
+ "type": "string",
+ "enum": [
+ "MSSIM",
+ "RMS"
+ ]
+ },
"HTMLCompareConfig": {
"description": "Plain text comparison config, also used for PDF",
"type": "object",
@@ -137,6 +181,44 @@
"ImageCompareConfig": {
"description": "Image comparison config options",
"type": "object",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "RGB"
+ ],
+ "properties": {
+ "RGB": {
+ "$ref": "#/definitions/RGBCompareMode"
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "required": [
+ "RGBA"
+ ],
+ "properties": {
+ "RGBA": {
+ "$ref": "#/definitions/RGBACompareMode"
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "required": [
+ "Gray"
+ ],
+ "properties": {
+ "Gray": {
+ "$ref": "#/definitions/GrayCompareMode"
+ }
+ },
+ "additionalProperties": false
+ }
+ ],
"required": [
"threshold"
],
@@ -392,6 +474,77 @@
}
}
},
+ "RGBACompareMode": {
+ "oneOf": [
+ {
+ "description": "full RGBA comparison - probably not intuitive, rarely what you want outside of video processing Will do MSSIM on luma, then RMS on U and V and alpha channels. The calculation of the score is then pixel-wise the minimum of each pixels similarity. To account for perceived indifference in lower alpha regions, this down-weights the difference linearly with mean alpha channel.",
+ "type": "string",
+ "enum": [
+ "Hybrid"
+ ]
+ },
+ {
+ "description": "pre-blend the background in RGBA with this color, use the background RGB values you would assume the pictures to be seen on - usually either black or white",
+ "type": "object",
+ "required": [
+ "HybridBlended"
+ ],
+ "properties": {
+ "HybridBlended": {
+ "type": "object",
+ "required": [
+ "b",
+ "g",
+ "r"
+ ],
+ "properties": {
+ "b": {
+ "type": "integer",
+ "format": "uint8",
+ "minimum": 0.0
+ },
+ "g": {
+ "type": "integer",
+ "format": "uint8",
+ "minimum": 0.0
+ },
+ "r": {
+ "type": "integer",
+ "format": "uint8",
+ "minimum": 0.0
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
+ },
+ "RGBCompareMode": {
+ "oneOf": [
+ {
+ "description": "Comparing rgb images using structure. RGB structure similarity is performed by doing a channel split and taking the maximum deviation (minimum similarity) for the result. The image contains the complete deviations. Algorithm: RMS",
+ "type": "string",
+ "enum": [
+ "RMS"
+ ]
+ },
+ {
+ "description": "Comparing rgb images using structure. RGB structure similarity is performed by doing a channel split and taking the maximum deviation (minimum similarity) for the result. The image contains the complete deviations. Algorithm: MSSIM",
+ "type": "string",
+ "enum": [
+ "MSSIM"
+ ]
+ },
+ {
+ "description": "Comparing structure via MSSIM on Y channel, comparing color-diff-vectors on U and V summing the squares Please mind that the RGBSimilarity-Image does not contain plain RGB here. Probably what you want.",
+ "type": "string",
+ "enum": [
+ "Hybrid"
+ ]
+ }
+ ]
+ },
"Rule": {
"description": "Representing a single comparison rule",
"type": "object",
diff --git a/src/hash.rs b/src/hash.rs
index c709205..438ce4e 100644
--- a/src/hash.rs
+++ b/src/hash.rs
@@ -1,15 +1,16 @@
-use crate::{report, Deserialize, Serialize};
-use data_encoding::HEXLOWER;
-
-use crate::report::{DiffDetail, Difference};
-use schemars_derive::JsonSchema;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
+
+use data_encoding::HEXLOWER;
+use schemars_derive::JsonSchema;
use thiserror::Error;
use vg_errortools::fat_io_wrap_std;
use vg_errortools::FatIOError;
+use crate::{Deserialize, report, Serialize};
+use crate::report::{DiffDetail, Difference};
+
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Copy)]
pub enum HashFunction {
Sha256,
@@ -84,9 +85,10 @@ pub fn compare_files>(
#[cfg(test)]
mod test {
- use super::*;
use crate::hash::HashFunction::Sha256;
+ use super::*;
+
#[test]
fn identity() {
let f1 = Sha256
@@ -100,7 +102,7 @@ mod test {
#[test]
fn hash_pinning() {
- let sum = "bc3abb411d305c4436185c474be3db2608e910612a573f6791b143d7d749b699";
+ let sum = "378f768a589f29fcbd23835ec4764a53610fc910e60b540e1e5204bdaf2c73a0";
let f1 = Sha256
.hash_file(File::open("tests/integ/data/images/diff_100_DPI.png").unwrap())
.unwrap();
diff --git a/src/image.rs b/src/image.rs
index 8fa5671..ccbb5a9 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -1,29 +1,93 @@
-use crate::report::DiffDetail;
-use crate::{get_file_name, report};
+use std::path::Path;
+
+use image::{DynamicImage, Rgb};
+use image_compare::{Algorithm, Metric, Similarity};
use schemars_derive::JsonSchema;
use serde::{Deserialize, Serialize};
-use std::path::{Path, PathBuf};
use thiserror::Error;
use tracing::error;
+use crate::{get_file_name, report};
+use crate::report::DiffDetail;
+
#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
-/// Image comparison config options
-pub struct ImageCompareConfig {
- /// Threshold for image comparison < 0.5 is very dissimilar, 1.0 is identical
- pub threshold: f64,
+pub enum RGBACompareMode {
+ /// full RGBA comparison - probably not intuitive, rarely what you want outside of video processing
+ /// Will do MSSIM on luma, then RMS on U and V and alpha channels.
+ /// The calculation of the score is then pixel-wise the minimum of each pixels similarity.
+ /// To account for perceived indifference in lower alpha regions, this down-weights the difference linearly with mean alpha channel.
+ Hybrid,
+ /// pre-blend the background in RGBA with this color, use the background RGB values you would assume the pictures to be seen on - usually either black or white
+ HybridBlended { r: u8, b: u8, g: u8 },
}
-impl ImageCompareConfig {
- /// create an [`ImageCompareConfig`] given the threshold
- pub fn from_threshold(threshold: f64) -> Self {
- ImageCompareConfig { threshold }
+impl Default for RGBACompareMode {
+ fn default() -> Self {
+ Self::HybridBlended { r: 0, b: 0, g: 0 }
}
}
-impl Default for ImageCompareConfig {
- fn default() -> Self {
- ImageCompareConfig::from_threshold(1.0)
- }
+#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone, Default)]
+pub enum RGBCompareMode {
+ ///Comparing rgb images using structure. RGB structure similarity is performed by doing a channel split and taking the maximum deviation (minimum similarity) for the result. The image contains the complete deviations. Algorithm: RMS
+ RMS,
+ ///Comparing rgb images using structure. RGB structure similarity is performed by doing a channel split and taking the maximum deviation (minimum similarity) for the result. The image contains the complete deviations. Algorithm: MSSIM
+ MSSIM,
+ ///Comparing structure via MSSIM on Y channel, comparing color-diff-vectors on U and V summing the squares Please mind that the RGBSimilarity-Image does not contain plain RGB here. Probably what you want.
+ #[default]
+ Hybrid,
+}
+
+#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
+/// The distance algorithm to use for grayscale comparison, see
+/// https://github.com/ChrisRega/image-compare for equations
+pub enum GrayStructureAlgorithm {
+ /// SSIM with 8x8 pixel windows and averaging over the result
+ MSSIM,
+ /// Classic RMS distance
+ RMS,
+}
+
+/// See https://github.com/ChrisRega/image-compare for equations
+/// Distance metrics for histograms for grayscale comparison
+#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
+pub enum GrayHistogramCompareMetric {
+ /// Correlation $d(H_1,H_2) = \frac{\sum_I (H_1(I) - \bar{H_1}) (H_2(I) - \bar{H_2})}{\sqrt{\sum_I(H_1(I) - \bar{H_1})^2 \sum_I(H_2(I) - \bar{H_2})^2}}$
+ Correlation,
+ /// Chi-Square $d(H_1,H_2) = \sum _I \frac{\left(H_1(I)-H_2(I)\right)^2}{H_1(I)}$
+ ChiSquare,
+ /// Intersection $d(H_1,H_2) = \sum _I \min (H_1(I), H_2(I))$
+ Intersection,
+ /// Hellinger distance $d(H_1,H_2) = \sqrt{1 - \frac{1}{\sqrt{\int{H_1} \int{H_2}}} \sum_I \sqrt{H_1(I) \cdot H_2(I)}}$
+ Hellinger,
+}
+
+#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
+pub enum GrayCompareMode {
+ /// Compare gray values pixel structure
+ Structure(GrayStructureAlgorithm),
+ /// Compare gray values by histogram
+ Histogram(GrayHistogramCompareMetric),
+}
+
+#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
+pub enum CompareMode {
+ /// Compare images as RGB
+ RGB(RGBCompareMode),
+ /// Compare images as RGBA
+ RGBA(RGBACompareMode),
+ /// Compare images as luminance / grayscale
+ Gray(GrayCompareMode),
+}
+
+#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
+/// Image comparison config options
+pub struct ImageCompareConfig {
+ /// Threshold for image comparison < 0.5 is very dissimilar, 1.0 is identical
+ pub threshold: f64,
+ #[serde(flatten)]
+ /// How to compare the two images
+ pub mode: CompareMode,
}
#[derive(Debug, Error)]
@@ -38,26 +102,113 @@ pub enum Error {
FileNameParsing(String),
}
+struct ComparisonResult {
+ score: f64,
+ image: Option,
+}
+
+impl From for ComparisonResult {
+ fn from(value: Similarity) -> Self {
+ Self {
+ image: Some(value.image.to_color_map()),
+ score: value.score,
+ }
+ }
+}
+
pub fn compare_paths>(
nominal_path: P,
actual_path: P,
config: &ImageCompareConfig,
) -> Result {
- let nominal = image::open(nominal_path.as_ref())?.into_rgba8();
- let actual = image::open(actual_path.as_ref())?.into_rgba8();
-
- let result = image_compare::rgba_hybrid_compare(&nominal, &actual)?;
- let nominal_file_name =
- get_file_name(nominal_path.as_ref()).ok_or(Error::FileNameParsing(format!(
- "Could not extract filename from path {:?}",
- nominal_path.as_ref()
- )))?;
- let out_path = (nominal_file_name + "diff_image.png").to_string();
- let mut result_diff = report::Difference::new_for_file(&nominal_path, &actual_path);
+ let nominal = image::open(nominal_path.as_ref())?;
+ let actual = image::open(actual_path.as_ref())?;
+ let result: ComparisonResult = match &config.mode {
+ CompareMode::RGBA(c) => {
+ let nominal = nominal.into_rgba8();
+ let actual = actual.into_rgba8();
+ match c {
+ RGBACompareMode::Hybrid => {
+ image_compare::rgba_hybrid_compare(&nominal, &actual)?.into()
+ }
+ RGBACompareMode::HybridBlended { r, g, b } => {
+ image_compare::rgba_blended_hybrid_compare(
+ (&nominal).into(),
+ (&actual).into(),
+ Rgb([*r, *g, *b]),
+ )?
+ .into()
+ }
+ }
+ }
+ CompareMode::RGB(c) => {
+ let nominal = nominal.into_rgb8();
+ let actual = actual.into_rgb8();
+ match c {
+ RGBCompareMode::RMS => image_compare::rgb_similarity_structure(
+ &Algorithm::RootMeanSquared,
+ &nominal,
+ &actual,
+ )?
+ .into(),
+ RGBCompareMode::MSSIM => image_compare::rgb_similarity_structure(
+ &Algorithm::MSSIMSimple,
+ &nominal,
+ &actual,
+ )?
+ .into(),
+ RGBCompareMode::Hybrid => {
+ image_compare::rgb_hybrid_compare(&nominal, &actual)?.into()
+ }
+ }
+ }
+ CompareMode::Gray(c) => {
+ let nominal = nominal.into_luma8();
+ let actual = actual.into_luma8();
+ match c {
+ GrayCompareMode::Structure(c) => match c {
+ GrayStructureAlgorithm::MSSIM => image_compare::gray_similarity_structure(
+ &Algorithm::MSSIMSimple,
+ &nominal,
+ &actual,
+ )?
+ .into(),
+ GrayStructureAlgorithm::RMS => image_compare::gray_similarity_structure(
+ &Algorithm::RootMeanSquared,
+ &nominal,
+ &actual,
+ )?
+ .into(),
+ },
+ GrayCompareMode::Histogram(c) => {
+ let metric = match c {
+ GrayHistogramCompareMetric::Correlation => Metric::Correlation,
+ GrayHistogramCompareMetric::ChiSquare => Metric::ChiSquare,
+ GrayHistogramCompareMetric::Intersection => Metric::Intersection,
+ GrayHistogramCompareMetric::Hellinger => Metric::Hellinger,
+ };
+ let score =
+ image_compare::gray_similarity_histogram(metric, &nominal, &actual)?;
+ ComparisonResult { score, image: None }
+ }
+ }
+ }
+ };
+ let mut result_diff = report::Difference::new_for_file(&nominal_path, &actual_path);
if result.score < config.threshold {
- let color_map = result.image.to_color_map();
- color_map.save(PathBuf::from(&out_path))?;
+ let out_path_set = if let Some(i) = result.image {
+ let nominal_file_name =
+ get_file_name(nominal_path.as_ref()).ok_or(Error::FileNameParsing(format!(
+ "Could not extract filename from path {:?}",
+ nominal_path.as_ref()
+ )))?;
+ let out_path = (nominal_file_name + "diff_image.png").to_string();
+ i.save(&out_path)?;
+ Some(out_path)
+ } else {
+ None
+ };
let error_message = format!(
"Diff for image {} was not met, expected {}, found {}",
@@ -66,8 +217,9 @@ pub fn compare_paths>(
result.score
);
error!("{}", &error_message);
+
result_diff.push_detail(DiffDetail::Image {
- diff_image: out_path,
+ diff_image: out_path_set,
score: result.score,
});
result_diff.error();
@@ -77,15 +229,17 @@ pub fn compare_paths>(
#[cfg(test)]
mod test {
- use crate::image::{compare_paths, ImageCompareConfig};
- use crate::report::DiffDetail;
+ use super::*;
#[test]
fn identity() {
let result = compare_paths(
"tests/integ/data/images/actual/SaveImage_100DPI_default_size.jpg",
"tests/integ/data/images/actual/SaveImage_100DPI_default_size.jpg",
- &ImageCompareConfig { threshold: 1.0 },
+ &ImageCompareConfig {
+ threshold: 1.0,
+ mode: CompareMode::RGB(RGBCompareMode::Hybrid),
+ },
)
.unwrap();
assert!(!result.is_error);
@@ -96,7 +250,10 @@ mod test {
let result = compare_paths(
"tests/integ/data/images/expected/SaveImage_100DPI_default_size.jpg",
"tests/integ/data/images/actual/SaveImage_100DPI_default_size.jpg",
- &ImageCompareConfig { threshold: 1.0 },
+ &ImageCompareConfig {
+ threshold: 1.0,
+ mode: CompareMode::RGBA(RGBACompareMode::Hybrid),
+ },
)
.unwrap();
assert!(result.is_error);
@@ -105,11 +262,13 @@ mod test {
diff_image,
} = result.detail.first().unwrap()
{
- let img = image::open(diff_image).unwrap().into_rgb8();
+ let img = image::open(diff_image.as_ref().unwrap())
+ .unwrap()
+ .into_rgba8();
let nom = image::open("tests/integ/data/images/diff_100_DPI.png")
.unwrap()
- .into_rgb8();
- let diff_result = image_compare::rgb_hybrid_compare(&img, &nom)
+ .into_rgba8();
+ let diff_result = image_compare::rgba_hybrid_compare(&img, &nom)
.expect("Wrong dimensions of diff images!");
assert_eq!(diff_result.score, 1.0);
} else {
diff --git a/src/json.rs b/src/json.rs
index b31a15e..1ef98e9 100644
--- a/src/json.rs
+++ b/src/json.rs
@@ -1,12 +1,14 @@
-use crate::report::{DiffDetail, Difference};
-use crate::Error;
+use std::path::Path;
+
use itertools::Itertools;
use regex::Regex;
use schemars_derive::JsonSchema;
use serde::{Deserialize, Serialize};
-use std::path::Path;
use tracing::error;
+use crate::Error;
+use crate::report::{DiffDetail, Difference};
+
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
/// configuration for the json compare module
pub struct JsonConfig {
@@ -33,7 +35,8 @@ pub(crate) fn compare_files>(
let actual = vg_errortools::fat_io_wrap_std(&actual, &std::fs::read_to_string)?;
let ignores = config.get_ignore_list()?;
- let json_diff = json_diff::process::compare_jsons(&nominal, &actual, config.sort_arrays);
+ let json_diff =
+ json_diff::process::compare_jsons(&nominal, &actual, config.sort_arrays, &ignores);
let json_diff = match json_diff {
Ok(diff) => diff,
Err(e) => {
@@ -45,11 +48,7 @@ pub(crate) fn compare_files>(
return Ok(diff);
}
};
- let filtered_diff: Vec<_> = json_diff
- .all_diffs()
- .into_iter()
- .filter(|(_d, v)| !ignores.iter().any(|excl| excl.is_match(v.get_key())))
- .collect();
+ let filtered_diff: Vec<_> = json_diff.all_diffs();
if !filtered_diff.is_empty() {
for (d_type, key) in filtered_diff.iter() {
diff --git a/src/lib.rs b/src/lib.rs
index 56dec61..48524ee 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -379,11 +379,16 @@ pub fn validate_config(config_file: impl AsRef) -> bool {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::image::{CompareMode, RGBCompareMode};
+
#[test]
fn folder_not_found_is_false() {
let rule = Rule {
name: "test rule".to_string(),
- file_type: ComparisonMode::Image(ImageCompareConfig { threshold: 1.0 }),
+ file_type: ComparisonMode::Image(ImageCompareConfig {
+ threshold: 1.0,
+ mode: CompareMode::RGB(RGBCompareMode::Hybrid),
+ }),
pattern_include: vec!["*.".to_string()],
pattern_exclude: None,
};
diff --git a/src/report/mod.rs b/src/report/mod.rs
index ab2e7e2..b8a4f49 100644
--- a/src/report/mod.rs
+++ b/src/report/mod.rs
@@ -130,7 +130,7 @@ pub enum DiffDetail {
CSV(DiffType),
Image {
score: f64,
- diff_image: String,
+ diff_image: Option,
},
Text {
actual: String,
@@ -352,7 +352,7 @@ pub(crate) fn write_csv_detail(
pub fn write_image_detail(
nominal: impl AsRef,
actual: impl AsRef,
- diffs: &[(&f64, &String)],
+ diffs: &[(&f64, &Option)],
report_dir: impl AsRef,
) -> Result