From b5be6fd39e53b0eb8eeb24c31da487c0e4df64de Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Tue, 26 May 2026 15:36:17 +0200 Subject: [PATCH 1/8] Add `ImageEncoder::supported_colors` --- src/codecs/bmp/encoder.rs | 16 +-- src/codecs/jpeg/encoder.rs | 15 +-- src/codecs/png.rs | 22 ++-- src/codecs/tga/encoder.rs | 15 +-- src/codecs/webp/encoder.rs | 15 +-- src/images/dynimage.rs | 2 +- src/io/encoder.rs | 207 +++++++++++++++++++++++++++++-------- tests/encoder_colors.rs | 155 +++++++++++++++++++++++++++ 8 files changed, 360 insertions(+), 87 deletions(-) create mode 100644 tests/encoder_colors.rs diff --git a/src/codecs/bmp/encoder.rs b/src/codecs/bmp/encoder.rs index 31264a852d..d2c7ba95e1 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,14 @@ 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]> { + Some(&[ + ExtendedColorType::Rgb8, + ExtendedColorType::Rgba8, + ExtendedColorType::L1, + ExtendedColorType::L8, + ExtendedColorType::La8, + ]) } } diff --git a/src/codecs/jpeg/encoder.rs b/src/codecs/jpeg/encoder.rs index 816fb2b66b..5fb9975313 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,8 @@ 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 | La16 => Some(img.to_luma8().into()), - Rgba8 | Rgb16 | Rgb32F | Rgba16 | Rgba32F => Some(img.to_rgb8().into()), - } + fn supported_colors(&self) -> Option<&[ExtendedColorType]> { + Some(&[ExtendedColorType::Rgb8, ExtendedColorType::L8]) } } diff --git a/src/codecs/png.rs b/src/codecs/png.rs index b7cc944cdf..63b5347e18 100644 --- a/src/codecs/png.rs +++ b/src/codecs/png.rs @@ -932,17 +932,17 @@ impl ImageEncoder for PngEncoder { Ok(()) } - fn make_compatible_img( - &self, - _: crate::io::encoder::MethodSealedToImage, - img: &DynamicImage, - ) -> Option { - use ColorType::*; - match img.color() { - 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]> { + Some(&[ + ExtendedColorType::Rgb8, + ExtendedColorType::Rgba8, + ExtendedColorType::L8, + ExtendedColorType::La8, + ExtendedColorType::Rgb16, + ExtendedColorType::Rgba16, + ExtendedColorType::L16, + ExtendedColorType::La16, + ]) } } diff --git a/src/codecs/tga/encoder.rs b/src/codecs/tga/encoder.rs index 6a65d164cb..40bb70c5d5 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,13 @@ 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]> { + Some(&[ + ExtendedColorType::Rgb8, + ExtendedColorType::Rgba8, + ExtendedColorType::L8, + ExtendedColorType::La8, + ]) } } diff --git a/src/codecs/webp/encoder.rs b/src/codecs/webp/encoder.rs index 70423d8406..9243f0d4a8 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,13 @@ 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]> { + Some(&[ + ExtendedColorType::Rgb8, + ExtendedColorType::Rgba8, + ExtendedColorType::L8, + ExtendedColorType::La8, + ]) } } diff --git a/src/images/dynimage.rs b/src/images/dynimage.rs index 469ac2040e..3a0d6a0129 100644 --- a/src/images/dynimage.rs +++ b/src/images/dynimage.rs @@ -1389,7 +1389,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( diff --git a/src/io/encoder.rs b/src/io/encoder.rs index eb9ae816a4..5d253720e6 100644 --- a/src/io/encoder.rs +++ b/src/io/encoder.rs @@ -1,26 +1,6 @@ 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 +80,18 @@ pub trait ImageEncoder { )) } - /// Convert the image to a compatible format for the encoder. This is used by the encoding - /// methods on `DynamicImage`. + /// 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 ones are + /// supported vary from encoder to encoder. This method allows encoders to specify which color + /// types their [`write_image`](Self::write_image) method supports. + /// + /// This information is currently used by the save and write method on [`DynamicImage`] to + /// perform necessary conversions before encoding. /// - /// 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 { + /// The list of supported color types may be incomplete, but encoders provide a complete list + /// on a best effort basis. Duplicates are not allowed. + fn supported_colors(&self) -> Option<&[ExtendedColorType]> { None } } @@ -136,17 +117,159 @@ 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 { - use ColorType::*; - - match img.color() { - Rgb8 | Rgba8 | L8 | La8 => None, - L16 => Some(img.to_luma8().into()), - La16 => Some(img.to_luma_alpha8().into()), - Rgb16 | Rgb32F => Some(img.to_rgb8().into()), - Rgba16 | Rgba32F => Some(img.to_rgba8().into()), +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; + } + + Some(match to { + ColorType::L8 => img.to_luma8().into(), + ColorType::La8 => img.to_luma_alpha8().into(), + ColorType::Rgb8 => img.to_rgb8().into(), + ColorType::Rgba8 => img.to_rgba8().into(), + ColorType::L16 => img.to_luma16().into(), + ColorType::La16 => img.to_luma_alpha16().into(), + ColorType::Rgb16 => img.to_rgb16().into(), + ColorType::Rgba16 => img.to_rgba16().into(), + ColorType::Rgb32F => img.to_rgb32f().into(), + ColorType::Rgba32F => img.to_rgba32f().into(), + }) +} +fn to_supported_color(from: ColorType, supported: &[ExtendedColorType]) -> Option { + supported + .iter() + .filter_map(|c| c.color_type()) + .max_by_key(|&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, + _ => {} + } + + fn get_precision(c: ColorType) -> i16 { + match c { + ColorType::L8 | ColorType::La8 | ColorType::Rgb8 | ColorType::Rgba8 => 0, + ColorType::L16 | ColorType::La16 | ColorType::Rgb16 | ColorType::Rgba16 => 1, + ColorType::Rgb32F | ColorType::Rgba32F => 2, + } + } + + const PRECISION_LOST: u16 = 10; + const PRECISION_GAIN: u16 = 1; + match get_precision(to) - get_precision(from) { + m @ 1.. => loss += PRECISION_LOST * m as u16, + m @ ..=-1 => loss += PRECISION_GAIN * m.unsigned_abs(), + 0 => {} + } + + loss + }) +} + +#[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); + + 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())); +} From ce63ea5c97d9af5a3b7548138112c330d17b0713 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Tue, 26 May 2026 15:45:17 +0200 Subject: [PATCH 2/8] Fix docs --- src/io/encoder.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/io/encoder.rs b/src/io/encoder.rs index 5d253720e6..d5c12e4211 100644 --- a/src/io/encoder.rs +++ b/src/io/encoder.rs @@ -82,15 +82,12 @@ pub trait ImageEncoder { /// 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 ones are - /// supported vary from encoder to encoder. This method allows encoders to specify which color - /// types their [`write_image`](Self::write_image) method supports. + /// 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. /// /// This information is currently used by the save and write method on [`DynamicImage`] to /// perform necessary conversions before encoding. - /// - /// The list of supported color types may be incomplete, but encoders provide a complete list - /// on a best effort basis. Duplicates are not allowed. fn supported_colors(&self) -> Option<&[ExtendedColorType]> { None } From 4b39050fb51c4c3e2d52e389ee32e2f8c7370515 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Tue, 26 May 2026 15:47:49 +0200 Subject: [PATCH 3/8] Terser code --- src/codecs/bmp/encoder.rs | 9 ++------- src/codecs/jpeg/encoder.rs | 3 ++- src/codecs/png.rs | 12 ++---------- src/codecs/tga/encoder.rs | 8 ++------ src/codecs/webp/encoder.rs | 8 ++------ 5 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/codecs/bmp/encoder.rs b/src/codecs/bmp/encoder.rs index d2c7ba95e1..dac056d709 100644 --- a/src/codecs/bmp/encoder.rs +++ b/src/codecs/bmp/encoder.rs @@ -367,13 +367,8 @@ impl ImageEncoder for BmpEncoder { } fn supported_colors(&self) -> Option<&[ExtendedColorType]> { - Some(&[ - ExtendedColorType::Rgb8, - ExtendedColorType::Rgba8, - ExtendedColorType::L1, - ExtendedColorType::L8, - ExtendedColorType::La8, - ]) + use ExtendedColorType::*; + Some(&[Rgb8, Rgba8, L1, L8, La8]) } } diff --git a/src/codecs/jpeg/encoder.rs b/src/codecs/jpeg/encoder.rs index 5fb9975313..db1b51a84b 100644 --- a/src/codecs/jpeg/encoder.rs +++ b/src/codecs/jpeg/encoder.rs @@ -294,7 +294,8 @@ impl ImageEncoder for JpegEncoder { } fn supported_colors(&self) -> Option<&[ExtendedColorType]> { - Some(&[ExtendedColorType::Rgb8, ExtendedColorType::L8]) + use ExtendedColorType::*; + Some(&[Rgb8, L8]) } } diff --git a/src/codecs/png.rs b/src/codecs/png.rs index 63b5347e18..d31064e9b4 100644 --- a/src/codecs/png.rs +++ b/src/codecs/png.rs @@ -933,16 +933,8 @@ impl ImageEncoder for PngEncoder { } fn supported_colors(&self) -> Option<&[ExtendedColorType]> { - Some(&[ - ExtendedColorType::Rgb8, - ExtendedColorType::Rgba8, - ExtendedColorType::L8, - ExtendedColorType::La8, - ExtendedColorType::Rgb16, - ExtendedColorType::Rgba16, - ExtendedColorType::L16, - ExtendedColorType::La16, - ]) + 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 40bb70c5d5..0a033f69ca 100644 --- a/src/codecs/tga/encoder.rs +++ b/src/codecs/tga/encoder.rs @@ -254,12 +254,8 @@ impl ImageEncoder for TgaEncoder { } fn supported_colors(&self) -> Option<&[ExtendedColorType]> { - Some(&[ - ExtendedColorType::Rgb8, - ExtendedColorType::Rgba8, - ExtendedColorType::L8, - ExtendedColorType::La8, - ]) + use ExtendedColorType::*; + Some(&[Rgb8, Rgba8, L8, La8]) } } diff --git a/src/codecs/webp/encoder.rs b/src/codecs/webp/encoder.rs index 9243f0d4a8..e503b06d2f 100644 --- a/src/codecs/webp/encoder.rs +++ b/src/codecs/webp/encoder.rs @@ -111,12 +111,8 @@ impl ImageEncoder for WebPEncoder { } fn supported_colors(&self) -> Option<&[ExtendedColorType]> { - Some(&[ - ExtendedColorType::Rgb8, - ExtendedColorType::Rgba8, - ExtendedColorType::L8, - ExtendedColorType::La8, - ]) + use ExtendedColorType::*; + Some(&[Rgb8, Rgba8, L8, La8]) } } From 96a4bdd7b8c6eeb8a658ad295f275cfd64ffa232 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Tue, 26 May 2026 15:51:33 +0200 Subject: [PATCH 4/8] Fix max to min --- src/io/encoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/encoder.rs b/src/io/encoder.rs index d5c12e4211..f2a63c7698 100644 --- a/src/io/encoder.rs +++ b/src/io/encoder.rs @@ -142,7 +142,7 @@ fn to_supported_color(from: ColorType, supported: &[ExtendedColorType]) -> Optio supported .iter() .filter_map(|c| c.color_type()) - .max_by_key(|&to| { + .min_by_key(|&to| { let mut loss = 0; // channel losses are heavily penalized, since a lot of information is lost From 8a38afa93c64ff2de19000aa3e79c4b948211440 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 10 Jun 2026 17:59:15 +0200 Subject: [PATCH 5/8] Redo conversion to only change precision and alpha --- src/images/buffer.rs | 3 + src/images/dynimage.rs | 34 ++++---- src/io/encoder.rs | 188 +++++++++++++++++++++++++++++++++++------ 3 files changed, 179 insertions(+), 46 deletions(-) diff --git a/src/images/buffer.rs b/src/images/buffer.rs index 24a6631989..9a5a1cf11f 100644 --- a/src/images/buffer.rs +++ b/src/images/buffer.rs @@ -1115,6 +1115,9 @@ impl ImageBuffer { Ok(()) } + pub(crate) fn rgb_color_space(&self) -> CicpRgb { + self.color + } pub(crate) fn set_rgb_color_space(&mut self, color: CicpRgb) { self.color = color; } diff --git a/src/images/dynimage.rs b/src/images/dynimage.rs index a332082a1e..e2f4f60cdf 100644 --- a/src/images/dynimage.rs +++ b/src/images/dynimage.rs @@ -15,6 +15,7 @@ use crate::io::encoder::ImageEncoderBoxed; use crate::io::free_functions::{self, encoder_for_format}; use crate::io::{DecodedImageAttributes, DecoderPreparedImage}; use crate::math::{resize_dimensions, Rect}; +use crate::metadata::cicp::CicpRgb; use crate::metadata::Orientation; use crate::traits::Pixel; use crate::{ @@ -953,6 +954,13 @@ impl DynamicImage { dynamic_map!(self, ref mut p, p.set_color_space(cicp)) } + pub(crate) fn rgb_color_space(&self) -> CicpRgb { + dynamic_map!(self, ref p, p.rgb_color_space()) + } + pub(crate) fn set_rgb_color_space(&mut self, color: CicpRgb) { + dynamic_map!(self, ref mut p, p.set_rgb_color_space(color)); + } + /// Whether the image contains an alpha channel /// /// This is a convenience wrapper around `self.color().has_alpha()`. @@ -1588,7 +1596,7 @@ impl DynamicImage { // Forward compatibility: make sure we do not drop any details here. let rgb = cicp.try_into_rgb()?; let mut target = DynamicImage::new(self.width(), self.height(), color); - dynamic_map!(target, ref mut p, p.set_rgb_color_space(rgb)); + target.set_rgb_color_space(rgb); target.copy_from_color_space(self, options)?; *self = target; @@ -1615,11 +1623,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 +1631,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 +1641,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 +1655,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 fa8019f713..b1ee20d705 100644 --- a/src/io/encoder.rs +++ b/src/io/encoder.rs @@ -1,5 +1,11 @@ +use std::borrow::Cow; + +use crate::color::FromPrimitive; use crate::error::{ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind}; -use crate::{ColorType, DynamicImage, ExtendedColorType}; +use crate::{ + ColorType, DynamicImage, ExtendedColorType, GenericImageView, ImageBuffer, Luma, LumaA, Pixel, + Rgb, Rgba, +}; /// The trait all encoders implement pub trait ImageEncoder { @@ -86,8 +92,8 @@ pub trait ImageEncoder { /// vary from encoder to encoder. This method allows encoders to specify which color types /// their [`write_image`](Self::write_image) method supports. /// - /// This information is currently used by the save and write method on [`DynamicImage`] to - /// perform necessary conversions before encoding. + /// This information is currently used for automatic color conversion by the `save*` and `write*` + /// methods on [`DynamicImage`]. For more information, see [`DynamicImage::save`]. fn supported_colors(&self) -> Option<&[ExtendedColorType]> { None } @@ -125,26 +131,39 @@ pub(crate) fn make_compatible_img( return None; } - Some(match to { - ColorType::L8 => img.to_luma8().into(), - ColorType::La8 => img.to_luma_alpha8().into(), - ColorType::Rgb8 => img.to_rgb8().into(), - ColorType::Rgba8 => img.to_rgba8().into(), - ColorType::L16 => img.to_luma16().into(), - ColorType::La16 => img.to_luma_alpha16().into(), - ColorType::Rgb16 => img.to_rgb16().into(), - ColorType::Rgba16 => img.to_rgba16().into(), - ColorType::L32F => img.to_luma32f().into(), - ColorType::La32F => img.to_luma_alpha32f().into(), - ColorType::Rgb32F => img.to_rgb32f().into(), - ColorType::Rgba32F => img.to_rgba32f().into(), - }) + if color.has_alpha() != 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 decompose_color_type(to).precision != decompose_color_type(color).precision { + Cow::Owned(to_precision(&img, decompose_color_type(to).precision)) + } 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 @@ -154,28 +173,20 @@ fn to_supported_color(from: ColorType, supported: &[ExtendedColorType]) -> Optio const COLOR_LOST: u16 = 200; const COLOR_GAIN: u16 = 6; - match (from.has_alpha(), to.has_alpha()) { + match (from.has_alpha, to.has_alpha) { (true, false) => loss += ALPHA_LOST, (false, true) => loss += ALPHA_GAIN, _ => {} } - match (from.has_color(), to.has_color()) { + match (from.has_color, to.has_color) { (true, false) => loss += COLOR_LOST, (false, true) => loss += COLOR_GAIN, _ => {} } - fn get_precision(c: ColorType) -> i16 { - match c { - ColorType::L8 | ColorType::La8 | ColorType::Rgb8 | ColorType::Rgba8 => 0, - ColorType::L16 | ColorType::La16 | ColorType::Rgb16 | ColorType::Rgba16 => 1, - ColorType::L32F | ColorType::La32F | ColorType::Rgb32F | ColorType::Rgba32F => 2, - } - } - const PRECISION_LOST: u16 = 10; const PRECISION_GAIN: u16 = 1; - match get_precision(to) - get_precision(from) { + 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 => {} @@ -185,6 +196,127 @@ fn to_supported_color(from: ColorType, supported: &[ExtendedColorType]) -> Optio }) } +/// 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()), + } +} + +fn to_precision(image: &DynamicImage, target_precision: Precision) -> DynamicImage { + let image_color = decompose_color_type(image.color()); + + fn convert_precision + FromPrimitive + FromPrimitive>( + buffer: &[u8], + buffer_precision: Precision, + ) -> Vec { + match buffer_precision { + Precision::U8 => buffer + .iter() + .copied() + .map(FromPrimitive::from_primitive) + .collect(), + // casts are valid, because the slice comes from a Vec + Precision::U16 => bytemuck::cast_slice::<_, u16>(buffer) + .iter() + .copied() + .map(FromPrimitive::from_primitive) + .collect(), + Precision::F32 => bytemuck::cast_slice::<_, f32>(buffer) + .iter() + .copied() + .map(FromPrimitive::from_primitive) + .collect(), + } + } + fn create_dyn_image(width: u32, height: u32, data: Vec) -> DynamicImage + where + DynamicImage: From>>, + { + let buffer: ImageBuffer = ImageBuffer::from_vec(width, height, data).unwrap(); + DynamicImage::from(buffer) + } + + let bytes = image.as_bytes(); + let (w, h) = image.dimensions(); + + let mut out: DynamicImage = match target_precision { + Precision::U8 => { + let data: Vec = convert_precision(bytes, image_color.precision); + + match (image_color.has_color, image_color.has_alpha) { + (false, false) => create_dyn_image::>(w, h, data), + (false, true) => create_dyn_image::>(w, h, data), + (true, false) => create_dyn_image::>(w, h, data), + (true, true) => create_dyn_image::>(w, h, data), + } + } + Precision::U16 => { + let data: Vec = convert_precision(bytes, image_color.precision); + + match (image_color.has_color, image_color.has_alpha) { + (false, false) => create_dyn_image::>(w, h, data), + (false, true) => create_dyn_image::>(w, h, data), + (true, false) => create_dyn_image::>(w, h, data), + (true, true) => create_dyn_image::>(w, h, data), + } + } + Precision::F32 => { + let data: Vec = convert_precision(bytes, image_color.precision); + + match (image_color.has_color, image_color.has_alpha) { + (false, false) => create_dyn_image::>(w, h, data), + (false, true) => create_dyn_image::>(w, h, data), + (true, false) => create_dyn_image::>(w, h, data), + (true, true) => create_dyn_image::>(w, h, data), + } + } + }; + + out.set_rgb_color_space(image.rgb_color_space()); + + out +} + +#[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::*; From d9396d6bdfb08cdacc2a9e96095aae699d769d67 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 10 Jun 2026 18:02:53 +0200 Subject: [PATCH 6/8] Oopsie --- src/io/encoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/encoder.rs b/src/io/encoder.rs index b1ee20d705..10a9a1f94b 100644 --- a/src/io/encoder.rs +++ b/src/io/encoder.rs @@ -131,7 +131,7 @@ pub(crate) fn make_compatible_img( return None; } - if color.has_alpha() != to.has_color() { + 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; From 3a2fad68fca67049067c3244787947c7c0fd7d2d Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 10 Jun 2026 20:26:01 +0200 Subject: [PATCH 7/8] Simpler implementation --- src/images/buffer.rs | 27 ++++++++++-- src/images/dynimage.rs | 56 ++++++++++++++++++++---- src/io/encoder.rs | 96 ++++++------------------------------------ 3 files changed, 83 insertions(+), 96 deletions(-) diff --git a/src/images/buffer.rs b/src/images/buffer.rs index 9a5a1cf11f..e53adcf6db 100644 --- a/src/images/buffer.rs +++ b/src/images/buffer.rs @@ -1115,9 +1115,6 @@ impl ImageBuffer { Ok(()) } - pub(crate) fn rgb_color_space(&self) -> CicpRgb { - self.color - } pub(crate) fn set_rgb_color_space(&mut self, color: CicpRgb) { self.color = color; } @@ -1667,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 e2f4f60cdf..d4a8140454 100644 --- a/src/images/dynimage.rs +++ b/src/images/dynimage.rs @@ -15,7 +15,6 @@ use crate::io::encoder::ImageEncoderBoxed; use crate::io::free_functions::{self, encoder_for_format}; use crate::io::{DecodedImageAttributes, DecoderPreparedImage}; use crate::math::{resize_dimensions, Rect}; -use crate::metadata::cicp::CicpRgb; use crate::metadata::Orientation; use crate::traits::Pixel; use crate::{ @@ -954,13 +953,6 @@ impl DynamicImage { dynamic_map!(self, ref mut p, p.set_color_space(cicp)) } - pub(crate) fn rgb_color_space(&self) -> CicpRgb { - dynamic_map!(self, ref p, p.rgb_color_space()) - } - pub(crate) fn set_rgb_color_space(&mut self, color: CicpRgb) { - dynamic_map!(self, ref mut p, p.set_rgb_color_space(color)); - } - /// Whether the image contains an alpha channel /// /// This is a convenience wrapper around `self.color().has_alpha()`. @@ -1084,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] @@ -1596,7 +1634,7 @@ impl DynamicImage { // Forward compatibility: make sure we do not drop any details here. let rgb = cicp.try_into_rgb()?; let mut target = DynamicImage::new(self.width(), self.height(), color); - target.set_rgb_color_space(rgb); + dynamic_map!(target, ref mut p, p.set_rgb_color_space(rgb)); target.copy_from_color_space(self, options)?; *self = target; diff --git a/src/io/encoder.rs b/src/io/encoder.rs index 10a9a1f94b..9b3542f07b 100644 --- a/src/io/encoder.rs +++ b/src/io/encoder.rs @@ -1,11 +1,7 @@ use std::borrow::Cow; -use crate::color::FromPrimitive; use crate::error::{ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind}; -use crate::{ - ColorType, DynamicImage, ExtendedColorType, GenericImageView, ImageBuffer, Luma, LumaA, Pixel, - Rgb, Rgba, -}; +use crate::{ColorType, DynamicImage, ExtendedColorType}; /// The trait all encoders implement pub trait ImageEncoder { @@ -131,22 +127,29 @@ pub(crate) fn make_compatible_img( return None; } - if color.has_color() != to.has_color() { + 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() { + 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 decompose_color_type(to).precision != decompose_color_type(color).precision { - Cow::Owned(to_precision(&img, decompose_color_type(to).precision)) + 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 }; @@ -216,81 +219,6 @@ fn toggle_alpha(image: &DynamicImage) -> DynamicImage { } } -fn to_precision(image: &DynamicImage, target_precision: Precision) -> DynamicImage { - let image_color = decompose_color_type(image.color()); - - fn convert_precision + FromPrimitive + FromPrimitive>( - buffer: &[u8], - buffer_precision: Precision, - ) -> Vec { - match buffer_precision { - Precision::U8 => buffer - .iter() - .copied() - .map(FromPrimitive::from_primitive) - .collect(), - // casts are valid, because the slice comes from a Vec - Precision::U16 => bytemuck::cast_slice::<_, u16>(buffer) - .iter() - .copied() - .map(FromPrimitive::from_primitive) - .collect(), - Precision::F32 => bytemuck::cast_slice::<_, f32>(buffer) - .iter() - .copied() - .map(FromPrimitive::from_primitive) - .collect(), - } - } - fn create_dyn_image(width: u32, height: u32, data: Vec) -> DynamicImage - where - DynamicImage: From>>, - { - let buffer: ImageBuffer = ImageBuffer::from_vec(width, height, data).unwrap(); - DynamicImage::from(buffer) - } - - let bytes = image.as_bytes(); - let (w, h) = image.dimensions(); - - let mut out: DynamicImage = match target_precision { - Precision::U8 => { - let data: Vec = convert_precision(bytes, image_color.precision); - - match (image_color.has_color, image_color.has_alpha) { - (false, false) => create_dyn_image::>(w, h, data), - (false, true) => create_dyn_image::>(w, h, data), - (true, false) => create_dyn_image::>(w, h, data), - (true, true) => create_dyn_image::>(w, h, data), - } - } - Precision::U16 => { - let data: Vec = convert_precision(bytes, image_color.precision); - - match (image_color.has_color, image_color.has_alpha) { - (false, false) => create_dyn_image::>(w, h, data), - (false, true) => create_dyn_image::>(w, h, data), - (true, false) => create_dyn_image::>(w, h, data), - (true, true) => create_dyn_image::>(w, h, data), - } - } - Precision::F32 => { - let data: Vec = convert_precision(bytes, image_color.precision); - - match (image_color.has_color, image_color.has_alpha) { - (false, false) => create_dyn_image::>(w, h, data), - (false, true) => create_dyn_image::>(w, h, data), - (true, false) => create_dyn_image::>(w, h, data), - (true, true) => create_dyn_image::>(w, h, data), - } - } - }; - - out.set_rgb_color_space(image.rgb_color_space()); - - out -} - #[derive(Copy, Clone, PartialEq, Eq)] enum Precision { U8 = 0, From d27e946b2b654e2e38e47b47a5a4bdc4c1f71800 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 10 Jun 2026 20:43:07 +0200 Subject: [PATCH 8/8] Better docs --- src/io/encoder.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/io/encoder.rs b/src/io/encoder.rs index 9b3542f07b..c7fb95ac9a 100644 --- a/src/io/encoder.rs +++ b/src/io/encoder.rs @@ -88,8 +88,12 @@ pub trait ImageEncoder { /// vary from encoder to encoder. This method allows encoders to specify which color types /// their [`write_image`](Self::write_image) method supports. /// - /// This information is currently used for automatic color conversion by the `save*` and `write*` - /// methods on [`DynamicImage`]. For more information, see [`DynamicImage::save`]. + /// 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 }