use core::f64;
use image::{
codecs::png::PngDecoder, DynamicImage, GenericImage, GenericImageView, ImageOutputFormat, Rgba,
};
use std::io::{Read, Write};
pub struct Options {
pub threshold: f64,
pub include_aa: bool,
pub alpha: f64,
pub aa_color: [u8; 4],
pub diff_color: [u8; 4],
pub diff_color_alt: Option<[u8; 4]>,
pub diff_mask: bool,
}
impl Default for Options {
fn default() -> Self {
Options {
threshold: 0.1,
include_aa: false,
alpha: 0.1,
aa_color: [255, 255, 0, 255],
diff_color: [255, 0, 0, 255],
diff_color_alt: None,
diff_mask: false,
}
}
}
pub fn pixelmatch<IMG1: Read, IMG2: Read, OUT: Write>(
img1: IMG1,
img2: IMG2,
mut output: Option<&mut OUT>,
width: Option<u32>,
height: Option<u32>,
options: Option<Options>,
) -> Result<usize, Box<dyn std::error::Error>> {
let img1 = DynamicImage::from_decoder(PngDecoder::new(img1)?)?;
let img2 = DynamicImage::from_decoder(PngDecoder::new(img2)?)?;
let img1_dimensions = img1.dimensions();
if img1.dimensions() != img2.dimensions() {
return Err(<Box<dyn std::error::Error>>::from(
"Image sizes do not match.",
));
}
if let (Some(width), Some(height)) = (width, height) {
if (width, height) != img1_dimensions {
return Err(<Box<dyn std::error::Error>>::from(
"Image data size does not match width/height.",
));
}
}
let options = options.unwrap_or_default();
let mut img_out = match output {
Some(..) => Some(DynamicImage::new_rgba8(
img1_dimensions.0,
img1_dimensions.1,
)),
None => None,
};
let mut identical = true;
for (pixel1, pixel2) in img1.pixels().zip(img2.pixels()) {
if pixel1 != pixel2 {
identical = false;
break;
}
}
if identical {
if let (Some(output), Some(img_out)) = (&mut output, &mut img_out) {
if !options.diff_mask {
for pixel in img1.pixels() {
draw_gray_pixel(&pixel, options.alpha, img_out)?;
}
}
img_out.write_to(*output, ImageOutputFormat::Png)?;
}
return Ok(0);
}
let max_delta = 35215_f64 * options.threshold * options.threshold;
let mut diff: usize = 0;
for (pixel1, pixel2) in img1.pixels().zip(img2.pixels()) {
let delta = color_delta(&pixel1.2, &pixel2.2, false);
if delta.abs() > max_delta {
if !options.include_aa
&& (antialiased(
&img1,
pixel1.0,
pixel1.1,
img1_dimensions.0,
img1_dimensions.1,
&img2,
) || antialiased(
&img2,
pixel1.0,
pixel1.1,
img1_dimensions.0,
img1_dimensions.1,
&img1,
))
{
if let (Some(img_out), false) = (&mut img_out, options.diff_mask) {
img_out.put_pixel(pixel1.0, pixel1.1, Rgba(options.aa_color));
}
} else {
if let Some(img_out) = &mut img_out {
let color = if delta < 0.0 {
options.diff_color_alt.unwrap_or(options.diff_color)
} else {
options.diff_color
};
img_out.put_pixel(pixel1.0, pixel1.1, Rgba(color));
}
diff += 1;
}
} else if let (Some(img_out), false) = (&mut img_out, options.diff_mask) {
draw_gray_pixel(&pixel1, options.alpha, img_out)?;
}
}
if let (Some(output), Some(img_out)) = (&mut output, &mut img_out) {
img_out.write_to(*output, ImageOutputFormat::Png)?;
}
Ok(diff)
}
fn antialiased(
img1: &DynamicImage,
x: u32,
y: u32,
width: u32,
height: u32,
img2: &DynamicImage,
) -> bool {
let mut zeroes: u8 = if x == 0 || y == 0 || x == width - 1 || y == height - 1 {
1
} else {
0
};
let mut min = 0.0;
let mut max = 0.0;
let mut min_x = 0;
let mut min_y = 0;
let mut max_x = 0;
let mut max_y = 0;
let center_rgba = img1.get_pixel(x, y);
for adjacent_x in (if x > 0 { x - 1 } else { x })..=(if x < width - 1 { x + 1 } else { x }) {
for adjacent_y in (if y > 0 { y - 1 } else { y })..=(if y < height - 1 { y + 1 } else { y })
{
if adjacent_x == x && adjacent_y == y {
continue;
}
let rgba = img1.get_pixel(adjacent_x, adjacent_y);
let delta = color_delta(¢er_rgba, &rgba, true);
if delta == 0.0 {
zeroes += 1;
if zeroes > 2 {
return false;
}
continue;
}
if delta < min {
min = delta;
min_x = adjacent_x;
min_y = adjacent_y;
continue;
}
if delta > max {
max = delta;
max_x = adjacent_x;
max_y = adjacent_y;
}
}
}
if min == 0.0 || max == 0.0 {
return false;
}
(has_many_siblings(img1, min_x, min_y, width, height)
&& has_many_siblings(img2, min_x, min_y, width, height))
|| (has_many_siblings(img1, max_x, max_y, width, height)
&& has_many_siblings(img2, max_x, max_y, width, height))
}
fn has_many_siblings(img: &DynamicImage, x: u32, y: u32, width: u32, height: u32) -> bool {
let mut zeroes: u8 = if x == 0 || y == 0 || x == width - 1 || y == height - 1 {
1
} else {
0
};
let center_rgba = img.get_pixel(x, y);
for adjacent_x in (if x > 0 { x - 1 } else { x })..=(if x < width - 1 { x + 1 } else { x }) {
for adjacent_y in (if y > 0 { y - 1 } else { y })..=(if y < height - 1 { y + 1 } else { y })
{
if adjacent_x == x && adjacent_y == y {
continue;
}
let rgba = img.get_pixel(adjacent_x, adjacent_y);
if center_rgba == rgba {
zeroes += 1;
}
if zeroes > 2 {
return true;
}
}
}
false
}
fn color_delta(rgba1: &Rgba<u8>, rgba2: &Rgba<u8>, y_only: bool) -> f64 {
let mut r1 = rgba1[0] as f64;
let mut g1 = rgba1[1] as f64;
let mut b1 = rgba1[2] as f64;
let mut a1 = rgba1[3] as f64;
let mut r2 = rgba2[0] as f64;
let mut g2 = rgba2[1] as f64;
let mut b2 = rgba2[2] as f64;
let mut a2 = rgba2[3] as f64;
if (a1 - a2).abs() < f64::EPSILON
&& (r1 - r2).abs() < f64::EPSILON
&& (g1 - g2).abs() < f64::EPSILON
&& (b1 - b2).abs() < f64::EPSILON
{
return 0.0;
}
if a1 < 255.0 {
a1 /= 255.0;
r1 = blend(r1, a1);
g1 = blend(g1, a1);
b1 = blend(b1, a1);
}
if a2 < 255.0 {
a2 /= 255.0;
r2 = blend(r2, a2);
g2 = blend(g2, a2);
b2 = blend(b2, a2);
}
let y1 = rgb2y(r1, g1, b1);
let y2 = rgb2y(r2, g2, b2);
let y = y1 - y2;
if y_only {
return y;
}
let i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2);
let q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);
let delta = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
if y1 > y2 {
-delta
} else {
delta
}
}
fn draw_gray_pixel(
(x, y, rgba): &(u32, u32, Rgba<u8>),
alpha: f64,
output: &mut DynamicImage,
) -> Result<(), Box<dyn std::error::Error>> {
if !output.in_bounds(*x, *y) {
return Err(<Box<dyn std::error::Error>>::from(
"Pixel is not in bounds of output.",
));
}
let val = blend(
rgb2y(rgba[0], rgba[1], rgba[2]),
(alpha * rgba[3] as f64) / 255.0,
) as u8;
let gray_rgba = Rgba([val, val, val, val]);
output.put_pixel(*x, *y, gray_rgba);
Ok(())
}
fn blend<T: Into<f64>>(c: T, a: T) -> f64 {
255.0 + (c.into() - 255.0) * a.into()
}
fn rgb2y<T: Into<f64>>(r: T, g: T, b: T) -> f64 {
r.into() * 0.29889531 + g.into() * 0.58662247 + b.into() * 0.11448223
}
fn rgb2i<T: Into<f64>>(r: T, g: T, b: T) -> f64 {
r.into() * 0.59597799 - g.into() * 0.27417610 - b.into() * 0.32180189
}
fn rgb2q<T: Into<f64>>(r: T, g: T, b: T) -> f64 {
r.into() * 0.21147017 - g.into() * 0.52261711 + b.into() * 0.31114694
}