1use core::fmt;
7use image::{DynamicImage, GenericImage, GenericImageView, ImageResult};
8use std::fs::{create_dir, read_dir, File};
9use std::io;
10use std::path::{Path, PathBuf};
11use std::sync::mpsc;
12use std::thread;
13use structopt::StructOpt;
14
15#[derive(Debug)]
17pub enum ImgDiffError {
18 IoError(io::Error),
20
21 ImageError(image::ImageError),
23
24 MpscSendError(std::sync::mpsc::SendError<Pair<DiffImage>>),
26
27 PathToStringConversionFailed(PathBuf),
29}
30
31pub type ImgDiffResult<T> = Result<T, ImgDiffError>;
32
33impl fmt::Display for ImgDiffError {
34 fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
35 match *self {
36 ImgDiffError::IoError(ref e) => e.fmt(fmt),
37 ImgDiffError::ImageError(ref e) => e.fmt(fmt),
38 ImgDiffError::MpscSendError(ref e) => e.fmt(fmt),
39 ImgDiffError::PathToStringConversionFailed(ref e) => {
40 write!(fmt, "Path to string conversion failed Path: {:?}", e)
41 }
42 }
43 }
44}
45
46impl From<io::Error> for ImgDiffError {
47 fn from(err: io::Error) -> ImgDiffError {
48 ImgDiffError::IoError(err)
49 }
50}
51
52impl From<image::ImageError> for ImgDiffError {
53 fn from(err: image::ImageError) -> ImgDiffError {
54 ImgDiffError::ImageError(err)
55 }
56}
57
58impl From<std::sync::mpsc::SendError<Pair<DiffImage>>> for ImgDiffError {
59 fn from(err: std::sync::mpsc::SendError<Pair<DiffImage>>) -> ImgDiffError {
60 ImgDiffError::MpscSendError(err)
61 }
62}
63
64#[derive(Debug, StructOpt)]
65pub struct Config {
67 #[structopt(parse(from_os_str), short = "s")]
69 pub src_dir: PathBuf,
70 #[structopt(parse(from_os_str), short = "d")]
72 pub dest_dir: PathBuf,
73 #[structopt(parse(from_os_str), short = "f")]
75 pub diff_dir: PathBuf,
76 #[structopt(short = "v", long = "verbose")]
78 pub verbose: bool,
79}
80
81pub struct DiffImage {
82 path: PathBuf,
83 image: ImageResult<DynamicImage>,
84}
85
86pub struct Pair<T> {
87 src: T,
88 dest: T,
89}
90
91fn output_diff_file(
92 diff_image: DynamicImage,
93 diff_value: f64,
94 config: &Config,
95 src_path: PathBuf,
96 dest_path: PathBuf,
97) -> ImgDiffResult<()> {
98 if diff_value != 0.0 {
99 let path = dest_path
100 .to_str()
101 .ok_or_else(|| ImgDiffError::PathToStringConversionFailed(dest_path.clone()))?;
102 let diff_file_name = get_diff_file_name_and_validate_path(path, config)?;
103 let file_out = &mut File::create(&Path::new(&diff_file_name))?;
104 diff_image.write_to(file_out, image::PNG)?;
105
106 if config.verbose {
107 if let Some(path) = src_path.to_str() {
108 eprintln!("diff found in file: {:?}", String::from(path));
109 } else {
110 eprintln!("failed to convert path to string: {:?}", src_path);
111 }
112 }
113 }
114 Ok(())
115}
116
117fn max(a: u8, b: u8) -> u8 {
118 if a > b {
119 a
120 } else {
121 b
122 }
123}
124
125pub fn subtract_image(a: &DynamicImage, b: &DynamicImage) -> (f64, DynamicImage) {
126 let (x_dim, y_dim) = a.dimensions();
127 let mut diff_image = DynamicImage::new_rgba8(x_dim, y_dim);
128 let mut max_value: f64 = 0.0;
129 let mut current_value: f64 = 0.0;
130 for ((x, y, pixel_a), (_, _, pixel_b)) in a.pixels().zip(b.pixels()) {
131 max_value += f64::from(max(pixel_a[0], pixel_b[0]));
133 max_value += f64::from(max(pixel_a[1], pixel_b[1]));
134 max_value += f64::from(max(pixel_a[2], pixel_b[2]));
135 max_value += f64::from(max(pixel_a[3], pixel_b[3]));
136 let r = subtract_and_prevent_overflow(pixel_a[0], pixel_b[0]);
137 let g = subtract_and_prevent_overflow(pixel_a[1], pixel_b[1]);
138 let b = subtract_and_prevent_overflow(pixel_a[2], pixel_b[2]);
139 let a = subtract_and_prevent_overflow(pixel_a[3], pixel_b[3]);
140 current_value += f64::from(r);
141 current_value += f64::from(g);
142 current_value += f64::from(b);
143 current_value += f64::from(a);
144 diff_image.put_pixel(x, y, image::Rgba([255 - r, 255 - g, 255 - b, 255 - a]));
145 }
146 (((current_value * 100.0) / max_value), diff_image)
147}
148
149fn subtract_and_prevent_overflow(a: u8, b: u8) -> u8 {
150 if a > b {
151 a - b
152 } else {
153 b - a
154 }
155}
156
157pub fn do_diff(config: &Config) -> ImgDiffResult<()> {
159 let files_to_load = find_all_files_to_load(config.src_dir.clone(), &config)?;
161
162 let (transmitter, receiver) = mpsc::channel();
164 thread::spawn(move || -> ImgDiffResult<()> {
165 for (scr_path, dest_path) in files_to_load {
166 transmitter.send(Pair {
167 src: DiffImage {
168 path: scr_path.clone(),
169 image: image::open(scr_path),
170 },
171 dest: DiffImage {
172 path: dest_path.clone(),
173 image: image::open(dest_path),
174 },
175 })?;
176 }
177 Ok(())
178 });
179
180 for pair in receiver {
182 let src_image = pair.src.image?;
183 let dest_image = pair.dest.image?;
184 if src_image.dimensions() != dest_image.dimensions() {
185 print_dimensions_error(config, &pair.src.path)?;
186 } else {
187 let (diff_value, diff_image) = subtract_image(&src_image, &dest_image);
188 print_diff_result(config.verbose, &pair.src.path, diff_value);
189 output_diff_file(
190 diff_image,
191 diff_value,
192 config,
193 pair.src.path,
194 pair.dest.path,
195 )?
196 }
197 }
198
199 Ok(())
200}
201
202fn find_all_files_to_load(dir: PathBuf, config: &Config) -> ImgDiffResult<Vec<(PathBuf, PathBuf)>> {
204 let mut files: Vec<(PathBuf, PathBuf)> = vec![];
205 let entries = read_dir(dir)?;
206 for entry in entries {
207 let path = entry?.path();
208 if path.is_file() {
209 let entry_name = path
210 .to_str()
211 .ok_or_else(|| ImgDiffError::PathToStringConversionFailed(path.clone()))?;
212 let scr_name = config.src_dir.to_str().ok_or_else(|| {
213 ImgDiffError::PathToStringConversionFailed(config.src_dir.clone())
214 })?;
215 let dest_name = config.dest_dir.to_str().ok_or_else(|| {
216 ImgDiffError::PathToStringConversionFailed(config.dest_dir.clone())
217 })?;
218 let dest_file_name = entry_name.replace(scr_name, dest_name);
219 let dest_path = PathBuf::from(dest_file_name);
220 if dest_path.exists() {
221 files.push((path, dest_path));
222 }
223 } else {
224 let child_files = find_all_files_to_load(path, &config)?;
225 files.extend(child_files);
226 }
227 }
228
229 Ok(files)
230}
231
232fn get_diff_file_name_and_validate_path(
234 dest_file_name: &str,
235 config: &Config,
236) -> ImgDiffResult<String> {
237 let dest_name = config
238 .dest_dir
239 .to_str()
240 .ok_or_else(|| ImgDiffError::PathToStringConversionFailed(config.dest_dir.clone()))?;
241 let diff_name = config
242 .diff_dir
243 .to_str()
244 .ok_or_else(|| ImgDiffError::PathToStringConversionFailed(config.diff_dir.clone()))?;
245
246 let diff_file_name = dest_file_name.replace(dest_name, diff_name);
247 let diff_path = Path::new(&diff_file_name);
248
249 if let Some(diff_path_dir) = diff_path.parent() {
250 if !diff_path_dir.exists() {
251 if config.verbose {
252 println!("creating directory: {:?}", diff_path_dir);
253 }
254 create_path(diff_path)?;
255 }
256 }
257 Ok(diff_file_name)
258}
259
260fn print_diff_result(verbose: bool, entry: &PathBuf, diff_value: f64) {
262 if verbose {
263 println!(
264 "compared file: {:?} had diff value of: {:?}%",
265 entry, diff_value
266 );
267 } else {
268 println!("{:?}%", diff_value);
269 }
270}
271
272fn print_dimensions_error(config: &Config, path: &PathBuf) -> ImgDiffResult<()> {
274 println!("Images have different dimensions, skipping comparison");
275 if config.verbose {
276 let path = path
277 .to_str()
278 .ok_or_else(|| ImgDiffError::PathToStringConversionFailed(path.clone()));
279 eprintln!("diff found in file: {:?}", path);
280 }
281
282 Ok(())
283}
284
285fn create_path(path: &Path) -> ImgDiffResult<()> {
287 let mut buffer = path.to_path_buf();
288 if buffer.is_file() {
289 buffer.pop();
290 }
291 create_dir_if_not_there(buffer)?;
292 Ok(())
293}
294
295fn create_dir_if_not_there(mut buffer: PathBuf) -> ImgDiffResult<PathBuf> {
297 if buffer.pop() {
298 create_dir_if_not_there(buffer.clone())?;
299 if !buffer.exists() && buffer != Path::new("") {
300 create_dir(&buffer)?
301 }
302 }
303 Ok(buffer)
304}