From 5c5c0f3049d2b69b9363b1874256a9568367ceac Mon Sep 17 00:00:00 2001 From: "A. Molzer" <5550310+197g@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:35:51 +0200 Subject: [PATCH 1/2] Add alpha mask utilities to ImageBuffer The respective methods on `DynamicImage` are blocked on the type representation. We can't use any specific pixel type if the goal is preserving the type but `DynamicImage` has no Luma32F variant. For applying a mask we would have to take the parameter type as a non-`DynamicImage` somehow? Feels weird, we should just resolve to add at least 32-bit Luma to the crate, even if we do not add 32-bit LumaAlpha. --- src/error.rs | 8 +++++ src/images/buffer.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/error.rs b/src/error.rs index 928f83f7f3..f60a51af92 100644 --- a/src/error.rs +++ b/src/error.rs @@ -139,6 +139,8 @@ pub enum ParameterErrorKind { /// The cicp that was found. found: Cicp, }, + /// The operation is only applicable to pixels with an alpha channel. + NoAlphaChannel, } /// An error was encountered while decoding an image. @@ -450,6 +452,12 @@ impl fmt::Display for ParameterError { "The color space {found:?} does not match the expected {expected:?}", ) } + ParameterErrorKind::NoAlphaChannel => { + write!( + fmt, + "The operation requires an alpha channel but the pixel type does not have one", + ) + } }?; if let Some(underlying) = &self.underlying { diff --git a/src/images/buffer.rs b/src/images/buffer.rs index 6227a0ef25..91137f9832 100644 --- a/src/images/buffer.rs +++ b/src/images/buffer.rs @@ -888,6 +888,25 @@ where color_hint: None, // TODO: the pixel type might contain P::COLOR_TYPE if it satisfies PixelWithColorType } } + + /// Extract the alpha channel as a Luma image. + /// + /// If the pixel does not have an alpha channel, the value is filled with a fully opaque mask + /// using the maximum value of the corresponding subpixel type. + pub fn to_alpha_mask(&self) -> ImageBuffer, Vec> { + let pixels = self.inner_pixels().chunks_exact(P::CHANNEL_COUNT.into()); + let mut mask = vec![::DEFAULT_MAX_VALUE; pixels.len()]; + + if P::HAS_ALPHA { + for (p, alpha) in pixels.zip(mask.iter_mut()) { + // If the pixel has an alpha channel, use it. + *alpha = *p.last().unwrap(); + } + } + + ImageBuffer::from_vec(self.width, self.height, mask) + .expect("used the right pixel and channel count") + } } impl ImageBuffer @@ -989,6 +1008,42 @@ where pub fn put_pixel(&mut self, x: u32, y: u32, pixel: P) { *self.get_pixel_mut(x, y) = pixel; } + + /// Fill the alpha channel of this image from a Luma mask. + /// + /// Returns an [`ImageError::Parameter`] if the mask dimensions do not match the image + /// dimensions. Otherwise, if the pixel type does not have an alpha channel this is a no-op. + pub fn apply_alpha_mask( + &mut self, + mask: &ImageBuffer, RhsContainer>, + ) -> ImageResult<()> + where + RhsContainer: Deref, + { + if (self.width, self.height) != (mask.width(), mask.height()) { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::DimensionMismatch, + ))); + } + + if !P::HAS_ALPHA { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::NoAlphaChannel, + ))); + } + + let pixels = self + .inner_pixels_mut() + .chunks_exact_mut(P::CHANNEL_COUNT.into()); + + let mask = mask.inner_pixels(); + for (p, alpha) in pixels.zip(mask.iter()) { + // If the pixel has an alpha channel, use it. + *p.last_mut().unwrap() = *alpha; + } + + Ok(()) + } } impl ImageBuffer { @@ -2114,6 +2169,27 @@ mod test { let result = target.copy_from_color_space(&source, options); assert!(matches!(result, Err(crate::ImageError::Parameter(_)))); } + + #[test] + fn alpha_mask_of_gray() { + let image: GrayImage = ImageBuffer::new(4, 4); + let mask = image.to_alpha_mask(); + assert_eq!(mask.as_raw(), &[255; 16]); + } + + #[test] + #[rustfmt::skip] + fn alpha_mask_extraction() { + let image: ImageBuffer, _> = ImageBuffer::from_raw(4, 4, vec![ + 0, 1, 0, 2, 0, 3, 0, 4, + 0, 5, 0, 6, 0, 7, 0, 8, + 0, 9, 0, 10, 0, 11, 0, 12, + 0, 13, 0, 14, 0, 15, 0, 16, + ]).unwrap(); + + let mask = image.to_alpha_mask(); + assert_eq!(mask.as_raw(), &(1u8..17).collect::>()); + } } #[cfg(test)] From ebcd84b5369fa82587784fd66b76ce83850b3785 Mon Sep 17 00:00:00 2001 From: Aurelia Molzer <5550310+197g@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:27:10 +0200 Subject: [PATCH 2/2] Rename modification to apply_alpha_channel Also adds a test suite of the method for different failure mechanisms, pixel types and size mismatches. --- src/images/buffer.rs | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/images/buffer.rs b/src/images/buffer.rs index 91137f9832..d79f1a7175 100644 --- a/src/images/buffer.rs +++ b/src/images/buffer.rs @@ -1013,7 +1013,7 @@ where /// /// Returns an [`ImageError::Parameter`] if the mask dimensions do not match the image /// dimensions. Otherwise, if the pixel type does not have an alpha channel this is a no-op. - pub fn apply_alpha_mask( + pub fn apply_alpha_channel( &mut self, mask: &ImageBuffer, RhsContainer>, ) -> ImageResult<()> @@ -2190,6 +2190,44 @@ mod test { let mask = image.to_alpha_mask(); assert_eq!(mask.as_raw(), &(1u8..17).collect::>()); } + + #[test] + fn apply_alpha_mask() { + let mut image: ImageBuffer, _> = ImageBuffer::new(4, 4); + + let alpha = ImageBuffer::from_pixel(4, 4, Luma([255])); + image.apply_alpha_channel(&alpha).expect("can apply"); + + for pixel in image.pixels() { + assert_eq!(pixel.0, [0, 255]); + } + } + + #[test] + fn apply_alpha_mask_rgb() { + let mut image: ImageBuffer, _> = ImageBuffer::new(4, 4); + + let alpha = ImageBuffer::from_pixel(4, 4, Luma([255])); + image.apply_alpha_channel(&alpha).expect("can apply"); + + for pixel in image.pixels() { + assert_eq!(pixel.0, [0, 0, 0, 255]); + } + } + + #[test] + fn can_not_apply_alpha_mask() { + ImageBuffer::, _>::new(4, 4) + .apply_alpha_channel(&ImageBuffer::new(1, 1)) + .expect_err("can not apply with wrong dimensions"); + + ImageBuffer::, _>::new(4, 4) + .apply_alpha_channel(&ImageBuffer::new(4, 4)) + .expect_err("can not apply without alpha channel"); + ImageBuffer::, _>::new(4, 4) + .apply_alpha_channel(&ImageBuffer::new(4, 4)) + .expect_err("can not apply without alpha channel"); + } } #[cfg(test)]