image_diff_review 0.2.0

Reporting tool of image differences for snaphost testing
Documentation
use crate::difference::ImageInfoResult::Loaded;
use crate::pair::Pair;
use image::{Pixel, Rgb, RgbImage};
use std::fmt::{Display, Formatter};
use std::path::Path;

#[derive(Debug)]
pub(crate) struct Size {
    pub width: u32,
    pub height: u32,
}

#[derive(Debug)]
pub(crate) struct ImageInfo {
    pub size: Size,
}

impl Size {
    pub fn new(width: u32, height: u32) -> Self {
        Size { width, height }
    }
}

impl Display for Size {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}x{}", self.width, self.height)
    }
}

impl ImageInfo {
    pub fn from_image(image: &RgbImage) -> Self {
        ImageInfo {
            size: Size::new(image.width(), image.height()),
        }
    }
}

pub(crate) enum ImageInfoResult {
    Loaded(ImageInfo),
    Missing,
    Error(String),
}

impl ImageInfoResult {
    pub fn info(&self) -> Option<&ImageInfo> {
        match self {
            Loaded(info) => Some(info),
            _ => None,
        }
    }
}

#[derive(Debug)]
pub(crate) enum Difference {
    None,
    MissingFile,
    LoadError,
    SizeMismatch,
    Content {
        n_different_pixels: u64,
        distance_sum: u64,
        diff_image: RgbImage,
    },
}

pub(crate) struct PairResult {
    pub pair: Pair,
    pub difference: Difference,
    pub left_info: ImageInfoResult,
    pub right_info: ImageInfoResult,
}

fn load_image(path: &Path) -> crate::Result<RgbImage> {
    Ok(image::ImageReader::open(path)?.decode()?.into_rgb8())
}

fn load_image_with_info(path: &Path) -> (Option<RgbImage>, ImageInfoResult) {
    if !path.exists() {
        return (None, ImageInfoResult::Missing);
    }
    match load_image(path) {
        Ok(image) => {
            let info = ImageInfo::from_image(&image);
            (Some(image), ImageInfoResult::Loaded(info))
        }
        Err(e) => (None, ImageInfoResult::Error(e.to_string())),
    }
}

fn compute_pair_diff(pair: &Pair) -> (Difference, ImageInfoResult, ImageInfoResult) {
    let (left, left_info) = load_image_with_info(&pair.left);
    let (right, right_info) = load_image_with_info(&pair.right);

    let (left, right) = match (left, right) {
        (Some(left), Some(right)) => (left, right),
        _ => {
            return (
                match (&left_info, &right_info) {
                    (_, ImageInfoResult::Error(_)) | (ImageInfoResult::Error(_), _) => {
                        Difference::LoadError
                    }
                    (_, ImageInfoResult::Missing) | (ImageInfoResult::Missing, _) => {
                        Difference::MissingFile
                    }
                    _ => unreachable!(),
                },
                left_info,
                right_info,
            )
        }
    };

    if left.width() != right.width() || left.height() != right.height() {
        return (Difference::SizeMismatch, left_info, right_info);
    }

    let n_different_pixels: u64 = left
        .pixels()
        .zip(right.pixels())
        .map(|(p1, p2)| if p1 != p2 { 1 } else { 0 })
        .sum();

    if n_different_pixels == 0 {
        return (Difference::None, left_info, right_info);
    }

    let mut distance_sum: u64 = 0;

    let diff_image_data: Vec<u8> = left
        .pixels()
        .zip(right.pixels())
        .flat_map(|(p1, p2)| {
            let (abs_v, v) = compute_distance(p1, p2);
            distance_sum += abs_v as u64;
            if v < 0 {
                [abs_v as u8, 0, 0]
            } else {
                [0, abs_v as u8, 0]
            }
        })
        .collect();
    let diff_image = RgbImage::from_vec(left.width(), left.height(), diff_image_data).unwrap();
    (
        Difference::Content {
            n_different_pixels,
            distance_sum,
            diff_image,
        },
        left_info,
        right_info,
    )
}

fn compute_distance(p1: &Rgb<u8>, p2: &Rgb<u8>) -> (i32, i32) {
    p1.channels()
        .iter()
        .zip(p2.channels())
        .fold((0, 0), |(abs_v, v), (c1, c2)| {
            let new = (*c2 as i32) - (*c1 as i32);
            let abs_new = new.abs();
            if abs_new > abs_v {
                (abs_new, new)
            } else {
                (abs_v, v)
            }
        })
}

pub(crate) fn compute_differences(pairs: Vec<Pair>) -> Vec<PairResult> {
    pairs
        .into_iter()
        .map(|pair| {
            let (difference, left_info, right_info) = compute_pair_diff(&pair);
            PairResult {
                pair,
                difference,
                left_info,
                right_info,
            }
        })
        .collect()
}