img_diff/
lib.rs

1//! # img_diff
2//!
3//! `img_diff` is a cmd line tool to diff images in 2 folders
4//! you can pass -h to see the help
5//!
6use 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/// An enumeration of ImgDiff possible Errors
16#[derive(Debug)]
17pub enum ImgDiffError {
18    /// An I/O Error occurred while decoding the image
19    IoError(io::Error),
20
21    ///
22    ImageError(image::ImageError),
23
24    ///
25    MpscSendError(std::sync::mpsc::SendError<Pair<DiffImage>>),
26
27    /// Path to string conversion failed
28    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)]
65/// diff images in 2 structurally similar folders and output diff images
66pub struct Config {
67    /// the folder to read
68    #[structopt(parse(from_os_str), short = "s")]
69    pub src_dir: PathBuf,
70    /// the folder to compare the read images
71    #[structopt(parse(from_os_str), short = "d")]
72    pub dest_dir: PathBuf,
73    /// the folder to output the diff images if a diff is found
74    #[structopt(parse(from_os_str), short = "f")]
75    pub diff_dir: PathBuf,
76    /// toggle verbose mode
77    #[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        // TODO(miguelmendes): find a way to avoid groups of 4 algorithm
132        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
157/// Diffs all images using a channel to parallelize the file IO and processing.
158pub fn do_diff(config: &Config) -> ImgDiffResult<()> {
159    // Get a full list of all images to load (scr and dest pairs)
160    let files_to_load = find_all_files_to_load(config.src_dir.clone(), &config)?;
161
162    // open a channel to load pairs of images from disk
163    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    // do the comparison in the receiving channel
181    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
202/// Recursively finds all files to compare based on the directory
203fn 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
232/// helper to create necessary folders for IO operations to be successful
233fn 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
260/// print diff result
261fn 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
272/// print dimensions errors
273fn 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
285/// Helper to create folder hierarchies
286fn 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
295/// recursive way to create folders hierarchies
296fn 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}