diff --git a/src/codecs/bmp/encoder.rs b/src/codecs/bmp/encoder.rs index 31264a852d..dac056d709 100644 --- a/src/codecs/bmp/encoder.rs +++ b/src/codecs/bmp/encoder.rs @@ -5,7 +5,7 @@ use crate::error::{ EncodingError, ImageError, ImageFormatHint, ImageResult, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; -use crate::{DynamicImage, ExtendedColorType, ImageEncoder, ImageFormat}; +use crate::{ExtendedColorType, ImageEncoder, ImageFormat}; const BITMAPFILEHEADER_SIZE: u32 = 14; const BITMAPINFOHEADER_SIZE: u32 = 40; @@ -366,12 +366,9 @@ impl ImageEncoder for BmpEncoder { self.encode(buf, width, height, color_type) } - fn make_compatible_img( - &self, - _: crate::io::encoder::MethodSealedToImage, - img: &DynamicImage, - ) -> Option { - crate::io::encoder::dynimage_conversion_8bit(img) + fn supported_colors(&self) -> Option<&[ExtendedColorType]> { + use ExtendedColorType::*; + Some(&[Rgb8, Rgba8, L1, L8, La8]) } } diff --git a/src/codecs/jpeg/encoder.rs b/src/codecs/jpeg/encoder.rs index 30dab266ef..db1b51a84b 100644 --- a/src/codecs/jpeg/encoder.rs +++ b/src/codecs/jpeg/encoder.rs @@ -5,7 +5,7 @@ use std::{error, fmt}; use crate::error::{ EncodingError, ImageError, ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind, }; -use crate::{ColorType, DynamicImage, ExtendedColorType, ImageEncoder, ImageFormat}; +use crate::{ExtendedColorType, ImageEncoder, ImageFormat}; use jpeg_encoder::Encoder; @@ -293,17 +293,9 @@ impl ImageEncoder for JpegEncoder { Ok(()) } - fn make_compatible_img( - &self, - _: crate::io::encoder::MethodSealedToImage, - img: &DynamicImage, - ) -> Option { - use ColorType::*; - match img.color() { - L8 | Rgb8 => None, - La8 | L16 | L32F | La16 | La32F => Some(img.to_luma8().into()), - Rgba8 | Rgb16 | Rgb32F | Rgba16 | Rgba32F => Some(img.to_rgb8().into()), - } + fn supported_colors(&self) -> Option<&[ExtendedColorType]> { + use ExtendedColorType::*; + Some(&[Rgb8, L8]) } } diff --git a/src/codecs/png.rs b/src/codecs/png.rs index 4f22485044..de72fab839 100644 --- a/src/codecs/png.rs +++ b/src/codecs/png.rs @@ -945,19 +945,9 @@ impl ImageEncoder for PngEncoder { Ok(()) } - fn make_compatible_img( - &self, - _: crate::io::encoder::MethodSealedToImage, - img: &DynamicImage, - ) -> Option { - use ColorType::*; - match img.color() { - L32F => Some(img.to_luma16().into()), - La32F => Some(img.to_luma_alpha16().into()), - Rgb32F => Some(img.to_rgb16().into()), - Rgba32F => Some(img.to_rgba16().into()), - L8 | La8 | Rgb8 | Rgba8 | L16 | La16 | Rgb16 | Rgba16 => None, - } + fn supported_colors(&self) -> Option<&[ExtendedColorType]> { + use ExtendedColorType::*; + Some(&[Rgb8, Rgba8, L8, La8, Rgb16, Rgba16, L16, La16]) } } diff --git a/src/codecs/tga/encoder.rs b/src/codecs/tga/encoder.rs index 6a65d164cb..0a033f69ca 100644 --- a/src/codecs/tga/encoder.rs +++ b/src/codecs/tga/encoder.rs @@ -1,6 +1,6 @@ use super::header::Header; use crate::{codecs::tga::header::ImageType, error::EncodingError, utils::vec_try_with_capacity}; -use crate::{DynamicImage, ExtendedColorType, ImageEncoder, ImageError, ImageFormat, ImageResult}; +use crate::{ExtendedColorType, ImageEncoder, ImageError, ImageFormat, ImageResult}; use std::{error, fmt, io::Write}; /// Errors that can occur during encoding and saving of a TGA image. @@ -253,12 +253,9 @@ impl ImageEncoder for TgaEncoder { self.encode(buf, width, height, color_type) } - fn make_compatible_img( - &self, - _: crate::io::encoder::MethodSealedToImage, - img: &DynamicImage, - ) -> Option { - crate::io::encoder::dynimage_conversion_8bit(img) + fn supported_colors(&self) -> Option<&[ExtendedColorType]> { + use ExtendedColorType::*; + Some(&[Rgb8, Rgba8, L8, La8]) } } diff --git a/src/codecs/webp/encoder.rs b/src/codecs/webp/encoder.rs index 70423d8406..e503b06d2f 100644 --- a/src/codecs/webp/encoder.rs +++ b/src/codecs/webp/encoder.rs @@ -3,7 +3,7 @@ use std::io::Write; use crate::error::{EncodingError, UnsupportedError, UnsupportedErrorKind}; -use crate::{DynamicImage, ExtendedColorType, ImageEncoder, ImageError, ImageFormat, ImageResult}; +use crate::{ExtendedColorType, ImageEncoder, ImageError, ImageFormat, ImageResult}; /// WebP Encoder. /// @@ -110,12 +110,9 @@ impl ImageEncoder for WebPEncoder { Ok(()) } - fn make_compatible_img( - &self, - _: crate::io::encoder::MethodSealedToImage, - img: &DynamicImage, - ) -> Option { - crate::io::encoder::dynimage_conversion_8bit(img) + fn supported_colors(&self) -> Option<&[ExtendedColorType]> { + use ExtendedColorType::*; + Some(&[Rgb8, Rgba8, L8, La8]) } } diff --git a/src/images/buffer.rs b/src/images/buffer.rs index 24a6631989..e53adcf6db 100644 --- a/src/images/buffer.rs +++ b/src/images/buffer.rs @@ -1664,6 +1664,30 @@ where } buffer } + + pub(crate) fn convert_precision( + &self, + ) -> ImageBuffer> + where + ToType::Subpixel: FromPrimitive, + { + assert_eq!(P::CHANNEL_COUNT, ToType::CHANNEL_COUNT); + assert_eq!(P::COLOR_MODEL, ToType::COLOR_MODEL); + + // outlined inner function to avoid monomorphization bloat + fn inner(buffer: &[From]) -> Vec + where + From: Copy, + To: FromPrimitive, + { + buffer.iter().copied().map(To::from_primitive).collect() + } + + let data = inner(self.subpixels()); + let mut buffer = ImageBuffer::from_raw(self.width, self.height, data).unwrap(); + buffer.copy_color_space_from(self); + buffer + } } /// Inputs to [`ImageBuffer::copy_from_color_space`]. diff --git a/src/images/dynimage.rs b/src/images/dynimage.rs index 862b999550..d4a8140454 100644 --- a/src/images/dynimage.rs +++ b/src/images/dynimage.rs @@ -1076,6 +1076,52 @@ impl DynamicImage { } } + pub(crate) fn to_u8(&self) -> DynamicImage { + use DynamicImage::*; + match self { + ImageLuma8(_) | ImageLumaA8(_) | ImageRgb8(_) | ImageRgba8(_) => self.clone(), + + ImageLuma16(buffer) => ImageLuma8(buffer.convert_precision()), + ImageLumaA16(buffer) => ImageLumaA8(buffer.convert_precision()), + ImageRgb16(buffer) => ImageRgb8(buffer.convert_precision()), + ImageRgba16(buffer) => ImageRgba8(buffer.convert_precision()), + ImageLuma32F(buffer) => ImageLuma8(buffer.convert_precision()), + ImageLumaA32F(buffer) => ImageLumaA8(buffer.convert_precision()), + ImageRgb32F(buffer) => ImageRgb8(buffer.convert_precision()), + ImageRgba32F(buffer) => ImageRgba8(buffer.convert_precision()), + } + } + pub(crate) fn to_u16(&self) -> DynamicImage { + use DynamicImage::*; + match self { + ImageLuma16(_) | ImageLumaA16(_) | ImageRgb16(_) | ImageRgba16(_) => self.clone(), + + ImageLuma8(buffer) => ImageLuma16(buffer.convert_precision()), + ImageLumaA8(buffer) => ImageLumaA16(buffer.convert_precision()), + ImageRgb8(buffer) => ImageRgb16(buffer.convert_precision()), + ImageRgba8(buffer) => ImageRgba16(buffer.convert_precision()), + ImageLuma32F(buffer) => ImageLuma16(buffer.convert_precision()), + ImageLumaA32F(buffer) => ImageLumaA16(buffer.convert_precision()), + ImageRgb32F(buffer) => ImageRgb16(buffer.convert_precision()), + ImageRgba32F(buffer) => ImageRgba16(buffer.convert_precision()), + } + } + pub(crate) fn to_f32(&self) -> DynamicImage { + use DynamicImage::*; + match self { + ImageLuma32F(_) | ImageLumaA32F(_) | ImageRgb32F(_) | ImageRgba32F(_) => self.clone(), + + ImageLuma8(buffer) => ImageLuma32F(buffer.convert_precision()), + ImageLumaA8(buffer) => ImageLumaA32F(buffer.convert_precision()), + ImageRgb8(buffer) => ImageRgb32F(buffer.convert_precision()), + ImageRgba8(buffer) => ImageRgba32F(buffer.convert_precision()), + ImageLuma16(buffer) => ImageLuma32F(buffer.convert_precision()), + ImageLumaA16(buffer) => ImageLumaA32F(buffer.convert_precision()), + ImageRgb16(buffer) => ImageRgb32F(buffer.convert_precision()), + ImageRgba16(buffer) => ImageRgba32F(buffer.convert_precision()), + } + } + /// Return a grayscale version of this image. /// Returns either a `Luma` or `LumaA` image. #[must_use] @@ -1599,7 +1645,7 @@ impl DynamicImage { &self, encoder: Box, ) -> ImageResult<()> { - let converted = encoder.make_compatible_img(crate::io::encoder::MethodSealedToImage, self); + let converted = crate::io::encoder::make_compatible_img(self, encoder.supported_colors()); let img = converted.as_ref().unwrap_or(self); encoder.write_image( @@ -1615,11 +1661,7 @@ impl DynamicImage { /// Assumes the writer is buffered. In most cases, you should wrap your writer in a `BufWriter` /// for best performance. /// - /// ## Color Conversion - /// - /// Unlike other encoding methods in this crate, methods on `DynamicImage` try to automatically - /// convert the image to some color type supported by the encoder. This may result in a loss of - /// precision or the removal of the alpha channel. + /// For information about possible color conversions, see [`DynamicImage::save`]. pub fn write_to(&self, mut w: W, format: ImageFormat) -> ImageResult<()> { let encoder = encoder_for_format(format, &mut w)?; self.write_with_encoder_impl(encoder) @@ -1627,11 +1669,7 @@ impl DynamicImage { /// Encode this image with the provided encoder. /// - /// ## Color Conversion - /// - /// Unlike other encoding methods in this crate, methods on `DynamicImage` try to automatically - /// convert the image to some color type supported by the encoder. This may result in a loss of - /// precision or the removal of the alpha channel. + /// For information about possible color conversions, see [`DynamicImage::save`]. pub fn write_with_encoder(&self, encoder: impl ImageEncoder) -> ImageResult<()> { self.write_with_encoder_impl(Box::new(encoder)) } @@ -1641,8 +1679,10 @@ impl DynamicImage { /// ## Color Conversion /// /// Unlike other encoding methods in this crate, methods on `DynamicImage` try to automatically - /// convert the image to some color type supported by the encoder. This may result in a loss of - /// precision or the removal of the alpha channel. + /// convert the image to some color type supported by the underlying encoder. This may result in a loss of + /// precision and/or the removal of the alpha channel. + /// + /// Conversions are not guaranteed to happen and may change in the future. pub fn save(&self, path: Q) -> ImageResult<()> where Q: AsRef, @@ -1653,11 +1693,7 @@ impl DynamicImage { /// Saves the buffer to a file with the specified format. /// - /// ## Color Conversion - /// - /// Unlike other encoding methods in this crate, methods on `DynamicImage` try to automatically - /// convert the image to some color type supported by the encoder. This may result in a loss of - /// precision or the removal of the alpha channel. + /// For information about possible color conversions, see [`DynamicImage::save`]. pub fn save_with_format(&self, path: Q, format: ImageFormat) -> ImageResult<()> where Q: AsRef, diff --git a/src/io/encoder.rs b/src/io/encoder.rs index f520f7293e..c7fb95ac9a 100644 --- a/src/io/encoder.rs +++ b/src/io/encoder.rs @@ -1,26 +1,8 @@ +use std::borrow::Cow; + use crate::error::{ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind}; use crate::{ColorType, DynamicImage, ExtendedColorType}; -/// Nominally public but DO NOT expose this type. -/// -/// To be somewhat sure here's a compile fail test: -/// -/// ```compile_fail -/// use image::MethodSealedToImage; -/// ``` -/// -/// ```compile_fail -/// use image::io::MethodSealedToImage; -/// ``` -/// -/// The same implementation strategy for a partially public trait is used in the standard library, -/// for the different effect of forbidding `Error::type_id` overrides thus making them reliable for -/// their calls through the `dyn` version of the trait. -/// -/// Read more: -#[derive(Clone, Copy)] -pub struct MethodSealedToImage; - /// The trait all encoders implement pub trait ImageEncoder { /// Writes all the bytes in an image to the encoder. @@ -100,17 +82,19 @@ pub trait ImageEncoder { )) } - /// Convert the image to a compatible format for the encoder. This is used by the encoding - /// methods on `DynamicImage`. - /// - /// Note that this is method is sealed to the crate and effectively pub(crate) due to the - /// argument type not being nameable. - #[doc(hidden)] - fn make_compatible_img( - &self, - _: MethodSealedToImage, - _input: &DynamicImage, - ) -> Option { + /// All color types supported by this encoder. If `None`, supported colors aren't known. + /// + /// Encoders typically only support a select few color types for writing, and supported ones + /// vary from encoder to encoder. This method allows encoders to specify which color types + /// their [`write_image`](Self::write_image) method supports. + /// + /// If `Some` list is returned, it must not be empty and must not contain duplicates. + /// + /// # Notes + /// + /// One of the use cases for the information returned by this method is to enable automatic + /// color conversion when saving images. See [`DynamicImage::save`] for more information. + fn supported_colors(&self) -> Option<&[ExtendedColorType]> { None } } @@ -136,17 +120,219 @@ impl ImageEncoderBoxed for T { } } -/// Implement `dynimage_conversion_sequence` for the common case of supporting only 8-bit colors -/// (with and without alpha). -#[allow(unused)] -pub(crate) fn dynimage_conversion_8bit(img: &DynamicImage) -> Option { +pub(crate) fn make_compatible_img( + img: &DynamicImage, + supported: Option<&[ExtendedColorType]>, +) -> Option { + let color = img.color(); + let to = to_supported_color(color, supported?)?; + if to == color { + // no conversion necessary + return None; + } + + let color = decompose_color_type(color); + let to = decompose_color_type(to); + + if color.has_color != to.has_color { + // We don't want to convert RGB <-> Luma, because it's not clear how + // this conversion should treat the color space information. + return None; + } + + // add or remove alpha as necessary + let img = if to.has_alpha != color.has_alpha { + Cow::Owned(toggle_alpha(img)) + } else { + Cow::Borrowed(img) + }; + + // adjust precision as necessary + let img = if to.precision != color.precision { + Cow::Owned(match to.precision { + Precision::U8 => img.to_u8(), + Precision::U16 => img.to_u16(), + Precision::F32 => img.to_f32(), + }) + } else { + img + }; + + match img { + Cow::Borrowed(_) => None, + Cow::Owned(img) => Some(img), + } +} +fn to_supported_color(from: ColorType, supported: &[ExtendedColorType]) -> Option { + let from = decompose_color_type(from); + + supported + .iter() + .filter_map(|c| c.color_type()) + .min_by_key(|&to| { + let to = decompose_color_type(to); + let mut loss = 0; + + // channel losses are heavily penalized, since a lot of information is lost + // channel gains are penalized, since they are inefficient and don't add any information + const ALPHA_LOST: u16 = 100; + const ALPHA_GAIN: u16 = 3; + const COLOR_LOST: u16 = 200; + const COLOR_GAIN: u16 = 6; + + match (from.has_alpha, to.has_alpha) { + (true, false) => loss += ALPHA_LOST, + (false, true) => loss += ALPHA_GAIN, + _ => {} + } + match (from.has_color, to.has_color) { + (true, false) => loss += COLOR_LOST, + (false, true) => loss += COLOR_GAIN, + _ => {} + } + + const PRECISION_LOST: u16 = 10; + const PRECISION_GAIN: u16 = 1; + match (to.precision as i16) - (from.precision as i16) { + m @ 1.. => loss += PRECISION_LOST * m as u16, + m @ ..=-1 => loss += PRECISION_GAIN * m.unsigned_abs(), + 0 => {} + } + + loss + }) +} + +/// If the image has an alpha channel, remove it. Otherwise, add an alpha channel with full opacity. +fn toggle_alpha(image: &DynamicImage) -> DynamicImage { + match image { + // no alpha => add it + DynamicImage::ImageLuma8(buffer) => DynamicImage::ImageLumaA8(buffer.convert()), + DynamicImage::ImageRgb8(buffer) => DynamicImage::ImageRgba8(buffer.convert()), + DynamicImage::ImageLuma16(buffer) => DynamicImage::ImageLumaA16(buffer.convert()), + DynamicImage::ImageRgb16(buffer) => DynamicImage::ImageRgba16(buffer.convert()), + DynamicImage::ImageLuma32F(buffer) => DynamicImage::ImageLumaA32F(buffer.convert()), + DynamicImage::ImageRgb32F(buffer) => DynamicImage::ImageRgba32F(buffer.convert()), + // alpha => remove it + DynamicImage::ImageLumaA8(buffer) => DynamicImage::ImageLuma8(buffer.convert()), + DynamicImage::ImageRgba8(buffer) => DynamicImage::ImageRgb8(buffer.convert()), + DynamicImage::ImageLumaA16(buffer) => DynamicImage::ImageLuma16(buffer.convert()), + DynamicImage::ImageRgba16(buffer) => DynamicImage::ImageRgb16(buffer.convert()), + DynamicImage::ImageLumaA32F(buffer) => DynamicImage::ImageLuma32F(buffer.convert()), + DynamicImage::ImageRgba32F(buffer) => DynamicImage::ImageRgb32F(buffer.convert()), + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum Precision { + U8 = 0, + U16 = 1, + F32 = 2, +} +struct ColorDesc { + /// RGB if true, Luma if false + has_color: bool, + /// Alpha if true, no alpha if false + has_alpha: bool, + precision: Precision, +} +fn decompose_color_type(color: ColorType) -> ColorDesc { use ColorType::*; + ColorDesc { + has_color: color.has_color(), + has_alpha: color.has_alpha(), + precision: match color { + L8 | La8 | Rgb8 | Rgba8 => Precision::U8, + L16 | La16 | Rgb16 | Rgba16 => Precision::U16, + L32F | La32F | Rgb32F | Rgba32F => Precision::F32, + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_conversion_png() { + let png_supported_colors = &[ + ExtendedColorType::Rgb8, + ExtendedColorType::Rgba8, + ExtendedColorType::L8, + ExtendedColorType::La8, + ExtendedColorType::Rgb16, + ExtendedColorType::Rgba16, + ExtendedColorType::L16, + ExtendedColorType::La16, + ]; + let to = |from| to_supported_color(from, png_supported_colors).unwrap_or(from); + + assert_eq!(to(ColorType::L8), ColorType::L8); + assert_eq!(to(ColorType::La8), ColorType::La8); + assert_eq!(to(ColorType::Rgb8), ColorType::Rgb8); + assert_eq!(to(ColorType::Rgba8), ColorType::Rgba8); + assert_eq!(to(ColorType::L16), ColorType::L16); + assert_eq!(to(ColorType::La16), ColorType::La16); + assert_eq!(to(ColorType::Rgb16), ColorType::Rgb16); + assert_eq!(to(ColorType::Rgba16), ColorType::Rgba16); + assert_eq!(to(ColorType::Rgb32F), ColorType::Rgb16); + assert_eq!(to(ColorType::Rgba32F), ColorType::Rgba16); + } + + #[test] + fn test_conversion_jpeg() { + let jpeg_supported_colors = &[ExtendedColorType::Rgb8, ExtendedColorType::L8]; + let to = |from| to_supported_color(from, jpeg_supported_colors).unwrap_or(from); + + assert_eq!(to(ColorType::L8), ColorType::L8); + assert_eq!(to(ColorType::La8), ColorType::L8); + assert_eq!(to(ColorType::Rgb8), ColorType::Rgb8); + assert_eq!(to(ColorType::Rgba8), ColorType::Rgb8); + assert_eq!(to(ColorType::L16), ColorType::L8); + assert_eq!(to(ColorType::La16), ColorType::L8); + assert_eq!(to(ColorType::Rgb16), ColorType::Rgb8); + assert_eq!(to(ColorType::Rgba16), ColorType::Rgb8); + assert_eq!(to(ColorType::Rgb32F), ColorType::Rgb8); + assert_eq!(to(ColorType::Rgba32F), ColorType::Rgb8); + } + + #[test] + fn test_conversion_bmp() { + let bmp_supported_colors = &[ + ExtendedColorType::Rgb8, + ExtendedColorType::Rgba8, + ExtendedColorType::L1, + ExtendedColorType::L8, + ExtendedColorType::La8, + ]; + let to = |from| to_supported_color(from, bmp_supported_colors).unwrap_or(from); + + assert_eq!(to(ColorType::L8), ColorType::L8); + assert_eq!(to(ColorType::La8), ColorType::La8); + assert_eq!(to(ColorType::Rgb8), ColorType::Rgb8); + assert_eq!(to(ColorType::Rgba8), ColorType::Rgba8); + assert_eq!(to(ColorType::L16), ColorType::L8); + assert_eq!(to(ColorType::La16), ColorType::La8); + assert_eq!(to(ColorType::Rgb16), ColorType::Rgb8); + assert_eq!(to(ColorType::Rgba16), ColorType::Rgba8); + assert_eq!(to(ColorType::Rgb32F), ColorType::Rgb8); + assert_eq!(to(ColorType::Rgba32F), ColorType::Rgba8); + } + + #[test] + fn test_conversion_hdr() { + let hdr_supported_colors = &[ExtendedColorType::Rgb32F]; + let to = |from| to_supported_color(from, hdr_supported_colors).unwrap_or(from); - match img.color() { - Rgb8 | Rgba8 | L8 | La8 => None, - L16 | L32F => Some(img.to_luma8().into()), - La16 | La32F => Some(img.to_luma_alpha8().into()), - Rgb16 | Rgb32F => Some(img.to_rgb8().into()), - Rgba16 | Rgba32F => Some(img.to_rgba8().into()), + assert_eq!(to(ColorType::L8), ColorType::Rgb32F); + assert_eq!(to(ColorType::La8), ColorType::Rgb32F); + assert_eq!(to(ColorType::Rgb8), ColorType::Rgb32F); + assert_eq!(to(ColorType::Rgba8), ColorType::Rgb32F); + assert_eq!(to(ColorType::L16), ColorType::Rgb32F); + assert_eq!(to(ColorType::La16), ColorType::Rgb32F); + assert_eq!(to(ColorType::Rgb16), ColorType::Rgb32F); + assert_eq!(to(ColorType::Rgba16), ColorType::Rgb32F); + assert_eq!(to(ColorType::Rgb32F), ColorType::Rgb32F); + assert_eq!(to(ColorType::Rgba32F), ColorType::Rgb32F); } } diff --git a/tests/encoder_colors.rs b/tests/encoder_colors.rs new file mode 100644 index 0000000000..ff9eba1109 --- /dev/null +++ b/tests/encoder_colors.rs @@ -0,0 +1,155 @@ +use image::{ExtendedColorType, ImageEncoder}; + +const ALL_COLORS: &[ExtendedColorType] = &[ + ExtendedColorType::A8, + ExtendedColorType::L1, + ExtendedColorType::La1, + ExtendedColorType::Rgb1, + ExtendedColorType::Rgba1, + ExtendedColorType::L2, + ExtendedColorType::La2, + ExtendedColorType::Rgb2, + ExtendedColorType::Rgba2, + ExtendedColorType::L4, + ExtendedColorType::La4, + ExtendedColorType::Rgb4, + ExtendedColorType::Rgba4, + ExtendedColorType::Rgb5x1, + ExtendedColorType::L8, + ExtendedColorType::La8, + ExtendedColorType::Rgb8, + ExtendedColorType::Rgba8, + ExtendedColorType::L16, + ExtendedColorType::La16, + ExtendedColorType::Rgb16, + ExtendedColorType::Rgba16, + ExtendedColorType::Bgr8, + ExtendedColorType::Bgra8, + ExtendedColorType::L32F, + ExtendedColorType::La32F, + ExtendedColorType::Rgb32F, + ExtendedColorType::Rgba32F, + ExtendedColorType::Cmyk8, + ExtendedColorType::Cmyk16, + ExtendedColorType::YCbCr8, +]; + +/// Verify that encoders' reported supported colors match the colors they can actually encode. +fn verify_supported_colors(f: impl Fn() -> E) { + let reference = f(); + let Some(supported_colors) = reference.supported_colors() else { + // If the encoder doesn't report supported colors, we can't verify anything + return; + }; + + let width = 8; + let height = 8; + + for &color in ALL_COLORS { + let buf = vec![0_u8; color.buffer_size(width, height) as usize]; + let result = f().write_image(&buf, width, height, color); + let actually_supported = result.is_ok(); + let claim_supported = supported_colors.contains(&color); + + if actually_supported && !claim_supported { + panic!( + "Encoder claimed to not support color {:?} but was able to encode it", + color + ); + } else if !actually_supported && claim_supported { + panic!( + "Encoder claimed to support color {:?} but failed to encode it: {:?}", + color, + result.err() + ); + } + } +} + +fn writer() -> impl std::io::Write + std::io::Seek { + std::io::Cursor::new(Vec::new()) +} + +#[cfg(feature = "avif")] +#[test] +fn test_encoder_avif() { + verify_supported_colors(|| image::codecs::avif::AvifEncoder::new(writer())); +} + +#[cfg(feature = "bmp")] +#[test] +fn test_encoder_bmp() { + verify_supported_colors(|| image::codecs::bmp::BmpEncoder::new(writer())); +} + +#[cfg(feature = "exr")] +#[test] +fn test_encoder_exr() { + verify_supported_colors(|| image::codecs::openexr::OpenExrEncoder::new(writer())); +} + +#[cfg(feature = "ff")] +#[test] +fn test_encoder_farbfeld() { + verify_supported_colors(|| image::codecs::farbfeld::FarbfeldEncoder::new(writer())); +} + +#[cfg(feature = "gif")] +#[test] +fn test_encoder_gif() { + verify_supported_colors(|| image::codecs::gif::GifEncoder::new(writer())); +} + +#[cfg(feature = "hdr")] +#[test] +fn test_encoder_hdr() { + verify_supported_colors(|| image::codecs::hdr::HdrEncoder::new(writer())); +} + +#[cfg(feature = "ico")] +#[test] +fn test_encoder_ico() { + verify_supported_colors(|| image::codecs::ico::IcoEncoder::new(writer())); +} + +#[cfg(feature = "jpeg")] +#[test] +fn test_encoder_jpeg() { + verify_supported_colors(|| image::codecs::jpeg::JpegEncoder::new(writer())); +} + +#[cfg(feature = "png")] +#[test] +fn test_encoder_png() { + verify_supported_colors(|| image::codecs::png::PngEncoder::new(writer())); +} + +#[cfg(feature = "pnm")] +#[test] +fn test_encoder_pnm() { + verify_supported_colors(|| image::codecs::pnm::PnmEncoder::new(writer())); +} + +#[cfg(feature = "qoi")] +#[test] +fn test_encoder_qoi() { + verify_supported_colors(|| image::codecs::qoi::QoiEncoder::new(writer())); +} + +#[cfg(feature = "tga")] +#[test] +fn test_encoder_tga() { + verify_supported_colors(|| image::codecs::tga::TgaEncoder::new(writer())); +} + +#[cfg(feature = "tiff")] +#[test] +fn test_encoder_tiff() { + verify_supported_colors(|| image::codecs::tiff::TiffEncoder::new(writer())); +} + +#[cfg(feature = "webp")] +#[test] +fn test_encoder_webp() { + verify_supported_colors(|| image::codecs::webp::WebPEncoder::new_lossless(writer())); +}