1use std::path::Path;
2
3use image::{DynamicImage, Rgb};
4use image_compare::{Algorithm, Metric, Similarity};
5use schemars_derive::JsonSchema;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8use tracing::error;
9
10use crate::{get_file_name, report};
11use crate::report::DiffDetail;
12
13#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
14pub enum RGBACompareMode {
15 Hybrid,
20 HybridBlended { r: u8, b: u8, g: u8 },
22}
23
24impl Default for RGBACompareMode {
25 fn default() -> Self {
26 Self::HybridBlended { r: 0, b: 0, g: 0 }
27 }
28}
29
30#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone, Default)]
31pub enum RGBCompareMode {
32 RMS,
34 MSSIM,
36 #[default]
38 Hybrid,
39}
40
41#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
42pub enum GrayStructureAlgorithm {
45 MSSIM,
47 RMS,
49}
50
51#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
54pub enum GrayHistogramCompareMetric {
55 Correlation,
57 ChiSquare,
59 Intersection,
61 Hellinger,
63}
64
65#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
66pub enum GrayCompareMode {
67 Structure(GrayStructureAlgorithm),
69 Histogram(GrayHistogramCompareMetric),
71}
72
73#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
74pub enum CompareMode {
75 RGB(RGBCompareMode),
77 RGBA(RGBACompareMode),
79 Gray(GrayCompareMode),
81}
82
83#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
84pub struct ImageCompareConfig {
86 pub threshold: f64,
88 #[serde(flatten)]
89 pub mode: CompareMode,
91}
92
93#[derive(Debug, Error)]
94pub enum Error {
95 #[error("Error loading image {0}")]
96 ImageDecoding(#[from] image::ImageError),
97 #[error("Problem creating hash report {0}")]
98 Reporting(#[from] report::Error),
99 #[error("Image comparison algorithm failed {0}")]
100 ImageComparison(#[from] image_compare::CompareError),
101 #[error("Problem processing file name {0}")]
102 FileNameParsing(String),
103}
104
105struct ComparisonResult {
106 score: f64,
107 image: Option<DynamicImage>,
108}
109
110impl From<Similarity> for ComparisonResult {
111 fn from(value: Similarity) -> Self {
112 Self {
113 image: Some(value.image.to_color_map()),
114 score: value.score,
115 }
116 }
117}
118
119pub fn compare_paths<P: AsRef<Path>>(
120 nominal_path: P,
121 actual_path: P,
122 config: &ImageCompareConfig,
123) -> Result<report::Difference, Error> {
124 let nominal = image::open(nominal_path.as_ref())?;
125 let actual = image::open(actual_path.as_ref())?;
126 let result: ComparisonResult = match &config.mode {
127 CompareMode::RGBA(c) => {
128 let nominal = nominal.into_rgba8();
129 let actual = actual.into_rgba8();
130 match c {
131 RGBACompareMode::Hybrid => {
132 image_compare::rgba_hybrid_compare(&nominal, &actual)?.into()
133 }
134 RGBACompareMode::HybridBlended { r, g, b } => {
135 image_compare::rgba_blended_hybrid_compare(
136 (&nominal).into(),
137 (&actual).into(),
138 Rgb([*r, *g, *b]),
139 )?
140 .into()
141 }
142 }
143 }
144 CompareMode::RGB(c) => {
145 let nominal = nominal.into_rgb8();
146 let actual = actual.into_rgb8();
147 match c {
148 RGBCompareMode::RMS => image_compare::rgb_similarity_structure(
149 &Algorithm::RootMeanSquared,
150 &nominal,
151 &actual,
152 )?
153 .into(),
154 RGBCompareMode::MSSIM => image_compare::rgb_similarity_structure(
155 &Algorithm::MSSIMSimple,
156 &nominal,
157 &actual,
158 )?
159 .into(),
160 RGBCompareMode::Hybrid => {
161 image_compare::rgb_hybrid_compare(&nominal, &actual)?.into()
162 }
163 }
164 }
165 CompareMode::Gray(c) => {
166 let nominal = nominal.into_luma8();
167 let actual = actual.into_luma8();
168 match c {
169 GrayCompareMode::Structure(c) => match c {
170 GrayStructureAlgorithm::MSSIM => image_compare::gray_similarity_structure(
171 &Algorithm::MSSIMSimple,
172 &nominal,
173 &actual,
174 )?
175 .into(),
176 GrayStructureAlgorithm::RMS => image_compare::gray_similarity_structure(
177 &Algorithm::RootMeanSquared,
178 &nominal,
179 &actual,
180 )?
181 .into(),
182 },
183 GrayCompareMode::Histogram(c) => {
184 let metric = match c {
185 GrayHistogramCompareMetric::Correlation => Metric::Correlation,
186 GrayHistogramCompareMetric::ChiSquare => Metric::ChiSquare,
187 GrayHistogramCompareMetric::Intersection => Metric::Intersection,
188 GrayHistogramCompareMetric::Hellinger => Metric::Hellinger,
189 };
190 let score =
191 image_compare::gray_similarity_histogram(metric, &nominal, &actual)?;
192 ComparisonResult { score, image: None }
193 }
194 }
195 }
196 };
197
198 let mut result_diff = report::Difference::new_for_file(&nominal_path, &actual_path);
199 if result.score < config.threshold {
200 let out_path_set = if let Some(i) = result.image {
201 let nominal_file_name =
202 get_file_name(nominal_path.as_ref()).ok_or(Error::FileNameParsing(format!(
203 "Could not extract filename from path {:?}",
204 nominal_path.as_ref()
205 )))?;
206 let out_path = (nominal_file_name + "diff_image.png").to_string();
207 i.save(&out_path)?;
208 Some(out_path)
209 } else {
210 None
211 };
212
213 let error_message = format!(
214 "Diff for image {} was not met, expected {}, found {}",
215 nominal_path.as_ref().to_string_lossy(),
216 config.threshold,
217 result.score
218 );
219 error!("{}", &error_message);
220
221 result_diff.push_detail(DiffDetail::Image {
222 diff_image: out_path_set,
223 score: result.score,
224 });
225 result_diff.error();
226 }
227 Ok(result_diff)
228}
229
230#[cfg(test)]
231mod test {
232 use super::*;
233
234 #[test]
235 fn identity() {
236 let result = compare_paths(
237 "tests/integ/data/images/actual/SaveImage_100DPI_default_size.jpg",
238 "tests/integ/data/images/actual/SaveImage_100DPI_default_size.jpg",
239 &ImageCompareConfig {
240 threshold: 1.0,
241 mode: CompareMode::RGB(RGBCompareMode::Hybrid),
242 },
243 )
244 .unwrap();
245 assert!(!result.is_error);
246 }
247
248 #[test]
249 fn pin_diff_image() {
250 let result = compare_paths(
251 "tests/integ/data/images/expected/SaveImage_100DPI_default_size.jpg",
252 "tests/integ/data/images/actual/SaveImage_100DPI_default_size.jpg",
253 &ImageCompareConfig {
254 threshold: 1.0,
255 mode: CompareMode::RGBA(RGBACompareMode::Hybrid),
256 },
257 )
258 .unwrap();
259 assert!(result.is_error);
260 if let DiffDetail::Image {
261 score: _,
262 diff_image,
263 } = result.detail.first().unwrap()
264 {
265 let img = image::open(diff_image.as_ref().unwrap())
266 .unwrap()
267 .into_rgba8();
268 let nom = image::open("tests/integ/data/images/diff_100_DPI.png")
269 .unwrap()
270 .into_rgba8();
271 let diff_result = image_compare::rgba_hybrid_compare(&img, &nom)
272 .expect("Wrong dimensions of diff images!");
273 assert_eq!(diff_result.score, 1.0);
274 } else {
275 unreachable!();
276 }
277 }
278}