From 3698a76c5df989756776369ccdb62d027c61e4cd Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 08:09:11 -0800 Subject: [PATCH] File formats: JPEG XL refuses colors left in a profile that is not an RGB one A color JPEG XL image whose embedded ICC profile the decoder cannot convert to sRGB kept its samples as stored and lost the profile when that profile was not an RGB one, so the image drew those samples as sRGB. It is now refused as unreadable, as a gray image in such a profile already is. --- crates/engine/src/cmd/fileio/jxlread.rs | 16 ++++++++++------ crates/engine/src/cmd/fileio/tests_jxlread.rs | 5 +++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/fileio/jxlread.rs b/crates/engine/src/cmd/fileio/jxlread.rs index 9e0a0b64c..1d4e3d138 100644 --- a/crates/engine/src/cmd/fileio/jxlread.rs +++ b/crates/engine/src/cmd/fileio/jxlread.rs @@ -4,10 +4,10 @@ //! //! Colors come back as sRGB whenever the decoder converts them without a color management //! system: XYB-encoded images, every color space a JPEG XL header names, and embedded ICC -//! profiles that describe one of them. Any other image with an embedded ICC profile comes back -//! with its samples as stored, as PNG and JPEG files with an embedded profile are drawn, and with -//! the profile when it is an RGB one. Gray images with an ICC profile the decoder cannot convert -//! are refused. 16-bit and floating-point samples become 8 bits, alpha comes back straight, +//! profiles that describe one of them. Any other image with an embedded RGB ICC profile comes +//! back with its samples as stored, as PNG and JPEG files with an embedded profile are drawn, and +//! with the profile. Gray images, and images whose profile is not an RGB one, are refused when the +//! decoder cannot convert their ICC profile. 16-bit and floating-point samples become 8 bits, alpha comes back straight, //! and CMYK images are refused. The file is untrusted: the decoder is given only a container's //! image boxes, the image's size is checked from its header before any pixels are decoded, and the //! decoder's image buffers are capped. @@ -235,13 +235,17 @@ fn to_srgb_without_cms(xyb: bool, icc: Option<&[u8]>) -> bool { } /// The embedded RGB profile when the decoder leaves colors in it. Gray colors without a -/// conversion to sRGB cannot be stored as RGB without changing their profile. +/// conversion to sRGB cannot be stored as RGB without changing their profile, and neither can +/// colors left in a profile that is not an RGB one. pub(crate) fn output_profile(xyb: bool, grayscale: bool, icc: Option<&[u8]>) -> Result>, String> { let converts = to_srgb_without_cms(xyb, icc); if grayscale && !converts { return Err(unreadable("unsupported grayscale ICC profile")); } - Ok(icc.filter(|icc| icc.get(16..20) == Some(b"RGB ") && !converts).map(<[u8]>::to_vec)) + if !converts && icc.and_then(|icc| icc.get(16..20)) != Some(b"RGB ") { + return Err(unreadable("unsupported ICC profile color space")); + } + Ok(icc.filter(|_| !converts).map(<[u8]>::to_vec)) } /// The decoder's error `e` as a message. When the decoder's buffers needed more than they were diff --git a/crates/engine/src/cmd/fileio/tests_jxlread.rs b/crates/engine/src/cmd/fileio/tests_jxlread.rs index 1774cb3a2..6e6af80c8 100644 --- a/crates/engine/src/cmd/fileio/tests_jxlread.rs +++ b/crates/engine/src/cmd/fileio/tests_jxlread.rs @@ -295,6 +295,11 @@ fn colors_left_in_their_icc_profile_keep_it() { None ); assert_eq!(output_profile(false, false, None).unwrap(), None); + // A color image left in a profile that is not an RGB one can't be stored as RGB. + let mut lab = rgb.clone(); + lab[16..20].copy_from_slice(b"Lab "); + assert_eq!(output_profile(false, false, Some(&lab)).unwrap_err(), "the JPEG XL file can't be read: unsupported ICC profile color space"); + assert_eq!(output_profile(true, false, Some(&lab)).unwrap(), None, "XYB-encoded colors become sRGB"); // The PNG carries the profile, the pixels and the resolution. let img = image::RgbaImage::from_raw(2, 1, vec![200, 100, 50, 255, 9, 9, 9, 0]).unwrap(); for icc in [Some(rgb), None] {