// Copyright 2018-2025 the Deno authors. MIT license. use bytemuck::cast_slice; use bytemuck::cast_slice_mut; use image::ColorType; use image::DynamicImage; use image::GenericImageView; use image::ImageBuffer; use image::Luma; use image::LumaA; use image::Pixel; use image::Primitive; use image::Rgb; use image::Rgba; use lcms2::PixelFormat; use lcms2::Pod; use lcms2::Profile; use lcms2::Transform; use num_traits::NumCast; use num_traits::SaturatingMul; use crate::CanvasError; pub(crate) trait PremultiplyAlpha { fn premultiply_alpha(&self) -> Self; } impl PremultiplyAlpha for LumaA { fn premultiply_alpha(&self) -> Self { let max_t = T::DEFAULT_MAX_VALUE; let mut pixel = [self.0[0], self.0[1]]; let alpha_index = pixel.len() - 1; let alpha = pixel[alpha_index]; let normalized_alpha = alpha.to_f32().unwrap() / max_t.to_f32().unwrap(); if normalized_alpha == 0.0 { return LumaA([pixel[0], pixel[alpha_index]]); } for rgb in pixel.iter_mut().take(alpha_index) { *rgb = NumCast::from((rgb.to_f32().unwrap() * normalized_alpha).round()) .unwrap() } LumaA([pixel[0], pixel[alpha_index]]) } } impl PremultiplyAlpha for Rgba { fn premultiply_alpha(&self) -> Self { let max_t = T::DEFAULT_MAX_VALUE; let mut pixel = [self.0[0], self.0[1], self.0[2], self.0[3]]; let alpha_index = pixel.len() - 1; let alpha = pixel[alpha_index]; let normalized_alpha = alpha.to_f32().unwrap() / max_t.to_f32().unwrap(); if normalized_alpha == 0.0 { return Rgba([pixel[0], pixel[1], pixel[2], pixel[alpha_index]]); } for rgb in pixel.iter_mut().take(alpha_index) { *rgb = NumCast::from((rgb.to_f32().unwrap() * normalized_alpha).round()) .unwrap() } Rgba([pixel[0], pixel[1], pixel[2], pixel[alpha_index]]) } } fn process_premultiply_alpha(image: &I) -> ImageBuffer> where I: GenericImageView, P: Pixel + PremultiplyAlpha + 'static, S: Primitive + 'static, { let (width, height) = image.dimensions(); let mut out = ImageBuffer::new(width, height); for (x, y, pixel) in image.pixels() { let pixel = pixel.premultiply_alpha(); out.put_pixel(x, y, pixel); } out } /// Premultiply the alpha channel of the image. pub(crate) fn premultiply_alpha( image: DynamicImage, ) -> Result { match image { DynamicImage::ImageLumaA8(image) => { Ok(process_premultiply_alpha(&image).into()) } DynamicImage::ImageLumaA16(image) => { Ok(process_premultiply_alpha(&image).into()) } DynamicImage::ImageRgba8(image) => { Ok(process_premultiply_alpha(&image).into()) } DynamicImage::ImageRgba16(image) => { Ok(process_premultiply_alpha(&image).into()) } DynamicImage::ImageRgb32F(_) => { Err(CanvasError::UnsupportedColorType(image.color())) } DynamicImage::ImageRgba32F(_) => { Err(CanvasError::UnsupportedColorType(image.color())) } // If the image does not have an alpha channel, return the image as is. _ => Ok(image), } } pub(crate) trait UnpremultiplyAlpha { /// To determine if the image is premultiplied alpha, /// checking premultiplied RGBA value is one where any of the R/G/B channel values exceeds the alpha channel value.\ /// https://www.w3.org/TR/webgpu/#color-spaces fn is_premultiplied_alpha(&self) -> bool; fn unpremultiply_alpha(&self) -> Self; } impl UnpremultiplyAlpha for Rgba { fn is_premultiplied_alpha(&self) -> bool { let max_t = T::DEFAULT_MAX_VALUE; let pixel = [self.0[0], self.0[1], self.0[2]]; let alpha_index = self.0.len() - 1; let alpha = self.0[alpha_index]; match pixel.iter().max() { Some(rgb_max) => rgb_max < &max_t.saturating_mul(&alpha), // usually doesn't reach here None => false, } } fn unpremultiply_alpha(&self) -> Self { let max_t = T::DEFAULT_MAX_VALUE; let mut pixel = [self.0[0], self.0[1], self.0[2], self.0[3]]; let alpha_index = pixel.len() - 1; let alpha = pixel[alpha_index]; for rgb in pixel.iter_mut().take(alpha_index) { *rgb = NumCast::from( (rgb.to_f32().unwrap() / (alpha.to_f32().unwrap() / max_t.to_f32().unwrap())) .round(), ) .unwrap(); } Rgba([pixel[0], pixel[1], pixel[2], pixel[alpha_index]]) } } impl UnpremultiplyAlpha for LumaA { fn is_premultiplied_alpha(&self) -> bool { let max_t = T::DEFAULT_MAX_VALUE; let pixel = [self.0[0]]; let alpha_index = self.0.len() - 1; let alpha = self.0[alpha_index]; pixel[0] < max_t.saturating_mul(&alpha) } fn unpremultiply_alpha(&self) -> Self { let max_t = T::DEFAULT_MAX_VALUE; let mut pixel = [self.0[0], self.0[1]]; let alpha_index = pixel.len() - 1; let alpha = pixel[alpha_index]; for rgb in pixel.iter_mut().take(alpha_index) { *rgb = NumCast::from( (rgb.to_f32().unwrap() / (alpha.to_f32().unwrap() / max_t.to_f32().unwrap())) .round(), ) .unwrap(); } LumaA([pixel[0], pixel[alpha_index]]) } } fn is_premultiplied_alpha(image: &I) -> bool where I: GenericImageView, P: Pixel + UnpremultiplyAlpha + 'static, S: Primitive + 'static, { image .pixels() .any(|(_, _, pixel)| pixel.is_premultiplied_alpha()) } fn process_unpremultiply_alpha(image: &I) -> ImageBuffer> where I: GenericImageView, P: Pixel + UnpremultiplyAlpha + 'static, S: Primitive + 'static, { let (width, height) = image.dimensions(); let mut out = ImageBuffer::new(width, height); for (x, y, pixel) in image.pixels() { let pixel = pixel.unpremultiply_alpha(); out.put_pixel(x, y, pixel); } out } /// Invert the premultiplied alpha channel of the image. pub(crate) fn unpremultiply_alpha( image: DynamicImage, ) -> Result { match image { DynamicImage::ImageLumaA8(image) => Ok(if is_premultiplied_alpha(&image) { process_unpremultiply_alpha(&image).into() } else { image.into() }), DynamicImage::ImageLumaA16(image) => { Ok(if is_premultiplied_alpha(&image) { process_unpremultiply_alpha(&image).into() } else { image.into() }) } DynamicImage::ImageRgba8(image) => Ok(if is_premultiplied_alpha(&image) { process_unpremultiply_alpha(&image).into() } else { image.into() }), DynamicImage::ImageRgba16(image) => Ok(if is_premultiplied_alpha(&image) { process_unpremultiply_alpha(&image).into() } else { image.into() }), DynamicImage::ImageRgb32F(_) => { Err(CanvasError::UnsupportedColorType(image.color())) } DynamicImage::ImageRgba32F(_) => { Err(CanvasError::UnsupportedColorType(image.color())) } // If the image does not have an alpha channel, return the image as is. _ => Ok(image), } } pub(crate) trait SliceToPixel { fn slice_to_pixel(pixel: &[u8]) -> Self; } impl SliceToPixel for Luma { fn slice_to_pixel(pixel: &[u8]) -> Self { let pixel: &[T] = cast_slice(pixel); let pixel = [pixel[0]]; Luma(pixel) } } impl SliceToPixel for LumaA { fn slice_to_pixel(pixel: &[u8]) -> Self { let pixel: &[T] = cast_slice(pixel); let pixel = [pixel[0], pixel[1]]; LumaA(pixel) } } impl SliceToPixel for Rgb { fn slice_to_pixel(pixel: &[u8]) -> Self { let pixel: &[T] = cast_slice(pixel); let pixel = [pixel[0], pixel[1], pixel[2]]; Rgb(pixel) } } impl SliceToPixel for Rgba { fn slice_to_pixel(pixel: &[u8]) -> Self { let pixel: &[T] = cast_slice(pixel); let pixel = [pixel[0], pixel[1], pixel[2], pixel[3]]; Rgba(pixel) } } pub(crate) trait TransformColorProfile { fn transform_color_profile( &mut self, transformer: &Transform, ) -> P where P: Pixel + SliceToPixel + 'static, S: Primitive + 'static; } macro_rules! impl_transform_color_profile { ($type:ty) => { impl TransformColorProfile for $type { fn transform_color_profile( &mut self, transformer: &Transform, ) -> P where P: Pixel + SliceToPixel + 'static, S: Primitive + 'static, { let mut pixel = cast_slice_mut(self.0.as_mut_slice()); transformer.transform_in_place(&mut pixel); P::slice_to_pixel(&pixel) } } }; } impl_transform_color_profile!(Luma); impl_transform_color_profile!(Luma); impl_transform_color_profile!(LumaA); impl_transform_color_profile!(LumaA); impl_transform_color_profile!(Rgb); impl_transform_color_profile!(Rgb); impl_transform_color_profile!(Rgba); impl_transform_color_profile!(Rgba); fn process_icc_profile_conversion( image: &I, color: ColorType, input_icc_profile: Profile, output_icc_profile: Profile, ) -> Result>, CanvasError> where I: GenericImageView, P: Pixel + SliceToPixel + TransformColorProfile + 'static, S: Primitive + 'static, { let (width, height) = image.dimensions(); let mut out = ImageBuffer::new(width, height); let pixel_format = match color { ColorType::L8 => Ok(PixelFormat::GRAY_8), ColorType::L16 => Ok(PixelFormat::GRAY_16), ColorType::La8 => Ok(PixelFormat::GRAYA_8), ColorType::La16 => Ok(PixelFormat::GRAYA_16), ColorType::Rgb8 => Ok(PixelFormat::RGB_8), ColorType::Rgb16 => Ok(PixelFormat::RGB_16), ColorType::Rgba8 => Ok(PixelFormat::RGBA_8), ColorType::Rgba16 => Ok(PixelFormat::RGBA_16), _ => Err(CanvasError::UnsupportedColorType(color)), }?; let transformer = Transform::new( &input_icc_profile, pixel_format, &output_icc_profile, pixel_format, output_icc_profile.header_rendering_intent(), ) .map_err(CanvasError::Lcms)?; for (x, y, mut pixel) in image.pixels() { let pixel = pixel.transform_color_profile(&transformer); out.put_pixel(x, y, pixel); } Ok(out) } /// Convert the color space of the image from the ICC profile to sRGB. pub(crate) fn to_srgb_from_icc_profile( image: DynamicImage, icc_profile: Option>, ) -> Result { match icc_profile { // If there is no color profile information, return the image as is. None => Ok(image), Some(icc_profile) => match Profile::new_icc(&icc_profile) { // If the color profile information is invalid, return the image as is. Err(_) => Ok(image), Ok(icc_profile) => { let srgb_icc_profile = Profile::new_srgb(); let color = image.color(); match image { DynamicImage::ImageLuma8(image) => Ok( process_icc_profile_conversion( &image, color, icc_profile, srgb_icc_profile, )? .into(), ), DynamicImage::ImageLuma16(image) => Ok( process_icc_profile_conversion( &image, color, icc_profile, srgb_icc_profile, )? .into(), ), DynamicImage::ImageLumaA8(image) => Ok( process_icc_profile_conversion( &image, color, icc_profile, srgb_icc_profile, )? .into(), ), DynamicImage::ImageLumaA16(image) => Ok( process_icc_profile_conversion( &image, color, icc_profile, srgb_icc_profile, )? .into(), ), DynamicImage::ImageRgb8(image) => Ok( process_icc_profile_conversion( &image, color, icc_profile, srgb_icc_profile, )? .into(), ), DynamicImage::ImageRgb16(image) => Ok( process_icc_profile_conversion( &image, color, icc_profile, srgb_icc_profile, )? .into(), ), DynamicImage::ImageRgba8(image) => Ok( process_icc_profile_conversion( &image, color, icc_profile, srgb_icc_profile, )? .into(), ), DynamicImage::ImageRgba16(image) => Ok( process_icc_profile_conversion( &image, color, icc_profile, srgb_icc_profile, )? .into(), ), DynamicImage::ImageRgb32F(_) => { Err(CanvasError::UnsupportedColorType(image.color())) } DynamicImage::ImageRgba32F(_) => { Err(CanvasError::UnsupportedColorType(image.color())) } _ => Err(CanvasError::UnsupportedColorType(image.color())), } } }, } } /// Create an image buffer from raw bytes. fn process_image_buffer_from_raw_bytes( width: u32, height: u32, buffer: &[u8], bytes_per_pixel: usize, ) -> ImageBuffer> where P: Pixel + SliceToPixel + 'static, S: Primitive + 'static, { let mut out = ImageBuffer::new(width, height); for (index, buffer) in buffer.chunks_exact(bytes_per_pixel).enumerate() { let pixel = P::slice_to_pixel(buffer); out.put_pixel(index as u32, index as u32, pixel); } out } pub(crate) fn create_image_from_raw_bytes( width: u32, height: u32, buffer: &[u8], ) -> Result { let total_pixels = (width * height) as usize; // avoid to divide by zero let bytes_per_pixel = buffer .len() .checked_div(total_pixels) .ok_or(CanvasError::InvalidSizeZero(width, height))?; // convert from a bytes per pixel to the color type of the image // https://github.com/image-rs/image/blob/2c986d353333d2604f0c3f1fcef262cc763c0001/src/color.rs#L38-L49 match bytes_per_pixel { 1 => Ok(DynamicImage::ImageLuma8( process_image_buffer_from_raw_bytes( width, height, buffer, bytes_per_pixel, ), )), 2 => Ok( // NOTE: ImageLumaA8 is also the same bytes per pixel. DynamicImage::ImageLuma16(process_image_buffer_from_raw_bytes( width, height, buffer, bytes_per_pixel, )), ), 3 => Ok(DynamicImage::ImageRgb8( process_image_buffer_from_raw_bytes( width, height, buffer, bytes_per_pixel, ), )), 4 => Ok( // NOTE: ImageLumaA16 is also the same bytes per pixel. DynamicImage::ImageRgba8(process_image_buffer_from_raw_bytes( width, height, buffer, bytes_per_pixel, )), ), 6 => Ok(DynamicImage::ImageRgb16( process_image_buffer_from_raw_bytes( width, height, buffer, bytes_per_pixel, ), )), 8 => Ok(DynamicImage::ImageRgba16( process_image_buffer_from_raw_bytes( width, height, buffer, bytes_per_pixel, ), )), 12 => Err(CanvasError::UnsupportedColorType(ColorType::Rgb32F)), 16 => Err(CanvasError::UnsupportedColorType(ColorType::Rgba32F)), _ => Err(CanvasError::UnsupportedColorType(ColorType::L8)), } } #[cfg(test)] mod tests { use image::Rgba; use super::*; #[test] fn test_premultiply_alpha() { let rgba = Rgba::([255, 128, 0, 128]); let rgba = rgba.premultiply_alpha(); assert_eq!(rgba, Rgba::([128, 64, 0, 128])); let rgba = Rgba::([255, 255, 255, 255]); let rgba = rgba.premultiply_alpha(); assert_eq!(rgba, Rgba::([255, 255, 255, 255])); } #[test] fn test_unpremultiply_alpha() { let rgba = Rgba::([127, 0, 0, 127]); let rgba = rgba.unpremultiply_alpha(); assert_eq!(rgba, Rgba::([255, 0, 0, 127])); } #[test] fn test_process_image_buffer_from_raw_bytes() { let buffer = &[255, 255, 0, 0, 0, 0, 255, 255]; let color = ColorType::Rgba16; let bytes_per_pixel = color.bytes_per_pixel() as usize; let image = DynamicImage::ImageRgba16(process_image_buffer_from_raw_bytes( 1, 1, buffer, bytes_per_pixel, )) .to_rgba16(); assert_eq!(image.get_pixel(0, 0), &Rgba::([65535, 0, 0, 65535])); } }