use super::info::DynamicImageTraitInfo;
use anyhow::{Result, bail};
use fast_image_resize::{FilterType, ResizeAlg, ResizeOptions, Resizer};
use image::{DynamicImage, Rgb, imageops::overlay};
use imageproc::map::map_pixels;
use versatiles_derive::context;
pub trait DynamicImageTraitOperation: DynamicImageTraitInfo {
fn to_no_alpha(&self) -> Result<DynamicImage>;
fn average_color(&self) -> Vec<u8>;
fn extract(&self, x: f64, y: f64, w: f64, h: f64, width_dst: u32, height_dst: u32) -> Result<DynamicImage>;
fn scaled_down(&self, factor: u32) -> Result<DynamicImage>;
fn into_flattened(self, color: Rgb<u8>) -> Result<DynamicImage>;
fn into_no_alpha_if_opaque(self) -> Result<DynamicImage>;
fn into_no_alpha(self) -> Result<DynamicImage>;
fn into_scaled_down(self, factor: u32) -> Result<DynamicImage>;
fn make_opaque(&mut self) -> Result<()>;
fn map_color_values<F>(&mut self, f: F)
where
F: Fn(u8) -> u8;
fn overlay(&mut self, top: &DynamicImage) -> Result<()>;
fn overlay_additive(&mut self, top: &DynamicImage) -> Result<()>;
}
impl DynamicImageTraitOperation for DynamicImage
where
DynamicImage: DynamicImageTraitInfo,
{
#[context("removing alpha from {:?} image (has_alpha={})", self.color(), self.has_alpha())]
fn to_no_alpha(&self) -> Result<DynamicImage> {
Ok(match self {
DynamicImage::ImageRgba8(_) => DynamicImage::from(self.to_rgb8()),
DynamicImage::ImageLumaA8(_) => DynamicImage::from(self.to_luma8()),
DynamicImage::ImageRgb8(_) | DynamicImage::ImageLuma8(_) => self.clone(),
_ => bail!("Unsupported image type for removing alpha: {:?}", self.color()),
})
}
fn average_color(&self) -> Vec<u8> {
let img = self.resize_exact(1, 1, image::imageops::FilterType::Triangle);
img.into_bytes()
}
#[context("extracting region ({:.2},{:.2},{:.2},{:.2}) from {}x{} into {}x{}", x, y, w, h, self.width(), self.height(), width_dst, height_dst)]
fn extract(&self, x: f64, y: f64, w: f64, h: f64, width_dst: u32, height_dst: u32) -> Result<DynamicImage> {
let mut dst_image = DynamicImage::new(width_dst, height_dst, self.color());
Resizer::new().resize(self, &mut dst_image, &ResizeOptions::default().crop(x, y, w, h))?;
Ok(dst_image)
}
#[context("downscaling {}x{} by factor {} ({:?})", self.width(), self.height(), factor, self.color())]
fn scaled_down(&self, factor: u32) -> Result<DynamicImage> {
assert!(factor > 0, "Scaling factor must be greater than zero");
let mut dst_image = DynamicImage::new(self.width() / factor, self.height() / factor, self.color());
Resizer::new().resize(
self,
&mut dst_image,
&ResizeOptions::default().resize_alg(ResizeAlg::Convolution(FilterType::Box)),
)?;
Ok(dst_image)
}
#[context("flattening image onto RGB({:?})", color)]
#[allow(clippy::cast_possible_truncation)]
fn into_flattened(self, color: Rgb<u8>) -> Result<DynamicImage> {
if !self.has_alpha() {
return Ok(self);
}
match self {
DynamicImage::ImageRgba8(img) => {
let c = [u16::from(color[0]), u16::from(color[1]), u16::from(color[2])];
Ok(DynamicImage::from(map_pixels(&img, |p| {
if p[3] == 255 {
Rgb([p[0], p[1], p[2]])
} else {
let a = u16::from(p[3]);
let b = u16::from(255 - p[3]);
Rgb([
(((u16::from(p[0]) * a) + c[0] * b + 127) / 255) as u8,
(((u16::from(p[1]) * a) + c[1] * b + 127) / 255) as u8,
(((u16::from(p[2]) * a) + c[2] * b + 127) / 255) as u8,
])
}
})))
}
_ => bail!("Unsupported image type {:?} for flattening", self.color()),
}
}
#[context("dropping alpha from image")]
fn into_no_alpha(self) -> Result<DynamicImage> {
Ok(match self {
DynamicImage::ImageRgba8(_) => DynamicImage::from(self.into_rgb8()),
DynamicImage::ImageLumaA8(_) => DynamicImage::from(self.into_luma8()),
DynamicImage::ImageRgb8(_) | DynamicImage::ImageLuma8(_) => self,
_ => bail!("Unsupported image type for removing alpha: {:?}", self.color()),
})
}
#[context("removing alpha if opaque")]
fn into_no_alpha_if_opaque(self) -> Result<DynamicImage> {
if self.has_alpha() && self.is_opaque() {
self.into_no_alpha()
} else {
Ok(self)
}
}
#[context("downscaling image by factor {factor}")]
fn into_scaled_down(self, factor: u32) -> Result<DynamicImage> {
if factor == 1 {
Ok(self)
} else {
self.scaled_down(factor)
}
}
#[context("forcing opacity for {:?} image", self.color())]
fn make_opaque(&mut self) -> Result<()> {
match *self {
DynamicImage::ImageRgba8(ref mut img) => {
for p in img.pixels_mut() {
p[3] = 255;
}
}
DynamicImage::ImageLumaA8(ref mut img) => {
for p in img.pixels_mut() {
p[1] = 255;
}
}
DynamicImage::ImageRgb8(_) | DynamicImage::ImageLuma8(_) => {}
_ => bail!("Unsupported image type for removing alpha: {:?}", self.color()),
}
Ok(())
}
fn map_color_values<F>(&mut self, f: F)
where
F: Fn(u8) -> u8,
{
match self {
DynamicImage::ImageLuma8(img) => {
for p in img.pixels_mut() {
p[0] = f(p[0]);
}
}
DynamicImage::ImageLumaA8(img) => {
for p in img.pixels_mut() {
p[0] = f(p[0]);
}
}
DynamicImage::ImageRgb8(img) => {
for p in img.pixels_mut() {
p[0] = f(p[0]);
p[1] = f(p[1]);
p[2] = f(p[2]);
}
}
DynamicImage::ImageRgba8(img) => {
for p in img.pixels_mut() {
p[0] = f(p[0]);
p[1] = f(p[1]);
p[2] = f(p[2]);
}
}
_ => panic!("Unsupported image type for mutating color values: {:?}", self.color()),
}
}
#[context("overlaying top {}x{} {:?} onto base {}x{} {:?}", top.width(), top.height(), top.color(), self.width(), self.height(), self.color())]
fn overlay(&mut self, top: &DynamicImage) -> Result<()> {
self.ensure_same_size(top)?;
overlay(self, top, 0, 0);
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
fn overlay_additive(&mut self, top: &DynamicImage) -> Result<()> {
self.ensure_same_size(top)?;
fn blend(base_px: &[u8; 4], top_px: &[u8; 4]) -> [u8; 4] {
let a_b = u16::from(base_px[3]);
let a_t = u16::from(top_px[3]);
let a_sum = a_b + a_t;
if a_sum == 0 {
return [0, 0, 0, 0];
}
let a_out = if a_sum >= 250 { 255 } else { a_sum };
let a_b = a_out - a_t; [
((u16::from(base_px[0]) * a_b + u16::from(top_px[0]) * a_t + a_out / 2) / a_out).min(255) as u8,
((u16::from(base_px[1]) * a_b + u16::from(top_px[1]) * a_t + a_out / 2) / a_out).min(255) as u8,
((u16::from(base_px[2]) * a_b + u16::from(top_px[2]) * a_t + a_out / 2) / a_out).min(255) as u8,
a_out as u8,
]
}
if let DynamicImage::ImageRgb8(top_img) = top {
*self = DynamicImage::ImageRgb8(top_img.clone());
return Ok(());
}
let DynamicImage::ImageRgba8(top_img) = top else {
bail!(
"overlay_additive: unsupported top image type {:?}, expected RGB8 or RGBA8",
top.color()
);
};
match self {
DynamicImage::ImageRgba8(base_img) => {
for (base_px, top_px) in base_img.pixels_mut().zip(top_img.pixels()) {
*base_px = image::Rgba(blend(&base_px.0, &top_px.0));
}
}
DynamicImage::ImageRgb8(base_img) => {
for (base_px, top_px) in base_img.pixels_mut().zip(top_img.pixels()) {
let p = blend(&[base_px.0[0], base_px.0[1], base_px.0[2], 255], &top_px.0);
*base_px = image::Rgb([p[0], p[1], p[2]]);
}
}
_ => {
bail!(
"overlay_additive: unsupported base image type {:?}, expected RGB8 or RGBA8",
self.color()
);
}
}
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::cast_possible_truncation)]
mod tests {
use super::*;
use crate::traits::*;
use image::ExtendedColorType as ECT;
use image::{GenericImageView, Pixel, Rgba};
use rstest::rstest;
#[rstest]
#[case::rgba(DynamicImage::new_test_rgba(), ECT::Rgb8, false)]
#[case::la(DynamicImage::new_test_greya(), ECT::L8, false)]
#[case::rgb(DynamicImage::new_test_rgb(), ECT::Rgb8, false)]
#[case::grey(DynamicImage::new_test_grey(), ECT::L8, false)]
fn to_no_alpha_drops_alpha_when_present(
#[case] src: DynamicImage,
#[case] expect_type: ECT,
#[case] expect_has_alpha: bool,
) {
let out = src.to_no_alpha().unwrap();
assert_eq!(out.extended_color_type(), expect_type);
assert_eq!(out.has_alpha(), expect_has_alpha);
}
#[rstest]
#[case::rgba(DynamicImage::new_test_rgba(), ECT::Rgb8)]
#[case::la(DynamicImage::new_test_greya(), ECT::L8)]
#[case::rgb(DynamicImage::new_test_rgb(), ECT::Rgb8)]
#[case::grey(DynamicImage::new_test_grey(), ECT::L8)]
fn into_no_alpha_variants(#[case] src: DynamicImage, #[case] expect_type: ECT) {
let out = src.into_no_alpha().unwrap();
assert_eq!(out.extended_color_type(), expect_type);
assert!(!out.has_alpha());
}
#[test]
fn average_color_on_solid_rgb_is_exact() {
let img = DynamicImage::from_fn(11, 11, |x, y| {
[100 - x as u8, 110 - y as u8, 120 + x as u8, 130 + y as u8]
});
assert_eq!(img.average_color(), [95, 105, 125, 135]);
}
#[rstest]
#[case::grey(DynamicImage::new_test_grey(),&[128])]
#[case::greya(DynamicImage::new_test_greya(),&[128,128])]
#[case::rgb(DynamicImage::new_test_rgb(),&[128, 127, 128])]
#[case::rgba(DynamicImage::new_test_rgba(),&[128, 127, 128, 127])]
fn average_color_on_gradients_is_centerish(#[case] img: DynamicImage, #[case] expect: &[u8]) {
assert_eq!(img.average_color(), expect);
}
#[rstest]
#[case::rgba(DynamicImage::new_test_rgba(), Some((4usize, 3usize)))]
#[case::la(DynamicImage::new_test_greya(), Some((2usize, 1usize)))]
#[case::rgb(DynamicImage::new_test_rgb(), None)]
#[case::luma(DynamicImage::new_test_grey(), None)]
fn make_opaque_behaviour(
#[case] mut img: DynamicImage,
#[case] alpha_layout: Option<(usize, usize)>, ) {
let before = img.as_bytes().to_vec();
let has_alpha_before = img.has_alpha();
img.make_opaque().unwrap();
assert!(img.is_opaque());
let after = img.as_bytes();
if let Some((stride, aidx)) = alpha_layout {
assert!(has_alpha_before, "expected an alpha channel");
for (i, (&a, &b)) in after.iter().zip(before.iter()).enumerate() {
if i % stride == aidx {
assert_eq!(a, 255, "alpha not set to 255 at byte index {i}");
} else {
assert_eq!(a, b, "color byte changed at index {i}");
}
}
assert!(img.has_alpha());
} else {
assert!(!has_alpha_before, "did not expect an alpha channel");
assert_eq!(after, &before[..]);
assert!(img.is_opaque());
}
}
#[rstest]
#[case::la(DynamicImage::new_test_greya(), ECT::La8, true, ECT::L8, false)]
#[case::luma(DynamicImage::new_test_grey(), ECT::L8, false, ECT::L8, false)]
#[case::rgb(DynamicImage::new_test_rgb(), ECT::Rgb8, false, ECT::Rgb8, false)]
#[case::rgba(DynamicImage::new_test_rgba(), ECT::Rgba8, true, ECT::Rgb8, false)]
fn into_no_alpha_if_opaque_behaviour(
#[case] img: DynamicImage,
#[case] expect_type_nonopaque: ECT,
#[case] expect_has_alpha_nonopaque: bool,
#[case] expect_type_opaque: ECT,
#[case] expect_has_alpha_opaque: bool,
) {
let out1 = img.clone().into_no_alpha_if_opaque().unwrap();
assert_eq!(out1.extended_color_type(), expect_type_nonopaque);
assert_eq!(out1.has_alpha(), expect_has_alpha_nonopaque);
let mut opaque_img = img.clone();
opaque_img.make_opaque().unwrap();
let out2 = opaque_img.into_no_alpha_if_opaque().unwrap();
assert_eq!(out2.extended_color_type(), expect_type_opaque);
assert_eq!(out2.has_alpha(), expect_has_alpha_opaque);
}
#[rstest]
#[case::luma(DynamicImage::new_test_grey(), None)]
#[case::rgb(DynamicImage::new_test_rgb(), None)]
#[case::luma_a(DynamicImage::new_test_greya(), Some((2usize, 1usize)))] #[case::rgba(DynamicImage::new_test_rgba(), Some((4usize, 3usize)))] fn map_color_values_applies_fn_to_all_color_channels(
#[case] mut img: DynamicImage,
#[case] alpha_layout: Option<(usize, usize)>, ) {
let before = img.as_bytes().to_vec();
img.map_color_values(|_| 0);
let after = img.as_bytes();
match alpha_layout {
None => {
assert!(after.iter().all(|&b| b == 0));
}
Some((stride, aidx)) => {
for (i, (&a, &b)) in after.iter().zip(before.iter()).enumerate() {
if i % stride == aidx {
assert_eq!(a, b, "alpha channel changed at index {i}");
} else {
assert_eq!(a, 0, "color channel not zeroed at index {i}");
}
}
}
}
}
#[rstest]
#[case::black(Rgba([0, 0, 0, 255]))]
#[case::white(Rgba([255, 255, 255, 255]))]
fn into_flattened_blends_with_background_when_alpha_present(#[case] bg: Rgba<u8>) {
let rgba = DynamicImage::new_test_rgba();
let flat = rgba.clone().into_flattened(bg.to_rgb()).unwrap();
assert_eq!(flat.extended_color_type(), ECT::Rgb8);
assert!(!flat.has_alpha());
let (x, y) = (10u32, 0u32);
let p_src = rgba.get_pixel(x, y).0;
let p_dst = flat.get_pixel(x, y).0;
assert_eq!(&p_dst, &p_src);
let (x, y) = (20u32, 255u32);
let p_dst = flat.get_pixel(x, y).0;
assert_eq!(p_dst, bg.0);
}
#[rstest]
#[case::factor_1(1, (256, 256))]
#[case::factor_2(2, (128, 128))]
#[case::factor_4(4, (64, 64))]
fn get_scaled_down_reduces_dimensions(#[case] factor: u32, #[case] expect_dims: (u32, u32)) {
let img = DynamicImage::new_test_rgb();
let out = img.clone().into_scaled_down(factor).unwrap();
assert_eq!(out.dimensions(), expect_dims);
let out2 = img.scaled_down(factor).unwrap();
assert_eq!(out2.dimensions(), expect_dims);
}
#[test]
fn get_extract_returns_requested_size() {
let img = DynamicImage::new_test_rgb();
let out = img.extract(64.0, 64.0, 128.0, 128.0, 128, 128).unwrap();
assert_eq!(out.dimensions(), (128, 128));
assert_eq!(out.extended_color_type(), ECT::Rgb8);
}
#[test]
fn overlay_draws_top_over_bottom() {
let mut bottom = DynamicImage::from_fn(16, 16, |_x, _y| [0, 0, 0]);
let top = DynamicImage::from_fn(16, 16, |_x, _y| [255, 0, 0]);
bottom.overlay(&top).unwrap();
for &(x, y) in &[(0, 0), (8, 8), (15, 15)] {
assert_eq!(bottom.get_pixel(x, y).0, [255, 0, 0, 255]);
}
}
#[test]
fn overlay_additive_non_overlapping_halves() {
let mut base = DynamicImage::from_raw(1, 1, vec![100u8, 100, 100, 127]).unwrap();
let top = DynamicImage::from_raw(1, 1, vec![200u8, 200, 200, 128]).unwrap();
base.overlay_additive(&top).unwrap();
let px = base.get_pixel(0, 0).0;
assert_eq!(px[3], 255); assert_eq!(px[0], 150);
assert_eq!(px[1], 150);
assert_eq!(px[2], 150);
}
#[test]
fn overlay_additive_both_transparent() {
let mut base = DynamicImage::from_raw(1, 1, vec![100u8, 0, 0, 0]).unwrap();
let top = DynamicImage::from_raw(1, 1, vec![200u8, 0, 0, 0]).unwrap();
base.overlay_additive(&top).unwrap();
let px = base.get_pixel(0, 0).0;
assert_eq!(px[3], 0);
}
#[test]
fn overlay_additive_partial_alpha() {
let mut base = DynamicImage::from_raw(1, 1, vec![0u8, 0, 255, 64]).unwrap();
let top = DynamicImage::from_raw(1, 1, vec![255u8, 0, 0, 128]).unwrap();
base.overlay_additive(&top).unwrap();
let px = base.get_pixel(0, 0).0;
assert_eq!(px[3], 192);
assert_eq!(px[0], 170); assert_eq!(px[1], 0);
assert_eq!(px[2], 85); }
#[test]
fn overlay_additive_alpha_clamps_at_255() {
let mut base = DynamicImage::from_raw(1, 1, vec![100u8, 100, 100, 200]).unwrap();
let top = DynamicImage::from_raw(1, 1, vec![100u8, 100, 100, 200]).unwrap();
base.overlay_additive(&top).unwrap();
let px = base.get_pixel(0, 0).0;
assert_eq!(px[3], 255);
}
#[test]
fn overlay_additive_rgb_copies_top() {
let mut base = DynamicImage::from_fn(4, 4, |_x, _y| [0, 0, 0]);
let top = DynamicImage::from_fn(4, 4, |_x, _y| [255, 128, 64]);
base.overlay_additive(&top).unwrap();
let px = base.get_pixel(0, 0).0;
assert_eq!(px, [255, 128, 64, 255]);
}
#[test]
fn overlay_additive_rgba_base_rgb_top() {
let mut base = DynamicImage::from_raw(1, 1, vec![0u8, 0, 255, 100]).unwrap();
let top = DynamicImage::from_fn(1, 1, |_x, _y| [255, 0, 0]);
base.overlay_additive(&top).unwrap();
let px = base.get_pixel(0, 0).0;
assert_eq!(px[3], 255); assert_eq!(px[0], 255);
}
#[rstest]
#[case::rgba_rgb(&[0, 0, 255, 100], &[255, 0, 0], &[255, 0, 0])]
#[case::rgb_rgba(&[0, 0, 255], &[255, 0, 0, 100], &[100, 0, 155])]
#[case::rgba_rgba(&[0, 0, 255, 100], &[255, 0, 0, 100], &[128, 0, 128, 200])]
#[case::near_opaque_snaps_to_255(&[100, 100, 100, 125], &[100, 100, 100, 125], &[100, 100, 100, 255])]
#[case::below_threshold_stays(&[100, 100, 100, 124], &[100, 100, 100, 125], &[100, 100, 100, 249])]
fn overlay_additive(#[case] base_bytes: &[u8], #[case] top_bytes: &[u8], #[case] expect_color: &[u8]) {
let mut base = DynamicImage::from_raw(1, 1, base_bytes.to_vec()).unwrap();
let top = DynamicImage::from_raw(1, 1, top_bytes.to_vec()).unwrap();
base.overlay_additive(&top).unwrap();
let px = base.raw_pixel(0, 0);
assert_eq!(px, expect_color);
}
#[test]
fn overlay_additive_size_mismatch_errors() {
let mut base = DynamicImage::from_raw(2, 2, vec![0u8; 16]).unwrap();
let top = DynamicImage::from_raw(3, 3, vec![0u8; 36]).unwrap();
assert!(base.overlay_additive(&top).is_err());
}
#[test]
fn overlay_additive_unsupported_top_type_errors() {
let mut base = DynamicImage::from_raw(2, 2, vec![0u8; 16]).unwrap(); let top = DynamicImage::new_luma8(2, 2); assert!(base.overlay_additive(&top).is_err());
}
#[test]
fn overlay_additive_unsupported_base_type_errors() {
let mut base = DynamicImage::new_luma8(2, 2); let top = DynamicImage::from_raw(2, 2, vec![0u8; 16]).unwrap(); assert!(base.overlay_additive(&top).is_err());
}
}