use super::convert::DynamicImageTraitConvert;
use anyhow::{Result, ensure};
use image::{DynamicImage, ExtendedColorType};
use versatiles_derive::context;
pub trait DynamicImageTraitInfo: DynamicImageTraitConvert {
fn bits_per_value(&self) -> u8;
fn channel_count(&self) -> u8;
fn diff(&self, other: &DynamicImage) -> Result<Vec<f64>>;
fn ensure_same_meta(&self, other: &DynamicImage) -> Result<()>;
fn ensure_same_size(&self, other: &DynamicImage) -> Result<()>;
fn extended_color_type(&self) -> ExtendedColorType;
fn into_optional(self) -> Option<DynamicImage>;
fn is_empty(&self) -> bool;
fn is_opaque(&self) -> bool;
}
impl DynamicImageTraitInfo for DynamicImage
where
DynamicImage: DynamicImageTraitConvert,
{
fn bits_per_value(&self) -> u8 {
u8::try_from(self.color().bits_per_pixel() / u16::from(self.color().channel_count()))
.expect("bits per channel fits in u8")
}
fn channel_count(&self) -> u8 {
self.color().channel_count()
}
#[context("computing per-channel diff: self {}x{} {:?} vs other {}x{} {:?}", self.width(), self.height(), self.color(), other.width(), other.height(), other.color())]
fn diff(&self, other: &DynamicImage) -> Result<Vec<f64>> {
self.ensure_same_meta(other)?;
let channels = self.color().channel_count() as usize;
let mut sqr_sum = vec![0u64; channels];
for (p1, p2) in self.iter_pixels().zip(other.iter_pixels()) {
for i in 0..channels {
let d = i64::from(p1[i]) - i64::from(p2[i]);
sqr_sum[i] += (d * d).cast_unsigned(); }
}
let n = f64::from(self.width() * self.height());
Ok(sqr_sum.iter().map(|v| (10.0 * (*v as f64) / n).ceil() / 10.0).collect())
}
#[context("validating same size and color: self {:?} vs other {:?}", self.color(), other.color())]
fn ensure_same_meta(&self, other: &DynamicImage) -> Result<()> {
self.ensure_same_size(other)?;
ensure!(
self.color() == other.color(),
"Pixel value type mismatch: self has {:?}, but the other image has {:?}",
self.color(),
other.color()
);
Ok(())
}
#[context("validating same dimensions: self {}x{}, other {}x{}", self.width(), self.height(), other.width(), other.height())]
fn ensure_same_size(&self, other: &DynamicImage) -> Result<()> {
ensure!(
self.width() == other.width(),
"Image width mismatch: self has width {}, but the other image has width {}",
self.width(),
other.width()
);
ensure!(
self.height() == other.height(),
"Image height mismatch: self has height {}, but the other image has height {}",
self.height(),
other.height()
);
Ok(())
}
fn extended_color_type(&self) -> ExtendedColorType {
self.color().into()
}
fn into_optional(self) -> Option<DynamicImage> {
if self.is_empty() { None } else { Some(self) }
}
fn is_empty(&self) -> bool {
if !self.color().has_alpha() {
return false;
}
let alpha_channel = (self.color().channel_count() - 1) as usize;
return self.iter_pixels().all(|p| p[alpha_channel] == 0);
}
fn is_opaque(&self) -> bool {
if !self.color().has_alpha() {
return true;
}
let alpha_channel = (self.color().channel_count() - 1) as usize;
return self.iter_pixels().all(|p| p[alpha_channel] == 255);
}
}
#[cfg(test)]
#[allow(clippy::cast_possible_truncation, clippy::float_cmp)]
mod tests {
use super::*;
use image::ExtendedColorType;
use rstest::rstest;
fn sample_l8() -> DynamicImage {
DynamicImage::from_fn(4, 3, |x, y| [((x + y) % 2) as u8])
}
fn sample_la8(alpha: u8) -> DynamicImage {
DynamicImage::from_fn(4, 3, |x, y| [((x * 2 + y) & 0xFF) as u8, alpha])
}
fn sample_rgb8() -> DynamicImage {
DynamicImage::from_fn(4, 3, |x, y| [x as u8, y as u8, (x + y) as u8])
}
fn sample_rgba8(alpha: u8) -> DynamicImage {
DynamicImage::from_fn(4, 3, |x, y| [x as u8, y as u8, (x + y) as u8, alpha])
}
#[rstest]
#[case::l8(sample_l8(), 8u8, 1u8)]
#[case::la8(sample_la8(255), 8u8, 2u8)]
#[case::rgb8(sample_rgb8(), 8u8, 3u8)]
#[case::rgba8(sample_rgba8(200), 8u8, 4u8)]
fn bits_and_channels(#[case] img: DynamicImage, #[case] bits: u8, #[case] chans: u8) {
assert_eq!(img.bits_per_value(), bits);
assert_eq!(img.channel_count(), chans);
}
#[rstest]
#[case::l8(sample_l8(), ExtendedColorType::L8, false)]
#[case::la8(sample_la8(123), ExtendedColorType::La8, true)]
#[case::rgb8(sample_rgb8(), ExtendedColorType::Rgb8, false)]
#[case::rgba8(sample_rgba8(42), ExtendedColorType::Rgba8, true)]
fn color_and_alpha(#[case] img: DynamicImage, #[case] ect: ExtendedColorType, #[case] has_alpha: bool) {
assert_eq!(img.extended_color_type(), ect);
assert_eq!(img.has_alpha(), has_alpha);
}
#[rstest]
#[case::l8_opaque(sample_l8(), false, true)]
#[case::la8_empty(sample_la8(0), true, false)]
#[case::la8_partial(sample_la8(100), false, false)]
#[case::la8_opaque(sample_la8(255), false, true)]
#[case::rgb8_opaque(sample_rgb8(), false, true)]
#[case::rgba8_empty(sample_rgba8(0), true, false)]
#[case::rgba8_partial(sample_rgba8(100), false, false)]
#[case::rgba8_opaque(sample_rgba8(255), false, true)]
fn empty_and_opaque(#[case] img: DynamicImage, #[case] expect_empty: bool, #[case] expect_opaque: bool) {
assert_eq!(img.is_empty(), expect_empty);
assert_eq!(img.is_opaque(), expect_opaque);
}
#[test]
fn into_optional_behaviour() {
let rgb = sample_rgb8();
assert!(rgb.clone().into_optional().is_some());
let rgba_empty = sample_rgba8(0);
assert!(rgba_empty.into_optional().is_none());
let la_opaque = sample_la8(255);
assert!(la_opaque.into_optional().is_some());
}
#[test]
fn ensure_same_size_and_meta_ok() {
let a = sample_rgb8();
let b = sample_rgb8();
a.ensure_same_size(&b).unwrap();
a.ensure_same_meta(&b).unwrap();
}
#[rstest]
#[case::mismatched_width(
3,
4,
5,
4,
"Image width mismatch: self has width 3, but the other image has width 5"
)]
#[case::mismatched_height(
4,
3,
4,
5,
"Image height mismatch: self has height 3, but the other image has height 5"
)]
fn ensure_same_size_errors(
#[case] w1: usize,
#[case] h1: usize,
#[case] w2: usize,
#[case] h2: usize,
#[case] expect: &str,
) {
let a = DynamicImage::from_fn(w1, h1, |x, y| [x as u8, y as u8, 0]);
let b = DynamicImage::from_fn(w2, h2, |x, y| [x as u8, y as u8, 0]);
assert_eq!(
a.ensure_same_size(&b).unwrap_err().chain().last().unwrap().to_string(),
expect
);
}
#[test]
fn ensure_same_meta_color_mismatch_error() {
let a = sample_rgb8();
let b = sample_rgba8(255);
assert_eq!(
a.ensure_same_meta(&b).unwrap_err().chain().last().unwrap().to_string(),
"Pixel value type mismatch: self has Rgb8, but the other image has Rgba8"
);
}
#[test]
fn diff_zero_for_identical_images() {
let a = sample_rgb8();
let b = sample_rgb8();
let d = a.diff(&b).unwrap();
assert_eq!(d, vec![0.0, 0.0, 0.0]);
}
#[test]
fn diff_scales_with_squared_error_and_rounds() {
let base = DynamicImage::from_fn(2, 2, |_, _| [10, 20, 30]);
let changed = DynamicImage::from_fn(2, 2, |_, _| [10, 20, 30]);
let mut raw = changed.as_bytes().to_vec();
raw[0] = raw[0].saturating_add(1);
let changed = DynamicImage::from_raw(2, 2, raw).unwrap();
let d = base.diff(&changed).unwrap();
assert_eq!(d.len(), 3);
assert!((d[0] - 0.3).abs() < f64::EPSILON);
assert_eq!(d[1], 0.0);
assert_eq!(d[2], 0.0);
}
}