From 9128bec43b5ca3065ccac432c8362bfbe87a5344 Mon Sep 17 00:00:00 2001 From: Alula <6276139+alula@users.noreply.github.com> Date: Sat, 7 Dec 2024 00:09:02 +0100 Subject: [PATCH 1/2] Implement writing ICC profiles for JPEG and PNG images. --- src/codecs/jpeg/encoder.rs | 50 ++++++++++++++++++++++++++++++++++++++ src/codecs/png.rs | 19 ++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/codecs/jpeg/encoder.rs b/src/codecs/jpeg/encoder.rs index 0c1f074985..075270daec 100644 --- a/src/codecs/jpeg/encoder.rs +++ b/src/codecs/jpeg/encoder.rs @@ -30,6 +30,7 @@ static SOS: u8 = 0xDA; static DQT: u8 = 0xDB; // Application segments start and end static APP0: u8 = 0xE0; +static APP2: u8 = 0xE2; // section K.1 // table K.1 @@ -346,6 +347,8 @@ pub struct JpegEncoder { chroma_actable: Cow<'static, [(u8, u16); 256]>, pixel_density: PixelDensity, + + icc_profile: Vec, } impl JpegEncoder { @@ -415,6 +418,8 @@ impl JpegEncoder { chroma_actable: Cow::Borrowed(&STD_CHROMA_AC_HUFF_LUT), pixel_density: PixelDensity::default(), + + icc_profile: Vec::new(), } } @@ -494,6 +499,9 @@ impl JpegEncoder { build_jfif_header(&mut buf, self.pixel_density); self.writer.write_segment(APP0, &buf)?; + // Write ICC profile chunks if present + self.write_icc_profile_chunks()?; + build_frame_header( &mut buf, 8, @@ -648,6 +656,43 @@ impl JpegEncoder { Ok(()) } + + fn write_icc_profile_chunks(&mut self) -> io::Result<()> { + if self.icc_profile.is_empty() { + return Ok(()); + } + + const MAX_CHUNK_SIZE: usize = 65533 - 14; + const MAX_CHUNK_COUNT: usize = 255; + const MAX_ICC_PROFILE_SIZE: usize = MAX_CHUNK_SIZE * MAX_CHUNK_COUNT; + + if self.icc_profile.len() > MAX_ICC_PROFILE_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "ICC profile too large", + )); + } + + let chunk_iter = self.icc_profile.chunks(MAX_CHUNK_SIZE); + let num_chunks = chunk_iter.len() as u8; + let mut segment = Vec::new(); + + for (i, chunk) in chunk_iter.enumerate() { + let chunk_number = (i + 1) as u8; + let length = 14 + chunk.len(); + + segment.clear(); + segment.reserve(length); + segment.extend_from_slice(b"ICC_PROFILE\0"); + segment.push(chunk_number); + segment.push(num_chunks); + segment.extend_from_slice(chunk); + + self.writer.write_segment(APP2, &segment)?; + } + + Ok(()) + } } impl ImageEncoder for JpegEncoder { @@ -661,6 +706,11 @@ impl ImageEncoder for JpegEncoder { ) -> ImageResult<()> { self.encode(buf, width, height, color_type) } + + fn set_icc_profile(&mut self, icc_profile: Vec) -> Result<(), UnsupportedError> { + self.icc_profile = icc_profile; + Ok(()) + } } fn build_jfif_header(m: &mut Vec, density: PixelDensity) { diff --git a/src/codecs/png.rs b/src/codecs/png.rs index 8c95ba6954..8cf296541a 100644 --- a/src/codecs/png.rs +++ b/src/codecs/png.rs @@ -6,6 +6,7 @@ //! * - The PNG Specification //! +use std::borrow::Cow; use std::fmt; use std::io::{BufRead, Seek, Write}; @@ -482,6 +483,7 @@ pub struct PngEncoder { w: W, compression: CompressionType, filter: FilterType, + icc_profile: Vec, } /// Compression level of a PNG encoder. The default setting is `Fast`. @@ -535,6 +537,7 @@ impl PngEncoder { w, compression: CompressionType::default(), filter: FilterType::default(), + icc_profile: Vec::new(), } } @@ -559,6 +562,7 @@ impl PngEncoder { w, compression, filter, + icc_profile: Vec::new(), } } @@ -604,7 +608,15 @@ impl PngEncoder { FilterType::Adaptive => (png::FilterType::Sub, png::AdaptiveFilterType::Adaptive), }; - let mut encoder = png::Encoder::new(self.w, width, height); + let mut info = png::Info::with_size(width, height); + + if !self.icc_profile.is_empty() { + info.icc_profile = Some(Cow::Borrowed(&self.icc_profile)); + } + + let mut encoder = + png::Encoder::with_info(self.w, info).map_err(|e| ImageError::IoError(e.into()))?; + encoder.set_color(ct); encoder.set_depth(bits); encoder.set_compression(comp); @@ -669,6 +681,11 @@ impl ImageEncoder for PngEncoder { ))), } } + + fn set_icc_profile(&mut self, icc_profile: Vec) -> Result<(), UnsupportedError> { + self.icc_profile = icc_profile; + Ok(()) + } } impl ImageError { From 472e3fba4556ec14d504b94128ecf1f3b6849c8b Mon Sep 17 00:00:00 2001 From: Kornel Date: Mon, 16 Dec 2024 21:47:54 +0100 Subject: [PATCH 2/2] Update deps --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 379cd3d6a2..87da3d4019 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,10 +43,10 @@ num-traits = { version = "0.2.0" } color_quant = { version = "1.1", optional = true } dav1d = { version = "0.10.3", optional = true } exr = { version = "1.5.0", optional = true } -gif = { version = "0.13", optional = true } +gif = { version = "0.13.1", optional = true } image-webp = { version = "0.2.0", optional = true } mp4parse = { version = "0.17.0", optional = true } -png = { version = "0.17.6", optional = true } +png = { version = "0.17.11", optional = true } qoi = { version = "0.4", optional = true } ravif = { version = "0.11.11", default-features = false, optional = true } rayon = { version = "1.7.0", optional = true } @@ -114,4 +114,4 @@ harness = false [[bench]] path = "benches/blur.rs" name = "blur" -harness = false \ No newline at end of file +harness = false