diff --git a/examples/fast_blur/main.rs b/examples/fast_blur/main.rs index 4523934e9a..845ffc1da6 100644 --- a/examples/fast_blur/main.rs +++ b/examples/fast_blur/main.rs @@ -1,12 +1,12 @@ use image::imageops::GaussianBlurParameters; -use image::ImageReader; +use image::ImageReaderOptions; fn main() { let path = concat!( env!("CARGO_MANIFEST_DIR"), "/tests/images/tiff/testsuite/mandrill.tiff" ); - let img = ImageReader::open(path).unwrap().decode().unwrap(); + let img = ImageReaderOptions::open(path).unwrap().decode().unwrap(); let img2 = img.blur_advanced(GaussianBlurParameters::new_from_sigma(10.0)); diff --git a/fuzz-afl/fuzzers/fuzz_pnm.rs b/fuzz-afl/fuzzers/fuzz_pnm.rs index 7cb179f2bf..e99c7daae2 100644 --- a/fuzz-afl/fuzzers/fuzz_pnm.rs +++ b/fuzz-afl/fuzzers/fuzz_pnm.rs @@ -1,16 +1,18 @@ extern crate afl; extern crate image; -use image::{DynamicImage, ImageDecoder}; use image::error::{ImageError, ImageResult, LimitError, LimitErrorKind}; +use image::{DynamicImage, ImageDecoder}; #[inline(always)] fn pnm_decode(data: &[u8]) -> ImageResult { - let decoder = image::codecs::pnm::PnmDecoder::new(data)?; - let (width, height) = decoder.dimensions(); + let mut decoder = image::codecs::pnm::PnmDecoder::new(data)?; + let (width, height) = decoder.peek_layout()?.dimensions(); if width.saturating_mul(height) > 4_000_000 { - return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError))); + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::DimensionError, + ))); } DynamicImage::from_decoder(decoder) diff --git a/fuzz-afl/fuzzers/fuzz_webp.rs b/fuzz-afl/fuzzers/fuzz_webp.rs index ee51dd3920..507105032e 100644 --- a/fuzz-afl/fuzzers/fuzz_webp.rs +++ b/fuzz-afl/fuzzers/fuzz_webp.rs @@ -3,16 +3,18 @@ extern crate image; use std::io::Cursor; -use image::{DynamicImage, ImageDecoder}; use image::error::{ImageError, ImageResult, LimitError, LimitErrorKind}; +use image::{DynamicImage, ImageDecoder}; #[inline(always)] fn webp_decode(data: &[u8]) -> ImageResult { - let decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(data))?; - let (width, height) = decoder.dimensions(); + let mut decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(data))?; + let (width, height) = decoder.peek_layout()?.dimensions(); if width.saturating_mul(height) > 4_000_000 { - return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError))); + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::DimensionError, + ))); } DynamicImage::from_decoder(decoder) diff --git a/fuzz-afl/reproducers/reproduce_pnm.rs b/fuzz-afl/reproducers/reproduce_pnm.rs index 799e5d0dc3..4dcd96c727 100644 --- a/fuzz-afl/reproducers/reproduce_pnm.rs +++ b/fuzz-afl/reproducers/reproduce_pnm.rs @@ -7,8 +7,8 @@ mod utils; #[inline(always)] fn pnm_decode(data: &[u8]) -> ImageResult { - let decoder = image::codecs::pnm::PnmDecoder::new(data)?; - let (width, height) = decoder.dimensions(); + let mut decoder = image::codecs::pnm::PnmDecoder::new(data)?; + let (width, height) = decoder.peek_layout()?.dimensions(); if width.saturating_mul(height) > 4_000_000 { return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError))); diff --git a/fuzz-afl/reproducers/reproduce_webp.rs b/fuzz-afl/reproducers/reproduce_webp.rs index 93a87f717f..697a44332e 100644 --- a/fuzz-afl/reproducers/reproduce_webp.rs +++ b/fuzz-afl/reproducers/reproduce_webp.rs @@ -2,18 +2,20 @@ extern crate image; use std::io::Cursor; -use image::{DynamicImage, ImageDecoder}; use image::error::{ImageError, ImageResult, LimitError, LimitErrorKind}; +use image::{DynamicImage, ImageDecoder}; mod utils; #[inline(always)] fn webp_decode(data: &[u8]) -> ImageResult { - let decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(data))?; - let (width, height) = decoder.dimensions(); + let mut decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(data))?; + let (width, height) = decoder.peek_layout()?.dimensions(); if width.saturating_mul(height) > 4_000_000 { - return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError))); + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::DimensionError, + ))); } DynamicImage::from_decoder(decoder) diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index 606a410af2..f49f48f4af 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -4,11 +4,11 @@ extern crate libfuzzer_sys; extern crate image; use image::codecs::openexr::*; -use image::Limits; use image::ExtendedColorType; use image::ImageDecoder; use image::ImageEncoder; use image::ImageResult; +use image::Limits; use std::io::{BufRead, Cursor, Seek, Write}; // "just dont panic" @@ -17,10 +17,11 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { // TODO this method should probably already exist in the main image crate fn read_as_rgba_byte_image(read: impl BufRead + Seek) -> ImageResult<(u32, u32, Vec)> { let mut decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?; - match usize::try_from(decoder.total_bytes()) { + let layout = decoder.peek_layout()?; + match usize::try_from(layout.total_bytes()) { Ok(decoded_size) if decoded_size <= 256 * 1024 * 1024 => { decoder.set_limits(Limits::default())?; - let (width, height) = decoder.dimensions(); + let (width, height) = layout.dimensions(); let mut buffer = vec![0; decoded_size]; decoder.read_image(buffer.as_mut_slice())?; Ok((width, height, buffer)) diff --git a/fuzz/fuzzers/fuzzer_script_tga.rs b/fuzz/fuzzers/fuzzer_script_tga.rs index 3bd6252f9b..b14e4a9c9e 100644 --- a/fuzz/fuzzers/fuzzer_script_tga.rs +++ b/fuzz/fuzzers/fuzzer_script_tga.rs @@ -8,11 +8,11 @@ fuzz_target!(|data: &[u8]| { fn decode(data: &[u8]) -> Result<(), image::ImageError> { use image::ImageDecoder; - let decoder = image::codecs::tga::TgaDecoder::new(std::io::Cursor::new(data))?; - if decoder.total_bytes() > 4_000_000 { + let mut decoder = image::codecs::tga::TgaDecoder::new(std::io::Cursor::new(data))?; + if decoder.peek_layout()?.total_bytes() > 4_000_000 { return Ok(()); } - let mut buffer = vec![0; decoder.total_bytes() as usize]; + let mut buffer = vec![0; decoder.peek_layout()?.total_bytes() as usize]; decoder.read_image(&mut buffer)?; Ok(()) } diff --git a/src/codecs/avif/decoder.rs b/src/codecs/avif/decoder.rs index fa7ee75cd3..ae5207cb79 100644 --- a/src/codecs/avif/decoder.rs +++ b/src/codecs/avif/decoder.rs @@ -3,6 +3,7 @@ use crate::error::{ DecodingError, ImageFormatHint, LimitError, LimitErrorKind, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::DecodedImageAttributes; use crate::{ColorType, ImageDecoder, ImageError, ImageFormat, ImageResult}; /// /// The [AVIF] specification defines an image derivative of the AV1 bitstream, an open video codec. @@ -327,24 +328,25 @@ fn get_matrix( } impl ImageDecoder for AvifDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.picture.width(), self.picture.height()) - } - - fn color_type(&self) -> ColorType { - if self.picture.bit_depth() == 8 { - ColorType::Rgba8 - } else { - ColorType::Rgba16 - } + fn peek_layout(&mut self) -> ImageResult { + Ok(crate::ImageLayout { + width: self.picture.width(), + height: self.picture.height(), + color: if self.picture.bit_depth() == 8 { + ColorType::Rgba8 + } else { + ColorType::Rgba16 + }, + }) } fn icc_profile(&mut self) -> ImageResult>> { Ok(self.icc_profile.clone()) } - fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); let bit_depth = self.picture.bit_depth(); @@ -352,7 +354,7 @@ impl ImageDecoder for AvifDecoder { // if this happens then there is an incorrect implementation somewhere else assert!(bit_depth == 8 || bit_depth == 10 || bit_depth == 12); - let (width, height) = self.dimensions(); + let (width, height) = layout.dimensions(); // This is suspicious if this happens, better fail early if width == 0 || height == 0 { return Err(ImageError::Limits(LimitError::from_kind( @@ -439,7 +441,7 @@ impl ImageDecoder for AvifDecoder { } // Squashing alpha plane into a picture - if let Some(picture) = self.alpha_picture { + if let Some(picture) = &self.alpha_picture { if picture.pixel_layout() != PixelLayout::I400 { return Err(ImageError::Decoding(DecodingError::new( ImageFormat::Avif.into(), @@ -476,11 +478,7 @@ impl ImageDecoder for AvifDecoder { } } - Ok(()) - } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes::default()) } } diff --git a/src/codecs/bmp/decoder.rs b/src/codecs/bmp/decoder.rs index fa50952c87..e45c4245cb 100644 --- a/src/codecs/bmp/decoder.rs +++ b/src/codecs/bmp/decoder.rs @@ -11,7 +11,7 @@ use crate::color::ColorType; use crate::error::{ DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind, }; -use crate::io::ReadExt; +use crate::io::{DecodedImageAttributes, ReadExt}; use crate::{ImageDecoder, ImageFormat}; const BITMAPCOREHEADER_SIZE: u32 = 12; @@ -1386,31 +1386,29 @@ impl BmpDecoder { } impl ImageDecoder for BmpDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.width as u32, self.height as u32) - } - - fn color_type(&self) -> ColorType { - if self.indexed_color { - ColorType::L8 - } else if self.add_alpha_channel { - ColorType::Rgba8 - } else { - ColorType::Rgb8 - } + fn peek_layout(&mut self) -> ImageResult { + Ok(crate::ImageLayout { + width: self.width as u32, + height: self.height as u32, + color: if self.indexed_color { + ColorType::L8 + } else if self.add_alpha_channel { + ColorType::Rgba8 + } else { + ColorType::Rgb8 + }, + }) } fn icc_profile(&mut self) -> ImageResult>> { Ok(self.icc_profile.clone()) } - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); - self.read_image_data(buf) - } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); + self.read_image_data(buf)?; + Ok(DecodedImageAttributes::default()) } } @@ -1458,8 +1456,9 @@ mod test { 0x4d, 0x00, 0x2a, 0x00, ]; - let decoder = BmpDecoder::new(Cursor::new(&data)).unwrap(); - let mut buf = vec![0; usize::try_from(decoder.total_bytes()).unwrap()]; + let mut decoder = BmpDecoder::new(Cursor::new(&data)).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut buf = vec![0; usize::try_from(layout.total_bytes()).unwrap()]; assert!(decoder.read_image(&mut buf).is_ok()); } diff --git a/src/codecs/bmp/encoder.rs b/src/codecs/bmp/encoder.rs index 4cff61e21a..2bf053b952 100644 --- a/src/codecs/bmp/encoder.rs +++ b/src/codecs/bmp/encoder.rs @@ -376,9 +376,9 @@ mod tests { .expect("could not encode image"); } - let decoder = BmpDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); - - let mut buf = vec![0; decoder.total_bytes() as usize]; + let mut decoder = BmpDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); + let layout = decoder.peek_layout().unwrap(); + let mut buf = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut buf).expect("failed to decode"); buf } diff --git a/src/codecs/dds.rs b/src/codecs/dds.rs index d66a5d9de4..2cf5e89bdd 100644 --- a/src/codecs/dds.rs +++ b/src/codecs/dds.rs @@ -12,10 +12,10 @@ use byteorder_lite::{LittleEndian, ReadBytesExt}; #[allow(deprecated)] use crate::codecs::dxt::{DxtDecoder, DxtVariant}; -use crate::color::ColorType; use crate::error::{ DecodingError, ImageError, ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::DecodedImageAttributes; use crate::{ImageDecoder, ImageFormat}; /// Errors that can occur during decoding and parsing a DDS image @@ -326,21 +326,13 @@ impl DdsDecoder { } impl ImageDecoder for DdsDecoder { - fn dimensions(&self) -> (u32, u32) { - self.inner.dimensions() + fn peek_layout(&mut self) -> ImageResult { + self.inner.peek_layout() } - fn color_type(&self) -> ColorType { - self.inner.color_type() - } - - fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { self.inner.read_image(buf) } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) - } } #[cfg(test)] diff --git a/src/codecs/dxt.rs b/src/codecs/dxt.rs index ca9d2e68b8..565c5200df 100644 --- a/src/codecs/dxt.rs +++ b/src/codecs/dxt.rs @@ -11,6 +11,7 @@ use std::io::{self, Read}; use crate::color::ColorType; use crate::error::{ImageError, ImageResult, ParameterError, ParameterErrorKind}; +use crate::io::DecodedImageAttributes; use crate::io::ReadExt; use crate::ImageDecoder; @@ -129,26 +130,24 @@ impl DxtDecoder { // Note that, due to the way that DXT compression works, a scanline is considered to consist out of // 4 lines of pixels. impl ImageDecoder for DxtDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.width_blocks * 4, self.height_blocks * 4) - } - - fn color_type(&self) -> ColorType { - self.variant.color_type() + fn peek_layout(&mut self) -> ImageResult { + Ok(crate::ImageLayout { + width: self.width_blocks * 4, + height: self.height_blocks * 4, + color: self.variant.color_type(), + }) } - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); #[allow(deprecated)] for chunk in buf.chunks_mut(self.scanline_bytes().max(1) as usize) { self.read_scanline(chunk)?; } - Ok(()) - } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes::default()) } } diff --git a/src/codecs/farbfeld.rs b/src/codecs/farbfeld.rs index e7e1a29305..2638e58a1f 100644 --- a/src/codecs/farbfeld.rs +++ b/src/codecs/farbfeld.rs @@ -22,6 +22,7 @@ use crate::color::ExtendedColorType; use crate::error::{ DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::DecodedImageAttributes; use crate::{ColorType, ImageDecoder, ImageEncoder, ImageFormat}; /// farbfeld Reader @@ -195,22 +196,19 @@ impl FarbfeldDecoder { } impl ImageDecoder for FarbfeldDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.reader.width, self.reader.height) - } - - fn color_type(&self) -> ColorType { - ColorType::Rgba16 + fn peek_layout(&mut self) -> ImageResult { + Ok(crate::ImageLayout { + width: self.reader.width, + height: self.reader.height, + color: ColorType::Rgba16, + }) } - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); self.reader.read_exact(buf)?; - Ok(()) - } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes::default()) } } diff --git a/src/codecs/gif.rs b/src/codecs/gif.rs index 5771bac37e..f77db810ec 100644 --- a/src/codecs/gif.rs +++ b/src/codecs/gif.rs @@ -40,6 +40,7 @@ use crate::error::{ DecodingError, EncodingError, ImageError, ImageResult, LimitError, LimitErrorKind, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::{DecodedImageAttributes, DecodedMetadataHint, DecoderAttributes}; use crate::traits::Pixel; use crate::{ AnimationDecoder, ExtendedColorType, ImageBuffer, ImageDecoder, ImageEncoder, ImageFormat, @@ -48,21 +49,57 @@ use crate::{ /// GIF decoder pub struct GifDecoder { - reader: gif::Decoder, + options: gif::DecodeOptions, + reader: Option, + decoder: Option>, limits: Limits, } impl GifDecoder { /// Creates a new decoder that decodes the input steam `r` pub fn new(r: R) -> ImageResult> { - let mut decoder = gif::DecodeOptions::new(); - decoder.set_color_output(ColorOutput::RGBA); + let mut options = gif::DecodeOptions::new(); + options.set_color_output(ColorOutput::RGBA); Ok(GifDecoder { - reader: decoder.read_info(r).map_err(ImageError::from_decoding)?, + options, + reader: Some(r), + decoder: None, limits: Limits::no_limits(), }) } + + // We're manipulating the lifetime. The early return must not borrow from `self.decoder` for + // the whole scope of the function thus this check does not work with if-let patterns until at + // least the next generation borrow checker (as of 1.89). + #[allow(clippy::unnecessary_unwrap)] + fn ensure_decoder(&mut self) -> ImageResult<&mut gif::Decoder> { + if self.decoder.is_some() { + return Ok(self.decoder.as_mut().unwrap()); + } + + let Some(reader) = self.reader.take() else { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + ))); + }; + + let decoder = self + .options + .clone() + .read_info(reader) + .map_err(ImageError::from_decoding)?; + + Ok(self.decoder.insert(decoder)) + } + + fn layout_from_decoder(decoder: &gif::Decoder) -> crate::ImageLayout { + crate::ImageLayout { + width: u32::from(decoder.width()), + height: u32::from(decoder.height()), + color: ColorType::Rgba8, + } + } } /// Wrapper struct around a `Cursor>` @@ -86,21 +123,26 @@ impl Read for GifReader { } impl ImageDecoder for GifDecoder { - fn dimensions(&self) -> (u32, u32) { - ( - u32::from(self.reader.width()), - u32::from(self.reader.height()), - ) + fn attributes(&self) -> DecoderAttributes { + DecoderAttributes { + xmp: DecodedMetadataHint::AfterFinish, + icc: DecodedMetadataHint::AfterFinish, + iptc: DecodedMetadataHint::None, + exif: DecodedMetadataHint::None, + is_animated: true, + ..DecoderAttributes::default() + } } - fn color_type(&self) -> ColorType { - ColorType::Rgba8 + fn peek_layout(&mut self) -> ImageResult { + let decoder = self.ensure_decoder()?; + Ok(Self::layout_from_decoder(decoder)) } fn set_limits(&mut self, limits: Limits) -> ImageResult<()> { limits.check_support(&crate::LimitSupport::default())?; - let (width, height) = self.dimensions(); + let (width, height) = self.peek_layout()?.dimensions(); limits.check_dimensions(width, height)?; self.limits = limits; @@ -108,11 +150,13 @@ impl ImageDecoder for GifDecoder { Ok(()) } - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let decoder = self.ensure_decoder()?; + let layout @ crate::ImageLayout { width, height, .. } = Self::layout_from_decoder(decoder); + + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); - let frame = match self - .reader + let frame = match decoder .next_frame_info() .map_err(ImageError::from_decoding)? { @@ -124,8 +168,6 @@ impl ImageDecoder for GifDecoder { } }; - let (width, height) = self.dimensions(); - if frame.left == 0 && frame.width == width && (u64::from(frame.top) + u64::from(frame.height) <= u64::from(height)) @@ -135,7 +177,7 @@ impl ImageDecoder for GifDecoder { // we can directly write it into the buffer without causing line wraparound. let line_length = usize::try_from(width) .unwrap() - .checked_mul(self.color_type().bytes_per_pixel() as usize) + .checked_mul(ColorType::Rgba8.bytes_per_pixel() as usize) .unwrap(); // isolate the portion of the buffer to read the frame data into. @@ -145,14 +187,15 @@ impl ImageDecoder for GifDecoder { let (buf, blank_bottom) = rest.split_at_mut(line_length.checked_mul(frame.height as usize).unwrap()); - debug_assert_eq!(buf.len(), self.reader.buffer_size()); + debug_assert_eq!(buf.len(), decoder.buffer_size()); // this is only necessary in case the buffer is not zeroed for b in blank_top { *b = 0; } + // fill the middle section with the frame data - self.reader + decoder .read_into_buffer(buf) .map_err(ImageError::from_decoding)?; // this is only necessary in case the buffer is not zeroed @@ -173,7 +216,9 @@ impl ImageDecoder for GifDecoder { let mut frame_buffer = vec![0; buffer_size]; self.limits.free_usize(buffer_size); - self.reader + let decoder = self.ensure_decoder()?; + + decoder .read_into_buffer(&mut frame_buffer[..]) .map_err(ImageError::from_decoding)?; @@ -211,26 +256,24 @@ impl ImageDecoder for GifDecoder { } } - Ok(()) + Ok(DecodedImageAttributes::default()) } fn icc_profile(&mut self) -> ImageResult>> { + let decoder = self.ensure_decoder()?; // Similar to XMP metadata - Ok(self.reader.icc_profile().map(Vec::from)) + Ok(decoder.icc_profile().map(Vec::from)) } fn xmp_metadata(&mut self) -> ImageResult>> { + let decoder = self.ensure_decoder()?; // XMP metadata must be part of the header which is read with `read_info`. - Ok(self.reader.xmp_metadata().map(Vec::from)) - } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(decoder.xmp_metadata().map(Vec::from)) } } struct GifFrameIterator { - reader: gif::Decoder, + decoder: Option>, width: u32, height: u32, @@ -244,13 +287,18 @@ struct GifFrameIterator { impl GifFrameIterator { fn new(decoder: GifDecoder) -> GifFrameIterator { - let (width, height) = decoder.dimensions(); + let (width, height) = match &decoder.decoder { + Some(decoder) => GifDecoder::layout_from_decoder(decoder).dimensions(), + // No more frames to get anyways. + None => (0, 0), + }; + let limits = decoder.limits.clone(); // intentionally ignore the background color for web compatibility GifFrameIterator { - reader: decoder.reader, + decoder: decoder.decoder, width, height, non_disposed_frame: None, @@ -291,8 +339,9 @@ impl Iterator for GifFrameIterator { let non_disposed_frame = self.non_disposed_frame.as_mut().unwrap(); // begin looping over each frame + let decoder = self.decoder.as_mut()?; - let frame = match self.reader.next_frame_info() { + let frame = match decoder.next_frame_info() { Ok(frame_info) => { if let Some(frame) = frame_info { FrameInfo::new_from_frame(frame) @@ -325,9 +374,10 @@ impl Iterator for GifFrameIterator { if let Err(e) = local_limits.reserve_buffer(frame.width, frame.height, COLOR_TYPE) { return Some(Err(e)); } + // Allocate the buffer now that the limits allowed it - let mut vec = vec![0; self.reader.buffer_size()]; - if let Err(err) = self.reader.read_into_buffer(&mut vec) { + let mut vec = vec![0; decoder.buffer_size()]; + if let Err(err) = decoder.read_into_buffer(&mut vec) { return Some(Err(ImageError::from_decoding(err))); } @@ -684,9 +734,10 @@ mod test { 0x77, 0xF5, 0x6D, 0x14, 0x00, 0x3B, ]; - let decoder = GifDecoder::new(Cursor::new(data)).unwrap(); - let mut buf = vec![0u8; decoder.total_bytes() as usize]; + let mut decoder = GifDecoder::new(Cursor::new(data)).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut buf = vec![0u8; layout.total_bytes() as usize]; assert!(decoder.read_image(&mut buf).is_ok()); } } diff --git a/src/codecs/hdr/decoder.rs b/src/codecs/hdr/decoder.rs index 7db0dd90a9..aa28dde80a 100644 --- a/src/codecs/hdr/decoder.rs +++ b/src/codecs/hdr/decoder.rs @@ -6,6 +6,7 @@ use std::{error, fmt}; use crate::error::{ DecodingError, ImageError, ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::DecodedImageAttributes; use crate::{ColorType, ImageDecoder, ImageFormat, Rgb}; /// Errors that can occur during decoding and parsing of a HDR image @@ -264,20 +265,21 @@ impl HdrDecoder { } impl ImageDecoder for HdrDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.meta.width, self.meta.height) - } - - fn color_type(&self) -> ColorType { - ColorType::Rgb32F + fn peek_layout(&mut self) -> ImageResult { + Ok(crate::ImageLayout { + width: self.meta.width, + height: self.meta.height, + color: ColorType::Rgb32F, + }) } - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); // Don't read anything if image is empty if self.meta.width == 0 || self.meta.height == 0 { - return Ok(()); + return Ok(DecodedImageAttributes::default()); } let mut scanline = vec![Default::default(); self.meta.width as usize]; @@ -295,11 +297,7 @@ impl ImageDecoder for HdrDecoder { } } - Ok(()) - } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes::default()) } } diff --git a/src/codecs/ico/decoder.rs b/src/codecs/ico/decoder.rs index 45113ac316..0b61bfd729 100644 --- a/src/codecs/ico/decoder.rs +++ b/src/codecs/ico/decoder.rs @@ -6,6 +6,7 @@ use crate::color::ColorType; use crate::error::{ DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::{DecodedImageAttributes, DecoderAttributes}; use crate::{ImageDecoder, ImageFormat}; use self::InnerDecoder::*; @@ -254,38 +255,39 @@ impl DirEntry { max_image_height: Some(self.real_height().into()), max_alloc: Some(256 * 256 * 4 * 2), // width * height * 4 bytes per pixel * safety factor of 2 }; - Ok(Png(Box::new(PngDecoder::with_limits(r, limits)?))) + Ok(Png(Box::new(PngDecoder::with_limits(r, limits)))) } else { Ok(Bmp(BmpDecoder::new_with_ico_format(r)?)) } } } +// We forward everything to png or bmp decoder. +#[deny(clippy::missing_trait_methods)] impl ImageDecoder for IcoDecoder { - fn dimensions(&self) -> (u32, u32) { - match self.inner_decoder { - Bmp(ref decoder) => decoder.dimensions(), - Png(ref decoder) => decoder.dimensions(), + fn attributes(&self) -> DecoderAttributes { + match &self.inner_decoder { + Bmp(decoder) => decoder.attributes(), + Png(decoder) => decoder.attributes(), } } - fn color_type(&self) -> ColorType { - match self.inner_decoder { - Bmp(ref decoder) => decoder.color_type(), - Png(ref decoder) => decoder.color_type(), + fn peek_layout(&mut self) -> ImageResult { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.peek_layout(), + Png(decoder) => decoder.peek_layout(), } } - fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); - match self.inner_decoder { + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + match &mut self.inner_decoder { Png(decoder) => { if self.selected_entry.image_length < PNG_SIGNATURE.len() as u32 { return Err(DecoderError::PngShorterThanHeader.into()); } // Check if the image dimensions match the ones in the image data. - let (width, height) = decoder.dimensions(); + let (width, height) = decoder.peek_layout()?.dimensions(); if !self.selected_entry.matches_dimensions(width, height) { return Err(DecoderError::ImageEntryDimensionMismatch { format: IcoEntryImageFormat::Png, @@ -300,14 +302,14 @@ impl ImageDecoder for IcoDecoder { // Embedded PNG images can only be of the 32BPP RGBA format. // https://blogs.msdn.microsoft.com/oldnewthing/20101022-00/?p=12473/ - if decoder.color_type() != ColorType::Rgba8 { + if decoder.peek_layout()?.color != ColorType::Rgba8 { return Err(DecoderError::PngNotRgba.into()); } decoder.read_image(buf) } - Bmp(mut decoder) => { - let (width, height) = decoder.dimensions(); + Bmp(decoder) => { + let (width, height) = decoder.peek_layout()?.dimensions(); if !self.selected_entry.matches_dimensions(width, height) { return Err(DecoderError::ImageEntryDimensionMismatch { format: IcoEntryImageFormat::Bmp, @@ -321,11 +323,11 @@ impl ImageDecoder for IcoDecoder { } // The ICO decoder needs an alpha channel to apply the AND mask. - if decoder.color_type() != ColorType::Rgba8 { + if decoder.peek_layout()?.color != ColorType::Rgba8 { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Bmp.into(), - UnsupportedErrorKind::Color(decoder.color_type().into()), + UnsupportedErrorKind::Color(decoder.peek_layout()?.color.into()), ), )); } @@ -367,10 +369,10 @@ impl ImageDecoder for IcoDecoder { } } - Ok(()) + Ok(DecodedImageAttributes::default()) } else if data_end == image_end { // accept images with no mask data - Ok(()) + Ok(DecodedImageAttributes::default()) } else { Err(DecoderError::InvalidDataSize.into()) } @@ -378,8 +380,62 @@ impl ImageDecoder for IcoDecoder { } } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + fn original_color_type(&mut self) -> ImageResult { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.original_color_type(), + Png(decoder) => decoder.original_color_type(), + } + } + + fn icc_profile(&mut self) -> ImageResult>> { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.icc_profile(), + Png(decoder) => decoder.icc_profile(), + } + } + + fn exif_metadata(&mut self) -> ImageResult>> { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.exif_metadata(), + Png(decoder) => decoder.exif_metadata(), + } + } + + fn xmp_metadata(&mut self) -> ImageResult>> { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.xmp_metadata(), + Png(decoder) => decoder.xmp_metadata(), + } + } + + fn iptc_metadata(&mut self) -> ImageResult>> { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.iptc_metadata(), + Png(decoder) => decoder.iptc_metadata(), + } + } + + fn set_limits(&mut self, limits: crate::Limits) -> ImageResult<()> { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.set_limits(limits), + Png(decoder) => decoder.set_limits(limits), + } + } + + fn more_images(&self) -> crate::io::SequenceControl { + // ICO files only provide a single image for now. This may change in the future, we might + // want to yield the others as thumbnails. + match &self.inner_decoder { + Bmp(decoder) => decoder.more_images(), + Png(decoder) => decoder.more_images(), + } + } + + fn finish(&mut self) -> ImageResult<()> { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.finish(), + Png(decoder) => decoder.finish(), + } } } @@ -441,8 +497,9 @@ mod test { 0x50, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x37, 0x61, ]; - let decoder = IcoDecoder::new(std::io::Cursor::new(&data)).unwrap(); - let mut buf = vec![0; usize::try_from(decoder.total_bytes()).unwrap()]; + let mut decoder = IcoDecoder::new(std::io::Cursor::new(&data)).unwrap(); + let bytes = decoder.peek_layout().unwrap().total_bytes(); + let mut buf = vec![0; usize::try_from(bytes).unwrap()]; assert!(decoder.read_image(&mut buf).is_err()); } } diff --git a/src/codecs/jpeg/decoder.rs b/src/codecs/jpeg/decoder.rs index 96c433f627..c2b0e6264a 100644 --- a/src/codecs/jpeg/decoder.rs +++ b/src/codecs/jpeg/decoder.rs @@ -7,7 +7,8 @@ use crate::color::ColorType; use crate::error::{ DecodingError, ImageError, ImageResult, LimitError, UnsupportedError, UnsupportedErrorKind, }; -use crate::metadata::Orientation; +use crate::io::decoder::DecodedMetadataHint; +use crate::io::{DecodedImageAttributes, DecoderAttributes}; use crate::{ImageDecoder, ImageFormat, Limits}; type ZuneColorSpace = zune_core::colorspace::ColorSpace; @@ -19,7 +20,6 @@ pub struct JpegDecoder { width: u16, height: u16, limits: Limits, - orientation: Option, // For API compatibility with the previous jpeg_decoder wrapper. // Can be removed later, which would be an API break. phantom: PhantomData, @@ -68,22 +68,36 @@ impl JpegDecoder { width, height, limits, - orientation: None, phantom: PhantomData, }) } } impl ImageDecoder for JpegDecoder { - fn dimensions(&self) -> (u32, u32) { - (u32::from(self.width), u32::from(self.height)) + fn attributes(&self) -> DecoderAttributes { + DecoderAttributes { + // As per specification, once we start with MCUs we can only have restarts. Also all + // our methods currently seek of their own accord anyways, it's just important to + // uphold this if we do not buffer the whole file. + icc: DecodedMetadataHint::InHeader, + exif: DecodedMetadataHint::InHeader, + xmp: DecodedMetadataHint::InHeader, + iptc: DecodedMetadataHint::InHeader, + ..DecoderAttributes::default() + } } - fn color_type(&self) -> ColorType { - ColorType::from_jpeg(self.orig_color_space) + fn peek_layout(&mut self) -> ImageResult { + Ok(crate::ImageLayout { + width: u32::from(self.width), + height: u32::from(self.height), + color: ColorType::from_jpeg(self.orig_color_space), + }) } fn icc_profile(&mut self) -> ImageResult>> { + // If this is changed to operate on a file, ensure all headers are done here and we have + // reached the MCU/RST portion of the stream. let options = zune_core::options::DecoderOptions::default() .set_strict_mode(false) .set_max_width(usize::MAX) @@ -95,6 +109,8 @@ impl ImageDecoder for JpegDecoder { } fn exif_metadata(&mut self) -> ImageResult>> { + // If this is changed to operate on a file, ensure all headers are done here and we have + // reached the MCU/RST portion of the stream. let options = zune_core::options::DecoderOptions::default() .set_strict_mode(false) .set_max_width(usize::MAX) @@ -104,16 +120,12 @@ impl ImageDecoder for JpegDecoder { decoder.decode_headers().map_err(ImageError::from_jpeg)?; let exif = decoder.exif().cloned(); - self.orientation = Some( - exif.as_ref() - .and_then(|exif| Orientation::from_exif_chunk(exif)) - .unwrap_or(Orientation::NoTransforms), - ); - Ok(exif) } fn xmp_metadata(&mut self) -> ImageResult>> { + // If this is changed to operate on a file, ensure all headers are done here and we have + // reached the MCU/RST portion of the stream. let options = zune_core::options::DecoderOptions::default() .set_strict_mode(false) .set_max_width(usize::MAX) @@ -126,6 +138,8 @@ impl ImageDecoder for JpegDecoder { } fn iptc_metadata(&mut self) -> ImageResult>> { + // If this is changed to operate on a file, ensure all headers are done here and we have + // reached the MCU/RST portion of the stream. let options = zune_core::options::DecoderOptions::default() .set_strict_mode(false) .set_max_width(usize::MAX) @@ -137,16 +151,10 @@ impl ImageDecoder for JpegDecoder { Ok(decoder.iptc().cloned()) } - fn orientation(&mut self) -> ImageResult { - // `exif_metadata` caches the orientation, so call it if `orientation` hasn't been set yet. - if self.orientation.is_none() { - let _ = self.exif_metadata()?; - } - Ok(self.orientation.unwrap()) - } + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; - fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { - let advertised_len = self.total_bytes(); + let advertised_len = layout.total_bytes(); let actual_len = buf.len() as u64; if actual_len != advertised_len { @@ -160,22 +168,21 @@ impl ImageDecoder for JpegDecoder { ))); } - let mut decoder = new_zune_decoder(&self.input, self.orig_color_space, self.limits); + let mut decoder = new_zune_decoder(&self.input, self.orig_color_space, &self.limits); decoder.decode_into(buf).map_err(ImageError::from_jpeg)?; - Ok(()) + + Ok(DecodedImageAttributes { + ..DecodedImageAttributes::default() + }) } fn set_limits(&mut self, limits: Limits) -> ImageResult<()> { limits.check_support(&crate::LimitSupport::default())?; - let (width, height) = self.dimensions(); + let (width, height) = self.peek_layout()?.dimensions(); limits.check_dimensions(width, height)?; self.limits = limits; Ok(()) } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) - } } impl ColorType { @@ -204,11 +211,11 @@ fn to_supported_color_space(orig: ZuneColorSpace) -> ZuneColorSpace { } } -fn new_zune_decoder( - input: &[u8], +fn new_zune_decoder<'input>( + input: &'input [u8], orig_color_space: ZuneColorSpace, - limits: Limits, -) -> zune_jpeg::JpegDecoder> { + limits: &Limits, +) -> zune_jpeg::JpegDecoder> { let target_color_space = to_supported_color_space(orig_color_space); let mut options = zune_core::options::DecoderOptions::default() .jpeg_set_out_colorspace(target_color_space) @@ -248,7 +255,14 @@ mod tests { #[test] fn test_exif_orientation() { let data = fs::read("tests/images/jpg/portrait_2.jpg").unwrap(); - let mut decoder = JpegDecoder::new(Cursor::new(data)).unwrap(); - assert_eq!(decoder.orientation().unwrap(), Orientation::FlipHorizontal); + let decoder = JpegDecoder::new(Cursor::new(data)).unwrap(); + + let mut reader = crate::ImageReader::from_decoder(Box::new(decoder)); + reader.decode().unwrap(); + + assert_eq!( + reader.last_attributes().orientation.unwrap(), + crate::metadata::Orientation::FlipHorizontal + ); } } diff --git a/src/codecs/jpeg/encoder.rs b/src/codecs/jpeg/encoder.rs index 45cb99af5e..7a297a3930 100644 --- a/src/codecs/jpeg/encoder.rs +++ b/src/codecs/jpeg/encoder.rs @@ -307,9 +307,9 @@ mod tests { use super::super::{JpegDecoder, JpegEncoder}; fn decode(encoded: &[u8]) -> Vec { - let decoder = JpegDecoder::new(Cursor::new(encoded)).expect("Could not decode image"); - - let mut decoded = vec![0; decoder.total_bytes() as usize]; + let mut decoder = JpegDecoder::new(Cursor::new(encoded)).expect("Could not decode image"); + let layout = decoder.peek_layout().unwrap(); + let mut decoded = vec![0; layout.total_bytes() as usize]; decoder .read_image(&mut decoded) .expect("Could not decode image"); diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 096d323f7d..edee12541d 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -22,7 +22,11 @@ //! - (chroma) subsampling not supported yet by the exr library use exr::prelude::*; -use crate::error::{DecodingError, ImageFormatHint, UnsupportedError, UnsupportedErrorKind}; +use crate::error::{ + DecodingError, ImageFormatHint, ParameterError, ParameterErrorKind, UnsupportedError, + UnsupportedErrorKind, +}; +use crate::io::DecodedImageAttributes; use crate::{ ColorType, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageResult, }; @@ -32,7 +36,7 @@ use std::io::{BufRead, Seek, Write}; /// An OpenEXR decoder. Immediately reads the meta data from the file. #[derive(Debug)] pub struct OpenExrDecoder { - exr_reader: exr::block::reader::Reader, + exr_reader: Option>, // select a header that is rgb and not deep header_index: usize, @@ -92,58 +96,77 @@ impl OpenExrDecoder { Ok(Self { alpha_preference, - exr_reader, + exr_reader: Some(exr_reader), header_index, alpha_present_in_file: has_alpha, }) } - - // does not leak exrs-specific meta data into public api, just does it for this module - fn selected_exr_header(&self) -> &exr::meta::header::Header { - &self.exr_reader.meta_data().headers[self.header_index] - } } impl ImageDecoder for OpenExrDecoder { - fn dimensions(&self) -> (u32, u32) { - let size = self - .selected_exr_header() - .shared_attributes - .display_window - .size; - (size.width() as u32, size.height() as u32) - } + fn peek_layout(&mut self) -> ImageResult { + let (width, height) = match &self.exr_reader { + Some(exr) => { + let header = &exr.meta_data().headers[self.header_index]; + let size = header.shared_attributes.display_window.size; + (size.width() as u32, size.height() as u32) + } + // We have already ended.. + None => { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::NoMoreData, + ))) + } + }; - fn color_type(&self) -> ColorType { let returns_alpha = self.alpha_preference.unwrap_or(self.alpha_present_in_file); - if returns_alpha { + let color = if returns_alpha { ColorType::Rgba32F } else { ColorType::Rgb32F - } + }; + + Ok(crate::ImageLayout { + width, + height, + color, + }) } - fn original_color_type(&self) -> ExtendedColorType { - if self.alpha_present_in_file { + fn original_color_type(&mut self) -> ImageResult { + let _ = self.peek_layout()?; + + Ok(if self.alpha_present_in_file { ExtendedColorType::Rgba32F } else { ExtendedColorType::Rgb32F - } + }) } // reads with or without alpha, depending on `self.alpha_preference` and `self.alpha_present_in_file` - fn read_image(self, unaligned_bytes: &mut [u8]) -> ImageResult<()> { - let _blocks_in_header = self.selected_exr_header().chunk_count as u64; - let channel_count = self.color_type().channel_count() as usize; + fn read_image(&mut self, unaligned_bytes: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; + let (width, height) = layout.dimensions(); + + let reader = self.exr_reader.take().ok_or_else(|| { + ImageError::Parameter(ParameterError::from_kind(ParameterErrorKind::NoMoreData)) + })?; - let display_window = self.selected_exr_header().shared_attributes.display_window; - let data_window_offset = - self.selected_exr_header().own_attributes.layer_position - display_window.position; + let _blocks_in_header = reader.headers()[self.header_index].chunk_count as u64; + let channel_count = layout.color.channel_count() as usize; + + let display_window = reader.headers()[self.header_index] + .shared_attributes + .display_window; + + let data_window_offset = reader.headers()[self.header_index] + .own_attributes + .layer_position + - display_window.position; { // check whether the buffer is large enough for the dimensions of the file - let (width, height) = self.dimensions(); - let bytes_per_pixel = self.color_type().bytes_per_pixel() as usize; + let bytes_per_pixel = usize::from(layout.color.bytes_per_pixel()); let expected_byte_count = (width as usize) .checked_mul(height as usize) .and_then(|size| size.checked_mul(bytes_per_pixel)); @@ -192,7 +215,7 @@ impl ImageDecoder for OpenExrDecoder { ) .first_valid_layer() // TODO select exact layer by self.header_index? .all_attributes() - .from_chunks(self.exr_reader) + .from_chunks(reader) .map_err(to_image_err)?; // TODO this copy is strictly not necessary, but the exr api is a little too simple for reading into a borrowed target slice @@ -202,11 +225,8 @@ impl ImageDecoder for OpenExrDecoder { unaligned_bytes.copy_from_slice(bytemuck::cast_slice( result.layer_data.channel_data.pixels.as_slice(), )); - Ok(()) - } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes::default()) } } @@ -385,9 +405,9 @@ mod test { /// Read the file from the specified path into an `Rgb32FImage`. fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult { - let decoder = OpenExrDecoder::with_alpha_preference(read, Some(false))?; - let (width, height) = decoder.dimensions(); - let buffer: Vec = decoder_to_vec(decoder)?; + let mut decoder = OpenExrDecoder::with_alpha_preference(read, Some(false))?; + let (width, height) = decoder.peek_layout()?.dimensions(); + let (buffer, _): (Vec, _) = decoder_to_vec(&mut decoder)?; ImageBuffer::from_raw(width, height, buffer) // this should be the only reason for the "from raw" call to fail, @@ -399,9 +419,9 @@ mod test { /// Read the file from the specified path into an `Rgba32FImage`. fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult { - let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?; - let (width, height) = decoder.dimensions(); - let buffer: Vec = decoder_to_vec(decoder)?; + let mut decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?; + let (width, height) = decoder.peek_layout()?.dimensions(); + let (buffer, _): (Vec, _) = decoder_to_vec(&mut decoder)?; ImageBuffer::from_raw(width, height, buffer) // this should be the only reason for the "from raw" call to fail, diff --git a/src/codecs/png.rs b/src/codecs/png.rs index c0ed7b2f40..892299ade4 100644 --- a/src/codecs/png.rs +++ b/src/codecs/png.rs @@ -4,23 +4,24 @@ //! //! # Related Links //! * - The PNG Specification - use std::borrow::Cow; use std::io::{BufRead, Seek, Write}; use png::{BlendOp, DeflateCompression, DisposeOp}; -use crate::animation::{Delay, Frame, Frames, Ratio}; -use crate::color::{Blend, ColorType, ExtendedColorType}; +use crate::animation::{Delay, Ratio}; +use crate::color::{ColorType, ExtendedColorType}; use crate::error::{ DecodingError, ImageError, ImageResult, LimitError, LimitErrorKind, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::decoder::DecodedMetadataHint; +use crate::io::{DecodedImageAttributes, DecoderAttributes, SequenceControl}; use crate::math::Rect; use crate::utils::vec_try_with_capacity; use crate::{ - AnimationDecoder, DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageDecoder, - ImageEncoder, ImageFormat, Limits, Luma, LumaA, Rgb, Rgba, RgbaImage, + DynamicImage, GenericImage, GenericImageView, ImageDecoder, ImageEncoder, ImageFormat, + ImageLayout, Limits, Luma, LumaA, Rgb, Rgba, }; // http://www.w3.org/TR/PNG-Structure.html @@ -31,27 +32,48 @@ const IPTC_KEYS: &[&str] = &["Raw profile type iptc", "Raw profile type 8bim"]; /// PNG decoder pub struct PngDecoder { + decoder: Option>, + reader: Option>, color_type: ColorType, - reader: png::Reader, limits: Limits, } impl PngDecoder { /// Creates a new decoder that decodes from the stream ```r``` - pub fn new(r: R) -> ImageResult> { + pub fn new(r: R) -> PngDecoder { Self::with_limits(r, Limits::no_limits()) } /// Creates a new decoder that decodes from the stream ```r``` with the given limits. - pub fn with_limits(r: R, limits: Limits) -> ImageResult> { - limits.check_support(&crate::LimitSupport::default())?; - + pub fn with_limits(r: R, limits: Limits) -> PngDecoder { let max_bytes = usize::try_from(limits.max_alloc.unwrap_or(u64::MAX)).unwrap_or(usize::MAX); let mut decoder = png::Decoder::new_with_limits(r, png::Limits { bytes: max_bytes }); decoder.set_ignore_text_chunk(false); + PngDecoder { + decoder: Some(decoder), + // We'll replace this once we have a reader. + color_type: ColorType::L8, + reader: None, + limits, + } + } + + fn ensure_reader_and_header(&mut self) -> ImageResult<&mut png::Reader> { + if self.reader.is_some() { + // We do this for borrow-checking issues, do not borrow self outside the conditional + // branch. So the None/Err case here is not reachable. + return self.reader.as_mut().ok_or_else(|| unreachable!()); + } + + let Some(mut decoder) = self.decoder.take() else { + return Err(reader_finished_already()); + }; + + self.limits.check_support(&crate::LimitSupport::default())?; + let info = decoder.read_header_info().map_err(ImageError::from_png)?; - limits.check_dimensions(info.width, info.height)?; + self.limits.check_dimensions(info.width, info.height)?; // By default the PNG decoder will scale 16 bpc to 8 bpc, so custom // transformations must be set. EXPAND preserves the default behavior @@ -59,6 +81,7 @@ impl PngDecoder { decoder.set_transformations(png::Transformations::EXPAND); let reader = decoder.read_info().map_err(ImageError::from_png)?; let (color_type, bits) = reader.output_color_type(); + let color_type = match (color_type, bits) { (png::ColorType::Grayscale, png::BitDepth::Eight) => ColorType::L8, (png::ColorType::Grayscale, png::BitDepth::Sixteen) => ColorType::L16, @@ -113,11 +136,8 @@ impl PngDecoder { } }; - Ok(PngDecoder { - color_type, - reader, - limits, - }) + self.color_type = color_type; + Ok(self.reader.insert(reader)) } /// Returns the gamma value of the image or None if no gamma value is indicated. @@ -129,8 +149,11 @@ impl PngDecoder { /// > capable of colour management are recommended to ignore the gAMA and cHRM chunks, and use /// > the values given above as if they had appeared in gAMA and cHRM chunks. pub fn gamma_value(&self) -> ImageResult> { - Ok(self - .reader + let Some(reader) = &self.reader else { + return Err(decoding_not_yet_started()); + }; + + Ok(reader .info() .source_gamma .map(|x| f64::from(x.into_scaled()) / 100_000.0)) @@ -147,7 +170,7 @@ impl PngDecoder { /// them will fail and an error will be returned instead of the frame. No further frames will /// be returned. pub fn apng(self) -> ImageResult> { - Ok(ApngDecoder::new(self)) + ApngDecoder::read_sequence_data(self) } /// Returns if the image contains an animation. @@ -157,7 +180,34 @@ impl PngDecoder { /// /// If a non-animated image is converted into an `ApngDecoder` then its iterator is empty. pub fn is_apng(&self) -> ImageResult { - Ok(self.reader.info().animation_control.is_some()) + let Some(reader) = &self.reader else { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + ))); + }; + + Ok(reader.info().animation_control.is_some()) + } +} + +fn attributes_from_info(info: &png::Info<'_>) -> DecodedImageAttributes { + let delay = info.frame_control().map(|fc| { + // PNG delays are rations in seconds. + let num = u32::from(fc.delay_num) * 1_000u32; + let denom = match fc.delay_den { + // The standard dictates to replace by 100 when the denominator is 0. + 0 => 100, + d => u32::from(d), + }; + + Delay::from_ratio(Ratio::new(num, denom)) + }); + + DecodedImageAttributes { + // We do not set x_offset and y_offset since the decoder performs composition according + // to Dispose and blend. For reading raw frames we'd pass the `fc.x_offset` here. + delay, + ..DecodedImageAttributes::default() } } @@ -168,31 +218,58 @@ fn unsupported_color(ect: ExtendedColorType) -> ImageError { )) } +fn decoding_not_yet_started() -> ImageError { + ImageError::Parameter(ParameterError::from_kind(ParameterErrorKind::NoMoreData)) +} + +fn decoding_started_already() -> ImageError { + ImageError::Parameter(ParameterError::from_kind(ParameterErrorKind::NoMoreData)) +} + +fn reader_finished_already() -> ImageError { + ImageError::Parameter(ParameterError::from_kind(ParameterErrorKind::NoMoreData)) +} + impl ImageDecoder for PngDecoder { - fn dimensions(&self) -> (u32, u32) { - self.reader.info().size() + fn peek_layout(&mut self) -> ImageResult { + let reader = self.ensure_reader_and_header()?; + let (width, height) = reader.info().size(); + + Ok(ImageLayout { + width, + height, + color: self.color_type, + }) } - fn color_type(&self) -> ColorType { - self.color_type + fn attributes(&self) -> DecoderAttributes { + DecoderAttributes { + // is any sort of iTXT chunk. + xmp: DecodedMetadataHint::AfterFinish, + // is any sort of iTXT chunk. + iptc: DecodedMetadataHint::AfterFinish, + // see iCCP chunk order. + icc: DecodedMetadataHint::InHeader, + // see eXIf chunk order. + exif: DecodedMetadataHint::InHeader, + ..DecoderAttributes::default() + } } fn icc_profile(&mut self) -> ImageResult>> { - Ok(self.reader.info().icc_profile.as_ref().map(|x| x.to_vec())) + let reader = self.ensure_reader_and_header()?; + Ok(reader.info().icc_profile.as_ref().map(|x| x.to_vec())) } fn exif_metadata(&mut self) -> ImageResult>> { - Ok(self - .reader - .info() - .exif_metadata - .as_ref() - .map(|x| x.to_vec())) + let reader = self.ensure_reader_and_header()?; + Ok(reader.info().exif_metadata.as_ref().map(|x| x.to_vec())) } fn xmp_metadata(&mut self) -> ImageResult>> { - if let Some(mut itx_chunk) = self - .reader + let reader = self.ensure_reader_and_header()?; + + if let Some(mut itx_chunk) = reader .info() .utf8_text .iter() @@ -205,12 +282,14 @@ impl ImageDecoder for PngDecoder { .map(|text| Some(text.as_bytes().to_vec())) .map_err(ImageError::from_png); } + Ok(None) } fn iptc_metadata(&mut self) -> ImageResult>> { - if let Some(mut text_chunk) = self - .reader + let reader = self.ensure_reader_and_header()?; + + if let Some(mut text_chunk) = reader .info() .compressed_latin1_text .iter() @@ -224,8 +303,7 @@ impl ImageDecoder for PngDecoder { .map_err(ImageError::from_png); } - if let Some(text_chunk) = self - .reader + if let Some(text_chunk) = reader .info() .uncompressed_latin1_text .iter() @@ -237,16 +315,18 @@ impl ImageDecoder for PngDecoder { Ok(None) } - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { use byteorder_lite::{BigEndian, ByteOrder, NativeEndian}; + let layout = self.peek_layout()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); - self.reader.next_frame(buf).map_err(ImageError::from_png)?; - // PNG images are big endian. For 16 bit per channel and larger types, - // the buffer may need to be reordered to native endianness per the - // contract of `read_image`. - // TODO: assumes equal channel bit depth. - let bpc = self.color_type().bytes_per_pixel() / self.color_type().channel_count(); + let reader = self.ensure_reader_and_header()?; + reader.next_frame(buf).map_err(ImageError::from_png)?; + + // PNG images are big endian. For 16 bit per channel and larger types, the buffer may need + // to be reordered to native endianness per the contract of `read_image`. Assumes equal + // depth which is the only supported output from `png` with our options. + let bpc = layout.color.bytes_per_pixel() / layout.color.channel_count(); match bpc { 1 => (), // No reodering necessary for u8 @@ -256,21 +336,28 @@ impl ImageDecoder for PngDecoder { }), _ => unreachable!(), } - Ok(()) - } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes { + ..DecodedImageAttributes::default() + }) } fn set_limits(&mut self, limits: Limits) -> ImageResult<()> { limits.check_support(&crate::LimitSupport::default())?; - let info = self.reader.info(); - limits.check_dimensions(info.width, info.height)?; - self.limits = limits; - // TODO: add `png::Reader::change_limits()` and call it here - // to also constrain the internal buffer allocations in the PNG crate - Ok(()) + + if let Some(decoder) = &mut self.decoder { + decoder.set_limits(png::Limits { + bytes: match limits.max_alloc { + None => usize::MAX, + Some(limit) => limit.try_into().unwrap_or(usize::MAX), + }, + }); + + self.limits = limits; + Ok(()) + } else { + Err(decoding_started_already()) + } } } @@ -284,11 +371,13 @@ impl ImageDecoder for PngDecoder { pub struct ApngDecoder { inner: PngDecoder, /// The current output buffer. - current: Option, + current: Option, /// The previous output buffer, used for dispose op previous. - previous: Option, + previous: Option, /// The dispose op of the current frame. dispose: DisposeOp, + /// Buffer to put the frame data which is to be composed onto the current frame. + raw_frame_buffer: Vec, /// The region to dispose of the previous frame. dispose_region: Option, @@ -299,79 +388,76 @@ pub struct ApngDecoder { } impl ApngDecoder { - fn new(inner: PngDecoder) -> Self { - let info = inner.reader.info(); - let remaining = match info.animation_control() { + fn read_sequence_data(mut inner: PngDecoder) -> ImageResult { + let reader = inner.ensure_reader_and_header()?; + let remaining = match reader.info().animation_control() { // The expected number of fcTL in the remaining image. Some(actl) => actl.num_frames, None => 0, }; + // If the IDAT has no fcTL then it is not part of the animation counted by // num_frames. All following fdAT chunks must be preceded by an fcTL - let has_thumbnail = info.frame_control.is_none(); - ApngDecoder { + let has_thumbnail = reader.info().frame_control.is_none(); + + Ok(ApngDecoder { inner, current: None, previous: None, + raw_frame_buffer: vec![], dispose: DisposeOp::Background, dispose_region: None, remaining, has_thumbnail, - } + }) } - // TODO: thumbnail(&mut self) -> Option> - /// Decode one subframe and overlay it on the canvas. - fn mix_next_frame(&mut self) -> Result, ImageError> { - // The iterator always produces RGBA8 images - const COLOR_TYPE: ColorType = ColorType::Rgba8; - - // Allocate the buffers, honoring the memory limits - let (width, height) = self.inner.dimensions(); - { - let limits = &mut self.inner.limits; - if self.previous.is_none() { - limits.reserve_buffer(width, height, COLOR_TYPE)?; - self.previous = Some(RgbaImage::new(width, height)); - } - - if self.current.is_none() { - limits.reserve_buffer(width, height, COLOR_TYPE)?; - self.current = Some(RgbaImage::new(width, height)); - } - } - + fn mix_next_frame( + &mut self, + buf: &mut [u8], + ) -> Result, ImageError> { // Remove this image from remaining. self.remaining = match self.remaining.checked_sub(1) { None => return Ok(None), Some(next) => next, }; + // Allocate the buffers, honoring the memory limits + let layout @ ImageLayout { + width, + height, + color, + .. + } = self.inner.peek_layout()?; + + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); + // Shorten ourselves to 0 in case of error. let remaining = self.remaining; self.remaining = 0; // Skip the thumbnail that is not part of the animation. if self.has_thumbnail { - // Clone the limits so that our one-off allocation that's destroyed after this scope doesn't persist - let mut limits = self.inner.limits.clone(); - - let buffer_size = self.inner.reader.output_buffer_size().ok_or_else(|| { - ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory)) - })?; - - limits.reserve_usize(buffer_size)?; - let mut buffer = vec![0; buffer_size]; - // TODO: add `png::Reader::change_limits()` and call it here - // to also constrain the internal buffer allocations in the PNG crate - self.inner - .reader - .next_frame(&mut buffer) - .map_err(ImageError::from_png)?; + let reader = self.inner.ensure_reader_and_header()?; + reader.next_frame(buf).map_err(ImageError::from_png)?; self.has_thumbnail = false; } + { + let limits = &mut self.inner.limits; + + if self.previous.is_none() { + limits.reserve_buffer(width, height, color)?; + self.previous = Some(DynamicImage::new(width, height, color)); + } + + if self.current.is_none() { + limits.reserve_buffer(width, height, color)?; + self.current = Some(DynamicImage::new(width, height, color)); + } + } + self.animatable_color_type()?; // We've initialized them earlier in this function @@ -379,7 +465,6 @@ impl ApngDecoder { let current = self.current.as_mut().unwrap(); // Dispose of the previous frame. - match self.dispose { DisposeOp::None => { previous.clone_from(current); @@ -397,9 +482,7 @@ impl ApngDecoder { } } else { // The first frame is always a background frame. - current.pixels_mut().for_each(|pixel| { - *pixel = Rgba::from([0, 0, 0, 0]); - }); + current.as_mut_bytes().fill(0); } } DisposeOp::Previous => { @@ -417,136 +500,131 @@ impl ApngDecoder { // and will be destroyed at the end of the scope. // Clone the limits so that any changes to them die with the allocations. let mut limits = self.inner.limits.clone(); + let reader = self.inner.ensure_reader_and_header()?; // Read next frame data. - let raw_frame_size = self.inner.reader.output_buffer_size().ok_or_else(|| { + let raw_frame_size = reader.output_buffer_size().ok_or_else(|| { ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory)) })?; - limits.reserve_usize(raw_frame_size)?; - let mut buffer = vec![0; raw_frame_size]; + // The frame size depends on frame control. If possible, we want to read it into the + // (temporary) output buffer that's been allocated for us anyways. + let buffer = if raw_frame_size <= buf.len() { + &mut buf[..raw_frame_size] + } else if raw_frame_size <= self.raw_frame_buffer.len() { + &mut self.raw_frame_buffer[..raw_frame_size] + } else { + limits.free_usize(self.raw_frame_buffer.len()); + limits.reserve_usize(raw_frame_size)?; + self.raw_frame_buffer.resize(raw_frame_size, 0); + &mut self.raw_frame_buffer[..] + }; + // TODO: add `png::Reader::change_limits()` and call it here // to also constrain the internal buffer allocations in the PNG crate - self.inner - .reader - .next_frame(&mut buffer) - .map_err(ImageError::from_png)?; - let info = self.inner.reader.info(); + reader.next_frame(buffer).map_err(ImageError::from_png)?; // Find out how to interpret the decoded frame. - let (width, height, px, py, blend); + let info = reader.info(); + let attributes = attributes_from_info(info); + + let (dispose_region, blend); match info.frame_control() { None => { - width = info.width; - height = info.height; - px = 0; - py = 0; + dispose_region = Rect { + width: info.width, + height: info.height, + x: 0, + y: 0, + }; + blend = BlendOp::Source; } Some(fc) => { - width = fc.width; - height = fc.height; - px = fc.x_offset; - py = fc.y_offset; + dispose_region = Rect { + width: fc.width, + height: fc.height, + x: fc.x_offset, + y: fc.y_offset, + }; + blend = fc.blend_op; self.dispose = fc.dispose_op; } } - self.dispose_region = Some(Rect { - x: px, - y: py, - width, - height, - }); - - // Turn the data into an rgba image proper. - limits.reserve_buffer(width, height, COLOR_TYPE)?; - let source = match self.inner.color_type { - ColorType::L8 => { - let image = ImageBuffer::, _>::from_raw(width, height, buffer).unwrap(); - DynamicImage::ImageLuma8(image).into_rgba8() - } - ColorType::La8 => { - let image = ImageBuffer::, _>::from_raw(width, height, buffer).unwrap(); - DynamicImage::ImageLumaA8(image).into_rgba8() - } - ColorType::Rgb8 => { - let image = ImageBuffer::, _>::from_raw(width, height, buffer).unwrap(); - DynamicImage::ImageRgb8(image).into_rgba8() - } - ColorType::Rgba8 => ImageBuffer::, _>::from_raw(width, height, buffer).unwrap(), - ColorType::L16 | ColorType::Rgb16 | ColorType::La16 | ColorType::Rgba16 => { - // TODO: to enable remove restriction in `animatable_color_type` method. - unreachable!("16-bit apng not yet support") - } - _ => unreachable!("Invalid png color"), - }; - // We've converted the raw frame to RGBA8 and disposed of the original allocation - limits.free_usize(raw_frame_size); + self.dispose_region = Some(dispose_region); match blend { BlendOp::Source => { - current - .copy_from(&source, px, py) - .expect("Invalid png image not detected in png"); + copy_pixel_bytes( + current.as_mut_bytes(), + &layout, + &buffer[..], + &dispose_region, + ); } BlendOp::Over => { // TODO: investigate speed, speed-ups, and bounds-checks. - for (x, y, p) in source.enumerate_pixels() { - current.get_pixel_mut(x + px, y + py).blend(p); - } + blend_pixel_bytes( + current.as_mut_bytes(), + &layout, + &buffer[..], + &dispose_region, + ) } } // Ok, we can proceed with actually remaining images. self.remaining = remaining; + // Return composited output buffer. + buf.copy_from_slice(current.as_bytes()); - Ok(Some(self.current.as_ref().unwrap())) + Ok(Some(attributes)) } fn animatable_color_type(&self) -> Result<(), ImageError> { match self.inner.color_type { - ColorType::L8 | ColorType::Rgb8 | ColorType::La8 | ColorType::Rgba8 => Ok(()), - // TODO: do not handle multi-byte colors. Remember to implement it in `mix_next_frame`. - ColorType::L16 | ColorType::Rgb16 | ColorType::La16 | ColorType::Rgba16 => { + ColorType::L8 + | ColorType::Rgb8 + | ColorType::La8 + | ColorType::Rgba8 + | ColorType::L16 + | ColorType::Rgb16 + | ColorType::La16 + | ColorType::Rgba16 => Ok(()), + _ => { + debug_assert!(false, "{:?} not a valid png color", self.inner.color_type); Err(unsupported_color(self.inner.color_type.into())) } - _ => unreachable!("{:?} not a valid png color", self.inner.color_type), } } } -impl<'a, R: BufRead + Seek + 'a> AnimationDecoder<'a> for ApngDecoder { - fn into_frames(self) -> Frames<'a> { - struct FrameIterator(ApngDecoder); +impl ImageDecoder for ApngDecoder { + fn attributes(&self) -> DecoderAttributes { + DecoderAttributes { + is_animated: true, + ..self.inner.attributes() + } + } - impl Iterator for FrameIterator { - type Item = ImageResult; + fn peek_layout(&mut self) -> ImageResult { + self.inner.peek_layout() + } - fn next(&mut self) -> Option { - let image = match self.0.mix_next_frame() { - Ok(Some(image)) => image.clone(), - Ok(None) => return None, - Err(err) => return Some(Err(err)), - }; + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + self.mix_next_frame(buf)? + .ok_or_else(reader_finished_already) + } - let info = self.0.inner.reader.info(); - let fc = info.frame_control().unwrap(); - // PNG delays are rations in seconds. - let num = u32::from(fc.delay_num) * 1_000u32; - let denom = match fc.delay_den { - // The standard dictates to replace by 100 when the denominator is 0. - 0 => 100, - d => u32::from(d), - }; - let delay = Delay::from_ratio(Ratio::new(num, denom)); - Some(Ok(Frame::from_parts(image, 0, 0, delay))) - } + fn more_images(&self) -> SequenceControl { + if self.remaining > 0 { + SequenceControl::MaybeMore + } else { + SequenceControl::None } - - Frames::new(Box::new(FrameIterator(self))) } } @@ -808,6 +886,67 @@ impl ImageError { } } +fn copy_pixel_bytes(bytes: &mut [u8], layout: &ImageLayout, from: &[u8], region: &Rect) { + let bpp = usize::from(layout.color.bytes_per_pixel()); + + let bytes_per_row = layout.width as usize * bpp; + let bytes_per_copy = region.width as usize * bpp; + + let start = region.x as usize * bpp + region.y as usize * bytes_per_row; + let from = &from[..region.height as usize * bytes_per_copy]; + + for (target, src) in bytes[start..] + .chunks_exact_mut(bytes_per_row) + .zip(from.chunks_exact(bytes_per_copy)) + { + target[..bytes_per_copy].copy_from_slice(src); + } +} + +fn blend_pixel_bytes(bytes: &mut [u8], layout: &ImageLayout, from: &[u8], region: &Rect) { + fn inner(bytes: &mut [u8], region: &[u8]) + where + P::Subpixel: bytemuck::Pod, + { + let target = bytemuck::cast_slice_mut::<_, P::Subpixel>(bytes); + let source = bytemuck::cast_slice::<_, P::Subpixel>(region); + + for (target, source) in target + .chunks_exact_mut(usize::from(P::CHANNEL_COUNT)) + .zip(source.chunks_exact(usize::from(P::CHANNEL_COUNT))) + { + P::from_slice_mut(target).blend(P::from_slice(source)); + } + } + + let row_transformer = match layout.color { + ColorType::L8 => inner::>, + ColorType::La8 => inner::>, + ColorType::Rgb8 => inner::>, + ColorType::Rgba8 => inner::>, + ColorType::L16 => inner::>, + ColorType::La16 => inner::>, + ColorType::Rgb16 => inner::>, + ColorType::Rgba16 => inner::>, + ColorType::Rgb32F | ColorType::Rgba32F => unreachable!("No floating point formats in PNG"), + }; + + let bpp = usize::from(layout.color.bytes_per_pixel()); + + let bytes_per_row = layout.width as usize * bpp; + let bytes_per_copy = region.width as usize * bpp; + + let start = region.x as usize * bpp + region.y as usize * bytes_per_row; + let from = &from[..region.height as usize * bytes_per_copy]; + + for (target, src) in bytes[start..] + .chunks_exact_mut(bytes_per_row) + .zip(from.chunks_exact(bytes_per_copy)) + { + row_transformer(&mut target[..bytes_per_copy], src); + } +} + #[cfg(test)] mod tests { use super::*; @@ -816,22 +955,26 @@ mod tests { #[test] fn ensure_no_decoder_off_by_one() { - let dec = PngDecoder::new(BufReader::new( + let mut dec = PngDecoder::new(BufReader::new( std::fs::File::open("tests/images/png/bugfixes/debug_triangle_corners_widescreen.png") .unwrap(), - )) - .expect("Unable to read PNG file (does it exist?)"); + )); - assert_eq![(2000, 1000), dec.dimensions()]; + let layout = dec + .peek_layout() + .expect("Unable to read PNG file (does it exist?)"); + + assert_eq![(2000, 1000), layout.dimensions()]; assert_eq![ ColorType::Rgb8, - dec.color_type(), + layout.color, "Image MUST have the Rgb8 format" ]; - let correct_bytes = decoder_to_vec(dec) - .expect("Unable to read file") + let (data, _) = decoder_to_vec(&mut dec).expect("Unable to read file"); + + let correct_bytes = data .bytes() .map(|x| x.expect("Unable to read byte")) .collect::>(); @@ -848,7 +991,9 @@ mod tests { .unwrap(); not_png[0] = 0; - let error = PngDecoder::new(Cursor::new(¬_png)).err().unwrap(); + let mut decoder = PngDecoder::new(Cursor::new(¬_png)); + let error = decoder.peek_layout().err().unwrap(); + let _ = error .source() .unwrap() diff --git a/src/codecs/pnm/decoder.rs b/src/codecs/pnm/decoder.rs index 2a67fc310c..213666be75 100644 --- a/src/codecs/pnm/decoder.rs +++ b/src/codecs/pnm/decoder.rs @@ -11,7 +11,7 @@ use crate::color::{ColorType, ExtendedColorType}; use crate::error::{ DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind, }; -use crate::io::ReadExt; +use crate::io::{DecodedImageAttributes, ReadExt}; use crate::{utils, ImageDecoder, ImageFormat}; use byteorder_lite::{BigEndian, ByteOrder, NativeEndian}; @@ -234,6 +234,40 @@ enum TupleType { RGBAlphaU16, } +impl TupleType { + fn expanded_color(self) -> ColorType { + match self { + TupleType::PbmBit => ColorType::L8, + TupleType::BWBit => ColorType::L8, + TupleType::BWAlphaBit => ColorType::La8, + TupleType::GrayU8 => ColorType::L8, + TupleType::GrayAlphaU8 => ColorType::La8, + TupleType::GrayU16 => ColorType::L16, + TupleType::GrayAlphaU16 => ColorType::La16, + TupleType::RGBU8 => ColorType::Rgb8, + TupleType::RGBAlphaU8 => ColorType::Rgba8, + TupleType::RGBU16 => ColorType::Rgb16, + TupleType::RGBAlphaU16 => ColorType::Rgba16, + } + } + + fn original_color(self) -> ExtendedColorType { + match self { + TupleType::PbmBit => ExtendedColorType::L1, + TupleType::BWBit => ExtendedColorType::L1, + TupleType::BWAlphaBit => ExtendedColorType::La1, + TupleType::GrayU8 => ExtendedColorType::L8, + TupleType::GrayAlphaU8 => ExtendedColorType::La8, + TupleType::GrayU16 => ExtendedColorType::L16, + TupleType::GrayAlphaU16 => ExtendedColorType::La16, + TupleType::RGBU8 => ExtendedColorType::Rgb8, + TupleType::RGBAlphaU8 => ExtendedColorType::Rgba8, + TupleType::RGBU16 => ExtendedColorType::Rgb16, + TupleType::RGBAlphaU16 => ExtendedColorType::Rgba16, + } + } +} + trait Sample { type Representation; @@ -280,25 +314,30 @@ impl PnmDecoder { _ => return Err(DecoderError::PnmMagicInvalid(magic).into()), }; - let decoder = match subtype { + // FIXME: PNM can contain multiple images. If it does they follow immediately after each + // other with no additional padding bytes. We do need to re-read the header. That structure + // would work nicely if we delay this read here to the internals of `peek_layout` instead + // then the whole decoder can indicate `is_sequence`. + let mut decoder = match subtype { PnmSubtype::Bitmap(enc) => PnmDecoder::read_bitmap_header(buffered_read, enc), PnmSubtype::Graymap(enc) => PnmDecoder::read_graymap_header(buffered_read, enc), PnmSubtype::Pixmap(enc) => PnmDecoder::read_pixmap_header(buffered_read, enc), PnmSubtype::ArbitraryMap => PnmDecoder::read_arbitrary_header(buffered_read), }?; + let layout = decoder.peek_layout()?; + if utils::check_dimension_overflow( - decoder.dimensions().0, - decoder.dimensions().1, - decoder.color_type().bytes_per_pixel(), + layout.width, + layout.height, + layout.color.bytes_per_pixel(), ) { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Pnm.into(), UnsupportedErrorKind::GenericFeature(format!( "Image dimensions ({}x{}) are too large", - decoder.dimensions().0, - decoder.dimensions().1 + layout.width, layout.height, )), ), )); @@ -591,44 +630,22 @@ trait HeaderReader: Read { impl HeaderReader for R where R: Read {} impl ImageDecoder for PnmDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.header.width(), self.header.height()) + fn peek_layout(&mut self) -> ImageResult { + Ok(crate::io::ImageLayout { + color: self.tuple.expanded_color(), + width: self.header.width(), + height: self.header.height(), + }) } - fn color_type(&self) -> ColorType { - match self.tuple { - TupleType::PbmBit => ColorType::L8, - TupleType::BWBit => ColorType::L8, - TupleType::BWAlphaBit => ColorType::La8, - TupleType::GrayU8 => ColorType::L8, - TupleType::GrayAlphaU8 => ColorType::La8, - TupleType::GrayU16 => ColorType::L16, - TupleType::GrayAlphaU16 => ColorType::La16, - TupleType::RGBU8 => ColorType::Rgb8, - TupleType::RGBAlphaU8 => ColorType::Rgba8, - TupleType::RGBU16 => ColorType::Rgb16, - TupleType::RGBAlphaU16 => ColorType::Rgba16, - } + fn original_color_type(&mut self) -> ImageResult { + Ok(self.tuple.original_color()) } - fn original_color_type(&self) -> ExtendedColorType { - match self.tuple { - TupleType::PbmBit => ExtendedColorType::L1, - TupleType::BWBit => ExtendedColorType::L1, - TupleType::BWAlphaBit => ExtendedColorType::La1, - TupleType::GrayU8 => ExtendedColorType::L8, - TupleType::GrayAlphaU8 => ExtendedColorType::La8, - TupleType::GrayU16 => ExtendedColorType::L16, - TupleType::GrayAlphaU16 => ExtendedColorType::La16, - TupleType::RGBU8 => ExtendedColorType::Rgb8, - TupleType::RGBAlphaU8 => ExtendedColorType::Rgba8, - TupleType::RGBU16 => ExtendedColorType::Rgb16, - TupleType::RGBAlphaU16 => ExtendedColorType::Rgba16, - } - } + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); match self.tuple { TupleType::PbmBit => self.read_samples::(1, buf), TupleType::BWBit => self.read_samples::(1, buf), @@ -641,11 +658,9 @@ impl ImageDecoder for PnmDecoder { TupleType::GrayAlphaU8 => self.read_samples::(2, buf), TupleType::GrayU16 => self.read_samples::(1, buf), TupleType::GrayAlphaU16 => self.read_samples::(2, buf), - } - } + }?; - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes::default()) } } @@ -972,13 +987,17 @@ TUPLTYPE BLACKANDWHITE # Comment line ENDHDR \x01\x00\x00\x01\x01\x00\x00\x01\x01\x00\x00\x01\x01\x00\x00\x01"; - let decoder = PnmDecoder::new(&pamdata[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::L8); - assert_eq!(decoder.original_color_type(), ExtendedColorType::L1); - assert_eq!(decoder.dimensions(), (4, 4)); + let mut decoder = PnmDecoder::new(&pamdata[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::L8); + assert_eq!( + decoder.original_color_type().unwrap(), + ExtendedColorType::L1 + ); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (4, 4)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!( image, @@ -1018,13 +1037,17 @@ TUPLTYPE BLACKANDWHITE_ALPHA # Comment line ENDHDR \x01\x00\x00\x01\x01\x00\x00\x01"; - let decoder = PnmDecoder::new(&pamdata[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::La8); - assert_eq!(decoder.original_color_type(), ExtendedColorType::La1); - assert_eq!(decoder.dimensions(), (2, 2)); + let mut decoder = PnmDecoder::new(&pamdata[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::La8); + assert_eq!( + decoder.original_color_type().unwrap(), + ExtendedColorType::La1 + ); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (2, 2)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!(image, vec![0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,]); match PnmDecoder::new(&pamdata[..]).unwrap().into_inner() { @@ -1058,12 +1081,13 @@ TUPLTYPE GRAYSCALE # Comment line ENDHDR \xde\xad\xbe\xef\xde\xad\xbe\xef\xde\xad\xbe\xef\xde\xad\xbe\xef"; - let decoder = PnmDecoder::new(&pamdata[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::L8); - assert_eq!(decoder.dimensions(), (4, 4)); + let mut decoder = PnmDecoder::new(&pamdata[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::L8); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (4, 4)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!( image, @@ -1103,13 +1127,17 @@ TUPLTYPE GRAYSCALE_ALPHA # Comment line ENDHDR \xdc\xba\x32\x10\xdc\xba\x32\x10"; - let decoder = PnmDecoder::new(&pamdata[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::La16); - assert_eq!(decoder.original_color_type(), ExtendedColorType::La16); - assert_eq!(decoder.dimensions(), (2, 1)); + let mut decoder = PnmDecoder::new(&pamdata[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::La16); + assert_eq!( + decoder.original_color_type().unwrap(), + ExtendedColorType::La16 + ); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (2, 1)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!( image, @@ -1152,12 +1180,13 @@ WIDTH 2 HEIGHT 2 ENDHDR \xde\xad\xbe\xef\xde\xad\xbe\xef\xde\xad\xbe\xef"; - let decoder = PnmDecoder::new(&pamdata[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::Rgb8); - assert_eq!(decoder.dimensions(), (2, 2)); + let mut decoder = PnmDecoder::new(&pamdata[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::Rgb8); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (2, 2)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!( image, @@ -1194,13 +1223,17 @@ TUPLTYPE RGB_ALPHA # Comment line ENDHDR \x00\x01\x02\x03\x0a\x0b\x0c\x0d\x05\x06\x07\x08"; - let decoder = PnmDecoder::new(&pamdata[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::Rgba8); - assert_eq!(decoder.original_color_type(), ExtendedColorType::Rgba8); - assert_eq!(decoder.dimensions(), (1, 3)); + let mut decoder = PnmDecoder::new(&pamdata[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::Rgba8); + assert_eq!( + decoder.original_color_type().unwrap(), + ExtendedColorType::Rgba8 + ); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (1, 3)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!(image, b"\x00\x11\x22\x33\xaa\xbb\xcc\xdd\x55\x66\x77\x88",); match PnmDecoder::new(&pamdata[..]).unwrap().into_inner() { @@ -1227,15 +1260,20 @@ ENDHDR // The data contains two rows of the image (each line is padded to the full byte). For // comments on its format, see documentation of `impl SampleType for PbmBit`. let pbmbinary = [&b"P4 6 2\n"[..], &[0b0110_1100_u8, 0b1011_0111]].concat(); - let decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::L8); - assert_eq!(decoder.original_color_type(), ExtendedColorType::L1); - assert_eq!(decoder.dimensions(), (6, 2)); + let mut decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::L8); + assert_eq!( + decoder.original_color_type().unwrap(), + ExtendedColorType::L1 + ); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (6, 2)); assert_eq!( decoder.subtype(), PnmSubtype::Bitmap(SampleEncoding::Binary) ); - let mut image = vec![0; decoder.total_bytes() as usize]; + + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!(image, vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0]); match PnmDecoder::new(&pbmbinary[..]).unwrap().into_inner() { @@ -1275,8 +1313,9 @@ ENDHDR let pbmbinary = BufReader::new(FailRead(Cursor::new(b"P1 1 1\n"))); - let decoder = PnmDecoder::new(pbmbinary).unwrap(); - let mut image = vec![0; decoder.total_bytes() as usize]; + let mut decoder = PnmDecoder::new(pbmbinary).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder .read_image(&mut image) .expect_err("Image is malformed"); @@ -1288,13 +1327,17 @@ ENDHDR // comments on its format, see documentation of `impl SampleType for PbmBit`. Tests all // whitespace characters that should be allowed (the 6 characters according to POSIX). let pbmbinary = b"P1 6 2\n 0 1 1 0 1 1\n1 0 1 1 0\t\n\x0b\x0c\r1"; - let decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::L8); - assert_eq!(decoder.original_color_type(), ExtendedColorType::L1); - assert_eq!(decoder.dimensions(), (6, 2)); + let mut decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::L8); + assert_eq!( + decoder.original_color_type().unwrap(), + ExtendedColorType::L1 + ); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (6, 2)); assert_eq!(decoder.subtype(), PnmSubtype::Bitmap(SampleEncoding::Ascii)); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!(image, vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0]); match PnmDecoder::new(&pbmbinary[..]).unwrap().into_inner() { @@ -1320,13 +1363,17 @@ ENDHDR // it is completely within specification for the ascii data not to contain separating // whitespace for the pbm format or any mix. let pbmbinary = b"P1 6 2\n011011101101"; - let decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::L8); - assert_eq!(decoder.original_color_type(), ExtendedColorType::L1); - assert_eq!(decoder.dimensions(), (6, 2)); + let mut decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::L8); + assert_eq!( + decoder.original_color_type().unwrap(), + ExtendedColorType::L1 + ); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (6, 2)); assert_eq!(decoder.subtype(), PnmSubtype::Bitmap(SampleEncoding::Ascii)); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!(image, vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0]); match PnmDecoder::new(&pbmbinary[..]).unwrap().into_inner() { @@ -1352,14 +1399,16 @@ ENDHDR // comments on its format, see documentation of `impl SampleType for PbmBit`. let elements = (0..16).collect::>(); let pbmbinary = [&b"P5 4 4 255\n"[..], &elements].concat(); - let decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::L8); - assert_eq!(decoder.dimensions(), (4, 4)); + let mut decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::L8); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (4, 4)); assert_eq!( decoder.subtype(), PnmSubtype::Graymap(SampleEncoding::Binary) ); - let mut image = vec![0; decoder.total_bytes() as usize]; + + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!(image, elements); match PnmDecoder::new(&pbmbinary[..]).unwrap().into_inner() { @@ -1385,14 +1434,16 @@ ENDHDR // The data contains two rows of the image (each line is padded to the full byte). For // comments on its format, see documentation of `impl SampleType for PbmBit`. let pbmbinary = b"P2 4 4 255\n 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15"; - let decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); - assert_eq!(decoder.color_type(), ColorType::L8); - assert_eq!(decoder.dimensions(), (4, 4)); + let mut decoder = PnmDecoder::new(&pbmbinary[..]).unwrap(); + assert_eq!(decoder.peek_layout().unwrap().color, ColorType::L8); + assert_eq!(decoder.peek_layout().unwrap().dimensions(), (4, 4)); assert_eq!( decoder.subtype(), PnmSubtype::Graymap(SampleEncoding::Ascii) ); - let mut image = vec![0; decoder.total_bytes() as usize]; + + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!(image, (0..16).collect::>()); match PnmDecoder::new(&pbmbinary[..]).unwrap().into_inner() { @@ -1416,8 +1467,9 @@ ENDHDR #[test] fn ppm_ascii() { let ascii = b"P3 1 1 2000\n0 1000 2000"; - let decoder = PnmDecoder::new(&ascii[..]).unwrap(); - let mut image = vec![0; decoder.total_bytes() as usize]; + let mut decoder = PnmDecoder::new(&ascii[..]).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!( image, @@ -1459,16 +1511,18 @@ ENDHDR ]; // Validate: we have a header. Note: we might already calculate that this will fail but // then we could not return information about the header to the caller. - let decoder = PnmDecoder::new(&data[..]).unwrap(); - let mut image = vec![0; decoder.total_bytes() as usize]; + let mut decoder = PnmDecoder::new(&data[..]).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; let _ = decoder.read_image(&mut image); } #[test] fn data_too_short() { let data = b"P3 16 16 1\n"; - let decoder = PnmDecoder::new(&data[..]).unwrap(); - let mut image = vec![0; decoder.total_bytes() as usize]; + let mut decoder = PnmDecoder::new(&data[..]).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; let _ = decoder.read_image(&mut image).unwrap_err(); } @@ -1488,8 +1542,9 @@ ENDHDR #[test] fn leading_zeros() { let data = b"P2 03 00000000000002 00100\n011 22 033\n44 055 66\n"; - let decoder = PnmDecoder::new(&data[..]).unwrap(); - let mut image = vec![0; decoder.total_bytes() as usize]; + let mut decoder = PnmDecoder::new(&data[..]).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; assert!(decoder.read_image(&mut image).is_ok()); } @@ -1502,7 +1557,7 @@ ENDHDR #[test] fn header_large_dimension() { let data = b"P4 1 01234567890\n"; - let decoder = PnmDecoder::new(&data[..]).unwrap(); - assert!(decoder.dimensions() == (1, 1234567890)); + let mut decoder = PnmDecoder::new(&data[..]).unwrap(); + assert!(decoder.peek_layout().unwrap().dimensions() == (1, 1234567890)); } } diff --git a/src/codecs/pnm/mod.rs b/src/codecs/pnm/mod.rs index ef4efcd5d3..06003062fa 100644 --- a/src/codecs/pnm/mod.rs +++ b/src/codecs/pnm/mod.rs @@ -36,14 +36,14 @@ mod tests { } let (header, loaded_color, loaded_image) = { - let decoder = PnmDecoder::new(&encoded_buffer[..]).unwrap(); - let color_type = decoder.color_type(); - let mut image = vec![0; decoder.total_bytes() as usize]; + let mut decoder = PnmDecoder::new(&encoded_buffer[..]).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder .read_image(&mut image) .expect("Failed to decode the image"); let (_, header) = PnmDecoder::new(&encoded_buffer[..]).unwrap().into_inner(); - (header, color_type, image) + (header, layout.color, image) }; assert_eq!(header.width(), width); @@ -69,14 +69,14 @@ mod tests { } let (header, loaded_color, loaded_image) = { - let decoder = PnmDecoder::new(&encoded_buffer[..]).unwrap(); - let color_type = decoder.color_type(); - let mut image = vec![0; decoder.total_bytes() as usize]; + let mut decoder = PnmDecoder::new(&encoded_buffer[..]).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder .read_image(&mut image) .expect("Failed to decode the image"); let (_, header) = PnmDecoder::new(&encoded_buffer[..]).unwrap().into_inner(); - (header, color_type, image) + (header, layout.color, image) }; assert_eq!(header.width(), width); @@ -97,14 +97,14 @@ mod tests { } let (header, loaded_color, loaded_image) = { - let decoder = PnmDecoder::new(&encoded_buffer[..]).unwrap(); - let color_type = decoder.color_type(); - let mut image = vec![0; decoder.total_bytes() as usize]; + let mut decoder = PnmDecoder::new(&encoded_buffer[..]).unwrap(); + let layout = decoder.peek_layout().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder .read_image(&mut image) .expect("Failed to decode the image"); let (_, header) = PnmDecoder::new(&encoded_buffer[..]).unwrap().into_inner(); - (header, color_type, image) + (header, layout.color, image) }; let mut buffer_u8 = vec![0; buffer.len() * 2]; diff --git a/src/codecs/qoi.rs b/src/codecs/qoi.rs index ab76cb75ad..a4c8c67c2b 100644 --- a/src/codecs/qoi.rs +++ b/src/codecs/qoi.rs @@ -1,6 +1,7 @@ //! Decoding and encoding of QOI images use crate::error::{DecodingError, EncodingError, UnsupportedError, UnsupportedErrorKind}; +use crate::io::DecodedImageAttributes; use crate::{ ColorType, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageResult, }; @@ -23,24 +24,22 @@ where } impl ImageDecoder for QoiDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.decoder.header().width, self.decoder.header().height) + fn peek_layout(&mut self) -> ImageResult { + let header = self.decoder.header(); + + Ok(crate::ImageLayout { + width: header.width, + height: header.height, + color: match header.channels { + qoi::Channels::Rgb => ColorType::Rgb8, + qoi::Channels::Rgba => ColorType::Rgba8, + }, + }) } - fn color_type(&self) -> ColorType { - match self.decoder.header().channels { - qoi::Channels::Rgb => ColorType::Rgb8, - qoi::Channels::Rgba => ColorType::Rgba8, - } - } - - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { self.decoder.decode_to_buf(buf).map_err(decoding_error)?; - Ok(()) - } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes::default()) } } @@ -111,10 +110,11 @@ mod tests { #[test] fn decode_test_image() { - let decoder = QoiDecoder::new(File::open("tests/images/qoi/basic-test.qoi").unwrap()) + let mut decoder = QoiDecoder::new(File::open("tests/images/qoi/basic-test.qoi").unwrap()) .expect("Unable to read QOI file"); - assert_eq!((5, 5), decoder.dimensions()); - assert_eq!(ColorType::Rgba8, decoder.color_type()); + let layout = decoder.peek_layout().unwrap(); + assert_eq!((5, 5), layout.dimensions()); + assert_eq!(ColorType::Rgba8, layout.color); } } diff --git a/src/codecs/tga/decoder.rs b/src/codecs/tga/decoder.rs index ef2f173370..c5b41c9f50 100644 --- a/src/codecs/tga/decoder.rs +++ b/src/codecs/tga/decoder.rs @@ -1,6 +1,6 @@ use super::header::{Header, ImageType, ALPHA_BIT_MASK}; -use crate::error::DecodingError; -use crate::io::ReadExt; +use crate::error::{DecodingError, LimitError, LimitErrorKind}; +use crate::io::{DecodedImageAttributes, ReadExt}; use crate::utils::vec_try_with_capacity; use crate::{ color::{ColorType, ExtendedColorType}, @@ -389,21 +389,29 @@ impl TgaDecoder { } impl ImageDecoder for TgaDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.width as u32, self.height as u32) - } + fn peek_layout(&mut self) -> ImageResult { + fn try_dimensions(value: usize) -> ImageResult { + value + .try_into() + .map_err(|_| LimitError::from_kind(LimitErrorKind::DimensionError)) + .map_err(ImageError::Limits) + } - fn color_type(&self) -> ColorType { - self.color_type + Ok(crate::ImageLayout { + width: try_dimensions(self.width)?, + height: try_dimensions(self.height)?, + color: self.color_type, + }) } - fn original_color_type(&self) -> ExtendedColorType { - self.original_color_type - .unwrap_or_else(|| self.color_type().into()) + fn original_color_type(&mut self) -> ImageResult { + let fallback = self.peek_layout()?.color; + Ok(self.original_color_type.unwrap_or_else(|| fallback.into())) } - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); // Decode the raw data // @@ -453,10 +461,6 @@ impl ImageDecoder for TgaDecoder { self.reverse_encoding_in_output(buf); - Ok(()) - } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes::default()) } } diff --git a/src/codecs/tga/encoder.rs b/src/codecs/tga/encoder.rs index 496e1e5647..01ded7012e 100644 --- a/src/codecs/tga/encoder.rs +++ b/src/codecs/tga/encoder.rs @@ -367,9 +367,11 @@ mod tests { .encode(image, width, height, c) .expect("could not encode image"); } - let decoder = TgaDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); - let mut buf = vec![0; decoder.total_bytes() as usize]; + let mut decoder = + TgaDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); + let layout = decoder.peek_layout().unwrap(); + let mut buf = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut buf).expect("failed to decode"); buf } @@ -479,9 +481,10 @@ mod tests { .expect("could not encode image"); } - let decoder = TgaDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); - - let mut buf = vec![0; decoder.total_bytes() as usize]; + let mut decoder = + TgaDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); + let layout = decoder.peek_layout().unwrap(); + let mut buf = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut buf).expect("failed to decode"); buf } diff --git a/src/codecs/tiff.rs b/src/codecs/tiff.rs index b475b611ff..022e08f8f3 100644 --- a/src/codecs/tiff.rs +++ b/src/codecs/tiff.rs @@ -17,6 +17,8 @@ use crate::error::{ DecodingError, EncodingError, ImageError, ImageResult, LimitError, LimitErrorKind, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::decoder::DecodedMetadataHint; +use crate::io::{DecodedImageAttributes, DecoderAttributes}; use crate::metadata::Orientation; use crate::{utils, ImageDecoder, ImageEncoder, ImageFormat}; @@ -27,12 +29,24 @@ pub struct TiffDecoder where R: BufRead + Seek, { + info: ImageState, + /// The individual allocations attribute to parts of the decoder. + limits: tiff::decoder::Limits, + // We only use an Option here so we can call with_limits on the decoder without moving. + inner: Option>, +} + +enum ImageState { + Initial, + At(ImageInfo), + Consumed, +} + +#[derive(Clone, Copy)] +struct ImageInfo { dimensions: (u32, u32), color_type: ColorType, original_color_type: ExtendedColorType, - - // We only use an Option here so we can call with_limits on the decoder without moving. - inner: Option>, } impl TiffDecoder @@ -41,12 +55,52 @@ where { /// Create a new `TiffDecoder`. pub fn new(r: R) -> Result, ImageError> { - let mut inner = Decoder::new(r).map_err(ImageError::from_tiff_decode)?; + let inner = Decoder::new(r).map_err(ImageError::from_tiff_decode)?; - let dimensions = inner.dimensions().map_err(ImageError::from_tiff_decode)?; - let tiff_color_type = inner.colortype().map_err(ImageError::from_tiff_decode)?; + Ok(TiffDecoder { + info: ImageState::Initial, + limits: tiff::decoder::Limits::default(), + inner: Some(inner), + }) + } + + fn peek_info(&mut self) -> ImageResult { + let Some(reader) = &mut self.inner else { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + ))); + }; + + // This image may have been consumed, we should advance. + if let ImageState::Consumed = self.info { + reader.next_image().map_err(ImageError::from_tiff_decode)?; + self.info = ImageState::Initial; + } + + if let ImageState::Initial = self.info { + self.reset_info_from_current_image()?; + } + + let ImageState::At(info) = self.info else { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + ))); + }; - match inner.find_tag_unsigned_vec::(Tag::SampleFormat) { + Ok(info) + } + + fn reset_info_from_current_image(&mut self) -> ImageResult<()> { + let Some(reader) = &mut self.inner else { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + ))); + }; + + let dimensions = reader.dimensions().map_err(ImageError::from_tiff_decode)?; + let tiff_color_type = reader.colortype().map_err(ImageError::from_tiff_decode)?; + + match reader.find_tag_unsigned_vec::(Tag::SampleFormat) { Ok(Some(sample_formats)) => { for format in sample_formats { check_sample_format(format, tiff_color_type)?; @@ -56,7 +110,7 @@ where Err(other) => return Err(ImageError::from_tiff_decode(other)), } - let planar_config = inner + let planar_config = reader .find_tag(Tag::PlanarConfiguration) .map(|res| res.and_then(|r| r.into_u16().ok()).unwrap_or_default()) .unwrap_or_default(); @@ -113,24 +167,53 @@ where _ => color_type.into(), }; - Ok(TiffDecoder { + self.info = ImageState::At(ImageInfo { dimensions, color_type, original_color_type, - inner: Some(inner), - }) + }); + + self.redistribute_limits(); + + Ok(()) + } + + fn redistribute_limits(&mut self) { + let ImageState::At(info) = &self.info else { + return; + }; + + if self.inner.is_none() { + return; + } + + let max_alloc = (self.limits.decoding_buffer_size as u64) + .saturating_add(self.limits.intermediate_buffer_size as u64); + + let max_intermediate_alloc = max_alloc.saturating_sub(info.total_bytes_buffer()); + let mut tiff_limits: tiff::decoder::Limits = Default::default(); + tiff_limits.decoding_buffer_size = + usize::try_from(max_alloc - max_intermediate_alloc).unwrap_or(usize::MAX); + tiff_limits.intermediate_buffer_size = + usize::try_from(max_intermediate_alloc).unwrap_or(usize::MAX); + tiff_limits.ifd_value_size = tiff_limits.intermediate_buffer_size; + + self.inner = Some(self.inner.take().unwrap().with_limits(tiff_limits)); } +} +impl ImageInfo { // The buffer can be larger for CMYK than the RGB output fn total_bytes_buffer(&self) -> u64 { - let dimensions = self.dimensions(); - let total_pixels = u64::from(dimensions.0) * u64::from(dimensions.1); + let (width, height) = self.dimensions; + let total_pixels = u64::from(width) * u64::from(height); let bytes_per_pixel = match self.original_color_type { ExtendedColorType::Cmyk8 => 4, ExtendedColorType::Cmyk16 => 8, - _ => u64::from(self.color_type().bytes_per_pixel()), + _ => u64::from(self.color_type.bytes_per_pixel()), }; + total_pixels.saturating_mul(bytes_per_pixel) } } @@ -249,16 +332,27 @@ impl Read for TiffReader { } impl ImageDecoder for TiffDecoder { - fn dimensions(&self) -> (u32, u32) { - self.dimensions + fn attributes(&self) -> DecoderAttributes { + DecoderAttributes { + // is any sort of iTXT chunk. + xmp: DecodedMetadataHint::PerImage, + icc: DecodedMetadataHint::PerImage, + exif: DecodedMetadataHint::PerImage, + // not provided above. + iptc: DecodedMetadataHint::None, + is_sequence: true, + ..DecoderAttributes::default() + } } - fn color_type(&self) -> ColorType { - self.color_type - } + fn peek_layout(&mut self) -> ImageResult { + let info = self.peek_info()?; - fn original_color_type(&self) -> ExtendedColorType { - self.original_color_type + Ok(crate::ImageLayout { + width: info.dimensions.0, + height: info.dimensions.1, + color: info.color_type, + }) } fn icc_profile(&mut self) -> ImageResult>> { @@ -287,61 +381,61 @@ impl ImageDecoder for TiffDecoder { .map_err(ImageError::from_tiff_decode) } - fn orientation(&mut self) -> ImageResult { - if let Some(decoder) = &mut self.inner { - Ok(decoder - .find_tag(Tag::Orientation) - .map_err(ImageError::from_tiff_decode)? - .and_then(|v| Orientation::from_exif(v.into_u16().ok()?.min(255) as u8)) - .unwrap_or(Orientation::NoTransforms)) - } else { - Ok(Orientation::NoTransforms) - } - } - fn set_limits(&mut self, limits: crate::Limits) -> ImageResult<()> { limits.check_support(&crate::LimitSupport::default())?; - let (width, height) = self.dimensions(); + let reserved = match self.info { + ImageState::At(info) => info, + // Construct a dummy info that did not consume any memory. + _ => ImageInfo { + dimensions: (0, 0), + color_type: ColorType::L8, + original_color_type: ExtendedColorType::L8, + }, + }; + + let (width, height) = reserved.dimensions; limits.check_dimensions(width, height)?; - let max_alloc = limits.max_alloc.unwrap_or(u64::MAX); - let max_intermediate_alloc = max_alloc.saturating_sub(self.total_bytes_buffer()); + let max_alloc = limits + .max_alloc + .and_then(|n| usize::try_from(n).ok()) + .unwrap_or(usize::MAX); - let mut tiff_limits: tiff::decoder::Limits = Default::default(); - tiff_limits.decoding_buffer_size = - usize::try_from(max_alloc - max_intermediate_alloc).unwrap_or(usize::MAX); - tiff_limits.intermediate_buffer_size = - usize::try_from(max_intermediate_alloc).unwrap_or(usize::MAX); - tiff_limits.ifd_value_size = tiff_limits.intermediate_buffer_size; - self.inner = Some(self.inner.take().unwrap().with_limits(tiff_limits)); + self.limits.decoding_buffer_size = max_alloc; + self.limits.intermediate_buffer_size = 0; + self.redistribute_limits(); Ok(()) } - fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let info = self.peek_info()?; + let layout = self.peek_layout()?; - match self - .inner - .unwrap() - .read_image() - .map_err(ImageError::from_tiff_decode)? - { - DecodingResult::U8(v) if self.original_color_type == ExtendedColorType::Cmyk8 => { + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); + + let Some(reader) = &mut self.inner else { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + ))); + }; + + match reader.read_image().map_err(ImageError::from_tiff_decode)? { + DecodingResult::U8(v) if info.original_color_type == ExtendedColorType::Cmyk8 => { let mut out_cur = Cursor::new(buf); for cmyk in v.chunks_exact(4) { out_cur.write_all(&cmyk_to_rgb(cmyk))?; } } - DecodingResult::U16(v) if self.original_color_type == ExtendedColorType::Cmyk16 => { + DecodingResult::U16(v) if info.original_color_type == ExtendedColorType::Cmyk16 => { let mut out_cur = Cursor::new(buf); for cmyk in v.chunks_exact(4) { out_cur.write_all(bytemuck::cast_slice(&cmyk_to_rgb16(cmyk)))?; } } - DecodingResult::U8(v) if self.original_color_type == ExtendedColorType::L1 => { - let width = self.dimensions.0; + DecodingResult::U8(v) if info.original_color_type == ExtendedColorType::L1 => { + let width = info.dimensions.0; let row_bytes = width.div_ceil(8); for (in_row, out_row) in v @@ -383,11 +477,42 @@ impl ImageDecoder for TiffDecoder { } DecodingResult::F16(_) => unreachable!(), } - Ok(()) + + let orientation = reader + .find_tag(Tag::Orientation) + .map_err(ImageError::from_tiff_decode)? + .and_then(|v| Orientation::from_exif(v.into_u16().ok()?.min(255) as u8)); + + // Indicate to advance. + self.info = ImageState::Consumed; + + Ok(DecodedImageAttributes { + orientation, + ..DecodedImageAttributes::default() + }) + } + + fn original_color_type(&mut self) -> ImageResult { + Ok(self.peek_info()?.original_color_type) + } + + fn exif_metadata(&mut self) -> ImageResult>> { + Ok(None) + } + + fn iptc_metadata(&mut self) -> ImageResult>> { + Ok(None) } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + fn more_images(&self) -> crate::io::SequenceControl { + self.inner + .as_ref() + .and_then(|reader| { + reader + .more_images() + .then_some(crate::io::SequenceControl::MaybeMore) + }) + .unwrap_or(crate::io::SequenceControl::None) } } diff --git a/src/codecs/webp/decoder.rs b/src/codecs/webp/decoder.rs index c6fca68894..f55e31bda1 100644 --- a/src/codecs/webp/decoder.rs +++ b/src/codecs/webp/decoder.rs @@ -2,7 +2,8 @@ use std::io::{BufRead, Read, Seek}; use crate::buffer::ConvertBuffer; use crate::error::{DecodingError, ImageError, ImageResult}; -use crate::metadata::Orientation; +use crate::io::decoder::DecodedMetadataHint; +use crate::io::{DecodedImageAttributes, DecoderAttributes}; use crate::{ AnimationDecoder, ColorType, Delay, Frame, Frames, ImageDecoder, ImageFormat, RgbImage, Rgba, RgbaImage, @@ -13,7 +14,6 @@ use crate::{ /// Supports both lossless and lossy WebP images. pub struct WebPDecoder { inner: image_webp::WebPDecoder, - orientation: Option, } impl WebPDecoder { @@ -21,7 +21,6 @@ impl WebPDecoder { pub fn new(r: R) -> ImageResult { Ok(Self { inner: image_webp::WebPDecoder::new(r).map_err(ImageError::from_webp_decode)?, - orientation: None, }) } @@ -39,28 +38,42 @@ impl WebPDecoder { } impl ImageDecoder for WebPDecoder { - fn dimensions(&self) -> (u32, u32) { - self.inner.dimensions() + fn attributes(&self) -> DecoderAttributes { + DecoderAttributes { + // As per extended file format description: + // + icc: DecodedMetadataHint::InHeader, + exif: DecodedMetadataHint::AfterFinish, + xmp: DecodedMetadataHint::AfterFinish, + ..DecoderAttributes::default() + } } - fn color_type(&self) -> ColorType { - if self.inner.has_alpha() { - ColorType::Rgba8 - } else { - ColorType::Rgb8 - } + fn peek_layout(&mut self) -> ImageResult { + let (width, height) = self.inner.dimensions(); + + Ok(crate::ImageLayout { + width, + height, + color: if self.inner.has_alpha() { + ColorType::Rgba8 + } else { + ColorType::Rgb8 + }, + }) } - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.peek_layout()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); self.inner .read_image(buf) - .map_err(ImageError::from_webp_decode) - } + .map_err(ImageError::from_webp_decode)?; - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes { + ..DecodedImageAttributes::default() + }) } fn icc_profile(&mut self) -> ImageResult>> { @@ -75,12 +88,6 @@ impl ImageDecoder for WebPDecoder { .exif_metadata() .map_err(ImageError::from_webp_decode)?; - self.orientation = Some( - exif.as_ref() - .and_then(|exif| Orientation::from_exif_chunk(exif)) - .unwrap_or(Orientation::NoTransforms), - ); - Ok(exif) } @@ -89,14 +96,6 @@ impl ImageDecoder for WebPDecoder { .xmp_metadata() .map_err(ImageError::from_webp_decode) } - - fn orientation(&mut self) -> ImageResult { - // `exif_metadata` caches the orientation, so call it if `orientation` hasn't been set yet. - if self.orientation.is_none() { - let _ = self.exif_metadata()?; - } - Ok(self.orientation.unwrap()) - } } impl<'a, R: 'a + BufRead + Seek> AnimationDecoder<'a> for WebPDecoder { diff --git a/src/hooks.rs b/src/hooks.rs index d9bbd8bf44..c083f29191 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -184,7 +184,8 @@ pub(crate) fn guess_format_extension(start: &[u8]) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::{ColorType, DynamicImage, ImageReader}; + use crate::io::DecodedImageAttributes; + use crate::{ColorType, DynamicImage, ImageReaderOptions}; use std::io::Cursor; const MOCK_HOOK_EXTENSION: &str = "MOCKHOOK"; @@ -192,18 +193,17 @@ mod tests { const MOCK_IMAGE_OUTPUT: [u8; 9] = [255, 0, 0, 0, 255, 0, 0, 0, 255]; struct MockDecoder {} impl ImageDecoder for MockDecoder { - fn dimensions(&self) -> (u32, u32) { - ((&MOCK_IMAGE_OUTPUT.len() / 3) as u32, 1) + fn peek_layout(&mut self) -> ImageResult { + Ok(crate::ImageLayout { + width: (MOCK_IMAGE_OUTPUT.len() / 3) as u32, + height: 1, + color: ColorType::Rgb8, + }) } - fn color_type(&self) -> ColorType { - ColorType::Rgb8 - } - fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { + + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { buf[..MOCK_IMAGE_OUTPUT.len()].copy_from_slice(&MOCK_IMAGE_OUTPUT); - Ok(()) - } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes::default()) } } fn is_mock_decoder_output(image: DynamicImage) -> bool { @@ -220,7 +220,7 @@ mod tests { assert!(decoding_hook_registered(OsStr::new(MOCK_HOOK_EXTENSION))); assert!(get_decoding_hook(OsStr::new(MOCK_HOOK_EXTENSION)).is_some()); - let image = ImageReader::open("tests/images/hook/extension.MoCkHoOk") + let image = ImageReaderOptions::open("tests/images/hook/extension.MoCkHoOk") .unwrap() .decode() .unwrap(); @@ -250,7 +250,7 @@ mod tests { Some(OsStr::new(MOCK_HOOK_EXTENSION).to_ascii_lowercase()) ); - let image = ImageReader::new(Cursor::new(TEST_INPUT_IMAGE)) + let image = ImageReaderOptions::new(Cursor::new(TEST_INPUT_IMAGE)) .with_guessed_format() .unwrap() .decode() diff --git a/src/images/dynimage.rs b/src/images/dynimage.rs index 78514913c5..bad55f6369 100644 --- a/src/images/dynimage.rs +++ b/src/images/dynimage.rs @@ -12,6 +12,7 @@ use crate::images::buffer::{ }; use crate::io::encoder::ImageEncoderBoxed; use crate::io::free_functions::{self, encoder_for_format}; +use crate::io::DecodedImageAttributes; use crate::math::{resize_dimensions, Rect}; use crate::metadata::Orientation; use crate::traits::Pixel; @@ -19,7 +20,7 @@ use crate::{ imageops, metadata::{Cicp, CicpColorPrimaries, CicpTransferCharacteristics}, ConvertColorOptions, ExtendedColorType, GenericImage, GenericImageView, ImageDecoder, - ImageEncoder, ImageFormat, ImageReader, Luma, LumaA, + ImageEncoder, ImageFormat, ImageReaderOptions, Luma, LumaA, }; /// A Dynamic Image @@ -240,8 +241,20 @@ impl DynamicImage { } /// Decodes an encoded image into a dynamic image. - pub fn from_decoder(decoder: impl ImageDecoder) -> ImageResult { - decoder_to_image(decoder) + pub fn from_decoder(mut decoder: impl ImageDecoder) -> ImageResult { + let mut image = DynamicImage::new_luma8(0, 0); + let layout = decoder.peek_layout()?; + image.decode_raw(&mut decoder, layout)?; + Ok(image) + } + + /// Assign decoded data from a decoder into this dynamic image. + pub(crate) fn decode_raw( + &mut self, + decoder: &mut dyn ImageDecoder, + layout: crate::ImageLayout, + ) -> ImageResult { + decoder_to_image(self, decoder, layout) } /// Encodes a dynamic image into a buffer. @@ -724,6 +737,17 @@ impl DynamicImage { ) } + /// Return this image's pixels as a native endian byte slice. + #[must_use] + pub(crate) fn as_mut_bytes(&mut self) -> &mut [u8] { + // we can do this because every variant contains an `ImageBuffer<_, Vec<_>>` + dynamic_map!( + *self, + ref mut image_buffer, + bytemuck::cast_slice_mut(image_buffer.inner_pixels_mut()) + ) + } + /// Shrink the capacity of the underlying [`Vec`] buffer to fit its length. /// /// The data may have excess capacity or padding for a number of reasons, depending on how it @@ -1132,19 +1156,17 @@ impl DynamicImage { dynamic_map!(*self, ref p => imageops::rotate270(p)) } - /// Rotates and/or flips the image as indicated by [Orientation]. + /// Rotates and/or flips the image as indicated by [`Orientation`]. /// /// This can be used to apply Exif orientation to an image, /// e.g. to correctly display a photo taken by a smartphone camera: /// /// ``` /// # fn only_check_if_this_compiles() -> Result<(), Box> { - /// use image::{DynamicImage, ImageReader, ImageDecoder}; + /// use image::{ImageReader, metadata::Orientation}; /// - /// let mut decoder = ImageReader::open("file.jpg")?.into_decoder()?; - /// let orientation = decoder.orientation()?; - /// let mut image = DynamicImage::from_decoder(decoder)?; - /// image.apply_orientation(orientation); + /// let mut image = ImageReader::open("file.jpg")?.decode()?; + /// image.apply_orientation(Orientation::Rotate90); /// # Ok(()) /// # } /// ``` @@ -1550,58 +1572,79 @@ impl Default for DynamicImage { } /// Decodes an image and stores it into a dynamic image -fn decoder_to_image(decoder: I) -> ImageResult { - let (w, h) = decoder.dimensions(); - let color_type = decoder.color_type(); - - let mut image = match color_type { +/// +/// FIXME: this should reuse existing buffers from the dynamic image. +pub(crate) fn decoder_to_image( + image: &mut DynamicImage, + decoder: &mut dyn ImageDecoder, + layout: crate::ImageLayout, +) -> ImageResult { + let crate::ImageLayout { + width: w, + height: h, + color: color_type, + } = layout; + + let attr; + + *image = match color_type { color::ColorType::Rgb8 => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgb8) } color::ColorType::Rgba8 => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgba8) } color::ColorType::L8 => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageLuma8) } color::ColorType::La8 => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageLumaA8) } color::ColorType::Rgb16 => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgb16) } color::ColorType::Rgba16 => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgba16) } color::ColorType::Rgb32F => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgb32F) } color::ColorType::Rgba32F => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgba32F) } color::ColorType::L16 => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageLuma16) } color::ColorType::La16 => { - let buf = free_functions::decoder_to_vec(decoder)?; + let buf; + (buf, attr) = free_functions::decoder_to_vec(decoder)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageLumaA16) } } @@ -1617,7 +1660,7 @@ fn decoder_to_image(decoder: I) -> ImageResult { image.set_rgb_primaries(Cicp::SRGB.primaries); image.set_transfer_function(Cicp::SRGB.transfer); - Ok(image) + Ok(attr) } /// Open the image located at the path specified. @@ -1629,7 +1672,7 @@ pub fn open

(path: P) -> ImageResult where P: AsRef, { - ImageReader::open(path)?.decode() + ImageReaderOptions::open(path)?.decode() } /// Read a tuple containing the (width, height) of the image located at the specified path. @@ -1641,7 +1684,7 @@ pub fn image_dimensions

(path: P) -> ImageResult<(u32, u32)> where P: AsRef, { - ImageReader::open(path)?.into_dimensions() + ImageReaderOptions::open(path)?.into_dimensions() } /// Writes the supplied buffer to a writer in the specified format. diff --git a/src/io.rs b/src/io.rs index 9f8232df93..2cd65e17ef 100644 --- a/src/io.rs +++ b/src/io.rs @@ -13,9 +13,6 @@ pub(crate) mod free_functions; pub(crate) mod image_reader_type; pub(crate) mod limits; -#[deprecated(note = "this type has been moved and renamed to image::ImageReader")] -/// Deprecated re-export of `ImageReader` as `Reader` -pub type Reader = ImageReader; #[deprecated(note = "this type has been moved to image::Limits")] /// Deprecated re-export of `Limits` pub type Limits = limits::Limits; @@ -23,7 +20,9 @@ pub type Limits = limits::Limits; /// Deprecated re-export of `LimitSupport` pub type LimitSupport = limits::LimitSupport; -pub(crate) use self::image_reader_type::ImageReader; +pub use decoder::{ + DecodedImageAttributes, DecodedMetadataHint, DecoderAttributes, SequenceControl, +}; /// Adds `read_exact_vec` pub(crate) trait ReadExt { @@ -43,3 +42,51 @@ impl ReadExt for R { } } } + +/// Communicate the layout of an image. +/// +/// Describes a packed rectangular layout with given bit-depth in [`ImageDecoder::init`]. Layouts +/// from `image` are row-major with no padding between rows and pixels packed by consecutive +/// channels. +#[non_exhaustive] +pub struct ImageLayout { + /// The color model of each pixel. + pub color: crate::ColorType, + /// The number of pixels in the horizontal direction. + pub width: u32, + /// The number of pixels in the vertical direction. + pub height: u32, +} + +impl ImageLayout { + /// Return width and height as a tuple, consistent with + /// [`GenericImageView::dimensions`][`crate::GenericImageView::dimensions`]. + /// + /// Note that this refers to underlying pixel matrix, not the orientation of the image as + /// indicated to be viewed in user facing applications by metadata. + pub fn dimensions(&self) -> (u32, u32) { + (self.width, self.height) + } + + /// A layout with no pixels, of the given [`ColorType`][`crate::ColorType`]. + pub fn empty(color: crate::ColorType) -> Self { + ImageLayout { + color, + width: 0, + height: 0, + } + } + + /// Returns the total number of bytes in the decoded image. + /// + /// This is the size of the buffer that must be passed to `read_image` or + /// `read_image_with_progress`. The returned value may exceed `usize::MAX`, in + /// which case it isn't actually possible to construct a buffer to decode all the image data + /// into. If, however, the size does not fit in a u64 then `u64::MAX` is returned. + pub fn total_bytes(&self) -> u64 { + let ImageLayout { width, height, .. } = *self; + let total_pixels = u64::from(width) * u64::from(height); + let bytes_per_pixel = u64::from(self.color.bytes_per_pixel()); + total_pixels.saturating_mul(bytes_per_pixel) + } +} diff --git a/src/io/decoder.rs b/src/io/decoder.rs index d01071c6c8..e428ea9db1 100644 --- a/src/io/decoder.rs +++ b/src/io/decoder.rs @@ -1,21 +1,97 @@ use crate::animation::Frames; -use crate::color::{ColorType, ExtendedColorType}; +use crate::color::ExtendedColorType; use crate::error::ImageResult; use crate::metadata::Orientation; +use crate::Delay; -/// The trait that all decoders implement +/// The interface for `image` to utilize in reading image files. +/// +/// This should be thought of as one side of protocol between `image` and a specific file format. +/// In the general case, the calls are expected to be made in the following order: +/// +/// ```text,bnf +/// set_limits* +/// > (peek_layout+ > {xmp,icc,exif,iptc}_metadata* > read_image)* +/// > finish +/// ``` +/// +/// Metadata (`icc_profile`, `exif_metadata`, etc.)is handled different for different image +/// containers. The should apply to the previous image. pub trait ImageDecoder { - /// Returns a tuple containing the width and height of the image - fn dimensions(&self) -> (u32, u32); + /// Set the decoder to have the specified limits. See [`Limits`] for the different kinds of + /// limits that is possible to set. + /// + /// Note to implementors: make sure you call [`Limits::check_support`] so that + /// decoding fails if any unsupported strict limits are set. Also make sure + /// you call [`Limits::check_dimensions`] to check the `max_image_width` and + /// `max_image_height` limits. + /// + /// **Note**: By default, _no_ limits are defined. This may be changed in future major version + /// increases. + /// + /// [`Limits`]: ./io/struct.Limits.html + /// [`Limits::check_support`]: ./io/struct.Limits.html#method.check_support + /// [`Limits::check_dimensions`]: ./io/struct.Limits.html#method.check_dimensions + fn set_limits(&mut self, limits: crate::Limits) -> ImageResult<()> { + limits.check_support(&crate::LimitSupport::default())?; + let (width, height) = self.peek_layout()?.dimensions(); + limits.check_dimensions(width, height)?; + Ok(()) + } - /// Returns the color type of the image data produced by this decoder - fn color_type(&self) -> ColorType; + /// Retrieve general information about the decoder / its format itself. + /// + /// This hint which methods should be called while decoding (a sequence of) images from this + /// decoder, e.g. when metadata is available and when it will be overridden. It also provides + /// basic capability information about the format. If, in the future, we added different basic + /// methods of retrieving color data then the attributes would indicate the preferred and/or + /// possible choices. + fn attributes(&self) -> DecoderAttributes { + DecoderAttributes::default() + } + + /// Consume the header of the image, determining the image's layout. + /// + /// This must be called before a call to [`Self::read_image`] to ensure that the initial + /// metadata has been read. In contrast to a constructor it can be called after configuring + /// limits and context which avoids resource issues for formats that buffer metadata. + /// + /// The layout returned by an implementation of [`ImageDecoder::peek_layout`] must match the + /// buffer expected in [`ImageDecoder::read_image`]. + fn peek_layout(&mut self) -> ImageResult; /// Returns the color type of the image file before decoding - fn original_color_type(&self) -> ExtendedColorType { - self.color_type().into() + fn original_color_type(&mut self) -> ImageResult { + Ok(self.peek_layout()?.color.into()) } + /// Read all the bytes in the image into a buffer. + /// + /// This function takes a slice of bytes and writes the pixel data of the image into it. + /// `buf` must not be assumed to be aligned to any byte boundaries. However, + /// alignment to 2 or 4 byte boundaries may result in small performance + /// improvements for certain decoder implementations. + /// + /// The returned pixel data will always be in native endian. This allows + /// `[u16]` and `[f32]` slices to be cast to `[u8]` and used for this method. + /// + /// # Panics + /// + /// This function should panic if `buf.len() != self.peek_layout().total_bytes()`. + /// + /// # Examples + /// + /// ``` + /// # use image::ImageDecoder; + /// fn read_16bit_image(mut decoder: impl ImageDecoder) -> Vec { + /// let layout = decoder.peek_layout().unwrap(); + /// let mut buf: Vec = vec![0; (layout.total_bytes() / 2) as usize]; + /// decoder.read_image(bytemuck::cast_slice_mut(&mut buf)).unwrap(); + /// buf + /// } + /// ``` + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult; + /// Returns the ICC color profile embedded in the image, or `Ok(None)` if the image does not have one. /// /// For formats that don't support embedded profiles this function should always return `Ok(None)`. @@ -46,101 +122,118 @@ pub trait ImageDecoder { Ok(None) } - /// Returns the orientation of the image. + /// Called to determine if there may be more images to decode. /// - /// This is usually obtained from the Exif metadata, if present. Formats that don't support - /// indicating orientation in their image metadata will return `Ok(Orientation::NoTransforms)`. - fn orientation(&mut self) -> ImageResult { - Ok(self - .exif_metadata()? - .and_then(|chunk| Orientation::from_exif_chunk(&chunk)) - .unwrap_or(Orientation::NoTransforms)) + /// This ends the decoding loop early when it indicates `None`. Otherwise, termination can only + /// be handled through errors. See also + /// [`ImageReader::into_frames`](crate::ImageReader::into_frames). + fn more_images(&self) -> SequenceControl { + SequenceControl::MaybeMore } - /// Returns the total number of bytes in the decoded image. + /// Consume the rest of the file, including any trailer. /// - /// This is the size of the buffer that must be passed to `read_image`. The returned value may - /// exceed `usize::MAX`, in which case it isn't actually possible to construct a buffer to - /// decode all the image data into. If, however, the size does not fit in a u64 then `u64::MAX` - /// is returned. - fn total_bytes(&self) -> u64 { - let dimensions = self.dimensions(); - let total_pixels = u64::from(dimensions.0) * u64::from(dimensions.1); - let bytes_per_pixel = u64::from(self.color_type().bytes_per_pixel()); - total_pixels.saturating_mul(bytes_per_pixel) + /// This method should ensure that metadata that used [`DecodedMetadataHint::AfterFinish`] has + /// all been ingested and can be retrieved. + fn finish(&mut self) -> ImageResult<()> { + Ok(()) } +} - /// Returns all the bytes in the image. - /// - /// This function takes a slice of bytes and writes the pixel data of the image into it. - /// `buf` does not need to be aligned to any byte boundaries. However, - /// alignment to 2 or 4 byte boundaries may result in small performance - /// improvements for certain decoder implementations. - /// - /// The returned pixel data will always be in native endian. This allows - /// `[u16]` and `[f32]` slices to be cast to `[u8]` and used for this method. - /// - /// # Panics - /// - /// This function panics if `buf.len() != self.total_bytes()`. - /// - /// # Examples - /// - /// ``` - /// # use image::ImageDecoder; - /// fn read_16bit_image(decoder: impl ImageDecoder) -> Vec { - /// let mut buf: Vec = vec![0; (decoder.total_bytes() / 2) as usize]; - /// decoder.read_image(bytemuck::cast_slice_mut(&mut buf)); - /// buf - /// } - /// ``` - fn read_image(self, buf: &mut [u8]) -> ImageResult<()> - where - Self: Sized; +/// Information meant to steer the protocol usage with the decoder. +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +pub struct DecoderAttributes { + /// Are there multiple images in this file that form an animation? + pub is_animated: bool, + /// Are there multiple images in this file, as an unrelated sequence? + pub is_sequence: bool, + /// When should ICC profiles be retrieved. + pub icc: DecodedMetadataHint, + /// A hint for polling EXIF metadata. + pub exif: DecodedMetadataHint, + /// A hint for polling XMP metadata. + pub xmp: DecodedMetadataHint, + /// A hint for polling IPTC metadata. + pub iptc: DecodedMetadataHint, +} - /// Set the decoder to have the specified limits. See [`Limits`] for the different kinds of - /// limits that is possible to set. - /// - /// Note to implementors: make sure you call [`Limits::check_support`] so that - /// decoding fails if any unsupported strict limits are set. Also make sure - /// you call [`Limits::check_dimensions`] to check the `max_image_width` and - /// `max_image_height` limits. - /// - /// **Note**: By default, _no_ limits are defined. This may be changed in future major version - /// increases. - /// - /// [`Limits`]: ./io/struct.Limits.html - /// [`Limits::check_support`]: ./io/struct.Limits.html#method.check_support - /// [`Limits::check_dimensions`]: ./io/struct.Limits.html#method.check_dimensions - fn set_limits(&mut self, limits: crate::Limits) -> ImageResult<()> { - limits.check_support(&crate::LimitSupport::default())?; - let (width, height) = self.dimensions(); - limits.check_dimensions(width, height)?; - Ok(()) - } +/// Additional attributes of an image available after decoding. +/// +/// The [`Default`] is implemented and returns a value suitable for very basic images from formats +/// that contain only one raster graphic. +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +pub struct DecodedImageAttributes { + /// The x-coordinate of the top-left rectangle of the image relative to canvas indicated by the + /// sequence of frames. + pub x: u32, + /// The y-coordinate of the top-left rectangle of the image relative to canvas indicated by the + /// sequence of frames. + pub y: u32, + /// A suggested presentation offset relative to the previous image. + pub delay: Option, + /// Orientation of the image, not relayed through EXIF metadata. + pub orientation: Option, +} + +/// A hint when metadata corresponding to the image is decoded. +/// +/// Note that while this is a hint, different variants give contradictory indication on when they +/// should be polled. When a metadatum is tagged as [`DecodedMetadataHint::PerImage`] it MUST be +/// polled after each image to ensure all are retrieved, iterating to the next image without +/// polling MAY reset and skip some metadata. Conversely, when a metadatum is tagged as +/// [`DecodedMetadataHint::AfterFinish`] it should not be considered fully valid until after a call +/// to [`ImageDecoder::finish`]. This call might be destructive with regards to the other kind of +/// metadata. +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +pub enum DecodedMetadataHint { + /// The metadata could be anywhere in the file and can only be reliably polled when the image + /// is finished. + #[default] + Unknown, + /// Explicitly indicate that the file must be polled fully before interpreting this kind of + /// metadata. + AfterFinish, + /// Metadata is available in the header and will be valid after the first call to + /// [`ImageDecoder::peek_layout`] and will remain valid for all subsequent images. + InHeader, + /// Metadata exists for each image in this file, it must be retrieved between peeking the + /// layout and reading the image. + PerImage, + /// There's no metadata of this type, the decoder would return `None` or an error. + None, +} - /// Use `read_image` instead; this method is an implementation detail needed so the trait can - /// be object safe. +/// Indicate if there may be more images to decode. +/// +/// More concrete indications may be added in the future. +#[non_exhaustive] +#[derive(Default)] +pub enum SequenceControl { + /// The format can not certainly say if there are more images. The caller should try to decode + /// more images until an error occurs (specifically + /// [`ParameterErrorKind::NoMoreData`](crate::error::ParameterErrorKind::NoMoreData)). + #[default] + MaybeMore, + /// The decoder is sure that no more images are present. /// - /// Note to implementors: This method should be implemented by calling `read_image` on - /// the boxed decoder... - /// ```ignore - /// fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - /// (*self).read_image(buf) - /// } - /// ``` - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()>; + /// Further attempts to decode images should not be made, but no strong guarantee is made about + /// returning an error in these cases. In particular, further attempts may further read the + /// image file and check for errors in trailing data. + None, } #[deny(clippy::missing_trait_methods)] impl ImageDecoder for Box { - fn dimensions(&self) -> (u32, u32) { - (**self).dimensions() + fn attributes(&self) -> DecoderAttributes { + (**self).attributes() } - fn color_type(&self) -> ColorType { - (**self).color_type() + fn peek_layout(&mut self) -> ImageResult { + (**self).peek_layout() } - fn original_color_type(&self) -> ExtendedColorType { + fn original_color_type(&mut self) -> ImageResult { (**self).original_color_type() } fn icc_profile(&mut self) -> ImageResult>> { @@ -155,24 +248,18 @@ impl ImageDecoder for Box { fn iptc_metadata(&mut self) -> ImageResult>> { (**self).iptc_metadata() } - fn orientation(&mut self) -> ImageResult { - (**self).orientation() - } - fn total_bytes(&self) -> u64 { - (**self).total_bytes() - } - fn read_image(self, buf: &mut [u8]) -> ImageResult<()> - where - Self: Sized, - { - T::read_image_boxed(self, buf) - } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - T::read_image_boxed(*self, buf) + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + (**self).read_image(buf) } fn set_limits(&mut self, limits: crate::Limits) -> ImageResult<()> { (**self).set_limits(limits) } + fn more_images(&self) -> SequenceControl { + (**self).more_images() + } + fn finish(&mut self) -> ImageResult<()> { + (**self).finish() + } } /// `AnimationDecoder` trait @@ -183,28 +270,29 @@ pub trait AnimationDecoder<'a> { #[cfg(test)] mod tests { - use super::{ColorType, ImageDecoder, ImageResult}; + use super::{DecodedImageAttributes, ImageDecoder, ImageResult}; + use crate::ColorType; #[test] fn total_bytes_overflow() { struct D; + impl ImageDecoder for D { - fn color_type(&self) -> ColorType { - ColorType::Rgb8 + fn peek_layout(&mut self) -> ImageResult { + Ok(crate::ImageLayout { + width: 0xffff_ffff, + height: 0xffff_ffff, + color: ColorType::Rgb8, + }) } - fn dimensions(&self) -> (u32, u32) { - (0xffff_ffff, 0xffff_ffff) - } - fn read_image(self, _buf: &mut [u8]) -> ImageResult<()> { - unimplemented!() - } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + + fn read_image(&mut self, _buf: &mut [u8]) -> ImageResult { + unreachable!("Must not be called in this test") } } - assert_eq!(D.total_bytes(), u64::MAX); - let v: ImageResult> = crate::io::free_functions::decoder_to_vec(D); + assert_eq!(D.peek_layout().unwrap().total_bytes(), u64::MAX); + let v = crate::DynamicImage::from_decoder(D); assert!(v.is_err()); } } diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 9fb6a37796..c524fe6b4e 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -4,7 +4,8 @@ use std::path::Path; use std::{iter, mem::size_of}; use crate::io::encoder::ImageEncoderBoxed; -use crate::{codecs::*, ExtendedColorType, ImageReader}; +use crate::io::DecodedImageAttributes; +use crate::{codecs::*, ExtendedColorType, ImageReaderOptions}; use crate::error::{ ImageError, ImageFormatHint, ImageResult, LimitError, LimitErrorKind, UnsupportedError, @@ -19,7 +20,7 @@ use crate::{DynamicImage, ImageDecoder, ImageFormat}; /// /// Try [`ImageReader`] for more advanced uses. pub fn load(r: R, format: ImageFormat) -> ImageResult { - let mut reader = ImageReader::new(r); + let mut reader = ImageReaderOptions::new(r); reader.set_format(format); reader.decode() } @@ -166,11 +167,13 @@ pub(crate) fn guess_format_impl(buffer: &[u8]) -> Option { /// of the output buffer is guaranteed. /// /// Panics if there isn't enough memory to decode the image. -pub(crate) fn decoder_to_vec(decoder: impl ImageDecoder) -> ImageResult> +pub(crate) fn decoder_to_vec( + decoder: &mut (impl ImageDecoder + ?Sized), +) -> ImageResult<(Vec, DecodedImageAttributes)> where T: crate::traits::Primitive + bytemuck::Pod, { - let total_bytes = usize::try_from(decoder.total_bytes()); + let total_bytes = usize::try_from(decoder.peek_layout()?.total_bytes()); if total_bytes.is_err() || total_bytes.unwrap() > isize::MAX as usize { return Err(ImageError::Limits(LimitError::from_kind( LimitErrorKind::InsufficientMemory, @@ -178,8 +181,8 @@ where } let mut buf = vec![num_traits::Zero::zero(); total_bytes.unwrap() / size_of::()]; - decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?; - Ok(buf) + let attr = decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?; + Ok((buf, attr)) } #[test] diff --git a/src/io/image_reader_type.rs b/src/io/image_reader_type.rs index d1a46e427b..aa7024c601 100644 --- a/src/io/image_reader_type.rs +++ b/src/io/image_reader_type.rs @@ -3,9 +3,14 @@ use std::fs::File; use std::io::{self, BufRead, BufReader, Cursor, Read, Seek, SeekFrom}; use std::path::Path; -use crate::error::{ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind}; -use crate::hooks; +use crate::error::{ + ImageFormatHint, ImageResult, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, +}; use crate::io::limits::Limits; +use crate::io::DecodedImageAttributes; +use crate::io::SequenceControl; +use crate::metadata::Orientation; +use crate::{hooks, Delay, Frame, Frames}; use crate::{DynamicImage, ImageDecoder, ImageError, ImageFormat}; use super::free_functions; @@ -16,10 +21,11 @@ enum Format { Extension(OsString), } -/// A multi-format image reader. +/// Determine the format for an image reader. /// -/// Wraps an input reader to facilitate automatic detection of an image's format, appropriate -/// decoding method, and dispatches into the set of supported [`ImageDecoder`] implementations. +/// Wraps an input stream to facilitate automatic detection of an image's format, appropriate +/// decoding method, and turn it into an [`ImageReader`] or a boxed [`ImageDecoder`] +/// implementation. For convenience, it also allows directly decoding into a [`DynamicImage`]. /// /// ## Usage /// @@ -28,9 +34,9 @@ enum Format { /// /// ```no_run /// # use image::ImageError; -/// # use image::ImageReader; +/// # use image::ImageReaderOptions; /// # fn main() -> Result<(), ImageError> { -/// let image = ImageReader::open("path/to/image.png")? +/// let image = ImageReaderOptions::open("path/to/image.png")? /// .decode()?; /// # Ok(()) } /// ``` @@ -41,7 +47,7 @@ enum Format { /// /// ``` /// # use image::ImageError; -/// # use image::ImageReader; +/// # use image::ImageReaderOptions; /// # fn main() -> Result<(), ImageError> { /// use std::io::Cursor; /// use image::ImageFormat; @@ -50,7 +56,7 @@ enum Format { /// 0 1\n\ /// 1 0\n"; /// -/// let mut reader = ImageReader::new(Cursor::new(raw_data)) +/// let mut reader = ImageReaderOptions::new(Cursor::new(raw_data)) /// .with_guessed_format() /// .expect("Cursor io never fails"); /// assert_eq!(reader.format(), Some(ImageFormat::Pnm)); @@ -64,8 +70,7 @@ enum Format { /// specification of the supposed image format with [`set_format`]. /// /// [`set_format`]: #method.set_format -/// [`ImageDecoder`]: ../trait.ImageDecoder.html -pub struct ImageReader { +pub struct ImageReaderOptions { /// The reader. Should be buffered. inner: R, /// The format, if one has been set or deduced. @@ -74,7 +79,7 @@ pub struct ImageReader { limits: Limits, } -impl<'a, R: 'a + BufRead + Seek> ImageReader { +impl<'a, R: 'a + BufRead + Seek> ImageReaderOptions { /// Create a new image reader without a preset format. /// /// Assumes the reader is already buffered. For optimal performance, @@ -86,7 +91,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { /// [`with_guessed_format`]: #method.with_guessed_format /// [`set_format`]: method.set_format pub fn new(buffered_reader: R) -> Self { - ImageReader { + ImageReaderOptions { inner: buffered_reader, format: None, limits: Limits::default(), @@ -98,7 +103,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { /// Assumes the reader is already buffered. For optimal performance, /// consider wrapping the reader with a `BufReader::new()`. pub fn with_format(buffered_reader: R, format: ImageFormat) -> Self { - ImageReader { + ImageReaderOptions { inner: buffered_reader, format: Some(Format::BuiltIn(format)), limits: Limits::default(), @@ -142,16 +147,8 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { self.inner } - /// Makes a decoder. - /// - /// For all formats except PNG, the limits are ignored and can be set with - /// `ImageDecoder::set_limits` after calling this function. PNG is handled specially because that - /// decoder has a different API which does not allow setting limits after construction. - fn make_decoder( - format: Format, - reader: R, - limits_for_png: Limits, - ) -> ImageResult> { + /// Take the readable IO stream and construct a decoder around it. + fn make_decoder(format: Format, reader: R) -> ImageResult> { #[allow(unused)] use crate::codecs::*; @@ -174,7 +171,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { #[cfg(feature = "avif-native")] ImageFormat::Avif => Box::new(avif::AvifDecoder::new(reader)?), #[cfg(feature = "png")] - ImageFormat::Png => Box::new(png::PngDecoder::with_limits(reader, limits_for_png)?), + ImageFormat::Png => Box::new(png::PngDecoder::new(reader)), #[cfg(feature = "gif")] ImageFormat::Gif => Box::new(gif::GifDecoder::new(reader)?), #[cfg(feature = "jpeg")] @@ -209,14 +206,22 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { }) } - /// Convert the reader into a decoder. + /// Convert the file into its raw decoder ready to read an image. pub fn into_decoder(mut self) -> ImageResult { - let mut decoder = - Self::make_decoder(self.require_format()?, self.inner, self.limits.clone())?; + let mut decoder = Self::make_decoder(self.require_format()?, self.inner)?; decoder.set_limits(self.limits)?; Ok(decoder) } + /// Convert the file into a reader object. + pub fn into_reader(mut self) -> ImageResult> { + let format = self.require_format()?; + let decoder = Self::make_decoder(format, self.inner)?; + let mut reader = ImageReader::from_decoder(decoder); + reader.set_limits(self.limits)?; + Ok(reader) + } + /// Make a format guess based on the content, replacing it on success. /// /// Returns `Ok` with the guess if no io error occurs. Additionally, replaces the current @@ -232,15 +237,15 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { /// /// ## Usage /// - /// This supplements the path based type deduction from [`ImageReader::open()`] with content based deduction. - /// This is more common in Linux and UNIX operating systems and also helpful if the path can - /// not be directly controlled. + /// This supplements the path based type deduction from [`Self::open`] with content based + /// deduction. This is more common in Linux and UNIX operating systems and also helpful if the + /// path can not be directly controlled. /// /// ```no_run /// # use image::ImageError; - /// # use image::ImageReader; + /// # use image::ImageReaderOptions; /// # fn main() -> Result<(), ImageError> { - /// let image = ImageReader::open("image.unknown")? + /// let image = ImageReaderOptions::open("image.unknown")? /// .with_guessed_format()? /// .decode()?; /// # Ok(()) } @@ -282,7 +287,9 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { /// /// If no format was determined, returns an `ImageError::Unsupported`. pub fn into_dimensions(self) -> ImageResult<(u32, u32)> { - self.into_decoder().map(|d| d.dimensions()) + let mut decoder = self.into_decoder()?; + let layout = decoder.peek_layout()?; + Ok((layout.width, layout.height)) } /// Read the image (replaces `load`). @@ -290,18 +297,8 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { /// Uses the current format to construct the correct reader for the format. /// /// If no format was determined, returns an `ImageError::Unsupported`. - pub fn decode(mut self) -> ImageResult { - let format = self.require_format()?; - - let mut limits = self.limits; - let mut decoder = Self::make_decoder(format, self.inner, limits.clone())?; - - // Check that we do not allocate a bigger buffer than we are allowed to - // FIXME: should this rather go in `DynamicImage::from_decoder` somehow? - limits.reserve(decoder.total_bytes())?; - decoder.set_limits(limits)?; - - DynamicImage::from_decoder(decoder) + pub fn decode(self) -> ImageResult { + self.into_reader()?.decode() } fn require_format(&mut self) -> ImageResult { @@ -314,7 +311,40 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { } } -impl ImageReader> { +/// An abstracted image reader. +/// +/// Wraps an image decoder, which operates on a stream after its format was determined. +/// [`ImageReaderOptions`] dispatches into the set of supported [`ImageDecoder`] implementations +/// and can wrap them up as an [`ImageReader`]. For decoder interface that are provided for +/// efficiency it negotiates support with the underlying decoder and then emulates them if +/// necessary. +pub struct ImageReader<'lt> { + /// The reader. Should be buffered. + inner: Box, + /// Settings of the reader, not the underlying decoder. + /// + /// Those apply to each individual `read_image` call, i.e. can be modified during reading. + settings: ImageReaderSettings, + /// Remaining limits for allocations by the reader. + limits: Limits, + /// A buffered cache of the last image attributes. + last_attributes: DecodedImageAttributes, +} + +#[derive(Clone, Copy)] +struct ImageReaderSettings { + apply_orientation: bool, +} + +impl Default for ImageReaderSettings { + fn default() -> Self { + ImageReaderSettings { + apply_orientation: true, + } + } +} + +impl ImageReaderOptions> { /// Open a file to read, format will be guessed from path. /// /// This will not attempt any io operation on the opened file. @@ -336,10 +366,229 @@ impl ImageReader> { .filter(|ext| !ext.is_empty()) .map(|ext| Format::Extension(ext.to_owned())); - Ok(ImageReader { + Ok(ImageReaderOptions { inner: BufReader::new(File::open(path)?), format, limits: Limits::default(), }) } } + +impl ImageReader<'_> { + /// Query the layout that the image will have. + pub fn peek_layout(&mut self) -> ImageResult { + self.inner.peek_layout() + } + + /// Decode the next image into a `DynamicImage`. + pub fn decode(&mut self) -> ImageResult { + let layout = self.inner.peek_layout()?; + // This is technically redundant but it's also cheap. + self.limits.check_dimensions(layout.width, layout.height)?; + // Check that we do not allocate a bigger buffer than we are allowed to + // FIXME: should this rather go in `DynamicImage::from_decoder` somehow? + self.limits.reserve(layout.total_bytes())?; + + // Run all the metadata extraction which we may need. + let icc = self.inner.icc_profile()?; + let exif = self.inner.exif_metadata()?; + + // Retrieve the raw image data as indicated by the layout. + let mut image = DynamicImage::new_luma8(0, 0); + let mut attr = image.decode_raw(self.inner.as_mut(), layout)?; + + // Apply the profile. If the profile itself is not valid or not present you get the default + // presumption: `sRGB`. Otherwise we will try to make sense of the profile and if it is not + // RGB we'll treat it as unspecified so that downstream will know that our handling of this + // _existing_ profile was not / could not be done with full fidelity. + if let Some(icc) = icc { + if let Some(cicp) = crate::metadata::cms_provider().parse_icc(&icc) { + // We largely ignore the error itself here, you just get the image with no color + // space attached to it. + if let Ok(rgb) = cicp.try_into_rgb() { + image.set_rgb_primaries(rgb.primaries); + image.set_transfer_function(rgb.transfer); + } else { + image.set_rgb_primaries(crate::metadata::CicpColorPrimaries::Unspecified); + image.set_transfer_function( + crate::metadata::CicpTransferCharacteristics::Unspecified, + ); + } + } + } + + // Determine which orientation to use. + if attr.orientation.is_none() { + attr.orientation = exif.and_then(|chunk| Orientation::from_exif_chunk(&chunk)); + } + + if self.settings.apply_orientation { + if let Some(orient) = attr.orientation { + image.apply_orientation(orient); + } + } + + self.last_attributes = attr; + Ok(image) + } + + /// Skip the next image, discard its image data. + /// + /// This will attempt to read the image data with as little allocation as possible while still + /// running the usual verification routines. It will inform the underlying decoder that it is + /// uninterested in all of the image data, then run its decoding routine. + pub fn skip(&mut self) -> ImageResult<()> { + // TODO: with `viewbox` (temporarily removed) we can inform the decoder that no data is + // required which may be quite efficient. Other variants of achieving the same may also be + // possible. We can just try out until one works. + + // Some decoders may still want a buffer, so we can't fully ignore it. + let layout = self.inner.peek_layout()?; + // This is technically redundant but it's also cheap. + self.limits.check_dimensions(layout.width, layout.height)?; + let bytes = layout.total_bytes(); + + if bytes < 512 { + let mut stack = [0u8; 512]; + self.inner.read_image(&mut stack[..bytes as usize])?; + } else { + // Check that we do not allocate a bigger buffer than we are allowed to + // FIXME: should this rather go in `DynamicImage::from_decoder` somehow? + // Or should we make an extension method on `ImageDecoder`? + let mut placeholder = DynamicImage::new_luma8(0, 0); + self.limits.reserve(bytes)?; + placeholder.decode_raw(self.inner.as_mut(), layout)?; + self.limits.free(bytes); + } + + Ok(()) + } + + /// Get the EXIF metadata of the pending image if any. + pub fn exif_metadata(&mut self) -> ImageResult>> { + let _ = self.inner.peek_layout(); + self.inner.exif_metadata() + } + + /// Get the ICC profile of the pending image if any. + pub fn icc_profile(&mut self) -> ImageResult>> { + let _ = self.inner.peek_layout(); + self.inner.icc_profile() + } + + /// Get the XMP metadata of the pending image if any. + pub fn xmp_metadata(&mut self) -> ImageResult>> { + let _ = self.inner.peek_layout(); + self.inner.xmp_metadata() + } + + /// Get the IPTC metadata of the pending image if any. + pub fn iptc_metadata(&mut self) -> ImageResult>> { + let _ = self.inner.peek_layout(); + self.inner.iptc_metadata() + } + + /// Get auxiliary attributes of the last image. + pub fn last_attributes(&self) -> DecodedImageAttributes { + self.last_attributes.clone() + } +} + +impl<'stream> ImageReader<'stream> { + /// Open the image in a readable stream. + /// + /// The format is guessed from a fixed array of bytes at stream's start. Hooks can be + /// configured to customize this behavior, see [`hooks`](crate::hooks) for details. + /// + /// The reader will use default limits. Use [`ImageReaderOptions`] to configure the reader + /// before use. + pub fn new(reader: R) -> ImageResult { + ImageReaderOptions::new(reader) + .with_guessed_format()? + .into_reader() + } + + /// Open the image located at the path specified. + /// + /// The image's format is determined from the path's file extension. Hooks can be configured to + /// customize this behavior, see [`hooks`](crate::hooks) for details. + /// + /// The reader will use default limits. Use [`ImageReaderOptions`] to configure the reader + /// before use. + pub fn open>(path: P) -> ImageResult { + ImageReaderOptions::open(path)?.into_reader() + } + + /// Read images from a boxed decoder. + /// + /// This can be used to interact with decoder instances that have not been created by `image` + /// or registered hooks. + /// + /// The [`ImageReader`] abstracts interaction with the decoder as user facing methods to decode + /// any further images that the decoder can provide. The decoder is assumed to be already + /// configured with limits but the reader will make some additional allocations for which it + /// has its own set of default limits. + pub fn from_decoder(boxed: Box) -> Self { + ImageReader { + inner: boxed, + settings: ImageReaderSettings::default(), + limits: Limits::default(), + last_attributes: Default::default(), + } + } + + /// Reconfigure the limits for decoding. + pub fn set_limits(&mut self, mut limits: Limits) -> ImageResult<()> { + if let Some(max_alloc) = &mut limits.max_alloc { + // We'll take half for ourselves, half to the decoder. + *max_alloc /= 2; + } + + self.inner.set_limits(limits.clone())?; + self.limits = limits; + Ok(()) + } + + /// Consume the reader as a series of frames. + /// + /// The iterator will end (start returning `None`) when the decoder indicates that no more + /// images are present in the stream by setting [`ImageDecoder::more_images`] to + /// [`SequenceControl::None`]. Decoding can return [`ParameterError`] in + /// [`ImageDecoder::peek_layout`] or [`ImageDecoder::read_image`] with kind set to + /// [`None`](crate::io::SequenceControl::None), which is also treated as end of stream. This + /// may be used by decoders which can not determine the number of images in advance. + pub fn into_frames(mut self) -> Frames<'stream> { + fn is_end_reached(err: &ImageError) -> bool { + if let ImageError::Parameter(ref param_err) = err { + matches!(param_err.kind(), ParameterErrorKind::NoMoreData) + } else { + false + } + } + + let iter = core::iter::from_fn(move || { + match self.inner.more_images() { + SequenceControl::MaybeMore => {} + SequenceControl::None => return None, + } + + let no_delay = Delay::from_saturating_duration(Default::default()); + + let frame = match self.decode() { + Ok(frame) => frame, + Err(ref err) if is_end_reached(err) => return None, + Err(err) => return Some(Err(err)), + }; + + let x = self.last_attributes.x; + let y = self.last_attributes.y; + let delay = self.last_attributes.delay.unwrap_or(no_delay); + let frame = frame.into_rgba8(); + + let frame = Frame::from_parts(frame, x, y, delay); + Some(Ok(frame)) + }); + + Frames::new(Box::new(iter)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 5464a12e72..ce31715f14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ //! # let bytes = vec![0u8]; //! //! let img = ImageReader::open("myimage.png")?.decode()?; -//! let img2 = ImageReader::new(Cursor::new(bytes)).with_guessed_format()?.decode()?; +//! let img2 = ImageReader::new(Cursor::new(bytes))?.decode()?; //! # Ok(()) //! # } //! ``` @@ -49,7 +49,6 @@ //! //! [`save`]: enum.DynamicImage.html#method.save //! [`write_to`]: enum.DynamicImage.html#method.write_to -//! [`ImageReader`]: struct.Reader.html //! //! # Image buffers //! @@ -99,7 +98,9 @@ //! # use image::codecs::png::PngDecoder; //! # let img: DynamicImage = unimplemented!(); //! # let reader: BufReader> = unimplemented!(); -//! let decoder = PngDecoder::new(&mut reader)?; +//! let mut decoder = PngDecoder::new(&mut reader); +//! let layout = decoder.peek_layout()?; +//! //! let icc = decoder.icc_profile(); //! let img = DynamicImage::from_decoder(decoder)?; //! # Ok(()) @@ -157,14 +158,16 @@ pub use crate::images::dynimage::{ image_dimensions, load_from_memory, load_from_memory_with_format, open, write_buffer_with_format, }; + pub use crate::io::free_functions::{guess_format, load, save_buffer, save_buffer_with_format}; pub use crate::io::{ decoder::{AnimationDecoder, ImageDecoder}, encoder::ImageEncoder, format::ImageFormat, - image_reader_type::ImageReader, + image_reader_type::{ImageReader, ImageReaderOptions}, limits::{LimitSupport, Limits}, + ImageLayout, }; pub use crate::images::dynimage::DynamicImage; diff --git a/src/metadata.rs b/src/metadata.rs index f0e55e5a70..bf01440576 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,5 +1,6 @@ //! Types describing image metadata pub(crate) mod cicp; +mod moxcms; use std::io::{Cursor, Read}; @@ -10,6 +11,15 @@ pub use self::cicp::{ CicpVideoFullRangeFlag, }; +pub(crate) trait CmsProvider { + fn transform(&self, from: Cicp, to: Cicp) -> Option; + fn parse_icc(&self, icc: &[u8]) -> Option; +} + +pub(crate) fn cms_provider() -> &'static dyn CmsProvider { + &moxcms::Moxcms +} + /// Describes the transformations to be applied to the image. /// Compatible with [Exif orientation](https://web.archive.org/web/20200412005226/https://www.impulseadventure.com/photo/exif-orientation.html). /// diff --git a/src/metadata/cicp.rs b/src/metadata/cicp.rs index eb6ee9121f..f9bd6f59e8 100644 --- a/src/metadata/cicp.rs +++ b/src/metadata/cicp.rs @@ -90,27 +90,6 @@ pub enum CicpColorPrimaries { Industry22 = 22, } -impl CicpColorPrimaries { - fn to_moxcms(self) -> moxcms::CicpColorPrimaries { - use moxcms::CicpColorPrimaries as M; - - match self { - CicpColorPrimaries::SRgb => M::Bt709, - CicpColorPrimaries::Unspecified => M::Unspecified, - CicpColorPrimaries::RgbM => M::Bt470M, - CicpColorPrimaries::RgbB => M::Bt470Bg, - CicpColorPrimaries::Bt601 => M::Bt601, - CicpColorPrimaries::Rgb240m => M::Smpte240, - CicpColorPrimaries::GenericFilm => M::GenericFilm, - CicpColorPrimaries::Rgb2020 => M::Bt2020, - CicpColorPrimaries::Xyz => M::Xyz, - CicpColorPrimaries::SmpteRp431 => M::Smpte431, - CicpColorPrimaries::SmpteRp432 => M::Smpte432, - CicpColorPrimaries::Industry22 => M::Ebu3213, - } - } -} - /// The transfer characteristics, expressing relation between encoded values and linear color /// values. /// @@ -170,32 +149,6 @@ pub enum CicpTransferCharacteristics { Bt2100Hlg = 18, } -impl CicpTransferCharacteristics { - fn to_moxcms(self) -> moxcms::TransferCharacteristics { - use moxcms::TransferCharacteristics as T; - - match self { - CicpTransferCharacteristics::Bt709 => T::Bt709, - CicpTransferCharacteristics::Unspecified => T::Unspecified, - CicpTransferCharacteristics::Bt470M => T::Bt470M, - CicpTransferCharacteristics::Bt470BG => T::Bt470Bg, - CicpTransferCharacteristics::Bt601 => T::Bt601, - CicpTransferCharacteristics::Smpte240m => T::Smpte240, - CicpTransferCharacteristics::Linear => T::Linear, - CicpTransferCharacteristics::Log100 => T::Log100, - CicpTransferCharacteristics::LogSqrt => T::Log100sqrt10, - CicpTransferCharacteristics::Iec61966_2_4 => T::Iec61966, - CicpTransferCharacteristics::Bt1361 => T::Bt1361, - CicpTransferCharacteristics::SRgb => T::Srgb, - CicpTransferCharacteristics::Bt2020_10bit => T::Bt202010bit, - CicpTransferCharacteristics::Bt2020_12bit => T::Bt202012bit, - CicpTransferCharacteristics::Smpte2084 => T::Smpte2084, - CicpTransferCharacteristics::Smpte428 => T::Smpte428, - CicpTransferCharacteristics::Bt2100Hlg => T::Hlg, - } - } -} - /// /// Refer to Rec H.273 Table 4. #[repr(u8)] @@ -251,32 +204,6 @@ pub enum CicpMatrixCoefficients { YCgCoRo = 17, } -impl CicpMatrixCoefficients { - fn to_moxcms(self) -> Option { - use moxcms::MatrixCoefficients as M; - - Some(match self { - CicpMatrixCoefficients::Identity => M::Identity, - CicpMatrixCoefficients::Unspecified => M::Unspecified, - CicpMatrixCoefficients::Bt709 => M::Bt709, - CicpMatrixCoefficients::UsFCC => M::Fcc, - CicpMatrixCoefficients::Bt470BG => M::Bt470Bg, - CicpMatrixCoefficients::Smpte170m => M::Smpte170m, - CicpMatrixCoefficients::Smpte240m => M::Smpte240m, - CicpMatrixCoefficients::YCgCo => M::YCgCo, - CicpMatrixCoefficients::Bt2020NonConstant => M::Bt2020Ncl, - CicpMatrixCoefficients::Bt2020Constant => M::Bt2020Cl, - CicpMatrixCoefficients::Smpte2085 => M::Smpte2085, - CicpMatrixCoefficients::ChromaticityDerivedNonConstant => M::ChromaticityDerivedNCL, - CicpMatrixCoefficients::ChromaticityDerivedConstant => M::ChromaticityDerivedCL, - CicpMatrixCoefficients::Bt2100 => M::ICtCp, - CicpMatrixCoefficients::IptPqC2 - | CicpMatrixCoefficients::YCgCoRe - | CicpMatrixCoefficients::YCgCoRo => return None, - }) - } -} - /// The used encoded value range. #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] @@ -310,23 +237,23 @@ pub(crate) enum DerivedLuminance { /// that some particular combination is supported. #[derive(Clone)] pub struct CicpTransform { - from: Cicp, - into: Cicp, - u8: RgbTransforms, - u16: RgbTransforms, - f32: RgbTransforms, + pub(crate) from: Cicp, + pub(crate) into: Cicp, + pub(crate) u8: RgbTransforms, + pub(crate) u16: RgbTransforms, + pub(crate) f32: RgbTransforms, // Converting RGB to Y in the output. - output_coefs: [f32; 3], + pub(crate) output_coefs: [f32; 3], } pub(crate) type CicpApplicable<'lt, C> = dyn Fn(&[C], &mut [C]) + Send + Sync + 'lt; #[derive(Clone)] -struct RgbTransforms { - slices: [Arc>; 4], - luma_rgb: [Arc>; 4], - rgb_luma: [Arc>; 4], - luma_luma: [Arc>; 4], +pub(crate) struct RgbTransforms { + pub(crate) slices: [Arc>; 4], + pub(crate) luma_rgb: [Arc>; 4], + pub(crate) rgb_luma: [Arc>; 4], + pub(crate) luma_luma: [Arc>; 4], } impl CicpTransform { @@ -343,76 +270,7 @@ impl CicpTransform { /// [`ImageBuffer::copy_from_color_space`][`crate::ImageBuffer::copy_from_color_space`], /// [`DynamicImage::copy_from_color_space`][`DynamicImage::copy_from_color_space`]. pub fn new(from: Cicp, into: Cicp) -> Option { - if !from.qualify_stability() || !into.qualify_stability() { - // To avoid regressions, we do not support all kinds of transforms from the start. - // Instead, a selected list will be gradually enlarged as more in-depth tests are done - // and the selected implementation library is checked for suitability in use. - return None; - } - - // Unused, but introduces symmetry to the supported color space transforms. That said we - // calculate the derived luminance coefficients for all color that have a matching moxcms - // profile so this really should not block anything. - let _input_coefs = from.into_rgb().derived_luminance()?; - let output_coefs = into.into_rgb().derived_luminance()?; - - let mox_from = from.to_moxcms_compute_profile()?; - let mox_into = into.to_moxcms_compute_profile()?; - - let opt = moxcms::TransformOptions::default(); - - let f32_fallback = { - let try_f32 = Self::LAYOUTS.map(|(from_layout, into_layout)| { - let (from, from_layout) = mox_from.map_layout(from_layout); - let (into, into_layout) = mox_into.map_layout(into_layout); - - from.create_transform_f32(from_layout, into, into_layout, opt) - .map(Arc:: + Send + Sync>::from) - .ok() - }); - - if try_f32.iter().any(Option::is_none) { - return None; - } - - try_f32.map(Option::unwrap) - }; - - // TODO: really these should be lazy, eh? - Some(CicpTransform { - from, - into, - u8: Self::build_transforms( - Self::LAYOUTS.map(|(from_layout, into_layout)| { - let (from, from_layout) = mox_from.map_layout(from_layout); - let (into, into_layout) = mox_into.map_layout(into_layout); - - from.create_transform_8bit(from_layout, into, into_layout, opt) - .map(Arc:: + Send + Sync>::from) - .ok() - }), - f32_fallback.clone(), - output_coefs, - )?, - u16: Self::build_transforms( - Self::LAYOUTS.map(|(from_layout, into_layout)| { - let (from, from_layout) = mox_from.map_layout(from_layout); - let (into, into_layout) = mox_into.map_layout(into_layout); - - from.create_transform_16bit(from_layout, into, into_layout, opt) - .map(Arc:: + Send + Sync>::from) - .ok() - }), - f32_fallback.clone(), - output_coefs, - )?, - f32: Self::build_transforms( - f32_fallback.clone().map(Some), - f32_fallback.clone(), - output_coefs, - )?, - output_coefs, - }) + crate::metadata::cms_provider().transform(from, into) } /// For a Pixel with known color layout (`ColorType`) get a transform that is accurate. @@ -447,228 +305,6 @@ impl CicpTransform { Ok(()) } - - fn build_transforms( - trs: [Option + Send + Sync>>; 4], - f32: [Arc + Send + Sync>; 4], - output_coef: [f32; 3], - ) -> Option> { - // We would use `[array]::try_map` here, but it is not stable yet. - if trs.iter().any(Option::is_none) { - return None; - } - - let trs = trs.map(Option::unwrap); - - // rgb-rgb transforms are done directly via moxcms. - let slices = trs.clone().map(|tr| { - Arc::new(move |input: &[P], output: &mut [P]| { - tr.transform(input, output).expect("transform failed") - }) as Arc - }); - - const N: usize = 256; - - // luma-rgb transforms expand the Luma to Rgb (and LumaAlpha to Rgba) - let luma_rgb = { - let [tr33, tr34, tr43, tr44] = f32.clone(); - - [ - Arc::new(move |input: &[P], output: &mut [P]| { - let mut ibuffer = [0.0f32; 3 * N]; - let mut obuffer = [0.0f32; 3 * N]; - - for (luma, output) in input.chunks(N).zip(output.chunks_mut(3 * N)) { - let n = luma.len(); - let ibuffer = &mut ibuffer[..3 * n]; - let obuffer = &mut obuffer[..3 * n]; - Self::expand_luma_rgb(luma, ibuffer); - tr33.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgb(obuffer, output); - } - }) as Arc, - Arc::new(move |input: &[P], output: &mut [P]| { - let mut ibuffer = [0.0f32; 3 * N]; - let mut obuffer = [0.0f32; 4 * N]; - - for (luma, output) in input.chunks(N).zip(output.chunks_mut(4 * N)) { - let n = luma.len(); - let ibuffer = &mut ibuffer[..3 * n]; - let obuffer = &mut obuffer[..4 * n]; - Self::expand_luma_rgb(luma, ibuffer); - tr34.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgba(obuffer, output); - } - }) as Arc, - Arc::new(move |input: &[P], output: &mut [P]| { - let mut ibuffer = [0.0f32; 4 * N]; - let mut obuffer = [0.0f32; 3 * N]; - - for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(3 * N)) { - let n = luma.len() / 2; - let ibuffer = &mut ibuffer[..4 * n]; - let obuffer = &mut obuffer[..3 * n]; - Self::expand_luma_rgba(luma, ibuffer); - tr43.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgb(obuffer, output); - } - }) as Arc, - Arc::new(move |input: &[P], output: &mut [P]| { - let mut ibuffer = [0.0f32; 4 * N]; - let mut obuffer = [0.0f32; 4 * N]; - - for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(4 * N)) { - let n = luma.len() / 2; - let ibuffer = &mut ibuffer[..4 * n]; - let obuffer = &mut obuffer[..4 * n]; - Self::expand_luma_rgba(luma, ibuffer); - tr44.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgba(obuffer, output); - } - }) as Arc, - ] - }; - - // rgb-luma transforms contract Rgb to Luma (and Rgba to LumaAlpha) - let rgb_luma = { - let [tr33, tr34, tr43, tr44] = f32.clone(); - - [ - Arc::new(move |input: &[P], output: &mut [P]| { - debug_assert_eq!(input.len() / 3, output.len()); - - let mut ibuffer = [0.0f32; 3 * N]; - let mut obuffer = [0.0f32; 3 * N]; - - for (rgb, output) in input.chunks(3 * N).zip(output.chunks_mut(N)) { - let n = output.len(); - let ibuffer = &mut ibuffer[..3 * n]; - let obuffer = &mut obuffer[..3 * n]; - Self::expand_rgb(rgb, ibuffer); - tr33.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgb_luma(obuffer, output, output_coef); - } - }) as Arc, - Arc::new(move |input: &[P], output: &mut [P]| { - debug_assert_eq!(input.len() / 3, output.len() / 2); - - let mut ibuffer = [0.0f32; 3 * N]; - let mut obuffer = [0.0f32; 4 * N]; - - for (rgb, output) in input.chunks(4 * N).zip(output.chunks_mut(2 * N)) { - let n = output.len() / 2; - let ibuffer = &mut ibuffer[..3 * n]; - let obuffer = &mut obuffer[..4 * n]; - Self::expand_rgb(rgb, ibuffer); - tr34.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgba_luma(obuffer, output, output_coef); - } - }) as Arc, - Arc::new(move |input: &[P], output: &mut [P]| { - debug_assert_eq!(input.len() / 4, output.len()); - - let mut ibuffer = [0.0f32; 4 * N]; - let mut obuffer = [0.0f32; 3 * N]; - - for (rgba, output) in input.chunks(4 * N).zip(output.chunks_mut(N)) { - let n = output.len(); - let ibuffer = &mut ibuffer[..4 * n]; - let obuffer = &mut obuffer[..3 * n]; - Self::expand_rgba(rgba, ibuffer); - tr43.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgb_luma(obuffer, output, output_coef); - } - }) as Arc, - Arc::new(move |input: &[P], output: &mut [P]| { - debug_assert_eq!(input.len() / 4, output.len() / 2); - - let mut ibuffer = [0.0f32; 4 * N]; - let mut obuffer = [0.0f32; 4 * N]; - - for (rgba, output) in input.chunks(4 * N).zip(output.chunks_mut(2 * N)) { - let n = output.len() / 2; - let ibuffer = &mut ibuffer[..4 * n]; - let obuffer = &mut obuffer[..4 * n]; - Self::expand_rgba(rgba, ibuffer); - tr44.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgba_luma(obuffer, output, output_coef); - } - }) as Arc, - ] - }; - - // luma-luma both expand and contract - let luma_luma = { - let [tr33, tr34, tr43, tr44] = f32.clone(); - - [ - Arc::new(move |input: &[P], output: &mut [P]| { - debug_assert_eq!(input.len(), output.len()); - let mut ibuffer = [0.0f32; 3 * N]; - let mut obuffer = [0.0f32; 3 * N]; - - for (luma, output) in input.chunks(N).zip(output.chunks_mut(N)) { - let n = luma.len(); - let ibuffer = &mut ibuffer[..3 * n]; - let obuffer = &mut obuffer[..3 * n]; - Self::expand_luma_rgb(luma, ibuffer); - tr33.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgb_luma(obuffer, output, output_coef); - } - }) as Arc, - Arc::new(move |input: &[P], output: &mut [P]| { - debug_assert_eq!(input.len(), output.len() / 2); - let mut ibuffer = [0.0f32; 3 * N]; - let mut obuffer = [0.0f32; 4 * N]; - - for (luma, output) in input.chunks(N).zip(output.chunks_mut(2 * N)) { - let n = luma.len(); - let ibuffer = &mut ibuffer[..3 * n]; - let obuffer = &mut obuffer[..4 * n]; - Self::expand_luma_rgb(luma, ibuffer); - tr34.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgba_luma(obuffer, output, output_coef); - } - }) as Arc, - Arc::new(move |input: &[P], output: &mut [P]| { - debug_assert_eq!(input.len() / 2, output.len()); - let mut ibuffer = [0.0f32; 4 * N]; - let mut obuffer = [0.0f32; 3 * N]; - - for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(N)) { - let n = luma.len() / 2; - let ibuffer = &mut ibuffer[..4 * n]; - let obuffer = &mut obuffer[..3 * n]; - Self::expand_luma_rgba(luma, ibuffer); - tr43.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgb_luma(obuffer, output, output_coef); - } - }) as Arc, - Arc::new(move |input: &[P], output: &mut [P]| { - debug_assert_eq!(input.len() / 2, output.len() / 2); - let mut ibuffer = [0.0f32; 4 * N]; - let mut obuffer = [0.0f32; 4 * N]; - - for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(2 * N)) { - let n = luma.len() / 2; - let ibuffer = &mut ibuffer[..4 * n]; - let obuffer = &mut obuffer[..4 * n]; - Self::expand_luma_rgba(luma, ibuffer); - tr44.transform(ibuffer, obuffer).expect("transform failed"); - Self::clamp_rgba_luma(obuffer, output, output_coef); - } - }) as Arc, - ] - }; - - Some(RgbTransforms { - slices, - luma_rgb, - rgb_luma, - luma_luma, - }) - } - pub(crate) fn transform_dynamic(&self, lhs: &mut DynamicImage, rhs: &DynamicImage) { const STEP: usize = 256; @@ -890,7 +526,8 @@ impl CicpTransform { self.f32.select_transform::(into) } - const LAYOUTS: [(LayoutWithColor, LayoutWithColor); 4] = [ + /// The order of transforms in the conversion tables. + pub(crate) const LAYOUTS: [(LayoutWithColor, LayoutWithColor); 4] = [ (LayoutWithColor::Rgb, LayoutWithColor::Rgb), (LayoutWithColor::Rgb, LayoutWithColor::Rgba), (LayoutWithColor::Rgba, LayoutWithColor::Rgb), @@ -1335,52 +972,6 @@ impl Cicp { full_range: CicpVideoFullRangeFlag::FullRange, }; - /// Get an compute representation of an ICC profile for RGB. - /// - /// Note you should *not* be using this profile for export in a file, as discussed below. - /// - /// This is straightforward for Rgb and RgbA representations. - /// - /// Our luma models a Y component of a YCbCr color space. It turns out that ICC V4 does - /// not support pure Luma in any other whitepoint apart from D50 (the native profile - /// connection space). The use of a grayTRC does *not* take the chromatic adaptation - /// matrix into account. Of course we can encode the adaptation into the TRC as a - /// coefficient, the Y component of the product of the whitepoint adaptation matrix - /// inverse and the pcs's whitepoint XYZ, but that is only correct for gray -> gray - /// conversion (and that coefficient should generally be `1`). - /// - /// Hence we use a YCbCr. The data->pcs path could be modelled by ("M" curves, matrix, "B" - /// curves) where B curves or M curves are all the identity, depending on whether constant or - /// non-constant luma is in use. This is a subset of the capabilities that a lutAToBType - /// allows. Unfortunately, this is not implemented in moxcms yet and for efficiency we would - /// like to have a masked `create_transform_*` in which the CbCr channels are discarded / - /// assumed 0 instead of them being in memory. Due to this special case and for supporting - /// conversions between sample types, we implement said promotion as part of conversion to - /// Rgba32F in this crate. - /// - /// For export to file, it would arguably correct to use a carefully crafted gray profile which - /// we may implement in another function. That is, we could setup a tone reproduction curve - /// which maps each sample value (which ICC regards as D50) into XYZ D50 in such a way that it - /// _appears_ with the correct D50 luminance that we would get if we had used the conversion - /// unders its true input whitepoint. The resulting color has a slightly wrong chroma as it is - /// linearly dependent on D50 instead, but it's brightness would be correctly presented. At - /// least for perceptual intent this might be alright. - fn to_moxcms_compute_profile(self) -> Option { - let mut rgb = moxcms::ColorProfile::new_srgb(); - - rgb.update_rgb_colorimetry_from_cicp(moxcms::CicpProfile { - color_primaries: self.primaries.to_moxcms(), - transfer_characteristics: self.transfer.to_moxcms(), - matrix_coefficients: self.matrix.to_moxcms()?, - full_range: match self.full_range { - CicpVideoFullRangeFlag::NarrowRange => false, - CicpVideoFullRangeFlag::FullRange => true, - }, - }); - - Some(ColorProfile { rgb }) - } - /// Whether we have invested enough testing to ensure that color values can be assumed to be /// stable and correspond to an intended effect, in particular if there even is a well-defined /// meaning to these color spaces. @@ -1517,28 +1108,6 @@ impl From for Cicp { } } -/// An RGB profile with its related (same tone-mapping) gray profile. -/// -/// This is the whole input information which we must be able to pass to the CMS in a support -/// transform, to handle all possible combinations of `ColorType` pixels that can be thrown at us. -/// For instance, in a previous iteration we had a separate gray profile here (but now handle that -/// internally by expansion to RGB through an YCbCr). Future iterations may add additional structs -/// to be computed for validating `CicpTransform::new`. -struct ColorProfile { - rgb: moxcms::ColorProfile, -} - -impl ColorProfile { - fn map_layout(&self, layout: LayoutWithColor) -> (&moxcms::ColorProfile, moxcms::Layout) { - match layout { - LayoutWithColor::Rgb => (&self.rgb, moxcms::Layout::Rgb), - LayoutWithColor::Rgba => (&self.rgb, moxcms::Layout::Rgba), - // See comment in `to_moxcms_profile`. - LayoutWithColor::Luma | LayoutWithColor::LumaAlpha => unreachable!(), - } - } -} - #[cfg(test)] #[test] fn moxcms() { diff --git a/src/metadata/moxcms.rs b/src/metadata/moxcms.rs new file mode 100644 index 0000000000..fc7e9516ff --- /dev/null +++ b/src/metadata/moxcms.rs @@ -0,0 +1,519 @@ +/// Contains bindings to `moxcms` as a [`super::CmsProvider`]. +use std::sync::Arc; + +use crate::{ + metadata::{ + cicp::ColorComponentForCicp, Cicp, CicpColorPrimaries, CicpMatrixCoefficients, + CicpTransferCharacteristics, CicpTransform, CicpVideoFullRangeFlag, + }, + traits::private::LayoutWithColor, +}; + +pub(crate) struct Moxcms; + +impl super::CmsProvider for Moxcms { + fn transform(&self, from: Cicp, into: Cicp) -> Option { + if !from.qualify_stability() || !into.qualify_stability() { + // To avoid regressions, we do not support all kinds of transforms from the start. + // Instead, a selected list will be gradually enlarged as more in-depth tests are done + // and the selected implementation library is checked for suitability in use. + return None; + } + + // Unused, but introduces symmetry to the supported color space transforms. That said we + // calculate the derived luminance coefficients for all color that have a matching moxcms + // profile so this really should not block anything. + let _input_coefs = from.into_rgb().derived_luminance()?; + let output_coefs = into.into_rgb().derived_luminance()?; + + let mox_from = from.to_moxcms_compute_profile()?; + let mox_into = into.to_moxcms_compute_profile()?; + + let opt = moxcms::TransformOptions::default(); + + let f32_fallback = { + let try_f32 = CicpTransform::LAYOUTS.map(|(from_layout, into_layout)| { + let (from, from_layout) = mox_from.map_layout(from_layout); + let (into, into_layout) = mox_into.map_layout(into_layout); + + from.create_transform_f32(from_layout, into, into_layout, opt) + .map(Arc:: + Send + Sync>::from) + .ok() + }); + + if try_f32.iter().any(Option::is_none) { + return None; + } + + try_f32.map(Option::unwrap) + }; + + Some(CicpTransform { + from, + into, + u8: Self::build_transforms( + CicpTransform::LAYOUTS.map(|(from_layout, into_layout)| { + let (from, from_layout) = mox_from.map_layout(from_layout); + let (into, into_layout) = mox_into.map_layout(into_layout); + + from.create_transform_8bit(from_layout, into, into_layout, opt) + .map(Arc:: + Send + Sync>::from) + .ok() + }), + f32_fallback.clone(), + output_coefs, + )?, + u16: Self::build_transforms( + CicpTransform::LAYOUTS.map(|(from_layout, into_layout)| { + let (from, from_layout) = mox_from.map_layout(from_layout); + let (into, into_layout) = mox_into.map_layout(into_layout); + + from.create_transform_16bit(from_layout, into, into_layout, opt) + .map(Arc:: + Send + Sync>::from) + .ok() + }), + f32_fallback.clone(), + output_coefs, + )?, + f32: Self::build_transforms( + f32_fallback.clone().map(Some), + f32_fallback.clone(), + output_coefs, + )?, + output_coefs, + }) + } + + fn parse_icc(&self, icc: &[u8]) -> Option { + let profile = moxcms::ColorProfile::new_from_slice(icc).ok()?; + let cicp = profile.cicp?; + + use moxcms::CicpColorPrimaries as Mcp; + use moxcms::MatrixCoefficients as Mmc; + use moxcms::TransferCharacteristics as Mtc; + + Some(Cicp { + primaries: match cicp.color_primaries { + Mcp::Bt709 => CicpColorPrimaries::SRgb, + Mcp::Unspecified | Mcp::Reserved => CicpColorPrimaries::Unspecified, + Mcp::Bt470M => CicpColorPrimaries::RgbM, + Mcp::Bt470Bg => CicpColorPrimaries::RgbB, + Mcp::Bt601 => CicpColorPrimaries::Bt601, + Mcp::Smpte240 => CicpColorPrimaries::Rgb240m, + Mcp::GenericFilm => CicpColorPrimaries::GenericFilm, + Mcp::Bt2020 => CicpColorPrimaries::Rgb2020, + Mcp::Xyz => CicpColorPrimaries::Xyz, + Mcp::Smpte431 => CicpColorPrimaries::SmpteRp431, + Mcp::Smpte432 => CicpColorPrimaries::SmpteRp432, + Mcp::Ebu3213 => CicpColorPrimaries::Industry22, + }, + transfer: match cicp.transfer_characteristics { + Mtc::Bt709 => CicpTransferCharacteristics::Bt709, + Mtc::Unspecified | Mtc::Reserved => CicpTransferCharacteristics::Unspecified, + Mtc::Bt470M => CicpTransferCharacteristics::Bt470M, + Mtc::Bt470Bg => CicpTransferCharacteristics::Bt470BG, + Mtc::Bt601 => CicpTransferCharacteristics::Bt601, + Mtc::Smpte240 => CicpTransferCharacteristics::Smpte240m, + Mtc::Linear => CicpTransferCharacteristics::Linear, + Mtc::Log100 => CicpTransferCharacteristics::Log100, + Mtc::Log100sqrt10 => CicpTransferCharacteristics::LogSqrt, + Mtc::Iec61966 => CicpTransferCharacteristics::Iec61966_2_4, + Mtc::Bt1361 => CicpTransferCharacteristics::Bt1361, + Mtc::Srgb => CicpTransferCharacteristics::SRgb, + Mtc::Bt202010bit => CicpTransferCharacteristics::Bt2020_10bit, + Mtc::Bt202012bit => CicpTransferCharacteristics::Bt2020_12bit, + Mtc::Smpte2084 => CicpTransferCharacteristics::Smpte2084, + Mtc::Smpte428 => CicpTransferCharacteristics::Smpte428, + Mtc::Hlg => CicpTransferCharacteristics::Bt2100Hlg, + }, + matrix: match cicp.matrix_coefficients { + Mmc::Identity => CicpMatrixCoefficients::Identity, + Mmc::Unspecified | Mmc::Reserved => CicpMatrixCoefficients::Unspecified, + Mmc::Bt709 => CicpMatrixCoefficients::Bt709, + Mmc::Fcc => CicpMatrixCoefficients::UsFCC, + Mmc::Bt470Bg => CicpMatrixCoefficients::Bt470BG, + Mmc::Smpte170m => CicpMatrixCoefficients::Smpte170m, + Mmc::Smpte240m => CicpMatrixCoefficients::Smpte240m, + Mmc::YCgCo => CicpMatrixCoefficients::YCgCo, + Mmc::Bt2020Ncl => CicpMatrixCoefficients::Bt2020NonConstant, + Mmc::Bt2020Cl => CicpMatrixCoefficients::Bt2020Constant, + Mmc::Smpte2085 => CicpMatrixCoefficients::Smpte2085, + Mmc::ChromaticityDerivedNCL => { + CicpMatrixCoefficients::ChromaticityDerivedNonConstant + } + Mmc::ChromaticityDerivedCL => CicpMatrixCoefficients::ChromaticityDerivedConstant, + Mmc::ICtCp => CicpMatrixCoefficients::Bt2100, + }, + full_range: match cicp.full_range { + false => CicpVideoFullRangeFlag::NarrowRange, + true => CicpVideoFullRangeFlag::FullRange, + }, + }) + } +} + +impl Moxcms { + fn build_transforms( + trs: [Option + Send + Sync>>; 4], + f32: [Arc + Send + Sync>; 4], + output_coef: [f32; 3], + ) -> Option> { + // We would use `[array]::try_map` here, but it is not stable yet. + if trs.iter().any(Option::is_none) { + return None; + } + + let trs = trs.map(Option::unwrap); + + // rgb-rgb transforms are done directly via moxcms. + let slices = trs.clone().map(|tr| { + Arc::new(move |input: &[P], output: &mut [P]| { + tr.transform(input, output).expect("transform failed") + }) as Arc + }); + + const N: usize = 256; + + // luma-rgb transforms expand the Luma to Rgb (and LumaAlpha to Rgba) + let luma_rgb = { + let [tr33, tr34, tr43, tr44] = f32.clone(); + + [ + Arc::new(move |input: &[P], output: &mut [P]| { + let mut ibuffer = [0.0f32; 3 * N]; + let mut obuffer = [0.0f32; 3 * N]; + + for (luma, output) in input.chunks(N).zip(output.chunks_mut(3 * N)) { + let n = luma.len(); + let ibuffer = &mut ibuffer[..3 * n]; + let obuffer = &mut obuffer[..3 * n]; + CicpTransform::expand_luma_rgb(luma, ibuffer); + tr33.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgb(obuffer, output); + } + }) as Arc, + Arc::new(move |input: &[P], output: &mut [P]| { + let mut ibuffer = [0.0f32; 3 * N]; + let mut obuffer = [0.0f32; 4 * N]; + + for (luma, output) in input.chunks(N).zip(output.chunks_mut(4 * N)) { + let n = luma.len(); + let ibuffer = &mut ibuffer[..3 * n]; + let obuffer = &mut obuffer[..4 * n]; + CicpTransform::expand_luma_rgb(luma, ibuffer); + tr34.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgba(obuffer, output); + } + }) as Arc, + Arc::new(move |input: &[P], output: &mut [P]| { + let mut ibuffer = [0.0f32; 4 * N]; + let mut obuffer = [0.0f32; 3 * N]; + + for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(3 * N)) { + let n = luma.len() / 2; + let ibuffer = &mut ibuffer[..4 * n]; + let obuffer = &mut obuffer[..3 * n]; + CicpTransform::expand_luma_rgba(luma, ibuffer); + tr43.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgb(obuffer, output); + } + }) as Arc, + Arc::new(move |input: &[P], output: &mut [P]| { + let mut ibuffer = [0.0f32; 4 * N]; + let mut obuffer = [0.0f32; 4 * N]; + + for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(4 * N)) { + let n = luma.len() / 2; + let ibuffer = &mut ibuffer[..4 * n]; + let obuffer = &mut obuffer[..4 * n]; + CicpTransform::expand_luma_rgba(luma, ibuffer); + tr44.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgba(obuffer, output); + } + }) as Arc, + ] + }; + + // rgb-luma transforms contract Rgb to Luma (and Rgba to LumaAlpha) + let rgb_luma = { + let [tr33, tr34, tr43, tr44] = f32.clone(); + + [ + Arc::new(move |input: &[P], output: &mut [P]| { + debug_assert_eq!(input.len() / 3, output.len()); + + let mut ibuffer = [0.0f32; 3 * N]; + let mut obuffer = [0.0f32; 3 * N]; + + for (rgb, output) in input.chunks(3 * N).zip(output.chunks_mut(N)) { + let n = output.len(); + let ibuffer = &mut ibuffer[..3 * n]; + let obuffer = &mut obuffer[..3 * n]; + CicpTransform::expand_rgb(rgb, ibuffer); + tr33.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgb_luma(obuffer, output, output_coef); + } + }) as Arc, + Arc::new(move |input: &[P], output: &mut [P]| { + debug_assert_eq!(input.len() / 3, output.len() / 2); + + let mut ibuffer = [0.0f32; 3 * N]; + let mut obuffer = [0.0f32; 4 * N]; + + for (rgb, output) in input.chunks(4 * N).zip(output.chunks_mut(2 * N)) { + let n = output.len() / 2; + let ibuffer = &mut ibuffer[..3 * n]; + let obuffer = &mut obuffer[..4 * n]; + CicpTransform::expand_rgb(rgb, ibuffer); + tr34.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgba_luma(obuffer, output, output_coef); + } + }) as Arc, + Arc::new(move |input: &[P], output: &mut [P]| { + debug_assert_eq!(input.len() / 4, output.len()); + + let mut ibuffer = [0.0f32; 4 * N]; + let mut obuffer = [0.0f32; 3 * N]; + + for (rgba, output) in input.chunks(4 * N).zip(output.chunks_mut(N)) { + let n = output.len(); + let ibuffer = &mut ibuffer[..4 * n]; + let obuffer = &mut obuffer[..3 * n]; + CicpTransform::expand_rgba(rgba, ibuffer); + tr43.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgb_luma(obuffer, output, output_coef); + } + }) as Arc, + Arc::new(move |input: &[P], output: &mut [P]| { + debug_assert_eq!(input.len() / 4, output.len() / 2); + + let mut ibuffer = [0.0f32; 4 * N]; + let mut obuffer = [0.0f32; 4 * N]; + + for (rgba, output) in input.chunks(4 * N).zip(output.chunks_mut(2 * N)) { + let n = output.len() / 2; + let ibuffer = &mut ibuffer[..4 * n]; + let obuffer = &mut obuffer[..4 * n]; + CicpTransform::expand_rgba(rgba, ibuffer); + tr44.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgba_luma(obuffer, output, output_coef); + } + }) as Arc, + ] + }; + + // luma-luma both expand and contract + let luma_luma = { + let [tr33, tr34, tr43, tr44] = f32.clone(); + + [ + Arc::new(move |input: &[P], output: &mut [P]| { + debug_assert_eq!(input.len(), output.len()); + let mut ibuffer = [0.0f32; 3 * N]; + let mut obuffer = [0.0f32; 3 * N]; + + for (luma, output) in input.chunks(N).zip(output.chunks_mut(N)) { + let n = luma.len(); + let ibuffer = &mut ibuffer[..3 * n]; + let obuffer = &mut obuffer[..3 * n]; + CicpTransform::expand_luma_rgb(luma, ibuffer); + tr33.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgb_luma(obuffer, output, output_coef); + } + }) as Arc, + Arc::new(move |input: &[P], output: &mut [P]| { + debug_assert_eq!(input.len(), output.len() / 2); + let mut ibuffer = [0.0f32; 3 * N]; + let mut obuffer = [0.0f32; 4 * N]; + + for (luma, output) in input.chunks(N).zip(output.chunks_mut(2 * N)) { + let n = luma.len(); + let ibuffer = &mut ibuffer[..3 * n]; + let obuffer = &mut obuffer[..4 * n]; + CicpTransform::expand_luma_rgb(luma, ibuffer); + tr34.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgba_luma(obuffer, output, output_coef); + } + }) as Arc, + Arc::new(move |input: &[P], output: &mut [P]| { + debug_assert_eq!(input.len() / 2, output.len()); + let mut ibuffer = [0.0f32; 4 * N]; + let mut obuffer = [0.0f32; 3 * N]; + + for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(N)) { + let n = luma.len() / 2; + let ibuffer = &mut ibuffer[..4 * n]; + let obuffer = &mut obuffer[..3 * n]; + CicpTransform::expand_luma_rgba(luma, ibuffer); + tr43.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgb_luma(obuffer, output, output_coef); + } + }) as Arc, + Arc::new(move |input: &[P], output: &mut [P]| { + debug_assert_eq!(input.len() / 2, output.len() / 2); + let mut ibuffer = [0.0f32; 4 * N]; + let mut obuffer = [0.0f32; 4 * N]; + + for (luma, output) in input.chunks(2 * N).zip(output.chunks_mut(2 * N)) { + let n = luma.len() / 2; + let ibuffer = &mut ibuffer[..4 * n]; + let obuffer = &mut obuffer[..4 * n]; + CicpTransform::expand_luma_rgba(luma, ibuffer); + tr44.transform(ibuffer, obuffer).expect("transform failed"); + CicpTransform::clamp_rgba_luma(obuffer, output, output_coef); + } + }) as Arc, + ] + }; + + Some(crate::metadata::cicp::RgbTransforms { + slices, + luma_rgb, + rgb_luma, + luma_luma, + }) + } +} + +/// An RGB profile with its related (same tone-mapping) gray profile. +/// +/// This is the whole input information which we must be able to pass to the CMS in a support +/// transform, to handle all possible combinations of `ColorType` pixels that can be thrown at us. +/// For instance, in a previous iteration we had a separate gray profile here (but now handle that +/// internally by expansion to RGB through an YCbCr). Future iterations may add additional structs +/// to be computed for validating `CicpTransform::new`. +struct ColorProfile { + rgb: moxcms::ColorProfile, +} + +impl ColorProfile { + fn map_layout(&self, layout: LayoutWithColor) -> (&moxcms::ColorProfile, moxcms::Layout) { + match layout { + LayoutWithColor::Rgb => (&self.rgb, moxcms::Layout::Rgb), + LayoutWithColor::Rgba => (&self.rgb, moxcms::Layout::Rgba), + // See comment in `to_moxcms_profile`. + LayoutWithColor::Luma | LayoutWithColor::LumaAlpha => unreachable!(), + } + } +} + +impl Cicp { + /// Get an compute representation of an ICC profile for RGB. + /// + /// Note you should *not* be using this profile for export in a file, as discussed below. + /// + /// This is straightforward for Rgb and RgbA representations. + /// + /// Our luma models a Y component of a YCbCr color space. It turns out that ICC V4 does + /// not support pure Luma in any other whitepoint apart from D50 (the native profile + /// connection space). The use of a grayTRC does *not* take the chromatic adaptation + /// matrix into account. Of course we can encode the adaptation into the TRC as a + /// coefficient, the Y component of the product of the whitepoint adaptation matrix + /// inverse and the pcs's whitepoint XYZ, but that is only correct for gray -> gray + /// conversion (and that coefficient should generally be `1`). + /// + /// Hence we use a YCbCr. The data->pcs path could be modelled by ("M" curves, matrix, "B" + /// curves) where B curves or M curves are all the identity, depending on whether constant or + /// non-constant luma is in use. This is a subset of the capabilities that a lutAToBType + /// allows. Unfortunately, this is not implemented in moxcms yet and for efficiency we would + /// like to have a masked `create_transform_*` in which the CbCr channels are discarded / + /// assumed 0 instead of them being in memory. Due to this special case and for supporting + /// conversions between sample types, we implement said promotion as part of conversion to + /// Rgba32F in this crate. + /// + /// For export to file, it would arguably correct to use a carefully crafted gray profile which + /// we may implement in another function. That is, we could setup a tone reproduction curve + /// which maps each sample value (which ICC regards as D50) into XYZ D50 in such a way that it + /// _appears_ with the correct D50 luminance that we would get if we had used the conversion + /// unders its true input whitepoint. The resulting color has a slightly wrong chroma as it is + /// linearly dependent on D50 instead, but it's brightness would be correctly presented. At + /// least for perceptual intent this might be alright. + fn to_moxcms_compute_profile(self) -> Option { + let mut rgb = moxcms::ColorProfile::new_srgb(); + + rgb.update_rgb_colorimetry_from_cicp(moxcms::CicpProfile { + color_primaries: self.primaries.to_moxcms(), + transfer_characteristics: self.transfer.to_moxcms(), + matrix_coefficients: self.matrix.to_moxcms()?, + full_range: match self.full_range { + CicpVideoFullRangeFlag::NarrowRange => false, + CicpVideoFullRangeFlag::FullRange => true, + }, + }); + + Some(ColorProfile { rgb }) + } +} + +impl CicpColorPrimaries { + fn to_moxcms(self) -> moxcms::CicpColorPrimaries { + use moxcms::CicpColorPrimaries as M; + + match self { + CicpColorPrimaries::SRgb => M::Bt709, + CicpColorPrimaries::Unspecified => M::Unspecified, + CicpColorPrimaries::RgbM => M::Bt470M, + CicpColorPrimaries::RgbB => M::Bt470Bg, + CicpColorPrimaries::Bt601 => M::Bt601, + CicpColorPrimaries::Rgb240m => M::Smpte240, + CicpColorPrimaries::GenericFilm => M::GenericFilm, + CicpColorPrimaries::Rgb2020 => M::Bt2020, + CicpColorPrimaries::Xyz => M::Xyz, + CicpColorPrimaries::SmpteRp431 => M::Smpte431, + CicpColorPrimaries::SmpteRp432 => M::Smpte432, + CicpColorPrimaries::Industry22 => M::Ebu3213, + } + } +} + +impl CicpTransferCharacteristics { + fn to_moxcms(self) -> moxcms::TransferCharacteristics { + use moxcms::TransferCharacteristics as T; + + match self { + CicpTransferCharacteristics::Bt709 => T::Bt709, + CicpTransferCharacteristics::Unspecified => T::Unspecified, + CicpTransferCharacteristics::Bt470M => T::Bt470M, + CicpTransferCharacteristics::Bt470BG => T::Bt470Bg, + CicpTransferCharacteristics::Bt601 => T::Bt601, + CicpTransferCharacteristics::Smpte240m => T::Smpte240, + CicpTransferCharacteristics::Linear => T::Linear, + CicpTransferCharacteristics::Log100 => T::Log100, + CicpTransferCharacteristics::LogSqrt => T::Log100sqrt10, + CicpTransferCharacteristics::Iec61966_2_4 => T::Iec61966, + CicpTransferCharacteristics::Bt1361 => T::Bt1361, + CicpTransferCharacteristics::SRgb => T::Srgb, + CicpTransferCharacteristics::Bt2020_10bit => T::Bt202010bit, + CicpTransferCharacteristics::Bt2020_12bit => T::Bt202012bit, + CicpTransferCharacteristics::Smpte2084 => T::Smpte2084, + CicpTransferCharacteristics::Smpte428 => T::Smpte428, + CicpTransferCharacteristics::Bt2100Hlg => T::Hlg, + } + } +} + +impl CicpMatrixCoefficients { + fn to_moxcms(self) -> Option { + use moxcms::MatrixCoefficients as M; + + Some(match self { + CicpMatrixCoefficients::Identity => M::Identity, + CicpMatrixCoefficients::Unspecified => M::Unspecified, + CicpMatrixCoefficients::Bt709 => M::Bt709, + CicpMatrixCoefficients::UsFCC => M::Fcc, + CicpMatrixCoefficients::Bt470BG => M::Bt470Bg, + CicpMatrixCoefficients::Smpte170m => M::Smpte170m, + CicpMatrixCoefficients::Smpte240m => M::Smpte240m, + CicpMatrixCoefficients::YCgCo => M::YCgCo, + CicpMatrixCoefficients::Bt2020NonConstant => M::Bt2020Ncl, + CicpMatrixCoefficients::Bt2020Constant => M::Bt2020Cl, + CicpMatrixCoefficients::Smpte2085 => M::Smpte2085, + CicpMatrixCoefficients::ChromaticityDerivedNonConstant => M::ChromaticityDerivedNCL, + CicpMatrixCoefficients::ChromaticityDerivedConstant => M::ChromaticityDerivedCL, + CicpMatrixCoefficients::Bt2100 => M::ICtCp, + CicpMatrixCoefficients::IptPqC2 + | CicpMatrixCoefficients::YCgCoRe + | CicpMatrixCoefficients::YCgCoRo => return None, + }) + } +} diff --git a/tests/limits.rs b/tests/limits.rs index f858a34ca4..ddf2228a62 100644 --- a/tests/limits.rs +++ b/tests/limits.rs @@ -16,7 +16,7 @@ use std::io::Cursor; use image::{ - load_from_memory_with_format, ImageDecoder, ImageFormat, ImageReader, Limits, RgbImage, + load_from_memory_with_format, ImageDecoder, ImageFormat, ImageReaderOptions, Limits, RgbImage, }; const WIDTH: u32 = 256; @@ -51,7 +51,10 @@ fn permissive_limits() -> Limits { let mut limits = Limits::no_limits(); limits.max_image_width = Some(WIDTH); limits.max_image_height = Some(HEIGHT); - limits.max_alloc = Some((WIDTH * HEIGHT * 5).into()); // `* 3`` would be an exact fit for RGB; `* 5`` allows some slack space + // `* 3`` would be an exact fit for RGB; + // `* 6` allows a duplicate buffer (ImageReader and internal) + // `* 8` gives some slack for reserving half in ImageReader + limits.max_alloc = Some((WIDTH * HEIGHT * 8).into()); limits } @@ -60,7 +63,7 @@ fn load_through_reader( format: ImageFormat, limits: Limits, ) -> Result { - let mut reader = ImageReader::new(Cursor::new(input)); + let mut reader = ImageReaderOptions::new(Cursor::new(input)); reader.set_format(format); reader.limits(limits); reader.decode() @@ -69,8 +72,6 @@ fn load_through_reader( #[test] #[cfg(feature = "gif")] fn gif() { - use image::codecs::gif::GifDecoder; - let image = test_image(ImageFormat::Gif); // sanity check that our image loads successfully without limits assert!(load_from_memory_with_format(&image, ImageFormat::Gif).is_ok()); @@ -78,22 +79,7 @@ fn gif() { assert!(load_through_reader(&image, ImageFormat::Gif, permissive_limits()).is_ok()); // image::ImageReader assert!(load_through_reader(&image, ImageFormat::Gif, width_height_limits()).is_err()); - assert!(load_through_reader(&image, ImageFormat::Gif, allocation_limits()).is_err()); // BROKEN! - - // GifDecoder - let mut decoder = GifDecoder::new(Cursor::new(&image)).unwrap(); - assert!(decoder.set_limits(width_height_limits()).is_err()); - // no tests for allocation limits because the caller is responsible for allocating the buffer in this case - - // Custom constructor on GifDecoder - #[allow(deprecated)] - { - assert!(GifDecoder::new(Cursor::new(&image)) - .unwrap() - .set_limits(width_height_limits()) - .is_err()); - // no tests for allocation limits because the caller is responsible for allocating the buffer in this case - } + assert!(load_through_reader(&image, ImageFormat::Gif, allocation_limits()).is_err()); } #[test] @@ -111,16 +97,9 @@ fn png() { assert!(load_through_reader(&image, ImageFormat::Png, allocation_limits()).is_err()); // PngDecoder - let mut decoder = PngDecoder::new(Cursor::new(&image)).unwrap(); - assert!(decoder.set_limits(width_height_limits()).is_err()); - // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. - // Unlike many others, the `png` crate does natively support memory limits for auxiliary buffers, - // but they are not passed down from `set_limits` - only from the `with_limits` constructor. - // The proper fix is known to require an API break: https://github.com/image-rs/image/issues/2084 - - // Custom constructor on PngDecoder - assert!(PngDecoder::with_limits(Cursor::new(&image), width_height_limits()).is_err()); - // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. + let mut decoder = PngDecoder::new(Cursor::new(&image)); + decoder.set_limits(width_height_limits()).unwrap(); + assert!(decoder.peek_layout().is_err()); } #[test] @@ -177,7 +156,8 @@ fn tiff() { // so there is a copy from the buffer allocated by `tiff` to a buffer allocated by `image`. // This results in memory usage overhead the size of the output buffer. let mut tiff_permissive_limits = permissive_limits(); - tiff_permissive_limits.max_alloc = Some((WIDTH * HEIGHT * 10).into()); // `* 9` would be exactly three output buffers, `* 10`` has some slack space + // `* 6` would be exactly two output buffers, `* 12`` accounts for ImageReader taking half. + tiff_permissive_limits.max_alloc = Some((WIDTH * HEIGHT * 12).into()); load_through_reader(&image, ImageFormat::Tiff, tiff_permissive_limits).unwrap(); // image::ImageReader @@ -186,6 +166,7 @@ fn tiff() { // TiffDecoder let mut decoder = TiffDecoder::new(Cursor::new(&image)).unwrap(); + decoder.peek_layout().unwrap(); assert!(decoder.set_limits(width_height_limits()).is_err()); // No tests for allocation limits because the caller is responsible for allocating the buffer in this case. } diff --git a/tests/limits_anim.rs b/tests/limits_anim.rs index c0983e8c46..f84417f89f 100644 --- a/tests/limits_anim.rs +++ b/tests/limits_anim.rs @@ -1,6 +1,6 @@ //! Test enforcement of size and memory limits for animation decoding APIs. -use image::{AnimationDecoder, ImageDecoder, ImageResult, Limits}; +use image::{ImageResult, Limits}; #[cfg(feature = "gif")] use image::codecs::gif::GifDecoder; @@ -9,10 +9,12 @@ use image::codecs::gif::GifDecoder; fn gif_decode(data: &[u8], limits: Limits) -> ImageResult<()> { use std::io::Cursor; - let mut decoder = GifDecoder::new(Cursor::new(data)).unwrap(); - decoder.set_limits(limits)?; + let decoder = GifDecoder::new(Cursor::new(data)).unwrap(); + let mut reader = image::ImageReader::from_decoder(Box::new(decoder)); + reader.set_limits(limits)?; + { - let frames = decoder.into_frames(); + let frames = reader.into_frames(); for result in frames { result?; } @@ -60,7 +62,9 @@ fn animated_full_frame_discard() { let mut limits_just_enough = Limits::default(); limits_just_enough.max_image_height = Some(1000); limits_just_enough.max_image_width = Some(1000); - limits_just_enough.max_alloc = Some(1000 * 1000 * 4 * 2); // 4 for RGBA, 2 for 2 buffers kept in memory simultaneously + // 4 for RGBA, 2 for 2 buffers kept in memory simultaneously. The reader will take half of this + // for internal use. + limits_just_enough.max_alloc = Some(1000 * 1000 * 4 * 6); gif_decode(&data, limits_just_enough) .expect("With these limits it should have decoded successfully"); @@ -94,7 +98,10 @@ fn animated_frame_combine() { let mut limits_enough = Limits::default(); limits_enough.max_image_height = Some(1000); limits_enough.max_image_width = Some(1000); - limits_enough.max_alloc = Some(1000 * 1000 * 4 * 3); // 4 for RGBA, 2 for 2 buffers kept in memory simultaneously + // See above. In addition to the internal frames, the reader will also allocate so some safety + // margin is given. Two full images, a small frame, plus the read-out buffer will take half the + // allocation. The smaller one also accounts for the extra margin if we make it a full frame. + limits_enough.max_alloc = Some(1000 * 1000 * 4 * 6); gif_decode(&data, limits_enough) .expect("With these limits it should have decoded successfully"); diff --git a/tests/metadata.rs b/tests/metadata.rs index 1375aa84e2..6843d68e7f 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -25,7 +25,8 @@ fn test_read_xmp_png() -> Result<(), image::ImageError> { let img_path = PathBuf::from_str(XMP_PNG_PATH).unwrap(); let data = fs::read(img_path)?; - let mut png_decoder = PngDecoder::new(std::io::Cursor::new(data))?; + let mut png_decoder = PngDecoder::new(std::io::Cursor::new(data)); + png_decoder.peek_layout()?; let metadata = png_decoder.xmp_metadata()?; assert!(metadata.is_some()); assert_eq!(EXPECTED_PNG_METADATA.as_bytes(), metadata.unwrap()); @@ -108,7 +109,8 @@ fn test_read_iptc_png() -> Result<(), image::ImageError> { let img_path = PathBuf::from_str(IPTC_PNG_PATH).unwrap(); let data = fs::read(img_path)?; - let mut png_decoder = PngDecoder::new(std::io::Cursor::new(data))?; + let mut png_decoder = PngDecoder::new(std::io::Cursor::new(data)); + png_decoder.peek_layout()?; let metadata = png_decoder.iptc_metadata()?; assert!(metadata.is_some()); assert_eq!(EXPECTED_METADATA, metadata.unwrap()); diff --git a/tests/reference_images.rs b/tests/reference_images.rs index d8807aa1a3..1d3463f0e7 100644 --- a/tests/reference_images.rs +++ b/tests/reference_images.rs @@ -4,9 +4,9 @@ use std::io; use std::path::PathBuf; use crc32fast::Hasher as Crc32; -use image::ColorType; -use image::DynamicImage; -use image::ImageReader; +#[cfg(feature = "png")] +use image::ImageDecoder as _; +use image::{ColorType, DynamicImage, ImageReaderOptions}; const BASE_PATH: [&str; 2] = [".", "tests"]; const IMAGE_DIR: &str = "images"; @@ -201,7 +201,7 @@ fn check_references() { match case.kind { ReferenceTestKind::AnimatedFrame { frame: frame_num } => { - let format = ImageReader::open(&img_path) + let format = ImageReaderOptions::open(&img_path) .unwrap() .with_guessed_format() .unwrap() @@ -210,7 +210,6 @@ fn check_references() { #[cfg(feature = "gif")] if format == Some(image::ImageFormat::Gif) { // Interpret the input file as an animation file - use image::AnimationDecoder; let stream = io::BufReader::new(fs::File::open(&img_path).unwrap()); let decoder = match image::codecs::gif::GifDecoder::new(stream) { Ok(decoder) => decoder, @@ -220,6 +219,7 @@ fn check_references() { } }; + let decoder = image::ImageReader::from_decoder(Box::new(decoder)); let mut frames = match decoder.into_frames().collect_frames() { Ok(frames) => frames, Err(image::ImageError::Unsupported(_)) => return, @@ -238,16 +238,18 @@ fn check_references() { #[cfg(feature = "png")] if format == Some(image::ImageFormat::Png) { // Interpret the input file as an animation file - use image::AnimationDecoder; let stream = io::BufReader::new(fs::File::open(&img_path).unwrap()); - let decoder = match image::codecs::png::PngDecoder::new(stream) { - Ok(decoder) => decoder.apng().unwrap(), + + let mut decoder = image::codecs::png::PngDecoder::new(stream); + let decoder = match decoder.peek_layout() { + Ok(_layout) => decoder.apng().unwrap(), Err(image::ImageError::Unsupported(_)) => return, Err(err) => { panic!("decoding of {img_path:?} failed with: {err}") } }; + let decoder = image::ImageReader::from_decoder(Box::new(decoder)); let mut frames = match decoder.into_frames().collect_frames() { Ok(frames) => frames, Err(image::ImageError::Unsupported(_)) => return,