diff --git a/benches/decode.rs b/benches/decode.rs index a2d3a50737..cfa788bc2c 100644 --- a/benches/decode.rs +++ b/benches/decode.rs @@ -1,7 +1,7 @@ use std::{fs, hint::black_box, iter, path}; use criterion::{criterion_group, criterion_main, Criterion}; -use image::{AnimationDecoder, ImageFormat}; +use image::{ImageFormat, ImageReader}; #[derive(Clone, Copy)] struct BenchDef { @@ -126,8 +126,14 @@ fn bench_load(c: &mut Criterion, def: &BenchDef) { fn decode_animation(buf: &[u8], format: ImageFormat) -> image::ImageResult { let reader = std::io::Cursor::new(buf); let frames = match format { - ImageFormat::Gif => image::codecs::gif::GifDecoder::new(reader)?.into_frames(), - ImageFormat::WebP => image::codecs::webp::WebPDecoder::new(reader)?.into_frames(), + ImageFormat::Gif => { + ImageReader::from_decoder(Box::new(image::codecs::gif::GifDecoder::new(reader)?)) + .into_frames() + } + ImageFormat::WebP => { + ImageReader::from_decoder(Box::new(image::codecs::webp::WebPDecoder::new(reader)?)) + .into_frames() + } _ => return Ok(0), }; Ok(frames.count()) 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..cac769e622 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 total_bytes = decoder.prepare_image()?.total_bytes(); - if width.saturating_mul(height) > 4_000_000 { - return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError))); + if total_bytes > 4_000_000 { + 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..7998f77a59 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 total_bytes = decoder.prepare_image()?.total_bytes(); - if width.saturating_mul(height) > 4_000_000 { - return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError))); + if total_bytes > 4_000_000 { + 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..8865fc1259 100644 --- a/fuzz-afl/reproducers/reproduce_pnm.rs +++ b/fuzz-afl/reproducers/reproduce_pnm.rs @@ -7,10 +7,10 @@ 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 total_bytes = decoder.prepare_image()?.total_bytes(); - if width.saturating_mul(height) > 4_000_000 { + if total_bytes > 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..37ca32a86b 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 total_bytes = decoder.prepare_image()?.total_bytes(); - if width.saturating_mul(height) > 4_000_000 { - return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError))); + if total_bytes > 4_000_000 { + 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..d4eb3bd5c0 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 prep = decoder.prepare_image()?; + match usize::try_from(prep.total_bytes()) { Ok(decoded_size) if decoded_size <= 256 * 1024 * 1024 => { decoder.set_limits(Limits::default())?; - let (width, height) = decoder.dimensions(); + let (width, height) = prep.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..8b63aed9bb 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.prepare_image()?.total_bytes() > 4_000_000 { return Ok(()); } - let mut buffer = vec![0; decoder.total_bytes() as usize]; + let mut buffer = vec![0; decoder.prepare_image()?.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 cfdcd63ee3..ab37babe81 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, DecoderPreparedImage}; use crate::metadata::Orientation; use crate::{ColorType, ImageDecoder, ImageError, ImageFormat, ImageResult}; /// @@ -369,28 +370,27 @@ 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 { + fn prepare_image(&mut self) -> ImageResult { + let color = if self.picture.bit_depth() == 8 { ColorType::Rgba8 } else { ColorType::Rgba16 - } + }; + + Ok(DecoderPreparedImage::new( + self.picture.width(), + self.picture.height(), + color, + )) } fn icc_profile(&mut self) -> ImageResult>> { Ok(self.icc_profile.clone()) } - fn orientation(&mut self) -> ImageResult { - Ok(self.orientation) - } - - 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 prepared = self.prepare_image()?; + assert_eq!(u64::try_from(buf.len()), Ok(prepared.total_bytes())); let bit_depth = self.picture.bit_depth(); @@ -398,7 +398,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) = prepared.layout.dimensions(); // This is suspicious if this happens, better fail early if width == 0 || height == 0 { return Err(ImageError::Limits(LimitError::from_kind( @@ -485,7 +485,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(), @@ -521,11 +521,10 @@ impl ImageDecoder for AvifDecoder { } } - Ok(()) - } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes { + orientation: Some(self.orientation), + ..DecodedImageAttributes::default() + }) } } diff --git a/src/codecs/bmp/decoder.rs b/src/codecs/bmp/decoder.rs index 32ada00e86..adb56cbd63 100644 --- a/src/codecs/bmp/decoder.rs +++ b/src/codecs/bmp/decoder.rs @@ -1,3 +1,4 @@ +use crate::io::DecoderPreparedImage; use crate::utils::vec_try_with_capacity; use std::cmp::{self, Ordering}; use std::io::{self, BufRead, Seek, SeekFrom}; @@ -9,7 +10,7 @@ use crate::color::ColorType; use crate::error::{ DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind, }; -use crate::io::image_reader_type::SpecCompliance; +use crate::io::{image_reader_type::SpecCompliance, DecodedImageAttributes}; use crate::{ImageDecoder, ImageFormat}; const BITMAPCOREHEADER_SIZE: u32 = 12; @@ -2427,31 +2428,31 @@ 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 { + fn prepare_image(&mut self) -> ImageResult { + let color = if self.indexed_color { ColorType::L8 } else if self.add_alpha_channel { ColorType::Rgba8 } else { ColorType::Rgb8 - } + }; + + Ok(DecoderPreparedImage::new( + self.width as u32, + self.height as u32, + color, + )) } 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.prepare_image()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); + self.read_image_data(buf)?; + Ok(DecodedImageAttributes::default()) } } @@ -2499,8 +2500,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.prepare_image().unwrap(); + let mut buf = vec![0; usize::try_from(layout.total_bytes()).unwrap()]; assert!(decoder.read_image(&mut buf).is_ok()); } @@ -2799,7 +2801,7 @@ mod test { // Get reference result from normal decoding let mut ref_decoder = BmpDecoder::new(Cursor::new(data.clone())).unwrap(); - let expected_bytes = ref_decoder.total_bytes() as usize; + let expected_bytes = ref_decoder.prepare_image().unwrap().total_bytes() as usize; let mut ref_buf = vec![0u8; expected_bytes]; let ref_icc_len = ref_decoder.icc_profile().unwrap().map(|p| p.len()); ref_decoder.read_image(&mut ref_buf).unwrap(); @@ -2863,10 +2865,11 @@ mod test { } // Verify dimensions are available after metadata - let (width, height) = decoder.dimensions(); + let layout = decoder.prepare_image().unwrap(); + let (width, height) = layout.layout.dimensions(); assert!(width > 0 && height > 0, "{path}: invalid dimensions"); assert_eq!( - decoder.total_bytes() as usize, + layout.total_bytes() as usize, expected_bytes, "{path}: total_bytes mismatch" ); @@ -3008,10 +3011,13 @@ mod test { .unwrap_or_else(|e| panic!("{description}: failed to read {path}: {e}")); // Default (lenient) mode: these files should be accepted - let decoder = BmpDecoder::new(Cursor::new(&data)).unwrap_or_else(|e| { + let mut decoder = BmpDecoder::new(Cursor::new(&data)).unwrap_or_else(|e| { panic!("{description}: decoding failed: {e:?}"); }); - let mut buf = vec![0u8; decoder.total_bytes() as usize]; + let layout = decoder.prepare_image().unwrap_or_else(|e| { + panic!("{description}: peek_layout failed: {e:?}"); + }); + let mut buf = vec![0u8; layout.total_bytes() as usize]; decoder.read_image(buf.as_mut_slice()).unwrap_or_else(|e| { panic!("{description}: read_image failed: {e:?}"); }); @@ -3043,12 +3049,14 @@ mod test { reference[10] = 0x36; // Decode both - let decoder = BmpDecoder::new(Cursor::new(&data)).unwrap(); - let mut buf = vec![0u8; decoder.total_bytes() as usize]; + let mut decoder = BmpDecoder::new(Cursor::new(&data)).unwrap(); + let len = decoder.prepare_image().unwrap().total_bytes(); + let mut buf = vec![0u8; len as usize]; decoder.read_image(&mut buf).unwrap(); - let ref_decoder = BmpDecoder::new(Cursor::new(&reference)).unwrap(); - let mut ref_buf = vec![0u8; ref_decoder.total_bytes() as usize]; + let mut ref_decoder = BmpDecoder::new(Cursor::new(&reference)).unwrap(); + let len = decoder.prepare_image().unwrap().total_bytes(); + let mut ref_buf = vec![0u8; len as usize]; ref_decoder.read_image(&mut ref_buf).unwrap(); assert_eq!( @@ -3076,8 +3084,9 @@ mod test { // Strict mode must fail on these images during full decode let strict_result = BmpDecoder::new_with_spec_compliance(Cursor::new(&data), SpecCompliance::Strict) - .and_then(|d| { - let mut buf = vec![0u8; d.total_bytes() as usize]; + .and_then(|mut d| { + let len = d.prepare_image()?.total_bytes(); + let mut buf = vec![0u8; len as usize]; d.read_image(buf.as_mut_slice()) }); assert!( @@ -3091,8 +3100,9 @@ mod test { fn test_decode_bmp_rle_overflow() { let data = std::fs::read("tests/images/bmp/images/lenient/rle_overflow.bmp") .expect("Test image missing"); - let decoder = BmpDecoder::new(Cursor::new(data)).unwrap(); - let mut buffer = vec![0u8; decoder.total_bytes() as usize]; + let mut decoder = BmpDecoder::new(Cursor::new(data)).unwrap(); + let len = decoder.prepare_image().unwrap().total_bytes(); + let mut buffer = vec![0u8; len as usize]; let result = decoder.read_image(&mut buffer); assert!(result.is_ok()); } @@ -3101,8 +3111,9 @@ mod test { fn test_decode_bmp_badrle() { let data = std::fs::read("tests/images/bmp/images/lenient/badrle.bmp") .expect("Test image missing"); - let decoder = BmpDecoder::new(Cursor::new(data)).unwrap(); - let mut buffer = vec![0u8; decoder.total_bytes() as usize]; + let mut decoder = BmpDecoder::new(Cursor::new(data)).unwrap(); + let len = decoder.prepare_image().unwrap().total_bytes(); + let mut buffer = vec![0u8; len as usize]; let result = decoder.read_image(&mut buffer); assert!(result.is_ok()); } @@ -3121,8 +3132,8 @@ mod test { // Test Lenient mode let decoder = BmpDecoder::new(Cursor::new(data.clone())).unwrap(); - let mut buffer = vec![0u8; decoder.total_bytes() as usize]; - let result = decoder.read_image(&mut buffer); + let mut decoder = crate::ImageReader::from_decoder(Box::new(decoder)); + let result = decoder.decode(); assert!( result.is_ok(), "Expected Ok in lenient mode for truncated file" @@ -3132,7 +3143,9 @@ mod test { let strict_decoder = BmpDecoder::new_with_spec_compliance(Cursor::new(data), SpecCompliance::Strict) .unwrap(); - let result = strict_decoder.read_image(&mut buffer); + let mut decoder = crate::ImageReader::from_decoder(Box::new(strict_decoder)); + let result = decoder.decode(); + assert!( result.is_err(), "Expected error in strict mode for truncated file" diff --git a/src/codecs/bmp/encoder.rs b/src/codecs/bmp/encoder.rs index dd3da87529..81a96803c9 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.prepare_image().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/dxt.rs b/src/codecs/dxt.rs new file mode 100644 index 0000000000..c68ec2cb99 --- /dev/null +++ b/src/codecs/dxt.rs @@ -0,0 +1,350 @@ +//! Decoding of DXT (S3TC) compression +//! +//! DXT is an image format that supports lossy compression +//! +//! # Related Links +//! * - Description of the DXT compression OpenGL extensions. +//! +//! Note: this module only implements bare DXT encoding/decoding, it does not parse formats that can contain DXT files like .dds + +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; + +/// What version of DXT compression are we using? +/// Note that DXT2 and DXT4 are left away as they're +/// just DXT3 and DXT5 with premultiplied alpha +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DxtVariant { + /// The DXT1 format. 48 bytes of RGB data in a 4x4 pixel square is + /// compressed into an 8 byte block of DXT1 data + DXT1, + /// The DXT3 format. 64 bytes of RGBA data in a 4x4 pixel square is + /// compressed into a 16 byte block of DXT3 data + DXT3, + /// The DXT5 format. 64 bytes of RGBA data in a 4x4 pixel square is + /// compressed into a 16 byte block of DXT5 data + DXT5, +} + +impl DxtVariant { + /// Returns the amount of bytes of raw image data + /// that is encoded in a single DXTn block + fn decoded_bytes_per_block(self) -> usize { + match self { + DxtVariant::DXT1 => 48, + DxtVariant::DXT3 | DxtVariant::DXT5 => 64, + } + } + + /// Returns the amount of bytes per block of encoded DXTn data + fn encoded_bytes_per_block(self) -> usize { + match self { + DxtVariant::DXT1 => 8, + DxtVariant::DXT3 | DxtVariant::DXT5 => 16, + } + } + + /// Returns the color type that is stored in this DXT variant + pub(crate) fn color_type(self) -> ColorType { + match self { + DxtVariant::DXT1 => ColorType::Rgb8, + DxtVariant::DXT3 | DxtVariant::DXT5 => ColorType::Rgba8, + } + } +} + +/// DXT decoder +pub(crate) struct DxtDecoder { + inner: R, + width_blocks: u32, + height_blocks: u32, + variant: DxtVariant, + row: u32, +} + +impl DxtDecoder { + /// Create a new DXT decoder that decodes from the stream ```r```. + /// As DXT is often stored as raw buffers with the width/height + /// somewhere else the width and height of the image need + /// to be passed in ```width``` and ```height```, as well as the + /// DXT variant in ```variant```. + /// width and height are required to be powers of 2 and at least 4. + /// otherwise an error will be returned + pub(crate) fn new( + r: R, + width: u32, + height: u32, + variant: DxtVariant, + ) -> Result, ImageError> { + if !width.is_multiple_of(4) || !height.is_multiple_of(4) { + // TODO: this is actually a bit of a weird case. We could return `DecodingError` but + // it's not really the format that is wrong However, the encoder should surely return + // `EncodingError` so it would be the logical choice for symmetry. + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::DimensionMismatch, + ))); + } + let width_blocks = width / 4; + let height_blocks = height / 4; + Ok(DxtDecoder { + inner: r, + width_blocks, + height_blocks, + variant, + row: 0, + }) + } + + fn scanline_bytes(&self) -> u64 { + self.variant.decoded_bytes_per_block() as u64 * u64::from(self.width_blocks) + } + + fn read_scanline(&mut self, buf: &mut [u8]) -> io::Result { + assert_eq!( + u64::try_from(buf.len()), + Ok( + #[allow(deprecated)] + self.scanline_bytes() + ) + ); + + let len = self.variant.encoded_bytes_per_block() * self.width_blocks as usize; + let mut src = Vec::new(); + self.inner.read_exact_vec(&mut src, len)?; + + match self.variant { + DxtVariant::DXT1 => decode_dxt1_row(&src, buf), + DxtVariant::DXT3 => decode_dxt3_row(&src, buf), + DxtVariant::DXT5 => decode_dxt5_row(&src, buf), + } + self.row += 1; + Ok(buf.len()) + } +} + +// 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 peek_layout(&mut self) -> ImageResult { + // Note: derived from an underlying size divided by 4 during parsing. + Ok(crate::ImageLayout::new( + self.width_blocks * 4, + self.height_blocks * 4, + self.variant.color_type(), + )) + } + + 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(DecodedImageAttributes::default()) + } +} + +/** + * Actual encoding/decoding logic below. + */ +type Rgb = [u8; 3]; + +/// decodes a 5-bit R, 6-bit G, 5-bit B 16-bit packed color value into 8-bit RGB +/// mapping is done so min/max range values are preserved. So for 5-bit +/// values 0x00 -> 0x00 and 0x1F -> 0xFF +fn enc565_decode(value: u16) -> Rgb { + let red = (value >> 11) & 0x1F; + let green = (value >> 5) & 0x3F; + let blue = (value) & 0x1F; + [ + (red * 0xFF / 0x1F) as u8, + (green * 0xFF / 0x3F) as u8, + (blue * 0xFF / 0x1F) as u8, + ] +} + +/* + * Functions for decoding DXT compression + */ + +/// Constructs the DXT5 alpha lookup table from the two alpha entries +/// if alpha0 > alpha1, constructs a table of [a0, a1, 6 linearly interpolated values from a0 to a1] +/// if alpha0 <= alpha1, constructs a table of [a0, a1, 4 linearly interpolated values from a0 to a1, 0, 0xFF] +fn alpha_table_dxt5(alpha0: u8, alpha1: u8) -> [u8; 8] { + let mut table = [alpha0, alpha1, 0, 0, 0, 0, 0, 0xFF]; + if alpha0 > alpha1 { + for i in 2..8u16 { + table[i as usize] = + (((8 - i) * u16::from(alpha0) + (i - 1) * u16::from(alpha1)) / 7) as u8; + } + } else { + for i in 2..6u16 { + table[i as usize] = + (((6 - i) * u16::from(alpha0) + (i - 1) * u16::from(alpha1)) / 5) as u8; + } + } + table +} + +/// decodes an 8-byte dxt color block into the RGB channels of a 16xRGB or 16xRGBA block. +/// source should have a length of 8, dest a length of 48 (RGB) or 64 (RGBA) +#[allow(clippy::needless_range_loop)] // False positive, the 0..3 loop is not an enumerate +fn decode_dxt_colors(source: &[u8], dest: &mut [u8], is_dxt1: bool) { + // sanity checks, also enable the compiler to elide all following bound checks + assert!(source.len() == 8 && (dest.len() == 48 || dest.len() == 64)); + // calculate pitch to store RGB values in dest (3 for RGB, 4 for RGBA) + let pitch = dest.len() / 16; + + // extract color data + let color0 = u16::from(source[0]) | (u16::from(source[1]) << 8); + let color1 = u16::from(source[2]) | (u16::from(source[3]) << 8); + let color_table = u32::from(source[4]) + | (u32::from(source[5]) << 8) + | (u32::from(source[6]) << 16) + | (u32::from(source[7]) << 24); + // let color_table = source[4..8].iter().rev().fold(0, |t, &b| (t << 8) | b as u32); + + // decode the colors to rgb format + let mut colors = [[0; 3]; 4]; + colors[0] = enc565_decode(color0); + colors[1] = enc565_decode(color1); + + // determine color interpolation method + if color0 > color1 || !is_dxt1 { + // linearly interpolate the other two color table entries + for i in 0..3 { + colors[2][i] = ((u16::from(colors[0][i]) * 2 + u16::from(colors[1][i]) + 1) / 3) as u8; + colors[3][i] = ((u16::from(colors[0][i]) + u16::from(colors[1][i]) * 2 + 1) / 3) as u8; + } + } else { + // linearly interpolate one other entry, keep the other at 0 + for i in 0..3 { + colors[2][i] = (u16::from(colors[0][i]) + u16::from(colors[1][i])).div_ceil(2) as u8; + } + } + + // serialize the result. Every color is determined by looking up + // two bits in color_table which identify which color to actually pick from the 4 possible colors + for i in 0..16 { + dest[i * pitch..i * pitch + 3] + .copy_from_slice(&colors[(color_table >> (i * 2)) as usize & 3]); + } +} + +/// Decodes a 16-byte bock of dxt5 data to a 16xRGBA block +fn decode_dxt5_block(source: &[u8], dest: &mut [u8]) { + assert!(source.len() == 16 && dest.len() == 64); + + // extract alpha index table (stored as little endian 64-bit value) + let alpha_table = source[2..8] + .iter() + .rev() + .fold(0, |t, &b| (t << 8) | u64::from(b)); + + // alpha level decode + let alphas = alpha_table_dxt5(source[0], source[1]); + + // serialize alpha + for i in 0..16 { + dest[i * 4 + 3] = alphas[(alpha_table >> (i * 3)) as usize & 7]; + } + + // handle colors + decode_dxt_colors(&source[8..16], dest, false); +} + +/// Decodes a 16-byte bock of dxt3 data to a 16xRGBA block +fn decode_dxt3_block(source: &[u8], dest: &mut [u8]) { + assert!(source.len() == 16 && dest.len() == 64); + + // extract alpha index table (stored as little endian 64-bit value) + let alpha_table = source[0..8] + .iter() + .rev() + .fold(0, |t, &b| (t << 8) | u64::from(b)); + + // serialize alpha (stored as 4-bit values) + for i in 0..16 { + dest[i * 4 + 3] = ((alpha_table >> (i * 4)) as u8 & 0xF) * 0x11; + } + + // handle colors + decode_dxt_colors(&source[8..16], dest, false); +} + +/// Decodes a 8-byte bock of dxt5 data to a 16xRGB block +fn decode_dxt1_block(source: &[u8], dest: &mut [u8]) { + assert!(source.len() == 8 && dest.len() == 48); + decode_dxt_colors(source, dest, true); +} + +/// Decode a row of DXT1 data to four rows of RGB data. +/// `source.len()` should be a multiple of 8, otherwise this panics. +fn decode_dxt1_row(source: &[u8], dest: &mut [u8]) { + assert!(source.len().is_multiple_of(8)); + let block_count = source.len() / 8; + assert!(dest.len() >= block_count * 48); + + // contains the 16 decoded pixels per block + let mut decoded_block = [0u8; 48]; + + for (x, encoded_block) in source.chunks(8).enumerate() { + decode_dxt1_block(encoded_block, &mut decoded_block); + + // copy the values from the decoded block to linewise RGB layout + for line in 0..4 { + let offset = (block_count * line + x) * 12; + dest[offset..offset + 12].copy_from_slice(&decoded_block[line * 12..(line + 1) * 12]); + } + } +} + +/// Decode a row of DXT3 data to four rows of RGBA data. +/// `source.len()` should be a multiple of 16, otherwise this panics. +fn decode_dxt3_row(source: &[u8], dest: &mut [u8]) { + assert!(source.len().is_multiple_of(16)); + let block_count = source.len() / 16; + assert!(dest.len() >= block_count * 64); + + // contains the 16 decoded pixels per block + let mut decoded_block = [0u8; 64]; + + for (x, encoded_block) in source.chunks(16).enumerate() { + decode_dxt3_block(encoded_block, &mut decoded_block); + + // copy the values from the decoded block to linewise RGB layout + for line in 0..4 { + let offset = (block_count * line + x) * 16; + dest[offset..offset + 16].copy_from_slice(&decoded_block[line * 16..(line + 1) * 16]); + } + } +} + +/// Decode a row of DXT5 data to four rows of RGBA data. +/// `source.len()` should be a multiple of 16, otherwise this panics. +fn decode_dxt5_row(source: &[u8], dest: &mut [u8]) { + assert!(source.len().is_multiple_of(16)); + let block_count = source.len() / 16; + assert!(dest.len() >= block_count * 64); + + // contains the 16 decoded pixels per block + let mut decoded_block = [0u8; 64]; + + for (x, encoded_block) in source.chunks(16).enumerate() { + decode_dxt5_block(encoded_block, &mut decoded_block); + + // copy the values from the decoded block to linewise RGB layout + for line in 0..4 { + let offset = (block_count * line + x) * 16; + dest[offset..offset + 16].copy_from_slice(&decoded_block[line * 16..(line + 1) * 16]); + } + } +} diff --git a/src/codecs/farbfeld.rs b/src/codecs/farbfeld.rs index d172eb9b67..1954124353 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, DecoderPreparedImage}; use crate::{ColorType, ImageDecoder, ImageEncoder, ImageFormat}; const MAGIC: &[u8] = b"farbfeld"; @@ -40,14 +41,16 @@ fn parse_header(r: &mut dyn Read) -> ImageResult<(u32, u32)> { let width = u32::from_be_bytes(header[8..12].try_into().unwrap()); let height = u32::from_be_bytes(header[12..16].try_into().unwrap()); - if crate::utils::check_dimension_overflow( - width, height, 8, // ExtendedColorType is always rgba16 - ) { + // ExtendedColorType is always rgba16 + let layout = crate::ImageLayout::new(width, height, ColorType::Rgba16); + + if layout.total_bytes_overflows_u64() { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Farbfeld.into(), UnsupportedErrorKind::GenericFeature(format!( - "Image dimensions ({width}x{height}) are too large" + "Image dimensions ({}x{}) are too large", + width, height )), ), )); @@ -86,23 +89,17 @@ impl FarbfeldDecoder { } impl ImageDecoder for FarbfeldDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.width, self.height) - } - - fn color_type(&self) -> ColorType { - ColorType::Rgba16 + fn prepare_image(&mut self) -> ImageResult { + let FarbfeldDecoder { width, height, .. } = *self; + Ok(DecoderPreparedImage::new(width, height, 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.prepare_image()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); self.reader.read_exact(buf)?; u16_swap_be_ne(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 ccf3aa3716..0ca63936b5 100644 --- a/src/codecs/gif.rs +++ b/src/codecs/gif.rs @@ -8,14 +8,16 @@ //! # Examples //! ```rust,no_run //! use image::codecs::gif::{GifDecoder, GifEncoder}; -//! use image::{ImageDecoder, AnimationDecoder}; +//! use image::ImageReader; //! use std::fs::File; //! use std::io::BufReader; +//! //! # fn main() -> std::io::Result<()> { //! // Decode a gif into frames //! let file_in = BufReader::new(File::open("foo.gif")?); -//! let mut decoder = GifDecoder::new(file_in).unwrap(); -//! let frames = decoder.into_frames(); +//! let mut decoder = Box::new(GifDecoder::new(file_in).unwrap()); +//! +//! let frames = ImageReader::from_decoder(decoder).into_frames(); //! let frames = frames.collect_frames().expect("error decoding gif"); //! //! // Encode frames into a gif and save to a file @@ -27,7 +29,7 @@ //! ``` #![allow(clippy::while_let_loop)] -use std::io::{self, BufRead, Read, Seek, Write}; +use std::io::{BufRead, Read, Seek, Write}; use std::num::NonZeroU32; use gif::ColorOutput; @@ -39,59 +41,141 @@ use crate::error::{ DecodingError, EncodingError, ImageError, ImageResult, LimitError, LimitErrorKind, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; -use crate::metadata::LoopCount; -use crate::{ - AnimationDecoder, ExtendedColorType, ImageBuffer, ImageDecoder, ImageEncoder, ImageFormat, - Limits, +use crate::io::{ + DecodedAnimationAttributes, DecodedImageAttributes, DecodedMetadataHint, DecoderPreparedImage, + FormatAttributes, }; +use crate::metadata::LoopCount; +use crate::traits::Pixel; +use crate::{ExtendedColorType, ImageBuffer, ImageDecoder, ImageEncoder, ImageFormat, Limits}; /// GIF decoder pub struct GifDecoder { - reader: gif::Decoder, + options: gif::DecodeOptions, + reader: Option, + decoder: Option>, + non_disposed_frame: Option, Vec>>, limits: Limits, } +const COLOR: ColorType = ColorType::Rgba8; + 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, + non_disposed_frame: 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). + // + // FIXME: would be nice to have a sub-object for these two attributes or an enum for the state + // machine so that we can `ensure_decoder` without borrowing the whole `GifDecoder` type. + #[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::new( + decoder.width().into(), + decoder.height().into(), + ColorType::Rgba8, + ) + } } impl ImageDecoder for GifDecoder { - fn dimensions(&self) -> (u32, u32) { - ( - u32::from(self.reader.width()), - u32::from(self.reader.height()), - ) + fn format_attributes(&self) -> FormatAttributes { + FormatAttributes { + // FIXME: may appear anywhere. + xmp: DecodedMetadataHint::InHeader, + icc: DecodedMetadataHint::InHeader, + iptc: DecodedMetadataHint::None, + // FIXME: there is some in a Photoshop 8BIM extension which we do not collect. + exif: DecodedMetadataHint::Unsupported, + supports_animation: true, + ..FormatAttributes::default() + } } - fn color_type(&self) -> ColorType { - ColorType::Rgba8 + fn animation_attributes(&mut self) -> Option { + let decoder = self.ensure_decoder().ok()?; + let loop_count = match decoder.repeat() { + gif::Repeat::Finite(n @ 1..) => { + LoopCount::Finite(NonZeroU32::new(n.into()).expect("repeat is non-zero")) + } + gif::Repeat::Finite(0) | gif::Repeat::Infinite => LoopCount::Infinite, + }; + + Some(DecodedAnimationAttributes { loop_count }) + } + + fn prepare_image(&mut self) -> ImageResult { + let decoder = self.ensure_decoder()?; + Ok(Self::layout_from_decoder(decoder).into()) } fn set_limits(&mut self, limits: Limits) -> ImageResult<()> { limits.check_support(&crate::LimitSupport::default())?; - let (width, height) = self.dimensions(); - limits.check_dimensions(width, height)?; + let layout = self.prepare_image()?; + limits.check_layout_dimensions(&layout)?; self.limits = limits; 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, + color, + } = Self::layout_from_decoder(decoder); + + // Allocate the buffer for the previous frame. + // This is done here and not in the constructor because + // the constructor cannot return an error when the allocation limit is exceeded. + if self.non_disposed_frame.is_none() { + self.limits.reserve_buffer(width, height, color)?; + self.non_disposed_frame = + Some(ImageBuffer::from_pixel(width, height, Rgba([0, 0, 0, 0]))); + } + + // Initialized from `ensure_decoder` above, re-acquired for borrow checker. + let decoder = self.decoder.as_mut().unwrap(); + 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)? { @@ -103,10 +187,7 @@ impl ImageDecoder for GifDecoder { } }; - let (width, height) = self.dimensions(); - - if frame.left == 0 - && frame.width == width + let frame_start_len = if (frame.left, frame.width) == (0, width) && (u64::from(frame.top) + u64::from(frame.height) <= u64::from(height)) { // If the frame matches the logical screen, or, as a more general case, @@ -114,26 +195,34 @@ 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(COLOR.bytes_per_pixel() as usize) .unwrap(); + let frame_start = line_length.checked_mul(frame.top as usize).unwrap(); + let frame_len = line_length.checked_mul(frame.height as usize).unwrap(); + Some((frame_start, frame_len)) + } else { + None + }; + + if let Some((frame_start, frame_len)) = frame_start_len { // isolate the portion of the buffer to read the frame data into. // the chunks above and below it are going to be zeroed. - let (blank_top, rest) = - buf.split_at_mut(line_length.checked_mul(frame.top as usize).unwrap()); - let (buf, blank_bottom) = - rest.split_at_mut(line_length.checked_mul(frame.height as usize).unwrap()); + let (blank_top, rest) = buf.split_at_mut(frame_start); + let (buf, blank_bottom) = rest.split_at_mut(frame_len); - 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 for b in blank_bottom { *b = 0; @@ -152,12 +241,14 @@ 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)?; let frame_buffer = ImageBuffer::from_raw(frame.width, frame.height, frame_buffer); - let image_buffer = ImageBuffer::from_raw(width, height, buf); + let image_buffer = ImageBuffer::from_raw(width, height, &mut *buf); // `buffer_size` uses wrapping arithmetic, thus might not report the // correct storage requirement if the result does not fit in `usize`. @@ -190,237 +281,144 @@ impl ImageDecoder for GifDecoder { } } - Ok(()) - } - - fn icc_profile(&mut self) -> ImageResult>> { - // Similar to XMP metadata - Ok(self.reader.icc_profile().map(Vec::from)) - } - - fn xmp_metadata(&mut self) -> ImageResult>> { - // 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) - } -} - -struct GifFrameIterator { - reader: gif::Decoder, - - width: u32, - height: u32, - - non_disposed_frame: Option, Vec>>, - limits: Limits, - // `is_end` is used to indicate whether the iterator has reached the end of the frames. - // Or encounter any un-recoverable error. - is_end: bool, -} - -impl GifFrameIterator { - fn new(decoder: GifDecoder) -> GifFrameIterator { - let (width, height) = decoder.dimensions(); - let limits = decoder.limits.clone(); - - // intentionally ignore the background color for web compatibility - - GifFrameIterator { - reader: decoder.reader, - width, - height, - non_disposed_frame: None, - limits, - is_end: false, - } - } -} - -impl Iterator for GifFrameIterator { - type Item = ImageResult; - - fn next(&mut self) -> Option> { - if self.is_end { - return None; - } - - // The iterator always produces RGBA8 images - const COLOR_TYPE: ColorType = ColorType::Rgba8; - - // Allocate the buffer for the previous frame. - // This is done here and not in the constructor because - // the constructor cannot return an error when the allocation limit is exceeded. - if self.non_disposed_frame.is_none() { - if let Err(e) = self - .limits - .reserve_buffer(self.width, self.height, COLOR_TYPE) - { - return Some(Err(e)); - } - self.non_disposed_frame = Some(ImageBuffer::from_pixel( - self.width, - self.height, - Rgba([0, 0, 0, 0]), - )); - } // Bind to a variable to avoid repeated `.unwrap()` calls let non_disposed_frame = self.non_disposed_frame.as_mut().unwrap(); - // begin looping over each frame + // if `frame_buffer`'s frame exactly matches the entire image, then + // use it directly, else create a new buffer to hold the composited + // image. + if let Some((frame_start, frame_len)) = frame_start_len { + // We can blend pixels in a fully contiguous region instead of row-by-row. + let non_disposed_data = + &mut non_disposed_frame.subpixels_mut()[frame_start..][..frame_len]; + let frame_data = &mut buf[frame_start..][..frame_len]; + blend_and_dispose_region(frame.disposal_method, non_disposed_data, frame_data); + } else { + // We have validated bounds already so no checked math. + let effective_left = frame.left.min(width); + let effective_width = (width - effective_left).min(frame.width); + + let row_len = width as usize * COLOR.bytes_per_pixel() as usize; + let data_len = effective_width as usize * COLOR.bytes_per_pixel() as usize; + let row_skip = effective_left as usize * COLOR.bytes_per_pixel() as usize; + + // process rows before, within and after the frame. Everything not in bounds is copied + // as if by `DisposalMethod::Previous`. + for y in 0..frame.top { + if y >= height { + break; + } - let frame = match self.reader.next_frame_info() { - Ok(Some(frame_info)) => FrameInfo::new_from_frame(frame_info), - Ok(None) => { - // no more frames - return None; + let start = y as usize * row_len; + let non_disposed_data = &mut non_disposed_frame.subpixels_mut()[start..][..row_len]; + let frame_data = &mut buf[start..][..row_len]; + frame_data.copy_from_slice(non_disposed_data); } - Err(err) => match err { - gif::DecodingError::Io(ref e) => { - if e.kind() == io::ErrorKind::UnexpectedEof { - // end of file reached, no more frames - self.is_end = true; - } - return Some(Err(ImageError::from_decoding(err))); - } - _ => { - return Some(Err(ImageError::from_decoding(err))); + + for y in frame.top..(frame.top + frame.height) { + if y >= height { + break; } - }, - }; - // All allocations we do from now on will be freed at the end of this function. - // Therefore, do not count them towards the persistent limits. - // Instead, create a local instance of `Limits` for this function alone - // which will be dropped along with all the buffers when they go out of scope. - let mut local_limits = self.limits.clone(); + let start = y as usize * row_len; - // Check the allocation we're about to perform against the limits - 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) { - return Some(Err(ImageError::from_decoding(err))); - } + let non_disposed_data = &mut non_disposed_frame.subpixels_mut()[start..][..row_len]; + let frame_data = &mut buf[start..][..row_len]; - // create the image buffer from the raw frame. - // `buffer_size` uses wrapping arithmetic, thus might not report the - // correct storage requirement if the result does not fit in `usize`. - // on the other hand, `ImageBuffer::from_raw` detects overflow and - // reports by returning `None`. - let Some(mut frame_buffer) = ImageBuffer::from_raw(frame.width, frame.height, vec) else { - return Some(Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Gif.into(), - UnsupportedErrorKind::GenericFeature(format!( - "Image dimensions ({}, {}) are too large", - frame.width, frame.height - )), - ), - ))); - }; + non_disposed_data[..row_skip].copy_from_slice(&frame_data[..row_skip]); + + blend_and_dispose_region( + frame.disposal_method, + &mut non_disposed_data[row_skip..][..data_len], + &mut frame_data[row_skip..][..data_len], + ); - // blend the current frame with the non-disposed frame, then update - // the non-disposed frame according to the disposal method. - #[inline] - fn blend_and_dispose_pixel( - dispose: DisposalMethod, - previous: &mut Rgba, - current: &mut Rgba, - ) { - // Instead of only checking the alpha channel, use a bitmask to check - // the entire pixel and allow for better auto-vectorization. - // Makes it about 5% to 10% faster - const ALPHA_MASK: u32 = u32::from_ne_bytes([0, 0, 0, 255]); - let pixel_alpha = u32::from_ne_bytes(current.0) & ALPHA_MASK; - if pixel_alpha == 0 { - *current = *previous; + let after_frame = row_skip + data_len; + non_disposed_data[after_frame..].copy_from_slice(&frame_data[after_frame..]); } - match dispose { - DisposalMethod::Any | DisposalMethod::Keep => { - // do not dispose - // (keep pixels from this frame) - // note: the `Any` disposal method is underspecified in the GIF - // spec, but most viewers treat it identically to `Keep` - *previous = *current; - } - DisposalMethod::Background => { - // restore to background color - // (background shows through transparent pixels in the next frame) - *previous = Rgba([0, 0, 0, 0]); - } - DisposalMethod::Previous => { - // restore to previous - // (dispose frames leaving the last none disposal frame) + for y in (frame.top + frame.height)..height { + if y >= height { + break; } + + let start = y as usize * row_len; + let non_disposed_data = &mut non_disposed_frame.subpixels_mut()[start..][..row_len]; + let frame_data = &mut buf[start..][..row_len]; + frame_data.copy_from_slice(non_disposed_data); } } - // if `frame_buffer`'s frame exactly matches the entire image, then - // use it directly, else create a new buffer to hold the composited - // image. + Ok(DecodedImageAttributes { + delay: Some(frame.delay), + ..Default::default() + }) + } - // binding to variable makes it clear to the compiler that the value does not change in the loop, - // improves benchmarks by 10% - let disposal_method = frame.disposal_method; - let image_buffer = if (frame.left, frame.top) == (0, 0) - && (self.width, self.height) == frame_buffer.dimensions() - { - for (pixel, previous_pixel) in frame_buffer - .pixels_mut() - .iter_mut() - .zip(non_disposed_frame.pixels_mut().iter_mut()) - { - blend_and_dispose_pixel(disposal_method, previous_pixel, pixel); - } - frame_buffer - } else { - // Check limits before allocating the buffer - if let Err(e) = local_limits.reserve_buffer(self.width, self.height, COLOR_TYPE) { - return Some(Err(e)); - } - ImageBuffer::from_fn(self.width, self.height, |x, y| { - let frame_x = x.wrapping_sub(frame.left); - let frame_y = y.wrapping_sub(frame.top); - let previous_pixel = non_disposed_frame.get_pixel_mut(x, y); + fn icc_profile(&mut self) -> ImageResult>> { + let decoder = self.ensure_decoder()?; + // Similar to XMP metadata + Ok(decoder.icc_profile().map(Vec::from)) + } - if frame_x < frame_buffer.width() && frame_y < frame_buffer.height() { - let mut pixel = *frame_buffer.get_pixel(frame_x, frame_y); - blend_and_dispose_pixel(disposal_method, previous_pixel, &mut pixel); - pixel - } else { - // out of bounds, return pixel from previous frame - *previous_pixel - } - }) - }; + 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(decoder.xmp_metadata().map(Vec::from)) + } +} - Some(Ok(animation::Frame::from_parts( - image_buffer, - 0, - 0, - frame.delay, - ))) +fn blend_and_dispose_region( + dispose: DisposalMethod, + non_disposed_data: &mut [u8], + frame_data: &mut [u8], +) { + for (disposed, pixel) in non_disposed_data + .chunks_exact_mut(4) + .zip(frame_data.chunks_exact_mut(4)) + { + // FIXME: internal dispatch on disposal method may be slow, investigate if this is + // properly and reliably vectorized. + let disposed = Rgba::::from_slice_mut(disposed); + let pixel = Rgba::::from_slice_mut(pixel); + blend_and_dispose_pixel(dispose, disposed, pixel); } } -impl<'a, R: BufRead + Seek + 'a> AnimationDecoder<'a> for GifDecoder { - fn loop_count(&self) -> LoopCount { - match self.reader.repeat() { - gif::Repeat::Finite(n @ 1..) => { - LoopCount::Finite(NonZeroU32::new(n.into()).expect("repeat is non-zero")) - } - gif::Repeat::Finite(0) | gif::Repeat::Infinite => LoopCount::Infinite, - } +// blend the current frame with the non-disposed frame, then update +// the non-disposed frame according to the disposal method. +#[inline] +fn blend_and_dispose_pixel( + dispose: DisposalMethod, + previous: &mut Rgba, + current: &mut Rgba, +) { + // Instead of only checking the alpha channel, use a bitmask to check + // the entire pixel and allow for better auto-vectorization. + // Makes it about 5% to 10% faster + const ALPHA_MASK: u32 = u32::from_ne_bytes([0, 0, 0, 255]); + let pixel_alpha = u32::from_ne_bytes(current.0) & ALPHA_MASK; + if pixel_alpha == 0 { + *current = *previous; } - fn into_frames(self) -> animation::Frames<'a> { - animation::Frames::new(Box::new(GifFrameIterator::new(self))) + match dispose { + DisposalMethod::Any | DisposalMethod::Keep => { + // do not dispose + // (keep pixels from this frame) + // note: the `Any` disposal method is underspecified in the GIF + // spec, but most viewers treat it identically to `Keep` + *previous = *current; + } + DisposalMethod::Background => { + // restore to background color + // (background shows through transparent pixels in the next frame) + *previous = Rgba([0, 0, 0, 0]); + } + DisposalMethod::Previous => { + // restore to previous + // (dispose frames leaving the last none disposal frame) + } } } @@ -668,6 +666,7 @@ impl ImageError { #[cfg(test)] mod test { use super::*; + use std::io; #[test] fn frames_exceeding_logical_screen_size() { @@ -681,9 +680,10 @@ mod test { 0x77, 0xF5, 0x6D, 0x14, 0x00, 0x3B, ]; - let decoder = GifDecoder::new(io::Cursor::new(data)).unwrap(); - let mut buf = vec![0u8; decoder.total_bytes() as usize]; + let mut decoder = GifDecoder::new(io::Cursor::new(data)).unwrap(); + let layout = decoder.prepare_image().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 6b2965c1fe..7db55450bf 100644 --- a/src/codecs/hdr/decoder.rs +++ b/src/codecs/hdr/decoder.rs @@ -6,7 +6,8 @@ use std::{error, fmt}; use crate::error::{ DecodingError, ImageError, ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind, }; -use crate::io::image_reader_type::SpecCompliance; +use crate::io::DecoderPreparedImage; +use crate::io::{image_reader_type::SpecCompliance, DecodedImageAttributes}; use crate::{ColorType, ImageDecoder, ImageFormat, Limits, Rgb}; /// Errors that can occur during decoding and parsing of a HDR image @@ -272,8 +273,9 @@ impl HdrDecoder { }; // color type is always rgb8 - if crate::utils::check_dimension_overflow(width, height, ColorType::Rgb8.bytes_per_pixel()) - { + let layout = crate::ImageLayout::new(width, height, ColorType::Rgb8); + + if layout.total_bytes_overflows_u64() { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Hdr.into(), @@ -302,12 +304,9 @@ 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 prepare_image(&mut self) -> ImageResult { + let HdrMetadata { width, height, .. } = self.meta; + Ok(DecoderPreparedImage::new(width, height, ColorType::Rgb32F)) } fn set_limits(&mut self, mut limits: Limits) -> ImageResult<()> { @@ -319,12 +318,13 @@ impl ImageDecoder for HdrDecoder { 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 layout = self.prepare_image()?; + 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]; @@ -343,11 +343,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 06b63f1527..ff4b7e75c1 100644 --- a/src/codecs/ico/decoder.rs +++ b/src/codecs/ico/decoder.rs @@ -6,6 +6,9 @@ use crate::color::ColorType; use crate::error::{ DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::{ + DecodedAnimationAttributes, DecodedImageAttributes, DecoderPreparedImage, FormatAttributes, +}; use crate::utils::seek_start_with_offset; use crate::{ImageDecoder, ImageFormat}; @@ -263,38 +266,47 @@ 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 format_attributes(&self) -> FormatAttributes { + match &self.inner_decoder { + Bmp(decoder) => decoder.format_attributes(), + Png(decoder) => decoder.format_attributes(), + } + } + + fn prepare_image(&mut self) -> ImageResult { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.prepare_image(), + Png(decoder) => decoder.prepare_image(), } } - fn color_type(&self) -> ColorType { - match self.inner_decoder { - Bmp(ref decoder) => decoder.color_type(), - Png(ref decoder) => decoder.color_type(), + fn animation_attributes(&mut self) -> Option { + match &mut self.inner_decoder { + Bmp(decoder) => decoder.animation_attributes(), + Png(decoder) => decoder.animation_attributes(), } } - 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()); } + let layout = decoder.prepare_image()?; // Check if the image dimensions match the ones in the image data. - let (width, height) = decoder.dimensions(); + let (width, height) = layout.layout.dimensions(); if !self.selected_entry.matches_dimensions(width, height) { return Err(DecoderError::ImageEntryDimensionMismatch { format: IcoEntryImageFormat::Png, @@ -309,14 +321,15 @@ 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 layout.layout.color != ColorType::Rgba8 { return Err(DecoderError::PngNotRgba.into()); } decoder.read_image(buf) } - Bmp(mut decoder) => { - let (width, height) = decoder.dimensions(); + Bmp(decoder) => { + let layout = decoder.prepare_image()?; + let (width, height) = layout.layout.dimensions(); if !self.selected_entry.matches_dimensions(width, height) { return Err(DecoderError::ImageEntryDimensionMismatch { format: IcoEntryImageFormat::Bmp, @@ -330,11 +343,11 @@ impl ImageDecoder for IcoDecoder { } // The ICO decoder needs an alpha channel to apply the AND mask. - if decoder.color_type() != ColorType::Rgba8 { + if layout.layout.color != ColorType::Rgba8 { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Bmp.into(), - UnsupportedErrorKind::Color(decoder.color_type().into()), + UnsupportedErrorKind::Color(layout.layout.color.into()), ), )); } @@ -377,10 +390,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()) } @@ -388,8 +401,55 @@ impl ImageDecoder for IcoDecoder { } } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + 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(), + } } } @@ -451,8 +511,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.prepare_image().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 1c960679be..3c84b68be5 100644 --- a/src/codecs/jpeg/decoder.rs +++ b/src/codecs/jpeg/decoder.rs @@ -7,8 +7,9 @@ use crate::color::ColorType; use crate::error::{ DecodingError, ImageError, ImageResult, LimitError, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::decoder::DecodedMetadataHint; use crate::io::image_reader_type::SpecCompliance; -use crate::metadata::Orientation; +use crate::io::{DecodedImageAttributes, DecoderPreparedImage, FormatAttributes}; use crate::{ImageDecoder, ImageFormat, Limits}; type ZuneColorSpace = zune_core::colorspace::ColorSpace; @@ -21,7 +22,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, @@ -71,7 +71,6 @@ impl JpegDecoder { width, height, limits, - orientation: None, phantom: PhantomData, }) } @@ -88,15 +87,30 @@ impl JpegDecoder { } impl ImageDecoder for JpegDecoder { - fn dimensions(&self) -> (u32, u32) { - (u32::from(self.width), u32::from(self.height)) + fn format_attributes(&self) -> FormatAttributes { + FormatAttributes { + // 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, + ..FormatAttributes::default() + } } - fn color_type(&self) -> ColorType { - ColorType::from_jpeg(self.orig_color_space) + fn prepare_image(&mut self) -> ImageResult { + Ok(DecoderPreparedImage::new( + self.width.into(), + self.height.into(), + 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(self.spec_compliance == SpecCompliance::Strict) .set_max_width(usize::MAX) @@ -108,6 +122,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(self.spec_compliance == SpecCompliance::Strict) .set_max_width(usize::MAX) @@ -117,16 +133,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(self.spec_compliance == SpecCompliance::Strict) .set_max_width(usize::MAX) @@ -139,6 +151,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(self.spec_compliance == SpecCompliance::Strict) .set_max_width(usize::MAX) @@ -150,16 +164,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.prepare_image()?; - 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 { @@ -177,23 +185,23 @@ impl ImageDecoder for JpegDecoder { &self.input, self.orig_color_space, self.spec_compliance == SpecCompliance::Strict, - self.limits, + &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(); - limits.check_dimensions(width, height)?; + let layout = self.prepare_image()?; + limits.check_layout_dimensions(&layout)?; self.limits = limits; Ok(()) } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) - } } impl ColorType { @@ -222,12 +230,12 @@ 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, strict_mode: bool, - 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) @@ -267,8 +275,16 @@ 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 image = crate::DynamicImage::new_luma8(0, 0); + let mut reader = crate::ImageReader::from_decoder(Box::new(decoder)); + let meta = reader.decode_to_dynimage(&mut image).unwrap(); + + assert_eq!( + meta.attributes().orientation.unwrap(), + crate::metadata::Orientation::FlipHorizontal + ); } #[test] @@ -277,15 +293,17 @@ mod tests { image.truncate(image.len() - 1000); // simulate a truncated image // Default (lenient) mode: truncated image should be accepted - let decoder = JpegDecoder::new(Cursor::new(&image)).unwrap(); - let mut buffer = vec![0u8; decoder.total_bytes() as usize]; + let mut decoder = JpegDecoder::new(Cursor::new(&image)).unwrap(); + let layout = decoder.prepare_image().unwrap(); + let mut buffer = vec![0u8; layout.total_bytes() as usize]; assert!(decoder.read_image(&mut buffer).is_ok()); // Strict mode: truncated image should be rejected - let decoder = + let mut decoder = JpegDecoder::new_with_spec_compliance(Cursor::new(&image), SpecCompliance::Strict) .unwrap(); - let mut buffer = vec![0u8; decoder.total_bytes() as usize]; + let layout = decoder.prepare_image().unwrap(); + let mut buffer = vec![0u8; layout.total_bytes() as usize]; assert!(decoder.read_image(&mut buffer).is_err()); } } diff --git a/src/codecs/jpeg/encoder.rs b/src/codecs/jpeg/encoder.rs index 11cbc0a359..ccf91d5f89 100644 --- a/src/codecs/jpeg/encoder.rs +++ b/src/codecs/jpeg/encoder.rs @@ -320,9 +320,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.prepare_image().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 de8d0041f8..d60ea7b516 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, DecoderPreparedImage}; 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,70 @@ 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 prepare_image(&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 - } + }; + + // We may have discarded the alpha channel. + Ok(DecoderPreparedImage::new(width, height, color)) } - fn original_color_type(&self) -> ExtendedColorType { - if self.alpha_present_in_file { + // reads with or without alpha, depending on `self.alpha_preference` and `self.alpha_present_in_file` + fn read_image(&mut self, unaligned_bytes: &mut [u8]) -> ImageResult { + let layout = self.prepare_image()?; + let (width, height) = layout.layout.dimensions(); + + let original = 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; + 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.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.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 +208,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 +218,11 @@ 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 { + original_color_type: Some(original), + ..DecodedImageAttributes::default() + }) } } @@ -385,9 +401,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.prepare_image()?.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 +415,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.prepare_image()?.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 6943e2c5bc..e6d26f4ced 100644 --- a/src/codecs/png.rs +++ b/src/codecs/png.rs @@ -4,25 +4,29 @@ //! //! # Related Links //! * - The PNG Specification - +use core::num::NonZeroU32; use std::borrow::Cow; use std::io::{BufRead, Seek, Write}; -use std::num::NonZeroU32; 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::{ + DecodedAnimationAttributes, DecodedImageAttributes, DecoderPreparedImage, FormatAttributes, + SequenceControl, +}; use crate::math::Rect; use crate::metadata::LoopCount; 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 @@ -33,27 +37,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 @@ -61,6 +86,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, @@ -115,11 +141,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. @@ -131,8 +154,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)) @@ -149,7 +175,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. @@ -159,28 +185,17 @@ 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()) - } -} - -fn unsupported_color(ect: ExtendedColorType) -> ImageError { - ImageError::Unsupported(UnsupportedError::from_format_and_kind( - ImageFormat::Png.into(), - UnsupportedErrorKind::Color(ect), - )) -} - -impl ImageDecoder for PngDecoder { - fn dimensions(&self) -> (u32, u32) { - self.reader.info().size() - } + let Some(reader) = &self.reader else { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + ))); + }; - fn color_type(&self) -> ColorType { - self.color_type + Ok(reader.info().animation_control.is_some()) } - fn original_color_type(&self) -> ExtendedColorType { - match (self.reader.info().color_type, self.reader.info().bit_depth) { + fn color_type_info(info: &png::Info<'_>) -> ExtendedColorType { + match (info.color_type, info.bit_depth) { (png::ColorType::Grayscale, png::BitDepth::One) => ExtendedColorType::L1, (png::ColorType::Grayscale, png::BitDepth::Two) => ExtendedColorType::L2, (png::ColorType::Grayscale, png::BitDepth::Four) => ExtendedColorType::L4, @@ -208,23 +223,90 @@ impl ImageDecoder for PngDecoder { (png::ColorType::Indexed, png::BitDepth::Sixteen) => ExtendedColorType::Unknown(16), } } +} + +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() + } +} + +fn unsupported_color(ect: ExtendedColorType) -> ImageError { + ImageError::Unsupported(UnsupportedError::from_format_and_kind( + ImageFormat::Png.into(), + UnsupportedErrorKind::Color(ect), + )) +} + +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 prepare_image(&mut self) -> ImageResult { + let reader = self.ensure_reader_and_header()?; + let (width, height) = reader.info().size(); + Ok(DecoderPreparedImage::new(width, height, self.color_type)) + } + + fn format_attributes(&self) -> FormatAttributes { + FormatAttributes { + // is any sort of iTXT chunk. + // FIXME: we do not collect these in advance. + xmp: DecodedMetadataHint::InHeader, + // is any sort of iTXT chunk. + // FIXME: we do not collect these in advance. + iptc: DecodedMetadataHint::InHeader, + // see iCCP chunk order. + icc: DecodedMetadataHint::InHeader, + // see eXIf chunk order. + exif: DecodedMetadataHint::InHeader, + ..FormatAttributes::default() + } + } + + /// Only for [`ApngDecoder`]. + fn animation_attributes(&mut self) -> Option { + None + } 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() @@ -237,12 +319,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() @@ -256,8 +340,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() @@ -269,14 +352,18 @@ impl ImageDecoder for PngDecoder { Ok(None) } - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - 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(); + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + let layout = self.prepare_image()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); + + let reader = self.ensure_reader_and_header()?; + let original_color_type = Self::color_type_info(reader.info()); + 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.layout.color.bytes_per_pixel() / layout.layout.color.channel_count(); match bpc { 1 => (), // No reodering necessary for u8 @@ -285,39 +372,48 @@ impl ImageDecoder for PngDecoder { }), _ => unreachable!(), } - Ok(()) - } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes { + original_color_type: Some(original_color_type), + ..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()) + } } } -/// An [`AnimationDecoder`] adapter of [`PngDecoder`]. +/// An animated adapter of [`PngDecoder`]. /// /// See [`PngDecoder::apng`] for more information. /// -/// [`AnimationDecoder`]: ../trait.AnimationDecoder.html /// [`PngDecoder`]: struct.PngDecoder.html /// [`PngDecoder::apng`]: struct.PngDecoder.html#method.apng 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, @@ -328,79 +424,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 = self.inner.prepare_image()?; + let ImageLayout { + width, + height, + color, + } = layout.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 @@ -408,7 +501,6 @@ impl ApngDecoder { let current = self.current.as_mut().unwrap(); // Dispose of the previous frame. - match self.dispose { DisposeOp::None => { previous.clone_from(current); @@ -426,9 +518,7 @@ impl ApngDecoder { } } else { // The first frame is always a background frame. - current.pixels_mut().iter_mut().for_each(|pixel| { - *pixel = Rgba::from([0, 0, 0, 0]); - }); + current.as_mut_bytes().fill(0); } } DisposeOp::Previous => { @@ -446,146 +536,149 @@ 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.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.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 loop_count(&self) -> LoopCount { - match self.inner.reader.info().animation_control() { - None => LoopCount::Infinite, - Some(actl) => match NonZeroU32::new(actl.num_plays) { - None => LoopCount::Infinite, - Some(n) => LoopCount::Finite(n), - }, +impl ImageDecoder for ApngDecoder { + fn format_attributes(&self) -> FormatAttributes { + FormatAttributes { + supports_animation: true, + ..self.inner.format_attributes() } } - fn into_frames(self) -> Frames<'a> { - struct FrameIterator(ApngDecoder); + fn animation_attributes(&mut self) -> Option { + let count = if let Ok(reader) = self.inner.ensure_reader_and_header() { + reader.info().animation_control() + } else { + return None; + }; - impl Iterator for FrameIterator { - type Item = ImageResult; + let loop_count = match count { + None => LoopCount::Infinite, + Some(actl) if actl.num_plays == 0 => LoopCount::Infinite, + Some(actl) => LoopCount::Finite( + NonZeroU32::new(actl.num_plays).expect("num_plays should be non-zero"), + ), + }; - 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)), - }; + Some(DecodedAnimationAttributes { loop_count }) + } - 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 prepare_image(&mut self) -> ImageResult { + self.inner.prepare_image() + } - Frames::new(Box::new(FrameIterator(self))) + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + self.mix_next_frame(buf)? + .ok_or_else(reader_finished_already) + } + + fn more_images(&self) -> SequenceControl { + if self.remaining > 0 { + SequenceControl::MaybeMore + } else { + SequenceControl::None + } } } @@ -879,6 +972,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::*; @@ -887,22 +1041,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?)"); + )); + + let layout = dec + .prepare_image() + .expect("Unable to read PNG file (does it exist?)"); - assert_eq![(2000, 1000), dec.dimensions()]; + assert_eq![(2000, 1000), layout.layout.dimensions()]; assert_eq![ ColorType::Rgb8, - dec.color_type(), + layout.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::>(); @@ -919,7 +1077,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.prepare_image().err().unwrap(); + let _ = error .source() .unwrap() @@ -949,7 +1109,8 @@ mod tests { .expect("Could not encode image"); } - let mut decoder = PngDecoder::new(Cursor::new(&encoded)).expect("Could not decode image"); + let mut decoder = PngDecoder::new(Cursor::new(&encoded)); + let _ = decoder.prepare_image().unwrap(); let decoded_xmp = decoder .xmp_metadata() .expect("Error decoding XMP") diff --git a/src/codecs/pnm/decoder.rs b/src/codecs/pnm/decoder.rs index 2e88f46dd5..cf21002ad4 100644 --- a/src/codecs/pnm/decoder.rs +++ b/src/codecs/pnm/decoder.rs @@ -11,7 +11,8 @@ use crate::color::{ColorType, ExtendedColorType}; use crate::error::{ DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind, }; -use crate::{utils, ImageDecoder, ImageFormat}; +use crate::io::{DecodedImageAttributes, DecoderPreparedImage}; +use crate::{ImageDecoder, ImageFormat}; /// All errors that can occur when attempting to parse a PNM #[derive(Debug, Clone)] @@ -231,6 +232,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 +315,26 @@ 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), }?; - if utils::check_dimension_overflow( - decoder.dimensions().0, - decoder.dimensions().1, - decoder.color_type().bytes_per_pixel(), - ) { + let layout = decoder.prepare_image()?; + + if layout.layout.total_bytes_overflows_u64() { 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.layout.width, layout.layout.height, )), ), )); @@ -591,44 +627,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 prepare_image(&mut self) -> ImageResult { + let width = self.header.width(); + let 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, - } + Ok(DecoderPreparedImage::new( + width, + height, + self.tuple.expanded_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.prepare_image()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); + let original_color_type = Some(self.tuple.original_color()); - 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 +655,12 @@ 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 { + original_color_type, + ..DecodedImageAttributes::default() + }) } } @@ -1001,14 +1016,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(); + let layout = decoder.prepare_image().unwrap(); + assert_eq!(layout.layout.color, ColorType::L8); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (4, 4)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; - decoder.read_image(&mut image).unwrap(); + let layout = decoder.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; + let attr = decoder.read_image(&mut image).unwrap(); + assert_eq!(attr.original_color_type.unwrap(), ExtendedColorType::L1); + assert_eq!( image, vec![ @@ -1047,14 +1065,16 @@ 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(); + let layout = decoder.prepare_image().unwrap(); + assert_eq!(layout.layout.color, ColorType::La8); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (2, 2)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; - decoder.read_image(&mut image).unwrap(); + let layout = decoder.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; + let attr = decoder.read_image(&mut image).unwrap(); + assert_eq!(attr.original_color_type.unwrap(), ExtendedColorType::La1); assert_eq!(image, vec![0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,]); match PnmDecoder::new(&pamdata[..]).unwrap().into_inner() { ( @@ -1087,12 +1107,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.prepare_image().unwrap().layout.color, ColorType::L8); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (4, 4)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!( image, @@ -1132,14 +1153,16 @@ 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(); + let layout = decoder.prepare_image().unwrap(); + assert_eq!(layout.layout.color, ColorType::La16); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (2, 1)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; - decoder.read_image(&mut image).unwrap(); + let layout = decoder.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; + let attr = decoder.read_image(&mut image).unwrap(); + assert_eq!(attr.original_color_type.unwrap(), ExtendedColorType::La16); assert_eq!( image, [ @@ -1181,12 +1204,16 @@ 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.prepare_image().unwrap().layout.color, + ColorType::Rgb8 + ); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (2, 2)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; + let layout = decoder.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!( image, @@ -1223,14 +1250,16 @@ 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(); + let layout = decoder.prepare_image().unwrap(); + assert_eq!(layout.layout.color, ColorType::Rgba8); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (1, 3)); assert_eq!(decoder.subtype(), PnmSubtype::ArbitraryMap); - let mut image = vec![0; decoder.total_bytes() as usize]; - decoder.read_image(&mut image).unwrap(); + let layout = decoder.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; + let attr = decoder.read_image(&mut image).unwrap(); + assert_eq!(attr.original_color_type.unwrap(), ExtendedColorType::Rgba8); assert_eq!(image, b"\x00\x11\x22\x33\xaa\xbb\xcc\xdd\x55\x66\x77\x88",); match PnmDecoder::new(&pamdata[..]).unwrap().into_inner() { ( @@ -1256,16 +1285,19 @@ 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(); + let layout = decoder.prepare_image().unwrap(); + assert_eq!(layout.layout.color, ColorType::L8); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (6, 2)); assert_eq!( decoder.subtype(), PnmSubtype::Bitmap(SampleEncoding::Binary) ); - let mut image = vec![0; decoder.total_bytes() as usize]; - decoder.read_image(&mut image).unwrap(); + + let layout = decoder.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; + let attr = decoder.read_image(&mut image).unwrap(); + assert_eq!(attr.original_color_type.unwrap(), ExtendedColorType::L1); assert_eq!(image, vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0]); match PnmDecoder::new(&pbmbinary[..]).unwrap().into_inner() { ( @@ -1304,8 +1336,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.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder .read_image(&mut image) .expect_err("Image is malformed"); @@ -1317,14 +1350,16 @@ 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(); + let layout = decoder.prepare_image().unwrap(); + assert_eq!(layout.layout.color, ColorType::L8); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (6, 2)); assert_eq!(decoder.subtype(), PnmSubtype::Bitmap(SampleEncoding::Ascii)); - let mut image = vec![0; decoder.total_bytes() as usize]; - decoder.read_image(&mut image).unwrap(); + let layout = decoder.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; + let attr = decoder.read_image(&mut image).unwrap(); + assert_eq!(attr.original_color_type.unwrap(), ExtendedColorType::L1); assert_eq!(image, vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0]); match PnmDecoder::new(&pbmbinary[..]).unwrap().into_inner() { ( @@ -1349,14 +1384,16 @@ 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(); + let layout = decoder.prepare_image().unwrap(); + assert_eq!(layout.layout.color, ColorType::L8); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (6, 2)); assert_eq!(decoder.subtype(), PnmSubtype::Bitmap(SampleEncoding::Ascii)); - let mut image = vec![0; decoder.total_bytes() as usize]; - decoder.read_image(&mut image).unwrap(); + let layout = decoder.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; + let attr = decoder.read_image(&mut image).unwrap(); + assert_eq!(attr.original_color_type.unwrap(), ExtendedColorType::L1); assert_eq!(image, vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0]); match PnmDecoder::new(&pbmbinary[..]).unwrap().into_inner() { ( @@ -1381,14 +1418,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.prepare_image().unwrap().layout.color, ColorType::L8); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (4, 4)); assert_eq!( decoder.subtype(), PnmSubtype::Graymap(SampleEncoding::Binary) ); - let mut image = vec![0; decoder.total_bytes() as usize]; + + let layout = decoder.prepare_image().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() { @@ -1414,14 +1453,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.prepare_image().unwrap().layout.color, ColorType::L8); + assert_eq!(decoder.prepare_image().unwrap().layout.dimensions(), (4, 4)); assert_eq!( decoder.subtype(), PnmSubtype::Graymap(SampleEncoding::Ascii) ); - let mut image = vec![0; decoder.total_bytes() as usize]; + + let layout = decoder.prepare_image().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() { @@ -1445,8 +1486,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.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; decoder.read_image(&mut image).unwrap(); assert_eq!( image, @@ -1488,16 +1530,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.prepare_image().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.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; let _ = decoder.read_image(&mut image).unwrap_err(); } @@ -1517,8 +1561,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.prepare_image().unwrap(); + let mut image = vec![0; layout.total_bytes() as usize]; assert!(decoder.read_image(&mut image).is_ok()); } @@ -1531,7 +1576,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.prepare_image().unwrap().layout.dimensions() == (1, 1234567890)); } } diff --git a/src/codecs/pnm/mod.rs b/src/codecs/pnm/mod.rs index ef4efcd5d3..2e0817e597 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.prepare_image().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.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.prepare_image().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.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.prepare_image().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.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..e24eed6bc1 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, DecoderPreparedImage}; use crate::{ ColorType, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageResult, }; @@ -23,24 +24,23 @@ where } impl ImageDecoder for QoiDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.decoder.header().width, self.decoder.header().height) - } - - fn color_type(&self) -> ColorType { - match self.decoder.header().channels { + fn prepare_image(&mut self) -> ImageResult { + let header = self.decoder.header(); + let color = match header.channels { qoi::Channels::Rgb => ColorType::Rgb8, qoi::Channels::Rgba => ColorType::Rgba8, - } - } + }; - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - self.decoder.decode_to_buf(buf).map_err(decoding_error)?; - Ok(()) + Ok(DecoderPreparedImage::new( + header.width, + header.height, + color, + )) } - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + fn read_image(&mut self, buf: &mut [u8]) -> ImageResult { + self.decoder.decode_to_buf(buf).map_err(decoding_error)?; + Ok(DecodedImageAttributes::default()) } } @@ -111,10 +111,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.prepare_image().unwrap(); + assert_eq!((5, 5), layout.layout.dimensions()); + assert_eq!(ColorType::Rgba8, layout.layout.color); } } diff --git a/src/codecs/tga/decoder.rs b/src/codecs/tga/decoder.rs index 5f672cea25..af2d4c4b1d 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, DecoderPreparedImage, ReadExt}; use crate::utils::vec_try_with_capacity; use crate::{ color::{ColorType, ExtendedColorType}, @@ -388,21 +388,23 @@ impl TgaDecoder { } impl ImageDecoder for TgaDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.width as u32, self.height as u32) - } + fn prepare_image(&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 - } + let width = try_dimensions(self.width)?; + let height = try_dimensions(self.height)?; - fn original_color_type(&self) -> ExtendedColorType { - self.original_color_type - .unwrap_or_else(|| self.color_type().into()) + Ok(DecoderPreparedImage::new(width, height, self.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.prepare_image()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); // Decode the raw data // @@ -454,10 +456,9 @@ 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 { + original_color_type: self.original_color_type, + ..DecodedImageAttributes::default() + }) } } diff --git a/src/codecs/tga/encoder.rs b/src/codecs/tga/encoder.rs index 496e1e5647..5c526a6312 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.prepare_image().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.prepare_image().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 875421fc68..2adf5f9b43 100644 --- a/src/codecs/tiff.rs +++ b/src/codecs/tiff.rs @@ -16,6 +16,8 @@ use crate::error::{ DecodingError, EncodingError, ImageError, ImageResult, LimitError, LimitErrorKind, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; +use crate::io::decoder::DecodedMetadataHint; +use crate::io::{DecodedImageAttributes, DecoderPreparedImage, FormatAttributes}; use crate::metadata::Orientation; use crate::{utils, ImageDecoder, ImageEncoder, ImageFormat}; @@ -28,14 +30,26 @@ 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>, + buffer: DecodingResult, +} + +enum ImageState { + Initial, + At(ImageInfo), + Consumed, +} + +#[derive(Clone, Copy)] +struct ImageInfo { dimensions: (u32, u32), color_type: ColorType, original_color_type: ExtendedColorType, ycbcr_coefficients: [f32; 3], - - // We only use an Option here so we can call with_limits on the decoder without moving. - inner: Option>, - buffer: DecodingResult, } impl TiffDecoder @@ -44,12 +58,53 @@ 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)?; + + Ok(TiffDecoder { + info: ImageState::Initial, + limits: tiff::decoder::Limits::default(), + inner: Some(inner), + buffer: DecodingResult::U8(vec![]), + }) + } + + 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, + ))); + }; + + 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 = inner.dimensions().map_err(ImageError::from_tiff_decode)?; - let tiff_color_type = inner.colortype().map_err(ImageError::from_tiff_decode)?; + let dimensions = reader.dimensions().map_err(ImageError::from_tiff_decode)?; + let tiff_color_type = reader.colortype().map_err(ImageError::from_tiff_decode)?; - match inner.find_tag_unsigned_vec::(Tag::SampleFormat) { + match reader.find_tag_unsigned_vec::(Tag::SampleFormat) { Ok(Some(sample_formats)) => { for format in sample_formats { check_sample_format(format, tiff_color_type)?; @@ -105,40 +160,53 @@ where let mut ycbcr_coefficients = [0.0; 3]; if matches!(tiff_color_type, tiff::ColorType::YCbCr(8)) { - check_ycbcr_subsampling(&mut inner)?; - ycbcr_coefficients = read_ycbcr_coefficients(&mut inner)?; + check_ycbcr_subsampling(reader)?; + ycbcr_coefficients = read_ycbcr_coefficients(reader)?; } - Ok(TiffDecoder { + self.info = ImageState::At(ImageInfo { dimensions, color_type, original_color_type, ycbcr_coefficients, - inner: Some(inner), - buffer: DecodingResult::U8(vec![]), - }) - } + }); - // 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); + self.redistribute_limits(); - let bytes_per_pixel = match self.original_color_type { - ExtendedColorType::Cmyk8 => 4, - ExtendedColorType::Cmyk16 => 8, - _ => u64::from(self.color_type().bytes_per_pixel()), + Ok(()) + } + + fn redistribute_limits(&mut self) { + let ImageState::At(info) = &self.info else { + return; }; - total_pixels.saturating_mul(bytes_per_pixel) + + 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)); } /// Interleave planes in our `buffer` into `output`. fn interleave_planes( &mut self, + info: ImageInfo, layout: tiff::decoder::BufferLayoutPreference, output: &mut [u8], ) -> ImageResult<()> { - if self.original_color_type != self.color_type.into() { + if info.original_color_type != info.color_type.into() { return Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormat::Tiff.into(), @@ -176,7 +244,7 @@ where // Gracefully handle a mismatch of expectations. This should not occur in practice as we // check that all planes have been read (see note on `read_image_to_buffer` usage below). - if planes.len() < usize::from(self.color_type.channel_count()) { + if planes.len() < usize::from(info.color_type.channel_count()) { return Err(ImageError::Decoding(DecodingError::new( ImageFormat::Tiff.into(), "Not enough planes read from TIFF image".to_string(), @@ -185,14 +253,30 @@ where utils::interleave_planes( output, - self.color_type, - &planes[..usize::from(self.color_type.channel_count())], + info.color_type, + &planes[..usize::from(info.color_type.channel_count())], ); Ok(()) } } +impl ImageInfo { + // The buffer can be larger for CMYK than the RGB output + fn total_bytes_buffer(&self) -> u64 { + 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()), + }; + + total_pixels.saturating_mul(bytes_per_pixel) + } +} + fn check_ycbcr_subsampling(decoder: &mut Decoder) -> ImageResult<()> { let compression = decoder .find_tag(Tag::Compression) @@ -361,16 +445,24 @@ impl ImageError { } impl ImageDecoder for TiffDecoder { - fn dimensions(&self) -> (u32, u32) { - self.dimensions + fn format_attributes(&self) -> FormatAttributes { + FormatAttributes { + // is any sort of iTXT chunk. + xmp: DecodedMetadataHint::PerImage, + icc: DecodedMetadataHint::PerImage, + exif: DecodedMetadataHint::PerImage, + // not provided above. + iptc: DecodedMetadataHint::Unsupported, + supports_sequence: true, + ..FormatAttributes::default() + } } - fn color_type(&self) -> ColorType { - self.color_type - } + fn prepare_image(&mut self) -> ImageResult { + let info = self.peek_info()?; + let (width, height) = info.dimensions; - fn original_color_type(&self) -> ExtendedColorType { - self.original_color_type + Ok(DecoderPreparedImage::new(width, height, info.color_type)) } fn icc_profile(&mut self) -> ImageResult>> { @@ -399,45 +491,49 @@ 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, + ycbcr_coefficients: [0.0; 3], + }, + }; + + 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(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 info = self.peek_info()?; + let layout = self.prepare_image()?; + + let original_color_type = Some(info.original_color_type); + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); - let layout = self - .inner - .as_mut() - .unwrap() + let Some(reader) = &mut self.inner else { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + ))); + }; + + let layout = reader .read_image_to_buffer(&mut self.buffer) .map_err(ImageError::from_tiff_decode)?; @@ -451,24 +547,25 @@ impl ImageDecoder for TiffDecoder { if layout.planes > 1 { // Note that we do not support planar layouts if we have to do conversion. Yet. See a // more detailed comment in the implementation. - return self.interleave_planes(layout, buf); + self.interleave_planes(info, layout, buf)?; + return Ok(DecodedImageAttributes::default()); } - match self.buffer { - DecodingResult::U8(v) if self.original_color_type == ExtendedColorType::Cmyk8 => { + match &self.buffer { + DecodingResult::U8(v) if info.original_color_type == ExtendedColorType::Cmyk8 => { let buf = buf.as_chunks_mut::<3>().0; for (cmyk, rgb) in v.as_chunks::<4>().0.iter().zip(buf) { *rgb = 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 buf = buf.as_chunks_mut::<6>().0; for (cmyk, rgb) in v.as_chunks::<4>().0.iter().zip(buf) { *rgb = bytemuck::cast(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 @@ -478,51 +575,78 @@ impl ImageDecoder for TiffDecoder { out_row.copy_from_slice(&utils::expand_bits(1, width, in_row)); } } - DecodingResult::U8(v) if self.original_color_type == ExtendedColorType::YCbCr8 => { - let [lr, lg, lb] = self.ycbcr_coefficients; + DecodingResult::U8(v) if info.original_color_type == ExtendedColorType::YCbCr8 => { + let [lr, lg, lb] = info.ycbcr_coefficients; let ycbcr = v.as_chunks::<3>().0; let out = buf.as_chunks_mut::<3>().0; ycbcr_to_rgb8(ycbcr, lr, lg, lb, out); } DecodingResult::U8(v) => { - buf.copy_from_slice(&v); + buf.copy_from_slice(v); } DecodingResult::U16(v) => { - buf.copy_from_slice(bytemuck::cast_slice(&v)); + buf.copy_from_slice(bytemuck::cast_slice(v)); } DecodingResult::U32(v) => { - buf.copy_from_slice(bytemuck::cast_slice(&v)); + buf.copy_from_slice(bytemuck::cast_slice(v)); } DecodingResult::U64(v) => { - buf.copy_from_slice(bytemuck::cast_slice(&v)); + buf.copy_from_slice(bytemuck::cast_slice(v)); } DecodingResult::I8(v) => { - buf.copy_from_slice(bytemuck::cast_slice(&v)); + buf.copy_from_slice(bytemuck::cast_slice(v)); } DecodingResult::I16(v) => { - buf.copy_from_slice(bytemuck::cast_slice(&v)); + buf.copy_from_slice(bytemuck::cast_slice(v)); } DecodingResult::I32(v) => { - buf.copy_from_slice(bytemuck::cast_slice(&v)); + buf.copy_from_slice(bytemuck::cast_slice(v)); } DecodingResult::I64(v) => { - buf.copy_from_slice(bytemuck::cast_slice(&v)); + buf.copy_from_slice(bytemuck::cast_slice(v)); } DecodingResult::F32(v) => { - buf.copy_from_slice(bytemuck::cast_slice(&v)); + buf.copy_from_slice(bytemuck::cast_slice(v)); } DecodingResult::F64(v) => { - buf.copy_from_slice(bytemuck::cast_slice(&v)); + buf.copy_from_slice(bytemuck::cast_slice(v)); } 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, + original_color_type, + ..DecodedImageAttributes::default() + }) + } + + 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 d49835ee19..4c4b13b6c0 100644 --- a/src/codecs/webp/decoder.rs +++ b/src/codecs/webp/decoder.rs @@ -1,21 +1,20 @@ -use std::io::{BufRead, Read, Seek}; +use std::io::{BufRead, Seek}; use image_webp::LoopCount; -use crate::buffer::ConvertBuffer; -use crate::error::{DecodingError, ImageError, ImageResult}; -use crate::metadata::Orientation; -use crate::{ - AnimationDecoder, ColorType, Delay, Frame, Frames, ImageDecoder, ImageFormat, RgbImage, Rgba, - RgbaImage, +use crate::error::{DecodingError, ImageError, ImageResult, ParameterError, ParameterErrorKind}; +use crate::io::{ + DecodedAnimationAttributes, DecodedImageAttributes, DecodedMetadataHint, DecoderPreparedImage, + FormatAttributes, SequenceControl, }; +use crate::{ColorType, Delay, ImageDecoder, ImageFormat, Rgba}; /// WebP Image format decoder. /// /// Supports both lossless and lossy WebP images. pub struct WebPDecoder { inner: image_webp::WebPDecoder, - orientation: Option, + current: u32, } impl WebPDecoder { @@ -23,7 +22,7 @@ impl WebPDecoder { pub fn new(r: R) -> ImageResult { Ok(Self { inner: image_webp::WebPDecoder::new(r).map_err(ImageError::from_webp_decode)?, - orientation: None, + current: 0, }) } @@ -41,28 +40,69 @@ impl WebPDecoder { } impl ImageDecoder for WebPDecoder { - fn dimensions(&self) -> (u32, u32) { - self.inner.dimensions() + fn format_attributes(&self) -> FormatAttributes { + FormatAttributes { + // As per extended file format description: + // + icc: DecodedMetadataHint::InHeader, + exif: DecodedMetadataHint::InHeader, + xmp: DecodedMetadataHint::InHeader, + ..FormatAttributes::default() + } } - fn color_type(&self) -> ColorType { - if self.inner.has_alpha() { + fn animation_attributes(&mut self) -> Option { + let loop_count = match self.inner.loop_count() { + LoopCount::Forever => crate::metadata::LoopCount::Infinite, + LoopCount::Times(n) => crate::metadata::LoopCount::Finite(n.into()), + }; + + Some(DecodedAnimationAttributes { loop_count }) + } + + fn prepare_image(&mut self) -> ImageResult { + let (width, height) = self.inner.dimensions(); + let color = if self.inner.has_alpha() { ColorType::Rgba8 } else { ColorType::Rgb8 - } + }; + + Ok(DecoderPreparedImage::new(width, height, color)) } - 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 is_animated = self.inner.is_animated(); - self.inner - .read_image(buf) - .map_err(ImageError::from_webp_decode) - } + if is_animated && self.current == self.inner.num_frames() { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::NoMoreData, + ))); + } + + let layout = self.prepare_image()?; + assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes())); + + // `read_frame` panics if the image is not animated. + let delay = if is_animated { + let delay = self + .inner + .read_frame(buf) + .map_err(ImageError::from_webp_decode)?; + Some(Delay::from_numer_denom_ms(delay, 1)) + } else { + self.inner + .read_image(buf) + .map_err(ImageError::from_webp_decode)?; + None + }; + + self.current += 1; - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) + Ok(DecodedImageAttributes { + delay, + ..DecodedImageAttributes::default() + }) } fn icc_profile(&mut self) -> ImageResult>> { @@ -77,12 +117,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) } @@ -92,67 +126,12 @@ impl ImageDecoder for WebPDecoder { .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 { - fn loop_count(&self) -> crate::metadata::LoopCount { - match self.inner.loop_count() { - LoopCount::Forever => crate::metadata::LoopCount::Infinite, - LoopCount::Times(n) => crate::metadata::LoopCount::Finite(n.into()), - } - } - - fn into_frames(self) -> Frames<'a> { - struct FramesInner { - decoder: WebPDecoder, - current: u32, - } - impl Iterator for FramesInner { - type Item = ImageResult; - - fn next(&mut self) -> Option { - if self.current == self.decoder.inner.num_frames() { - return None; - } - self.current += 1; - let (width, height) = self.decoder.inner.dimensions(); - - let (img, delay) = if self.decoder.inner.has_alpha() { - let mut img = RgbaImage::new(width, height); - match self.decoder.inner.read_frame(&mut img) { - Ok(delay) => (img, delay), - Err(image_webp::DecodingError::NoMoreFrames) => return None, - Err(e) => return Some(Err(ImageError::from_webp_decode(e))), - } - } else { - let mut img = RgbImage::new(width, height); - match self.decoder.inner.read_frame(&mut img) { - Ok(delay) => (img.convert(), delay), - Err(image_webp::DecodingError::NoMoreFrames) => return None, - Err(e) => return Some(Err(ImageError::from_webp_decode(e))), - } - }; - - Some(Ok(Frame::from_parts( - img, - 0, - 0, - Delay::from_numer_denom_ms(delay, 1), - ))) - } + fn more_images(&self) -> SequenceControl { + if self.current == self.inner.num_frames() { + SequenceControl::None + } else { + SequenceControl::MaybeMore } - - Frames::new(Box::new(FramesInner { - decoder: self, - current: 0, - })) } } @@ -160,6 +139,9 @@ impl ImageError { fn from_webp_decode(e: image_webp::DecodingError) -> Self { match e { image_webp::DecodingError::IoError(e) => ImageError::IoError(e), + image_webp::DecodingError::NoMoreFrames => { + ImageError::Parameter(ParameterError::from_kind(ParameterErrorKind::NoMoreData)) + } _ => ImageError::Decoding(DecodingError::new(ImageFormat::WebP.into(), e)), } } diff --git a/src/error.rs b/src/error.rs index 928f83f7f3..6c471bc3a9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -132,6 +132,8 @@ pub enum ParameterErrorKind { Generic(String), /// The end of the image has been reached. NoMoreData, + /// Passed a buffer not sized according to a layout. + BufferSizeMismatch, /// An operation expected a concrete color space but another was found. CicpMismatch { /// The cicp that was expected. @@ -444,6 +446,9 @@ impl fmt::Display for ParameterError { write!(fmt, "The parameter is malformed: {message}",) } ParameterErrorKind::NoMoreData => write!(fmt, "The end of the image has been reached",), + ParameterErrorKind::BufferSizeMismatch => { + write!(fmt, "The buffer length is incorrect",) + } ParameterErrorKind::CicpMismatch { expected, found } => { write!( fmt, diff --git a/src/hooks.rs b/src/hooks.rs index 9fa480f815..9690da2368 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -185,7 +185,8 @@ pub(crate) fn guess_format_extension(start: &[u8]) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::{load_from_memory, ColorType, DynamicImage, ImageReader}; + use crate::io::{DecodedImageAttributes, DecoderPreparedImage}; + use crate::{load_from_memory, ColorType, DynamicImage, ImageReaderOptions}; use std::io::Cursor; const MOCK_HOOK_EXTENSION: &str = "MOCKHOOK"; @@ -193,18 +194,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 prepare_image(&mut self) -> ImageResult { + Ok(DecoderPreparedImage::new( + (MOCK_IMAGE_OUTPUT.len() / 3) as u32, + 1, + 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 { @@ -221,7 +221,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/assets/hook/extension.MoCkHoOk") + let image = ImageReaderOptions::open("tests/assets/hook/extension.MoCkHoOk") .unwrap() .decode() .unwrap(); @@ -248,7 +248,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 d6a69d01c8..55597f9fb3 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, DecoderPreparedImage}; 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.prepare_image()?; + 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: DecoderPreparedImage, + ) -> 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.subpixels_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 @@ -1144,19 +1168,18 @@ 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::{ImageReaderOptions, metadata::Orientation}; + /// + /// let mut image = ImageReaderOptions::open("file.jpg")?.decode()?; + /// image.apply_orientation(Orientation::Rotate90); /// - /// let mut decoder = ImageReader::open("file.jpg")?.into_decoder()?; - /// let orientation = decoder.orientation()?; - /// let mut image = DynamicImage::from_decoder(decoder)?; - /// image.apply_orientation(orientation); /// # Ok(()) /// # } /// ``` @@ -1534,58 +1557,80 @@ 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: DecoderPreparedImage, +) -> ImageResult { + let crate::ImageLayout { + width: w, + height: h, + color: color_type, + .. + } = layout.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) } } @@ -1601,31 +1646,31 @@ 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. /// The image's format is determined from the path's file extension. /// -/// Try [`ImageReader`] for more advanced uses, including guessing the format based on the file's -/// content before its path. +/// Try [`ImageReaderOptions`] for more advanced uses, including guessing the format based on the +/// file's content before its path. 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. /// This is faster than fully loading the image and then getting its dimensions. /// -/// Try [`ImageReader`] for more advanced uses, including guessing the format based on the file's -/// content before its path or manually supplying the format. +/// Try [`ImageReaderOptions`] for more advanced uses, including guessing the format based on the +/// file's content before its path or manually supplying the format. 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. @@ -1652,9 +1697,9 @@ pub fn write_buffer_with_format( /// Makes an educated guess about the image format. /// TGA is not supported by this function. /// -/// Try [`ImageReader`] for more advanced uses. +/// Try [`ImageReaderOptions`] for more advanced uses. pub fn load_from_memory(buffer: &[u8]) -> ImageResult { - ImageReader::new(io::Cursor::new(buffer)) + ImageReaderOptions::new(io::Cursor::new(buffer)) .with_guessed_format()? .decode() } @@ -1664,7 +1709,7 @@ pub fn load_from_memory(buffer: &[u8]) -> ImageResult { /// This is just a simple wrapper that constructs an `std::io::Cursor` around the buffer and then /// calls `load` with that reader. /// -/// Try [`ImageReader`] for more advanced uses. +/// Try [`ImageReaderOptions`] for more advanced uses. /// /// [`load`]: fn.load.html #[inline(always)] diff --git a/src/io.rs b/src/io.rs index 273e59b391..fa0aafdfd5 100644 --- a/src/io.rs +++ b/src/io.rs @@ -13,6 +13,13 @@ pub(crate) mod free_functions; pub(crate) mod image_reader_type; pub(crate) mod limits; +pub use decoder::{ + DecodedAnimationAttributes, DecodedImageAttributes, DecodedMetadataHint, FormatAttributes, + SequenceControl, +}; + +pub use image_reader_type::DecodedImageMetadata; + /// Adds `read_exact_vec` pub(crate) trait ReadExt { fn read_exact_vec(&mut self, vec: &mut Vec, len: usize) -> io::Result<()>; @@ -31,3 +38,115 @@ impl ReadExt for R { } } } + +/// Communicate the layout of an image. +/// +/// Describes a packed rectangular layout with given bit-depth in +/// [`ImageDecoder::peek_layout`](crate::ImageDecoder::peek_layout). Standard layouts from `image` +/// are row-major with no padding between rows and pixels packed by consecutive channels. +/// +/// For external crates constructing an instance, use [`ImageLayout::empty`] with the intended +/// color type and then fill in all applicable fields. (It will become more convenient when Rust +/// lets you use record update syntax but that won't work for now). +#[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 { + /// A layout matching a [`DynamicImage`][`crate::DynamicImage`] of the given dimensions and + /// color type. + pub fn new(w: u32, h: u32, color: crate::ColorType) -> ImageLayout { + #[allow(deprecated)] + ImageLayout { + width: w, + height: h, + ..ImageLayout::empty(color) + } + } + + /// 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, + } + } + + /// The total number of bytes in the described image. + /// + /// See also [`DecodedLayout::total_bytes`]. + 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) + } + + /// Checks if the provided dimensions would overflow a `u64`. + /// + /// FIXME: instead have `try_total_bytes() -> Result`? But should it return an + /// unsupported error (as formats that use this do) or should it instead return another error + /// since the method is then unrelated to a format. + pub(crate) fn total_bytes_overflows_u64(&self) -> bool { + let &ImageLayout { width, height, .. } = self; + let bytes_per_pixel: u8 = self.color.bytes_per_pixel(); + u64::from(width) * u64::from(height) > u64::MAX / u64::from(bytes_per_pixel) + } +} + +/// Describes the next image for +/// [`ImageDecoder::prepare_image`](`crate::ImageDecoder::prepare_image`). +/// +/// For external crates constructing an instance, use [`Self::new`] with the intended color type +/// and then fill in all applicable fields. This initializes the layout, a minimal descriptor of an +/// expected [`DynamicImage`][`crate::DynamicImage`] (or equivalently sized other buffer). +#[non_exhaustive] +pub struct DecoderPreparedImage { + /// The layout of the primary image data. + pub layout: ImageLayout, +} + +/// Defaults all fields except the layout. +impl From for DecoderPreparedImage { + fn from(layout: ImageLayout) -> Self { + DecoderPreparedImage { layout } + } +} + +impl DecoderPreparedImage { + /// A layout matching a [`DynamicImage`][`crate::DynamicImage`] of the given dimensions and + /// color type. + pub fn new(w: u32, h: u32, color: crate::ColorType) -> DecoderPreparedImage { + DecoderPreparedImage { + layout: ImageLayout::new(w, h, color), + } + } + + /// The total number of bytes in the decoded image. + /// + /// This is the size of the buffer that must be passed to + /// [`ImageDecoder::read_image`](`crate::ImageDecoder::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 the + /// size does not fit in a u64 then `u64::MAX` is returned. For all practical purposes all + /// platforms will fail to allocate that much memory. + pub fn total_bytes(&self) -> u64 { + self.layout.total_bytes() + } +} diff --git a/src/io/decoder.rs b/src/io/decoder.rs index d89c0b7862..d3f2b296c6 100644 --- a/src/io/decoder.rs +++ b/src/io/decoder.rs @@ -1,24 +1,127 @@ -use crate::animation::Frames; -use crate::color::{ColorType, ExtendedColorType}; use crate::error::ImageResult; +use crate::io::DecoderPreparedImage; use crate::metadata::{LoopCount, Orientation}; +use crate::Delay; -/// The trait that all decoders implement +/// The interface for `image` to utilize in reading image files. +/// +/// Please carefully consider consuming this interface directly and prefer interaction with an +/// [`ImageReader`]. This is one directional of a protocol between `image` and format decoders. In +/// the general case, an implementation can expect calls to be made in the following order: +/// +/// ```text,bnf +/// decoding sequence = configure, { decode image }, "finish", { metadata } +/// +/// decode image = +/// "prepare_image", { metadata | "prepare_image" }, "read_image" +/// +/// configure = "set_limits" +/// +/// metadata = "xmp_metadata" | "icc_profile" | "exif_metadata" | "iptc_metadata" +/// ``` +/// +/// Deviation from this order can be treated as an error. Future changes to the protocol may +/// introduce additional methods. Decoders will indicate support for sequent variants in their +/// [`ImageDecoder::format_attributes`]. +/// +/// 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 layout = self.prepare_image()?; + limits.check_layout_dimensions(&layout)?; + 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 hints 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 format_attributes(&self) -> FormatAttributes { + FormatAttributes::default() + } - /// Returns the color type of the image file before decoding - fn original_color_type(&self) -> ExtendedColorType { - self.color_type().into() + /// Retrieve animation attributes. + /// + /// You should check [`FormatAttributes::supports_animation`] before calling this method. A + /// value will only be available on animated images. Additionally, most file formats store the + /// metadata in the header which might not be read until after calling + /// [`ImageDecoder::prepare_image`]. + /// + /// The value here is expected to remain constant when it is present. + fn animation_attributes(&mut self) -> Option { + None } + /// Consume the header of the image, determining the (next) image's layout. + /// + /// This shall be called before a call to [`ImageDecoder::read_image`] to ensure that the + /// initial metadata has been read. The returned layout indicates the expected buffer of + /// [`ImageDecoder::read_image`]. The caller is responsible for passing a buffer of the + /// appropriate size. + /// + /// This method should be idempotent on success, calling it multiple times in a row should + /// produce equivalent results. The decoder must _not_ advance to another image descriptor when + /// it is called when it has already reached one. + /// + /// In contrast to a constructor it can be called multiple times, even after reconfiguring + /// limits and context which avoids resource issues for formats that buffer metadata. + fn prepare_image(&mut self) -> ImageResult; + + /// 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.prepare_image().total_bytes()`. + /// + /// # Examples + /// + /// ``` + /// # use image::ImageDecoder; + /// fn read_16bit_image(mut decoder: impl ImageDecoder) -> Vec { + /// let layout = decoder.prepare_image().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)`. + /// For formats that don't support embedded profiles this function should always return + /// `Ok(None)`. Decoders for formats with non-standard color profiles may create a synthetic + /// profile, see our [`bmp`](`crate::codecs::bmp`) module for an example. + /// + /// A decoder that encounters tags which contain a color profile whose encoding it does not + /// support should return [`UnsupportedError`](`crate::error::UnsupportedError`). This allows a + /// reader to continue while differentiating from missing metadata. fn icc_profile(&mut self) -> ImageResult>> { Ok(None) } @@ -27,6 +130,10 @@ pub trait ImageDecoder { /// A third-party crate such as [`kamadak-exif`](https://docs.rs/kamadak-exif/) is required to actually parse it. /// /// For formats that don't support embedded profiles this function should always return `Ok(None)`. + /// + /// A decoder that encounters tags which contain XMP metadata whose encoding it does not + /// support should return [`UnsupportedError`](crate::error::UnsupportedError). This allows a + /// reader to continue while differentiating from missing metadata. fn exif_metadata(&mut self) -> ImageResult>> { Ok(None) } @@ -35,6 +142,10 @@ pub trait ImageDecoder { /// A third-party crate such as [`roxmltree`](https://docs.rs/roxmltree/) is required to actually parse it. /// /// For formats that don't support embedded profiles this function should always return `Ok(None)`. + /// + /// A decoder that encounters tags which contain XMP metadata whose encoding it does not + /// support should return [`UnsupportedError`](crate::error::UnsupportedError). This allows a + /// reader to continue while differentiating from missing metadata. fn xmp_metadata(&mut self) -> ImageResult>> { Ok(None) } @@ -42,106 +153,155 @@ pub trait ImageDecoder { /// Returns the raw [IPTC](https://en.wikipedia.org/wiki/IPTC_Information_Interchange_Model) chunk, if it is present. /// /// For formats that don't support embedded profiles this function should always return `Ok(None)`. + /// + /// A decoder that encounters tags which contain XMP metadata whose encoding it does not + /// support should return [`UnsupportedError`](crate::error::UnsupportedError). This allows a + /// reader to continue while differentiating from missing metadata. fn iptc_metadata(&mut self) -> ImageResult>> { 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 FormatAttributes { + /// Could there be multiple images in this file that form an animation? + pub supports_animation: bool, + /// Could there be multiple images in this file, as an unrelated sequence? + pub supports_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. +/// Additional attributes of animated image sequences. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct DecodedAnimationAttributes { + /// Loop count of the animated image. + pub loop_count: LoopCount, +} + +impl Default for DecodedAnimationAttributes { + fn default() -> Self { + Self { + loop_count: LoopCount::Infinite, + } + } +} + +/// 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, + /// Is the underlying data converted into a different pixel format or color model? /// - /// **Note**: By default, _no_ limits are defined. This may be changed in future major version - /// increases. + /// This field is currently an optional hint for encoding. The authoritative source for the + /// memory size required for [`ImageDecoder::read_image`][`crate::ImageDecoder`] remains the + /// [`color`][`crate::ImageLayout::color`] field, you do not need to time travel this + /// information. + pub original_color_type: 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. +/// +/// # Design consideration +/// +/// Each variant describes a way to fetch accurate and complete metadata for an individual image in +/// a file. This offloads some responsibility to the decoder, streamed formats that may contain +/// parts after the file need to be peek forwards to aggregate all metadata and then seek +/// backwards. During design we had a sequence that relied on [`ImageDecoder::finish`] but that did +/// not allow the reader to ensure all data was present. This call would also be destructive with +/// regards to the other kind of metadata. +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +pub enum DecodedMetadataHint { + /// The decoder does not support this datum. It will return `None` but there is no guarantee + /// that the datum is truly absent in the file. /// - /// [`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(()) - } + /// This is the default. + #[default] + Unsupported, + /// Metadata is available in the header and will be valid after the first call to + /// [`ImageDecoder::prepare_image`] 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 format_attributes(&self) -> FormatAttributes { + (**self).format_attributes() } - fn color_type(&self) -> ColorType { - (**self).color_type() + fn prepare_image(&mut self) -> ImageResult { + (**self).prepare_image() } - fn original_color_type(&self) -> ExtendedColorType { - (**self).original_color_type() + fn animation_attributes(&mut self) -> Option { + (**self).animation_attributes() } fn icc_profile(&mut self) -> ImageResult>> { (**self).icc_profile() @@ -155,58 +315,45 @@ 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) } -} - -/// `AnimationDecoder` trait -pub trait AnimationDecoder<'a> { - /// Consume the decoder producing a series of frames. - fn into_frames(self) -> Frames<'a>; - /// Loop count of the animated image. - fn loop_count(&self) -> LoopCount; + fn more_images(&self) -> SequenceControl { + (**self).more_images() + } + fn finish(&mut self) -> ImageResult<()> { + (**self).finish() + } } #[cfg(test)] mod tests { - use super::{ColorType, ImageDecoder, ImageResult}; + use super::{DecodedImageAttributes, DecoderPreparedImage, ImageDecoder, ImageResult}; + use crate::ColorType; #[test] fn total_bytes_overflow() { struct D; + impl ImageDecoder for D { - fn color_type(&self) -> ColorType { - ColorType::Rgb8 + fn prepare_image(&mut self) -> ImageResult { + Ok(DecoderPreparedImage::new( + 0xffff_ffff, + 0xffff_ffff, + 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.prepare_image().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 650de27a89..2f7c3eb04e 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, @@ -17,9 +18,9 @@ use crate::{DynamicImage, ImageDecoder, ImageFormat}; /// Assumes the reader is already buffered. For optimal performance, /// consider wrapping the reader with a `BufReader::new()`. /// -/// Try [`ImageReader`] for more advanced uses. +/// Try [`ImageReaderOptions`] 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.prepare_image()?.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 fa70045ed3..f2667c84f3 100644 --- a/src/io/image_reader_type.rs +++ b/src/io/image_reader_type.rs @@ -3,9 +3,15 @@ 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, ParameterError, ParameterErrorKind, UnsupportedError, + UnsupportedErrorKind, +}; use crate::io::limits::Limits; +use crate::io::{DecodedAnimationAttributes, DecodedImageAttributes, DecoderPreparedImage}; +use crate::io::{DecodedMetadataHint, SequenceControl}; +use crate::metadata::Orientation; +use crate::{hooks, Delay, Frame, Frames}; use crate::{DynamicImage, ImageDecoder, ImageError, ImageFormat}; use super::free_functions; @@ -27,10 +33,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 /// @@ -39,9 +46,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(()) } /// ``` @@ -52,7 +59,7 @@ enum Format { /// /// ``` /// # use image::ImageError; -/// # use image::ImageReader; +/// # use image::ImageReaderOptions; /// # fn main() -> Result<(), ImageError> { /// use std::io::Cursor; /// use image::ImageFormat; @@ -61,7 +68,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)); @@ -75,8 +82,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. @@ -87,7 +93,7 @@ pub struct ImageReader { spec_compliance: SpecCompliance, } -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, @@ -99,7 +105,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(), @@ -112,7 +118,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(), @@ -170,7 +176,6 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader { fn make_decoder( format: Format, reader: R, - limits_for_png: Limits, spec_compliance: SpecCompliance, ) -> ImageResult> { #[allow(unused)] @@ -195,7 +200,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")] @@ -237,18 +242,23 @@ 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(), - self.spec_compliance, - )?; + let mut decoder = + Self::make_decoder(self.require_format()?, self.inner, self.spec_compliance)?; 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, self.spec_compliance)?; + 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 @@ -264,15 +274,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(()) } @@ -314,7 +324,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.prepare_image()?; + Ok(layout.layout.dimensions()) } /// Read the image (replaces `load`). @@ -322,19 +334,9 @@ 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(), self.spec_compliance)?; - - // 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 { + let (image, _meta) = self.into_reader()?.decode()?; + Ok(image) } fn require_format(&mut self) -> ImageResult { @@ -347,7 +349,105 @@ 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, + /// The metadata of formats is stored in varying places. + metadata_buffers: MetadataBuffers, +} + +#[derive(Default)] +struct MetadataBuffers { + exif: MetadataBlock, + icc: MetadataBlock, + xmp: MetadataBlock, + iptc: MetadataBlock, + first_meta_retrieved: bool, +} + +/// Buffer state for one item of metadata, to surface the error at the right time. +#[derive(Default)] +enum MetadataBlock { + /// No buffered metadata. + #[default] + None, + Ok(Vec), + /// There was an error acquiring the metadata, this is the original error. + Err(ImageError), + /// The error was already polled. We continue to error but now with a replacement. + ErrorTaken, + /// The error was an `Unsupported`, similar to `ErrorTaken` but also return as + /// `ImageError::Unsupported` with the original format hint. + Unsupported(ImageFormatHint), +} + +impl MetadataBlock { + fn is_not_none(&self) -> bool { + !matches!(self, MetadataBlock::None) + } + + fn get(&mut self) -> ImageResult>> { + match self { + MetadataBlock::None => Ok(None), + MetadataBlock::Ok(data) => Ok(Some(data.clone())), + MetadataBlock::Err(err) => { + // Doing a little dance to change the variant to ErrorTaken in-place. + let replacement_err = ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + )); + + let err = core::mem::replace(err, replacement_err); + + *self = if let ImageError::Unsupported(e) = &err { + MetadataBlock::Unsupported(e.format_hint()) + } else { + MetadataBlock::ErrorTaken + }; + + Err(err) + } + MetadataBlock::ErrorTaken => Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + ))), + MetadataBlock::Unsupported(hint) => Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + hint.clone(), + UnsupportedErrorKind::GenericFeature("metadata".to_string()), + ), + )), + } + } +} + +#[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. @@ -369,7 +469,7 @@ 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(), @@ -377,3 +477,649 @@ impl ImageReader> { }) } } + +impl ImageReader<'_> { + /// Query the layout that the image will have. + pub fn peek_layout(&mut self) -> ImageResult { + self.inner.prepare_image() + } + + /// Decode the next image into a `DynamicImage`. + /// + /// # Examples + /// + #[cfg_attr(feature = "png", doc = "```")] + #[cfg_attr(not(feature = "png"), doc = "```no_run")] + /// use image::ImageReader; + /// + /// let mut reader = ImageReader::open("tests/images/png/iptc.png")?; + /// let (data, mut meta) = reader.decode()?; + /// + /// // This image has IPTC metadata attached to it. + /// let iptc = meta.iptc_metadata()?; + /// assert!(iptc.is_some()); + /// + /// # Ok::<_, image::error::ImageError>(()) + /// ``` + /// + /// # Related + /// + /// If you want to enable buffer reuse, consider using [`Self::decode_into`] will can use an + /// existing buffer in some instances. + pub fn decode(&mut self) -> ImageResult<(DynamicImage, DecodedImageMetadata<'_>)> { + let mut empty = DynamicImage::default(); + let meta = self.decode_to_dynimage(&mut empty)?; + Ok((empty, meta)) + } + + /// Decode an image into a provided buffer and retrieve metadata. + /// + /// # Examples + /// + /// ```ignore + /// // Enable if exposed. + /// use image::{DynamicImage, ImageReader}; + /// use glob::glob; + /// + /// let mut buffer = DynamicImage::default(); + /// + /// for entry in glob("tests/images/**/*.png").unwrap() { + /// let Ok(path) = entry else { + /// continue; + /// }; + /// + /// let mut reader = ImageReader::open(path)?; + /// let mut meta = reader.decode_to_dynimage(&mut buffer)?; + /// // … + /// # break; // Avoid actually looping in the test here. + /// } + /// + /// # Ok::<_, image::error::ImageError>(()) + /// ``` + pub(crate) fn decode_to_dynimage( + &mut self, + image: &mut DynamicImage, + ) -> ImageResult> { + let layout = self.inner.prepare_image()?; + self.fill_header_metadata_if_any(); + + // This is technically redundant but it's also cheap. + self.limits.check_layout_dimensions(&layout)?; + // 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())?; + + // Retrieve the raw image data as indicated by the layout. + self.last_attributes = image.decode_raw(self.inner.as_mut(), layout)?; + + let mut meta = DecodedImageMetadata { + inner: self.inner.as_mut(), + attributes: &mut self.last_attributes, + metadata_buffers: &mut self.metadata_buffers, + }; + + meta.apply_metdata(&self.settings, image)?; + + Ok(meta) + } + + /// Decode the next image into a pre-allocated buffer. + /// + /// Note that this will produce raw image data. You'll be on your own to ensure that metadata + /// such as the orientation of the image or color space transformations is accurately + /// represented. + /// + /// # Examples + /// + #[cfg_attr(feature = "png", doc = "```")] + #[cfg_attr(not(feature = "png"), doc = "```no_run")] + /// use image::ImageReader; + /// + /// let mut reader = ImageReader::open("tests/images/png/iptc.png")?; + /// let buf_size = reader.peek_layout()?.total_bytes(); + /// let mut buffer = vec![0; buf_size as usize]; + /// + /// let mut meta = reader.decode_into(&mut buffer)?; + /// // This image also has IPTC metadata attached to it. + /// let iptc = meta.iptc_metadata()?; + /// assert!(iptc.is_some()); + /// + /// # Ok::<_, image::error::ImageError>(()) + /// ``` + pub fn decode_into(&mut self, buffer: &mut [u8]) -> ImageResult> { + let layout = self.inner.prepare_image()?; + self.fill_header_metadata_if_any(); + + let actual = buffer.len(); + + if usize::try_from(layout.total_bytes()).ok() != Some(actual) { + return Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::BufferSizeMismatch, + ))); + } + + self.limits.check_layout_dimensions(&layout)?; + + self.last_attributes = self.inner.read_image(buffer)?; + + Ok(DecodedImageMetadata { + inner: self.inner.as_mut(), + attributes: &mut self.last_attributes, + metadata_buffers: &mut self.metadata_buffers, + }) + } + + /// 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_image(&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.prepare_image()?; + self.fill_header_metadata_if_any(); + + // This is technically redundant but it's also cheap. + self.limits.check_layout_dimensions(&layout)?; + 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::default(); + self.limits.reserve(bytes)?; + placeholder.decode_raw(self.inner.as_mut(), layout)?; + self.limits.free(bytes); + } + + Ok(()) + } + + /// Get the animation attributes of the file if any. + pub fn animation_attributes(&mut self) -> Option { + let _ = self.inner.prepare_image(); + self.inner.animation_attributes() + } + + /// Must be called after `peek_layout`. + /// + /// Polls the underlying decoder for any `InHeader` metadata that is constant across a file, + /// applicable to all images, and appears early. + fn fill_header_metadata_if_any(&mut self) { + type MetadataFn<'a> = + fn(&mut (dyn ImageDecoder + 'a)) -> Result>, ImageError>; + + // We retrieve `InHeader` metadata only once, before reading any our images. + let first_meta_retrieved = self.metadata_buffers.first_meta_retrieved; + let format_attrs = self.inner.format_attributes(); + + let attributes = [ + ( + format_attrs.exif, + &mut self.metadata_buffers.exif, + ::exif_metadata as MetadataFn, + ), + ( + format_attrs.icc, + &mut self.metadata_buffers.icc, + ::icc_profile as MetadataFn, + ), + ( + format_attrs.xmp, + &mut self.metadata_buffers.xmp, + ::xmp_metadata as MetadataFn, + ), + ( + format_attrs.iptc, + &mut self.metadata_buffers.iptc, + ::iptc_metadata as MetadataFn, + ), + ]; + + for (hint, buffer, getter) in attributes { + let should_buffer_now = match hint { + DecodedMetadataHint::InHeader => !first_meta_retrieved, + DecodedMetadataHint::PerImage => true, + DecodedMetadataHint::Unsupported | DecodedMetadataHint::None => false, + }; + + if !should_buffer_now { + continue; + } + + // We might have already tried this and succeeded. Expect the same result as last time + // but avoids the allocation associated with that. A repeated `None` should be cheap, + // most probably. This holds for variants that retrieve it once. + if matches!(hint, DecodedMetadataHint::InHeader) && buffer.is_not_none() { + continue; + } + + match getter(self.inner.as_mut()) { + Ok(None) => *buffer = MetadataBlock::None, + Ok(Some(metadata)) => *buffer = MetadataBlock::Ok(metadata), + Err(err) => { + *buffer = MetadataBlock::Err(err); + } + } + } + + // Note: on error we do not set this flag. You can try again. + self.metadata_buffers.first_meta_retrieved = true; + } +} + +impl<'stream> ImageReader<'stream> { + /// Open image data as a readable stream of image(s). + /// + /// 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. + /// + /// # Examples + /// + #[cfg_attr(feature = "png", doc = "```")] + #[cfg_attr(not(feature = "png"), doc = "```no_run")] + /// use std::io::{BufReader, Cursor}; + /// use image::ImageReader; + /// + /// let binary_data: Vec = /* */ + /// std::fs::read("tests/images/png/transparency/acid2.png")?; + /// let stream = BufReader::new(Cursor::new(binary_data)); + /// + /// let mut reader = ImageReader::new(stream)?; + /// let (data, meta) = reader.decode()?; + /// + /// # Ok::<_, image::error::ImageError>(()) + /// ``` + /// + /// # Related + /// + /// Use [`ImageReaderOptions`] to configure the reader in detail before use. In the simple case + /// where you only access a single image without looking at its metadata you may call + /// [`ImageReaderOptions::decode`] directly without creating an [`ImageReader`]. + 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. + /// + #[cfg_attr(feature = "png", doc = "```")] + #[cfg_attr(not(feature = "png"), doc = "```no_run")] + /// use image::ImageReader; + /// + /// let mut reader = ImageReader::open("tests/images/png/transparency/acid2.png")?; + /// let (data, meta) = reader.decode()?; + /// + /// # Ok::<_, image::error::ImageError>(()) + /// ``` + /// + /// # Related + /// + /// Use [`ImageReaderOptions`] to configure the reader in detail before use. In the simple case + /// where you only access a single image without looking at its metadata you may call + /// [`ImageReaderOptions::decode`] directly without creating an [`ImageReader`]. + 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(), + metadata_buffers: MetadataBuffers::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`](`crate::error::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 mut frame = DynamicImage::default(); + let frame_decoded = match self.decode_to_dynimage(&mut frame) { + Ok(frame) => frame, + Err(ref err) if is_end_reached(err) => return None, + Err(err) => return Some(Err(err)), + }; + + let x = frame_decoded.attributes().x; + let y = frame_decoded.attributes().y; + let delay = frame_decoded.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)) + } +} + +/// Result of [`ImageReader::decode_into`] that provides access to metadata. +pub struct DecodedImageMetadata<'reader> { + inner: &'reader mut (dyn ImageDecoder + 'reader), + attributes: &'reader mut DecodedImageAttributes, + metadata_buffers: &'reader mut MetadataBuffers, +} + +impl<'lt> DecodedImageMetadata<'lt> { + /// Get the EXIF metadata of the previous image if any. + pub fn exif_metadata(&mut self) -> ImageResult>> { + Self::access_block_with( + &mut self.metadata_buffers.exif, + self.inner.format_attributes().exif, + self.inner, + ::exif_metadata, + ) + } + + /// Get the ICC profile of the previous image if any. + pub fn icc_profile(&mut self) -> ImageResult>> { + Self::access_block_with( + &mut self.metadata_buffers.icc, + self.inner.format_attributes().icc, + self.inner, + ::icc_profile, + ) + } + + /// Get the XMP metadata of the previous image if any. + pub fn xmp_metadata(&mut self) -> ImageResult>> { + Self::access_block_with( + &mut self.metadata_buffers.xmp, + self.inner.format_attributes().xmp, + self.inner, + ::xmp_metadata, + ) + } + + /// Get the IPTC metadata of the previous image if any. + pub fn iptc_metadata(&mut self) -> ImageResult>> { + Self::access_block_with( + &mut self.metadata_buffers.iptc, + self.inner.format_attributes().iptc, + self.inner, + ::iptc_metadata, + ) + } + + fn apply_metdata( + &mut self, + settings: &ImageReaderSettings, + image: &mut DynamicImage, + ) -> Result<(), ImageError> { + // Run all the metadata extraction which we may need. + let icc = self.icc_profile()?; + let exif = self.exif_metadata()?; + + // 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, + ); + } + } + } + + let mut orientation = self.attributes.orientation; + + // Determine which orientation to use. + if orientation.is_none() { + orientation = exif.and_then(|chunk| Orientation::from_exif_chunk(&chunk)); + } + + self.attributes.orientation = orientation; + + if settings.apply_orientation { + if let Some(orient) = orientation { + image.apply_orientation(orient); + } + } + + Ok(()) + } + + fn access_block_with<'l>( + block: &mut MetadataBlock, + meta: DecodedMetadataHint, + decoder: &mut (dyn ImageDecoder + 'l), + access: fn(&'_ mut (dyn ImageDecoder + 'l)) -> ImageResult>>, + ) -> ImageResult>> { + match meta { + DecodedMetadataHint::InHeader => { + if matches!(block, MetadataBlock::ErrorTaken) { + // We can retry this, prefer rechecking from the source. + access(decoder) + } else { + block.get() + } + } + DecodedMetadataHint::PerImage => { + // This error is sensitive to the timing (after read_image it may or may not refer + // to the next image until we access the decoder with `peek_layout` again. We don't + // want to guess, any call after the first may substitute the error. + block.get() + } + DecodedMetadataHint::Unsupported | DecodedMetadataHint::None => Ok(None), + } + } + + /// Get auxiliary attributes of the previous image. + pub fn attributes(&self) -> &DecodedImageAttributes { + self.attributes + } +} + +#[cfg(test)] +mod tests { + use crate::{error::DecodingError, io::FormatAttributes}; + + use super::*; + + struct InjectedReader { + attr: FormatAttributes, + xmp_metadata: InjectedXmp, + xmp_per_image: Vec, + image: usize, + } + + #[derive(Clone, Debug, Default)] + enum InjectedXmp { + #[default] + None, + Data(Vec), + Unsupported, + DecodeErr, + } + + impl ImageDecoder for InjectedReader { + fn prepare_image(&mut self) -> ImageResult { + self.xmp_metadata = self + .xmp_per_image + .get(self.image) + .cloned() + .unwrap_or_default(); + Ok(DecoderPreparedImage::new(0, 0, crate::ColorType::Rgba8)) + } + + fn format_attributes(&self) -> FormatAttributes { + self.attr.clone() + } + + fn xmp_metadata(&mut self) -> ImageResult>> { + match self.xmp_metadata { + InjectedXmp::None => Ok(None), + InjectedXmp::Data(ref data) => Ok(Some(data.clone())), + InjectedXmp::DecodeErr => Err(ImageError::Decoding(DecodingError::new( + ImageFormatHint::Unknown, + "simulating that XMP metadata could not be decoded".to_string(), + ))), + InjectedXmp::Unsupported => Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormatHint::Unknown, + UnsupportedErrorKind::GenericFeature("".into()), + ), + )), + } + } + + fn read_image(&mut self, _: &mut [u8]) -> ImageResult { + if matches!(self.attr.xmp, DecodedMetadataHint::PerImage) { + self.xmp_metadata = InjectedXmp::None; + } + + self.image += 1; + Ok(Default::default()) + } + + fn more_images(&self) -> SequenceControl { + if Some(self.image) < self.xmp_per_image.len().checked_sub(1) { + SequenceControl::MaybeMore + } else { + SequenceControl::None + } + } + } + + #[test] + fn in_header_data_applies() -> Result<(), ImageError> { + const DATA: &[u8] = b"

FAKE"; + + let mut reader = ImageReader::from_decoder(Box::new(InjectedReader { + attr: FormatAttributes { + xmp: DecodedMetadataHint::InHeader, + ..FormatAttributes::default() + }, + xmp_metadata: InjectedXmp::default(), + xmp_per_image: vec![InjectedXmp::Data(DATA.to_vec()), InjectedXmp::None], + image: 0, + })); + + let (_, mut meta) = reader.decode()?; + assert_eq!(meta.xmp_metadata()?, Some(DATA.to_vec())); + + // In-Header applies to all images. + let (_, mut meta) = reader.decode()?; + assert_eq!(meta.xmp_metadata()?, Some(DATA.to_vec())); + + Ok(()) + } + + #[test] + fn error_stays_error() -> Result<(), ImageError> { + let mut reader = ImageReader::from_decoder(Box::new(InjectedReader { + attr: FormatAttributes { + xmp: DecodedMetadataHint::InHeader, + ..FormatAttributes::default() + }, + xmp_metadata: InjectedXmp::default(), + xmp_per_image: vec![InjectedXmp::DecodeErr], + image: 0, + })); + + let (_, mut meta) = reader.decode()?; + assert!(matches!(meta.xmp_metadata(), Err(ImageError::Decoding(_)))); + + // In-header should retry the meta data acquisition. + assert!(matches!(meta.xmp_metadata(), Err(ImageError::Decoding(_)))); + + Ok(()) + } + + #[test] + fn unsupported_error_repeats() -> Result<(), ImageError> { + let mut reader = ImageReader::from_decoder(Box::new(InjectedReader { + attr: FormatAttributes { + xmp: DecodedMetadataHint::PerImage, + ..FormatAttributes::default() + }, + xmp_metadata: InjectedXmp::default(), + xmp_per_image: vec![InjectedXmp::Unsupported], + image: 0, + })); + + let (_, mut meta) = reader.decode()?; + assert!(matches!( + meta.xmp_metadata(), + Err(ImageError::Unsupported(_)) + )); + + assert!(matches!( + meta.xmp_metadata(), + Err(ImageError::Unsupported(_)) + )); + + Ok(()) + } +} diff --git a/src/io/limits.rs b/src/io/limits.rs index 6b81611144..b7fd11639d 100644 --- a/src/io/limits.rs +++ b/src/io/limits.rs @@ -72,6 +72,13 @@ impl Limits { Ok(()) } + pub(crate) fn check_layout_dimensions( + &self, + layout: &crate::io::DecoderPreparedImage, + ) -> ImageResult<()> { + self.check_dimensions(layout.layout.width, layout.layout.height) + } + /// This function checks the `max_image_width` and `max_image_height` limits given /// the image width and height. pub fn check_dimensions(&self, width: u32, height: u32) -> ImageResult<()> { diff --git a/src/lib.rs b/src/lib.rs index 7c3a65625c..71c9c03026 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_etc = decoder.prepare_image()?; +//! //! 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}, + decoder::ImageDecoder, encoder::ImageEncoder, format::ImageFormat, - image_reader_type::{ImageReader, SpecCompliance}, + image_reader_type::{ImageReader, ImageReaderOptions, SpecCompliance}, limits::{LimitSupport, Limits}, + ImageLayout, }; pub use crate::images::dynimage::DynamicImage; diff --git a/src/metadata.rs b/src/metadata.rs index eb099386d3..121ce6e43e 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}, @@ -13,6 +14,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). /// @@ -75,21 +85,24 @@ impl Orientation { /// Extracts the image orientation from a raw Exif chunk. /// /// You can obtain the Exif chunk using - /// [ImageDecoder::exif_metadata](crate::ImageDecoder::exif_metadata). + /// [`DecodedImageAttributes::exif_metadata`](crate::io::DecodedImageMetadata::exif_metadata). + /// With a decoder, [ImageDecoder::exif_metadata](crate::ImageDecoder::exif_metadata) can be + /// used to fetch the metadata in some states as indicated by + /// [`DecodedImageMetadata::icc_profile`](crate::io::DecodedImageMetadata::icc_profile). /// - /// It is more convenient to use [ImageDecoder::orientation](crate::ImageDecoder::orientation) - /// than to invoke this function. - /// Only use this function if you extract and process the Exif chunk separately. + /// You usually only use this function if you extract and process more Exif chunk separately. #[must_use] pub fn from_exif_chunk(chunk: &[u8]) -> Option { Self::from_exif_chunk_inner(chunk).map(|res| res.0) } - /// Extracts the image orientation from a raw Exif chunk and sets the orientation in the Exif chunk to `Orientation::NoTransforms`. - /// This is useful if you want to apply the orientation yourself, and then encode the image with the rest of the Exif chunk intact. + /// Extracts the image orientation from a raw Exif chunk and sets the orientation in the Exif + /// chunk to [`Orientation::NoTransforms`]. This is useful if you want to apply the orientation + /// yourself, and then encode the image with the rest of the Exif chunk intact. /// - /// If the orientation data is not cleared from the Exif chunk after you apply the orientation data yourself, - /// the image will end up being rotated once again by any software that correctly handles Exif, leading to an incorrect result. + /// If the orientation data is not cleared from the Exif chunk after you apply the orientation + /// data yourself, the image will end up being rotated once again by any software that + /// correctly handles Exif, leading to an incorrect result. /// /// If the Exif value is present but invalid, `None` is returned and the Exif chunk is not modified. #[must_use] @@ -162,7 +175,7 @@ enum ExifEndian { } /// The number of times animated image should loop over. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum LoopCount { /// Loop the image Infinitely Infinite, diff --git a/src/metadata/cicp.rs b/src/metadata/cicp.rs index 05749cf5aa..e93f061005 100644 --- a/src/metadata/cicp.rs +++ b/src/metadata/cicp.rs @@ -109,25 +109,6 @@ impl CicpColorPrimaries { CicpColorPrimaries::Unspecified => false, } } - - 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 @@ -218,30 +199,6 @@ impl CicpTransferCharacteristics { | CicpTransferCharacteristics::Smpte2084 => false, } } - - 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, - } - } } /// @@ -299,32 +256,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)] @@ -358,23 +289,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 { @@ -397,73 +328,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) - .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) - .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) - .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. @@ -498,228 +363,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; @@ -941,7 +584,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), @@ -1496,52 +1140,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. @@ -1665,28 +1263,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..e78e3eb12b --- /dev/null +++ b/src/metadata/moxcms.rs @@ -0,0 +1,517 @@ +/// 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) + .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( + 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) + .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) + .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/src/utils/mod.rs b/src/utils/mod.rs index dd9d7d1722..bdfdca90ac 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -81,13 +81,6 @@ fn interleave_planes_inner( } } -/// Checks if the provided dimensions would cause an overflow. -#[allow(dead_code)] -// When no image formats that use it are enabled -pub(crate) fn check_dimension_overflow(width: u32, height: u32, bytes_per_pixel: u8) -> bool { - u64::from(width) * u64::from(height) > u64::MAX / u64::from(bytes_per_pixel) -} - #[allow(dead_code)] // When no image formats that use it are enabled pub(crate) fn vec_copy_to_u8(vec: &[T]) -> Vec diff --git a/tests/conversions.rs b/tests/conversions.rs index f1efd785b0..ae1e735a5a 100644 --- a/tests/conversions.rs +++ b/tests/conversions.rs @@ -5,10 +5,8 @@ use std::fs; use std::path::PathBuf; use std::str::FromStr; -use image::ImageDecoder; - #[cfg(feature = "tiff")] -use image::codecs::tiff::TiffDecoder; +use image::{codecs::tiff::TiffDecoder, ImageReader}; #[test] fn test_rgbu8_to_rgbu16() { @@ -50,20 +48,16 @@ fn test_decode_8bit_jpeg_ycbcr() -> Result<(), image::ImageError> { let data = fs::read(img_path)?; let tiff_decoder = TiffDecoder::new(std::io::Cursor::new(data))?; + let mut reader = ImageReader::from_decoder(Box::new(tiff_decoder)); - let (w, h) = tiff_decoder.dimensions(); - let original_type = tiff_decoder.original_color_type(); - let target_type = tiff_decoder.color_type(); - let total_bytes = tiff_decoder.total_bytes() as usize; - - assert_eq!(original_type, image::ExtendedColorType::YCbCr8); - assert_eq!(target_type, image::ColorType::Rgb8); + let layout = reader.peek_layout()?; + assert_eq!(layout.layout.color, image::ColorType::Rgb8); - let mut buffer = vec![0u8; total_bytes]; - tiff_decoder.read_image(&mut buffer)?; + let (image, meta) = reader.decode()?; + let original_type = meta.attributes().original_color_type; - assert_eq!(buffer.len(), (w * h * 3) as usize); - assert!(buffer.iter().any(|&x| x != 0)); + assert_eq!(original_type, Some(image::ExtendedColorType::YCbCr8)); + assert!(image.as_bytes().iter().any(|&x| x != 0)); Ok(()) } @@ -76,20 +70,16 @@ fn test_decode_8bit_ycbcr_lzw_bt709() -> Result<(), image::ImageError> { let data = fs::read(img_path)?; let tiff_decoder = TiffDecoder::new(std::io::Cursor::new(data))?; + let mut reader = ImageReader::from_decoder(Box::new(tiff_decoder)); - let (w, h) = tiff_decoder.dimensions(); + let layout = reader.peek_layout()?; + assert_eq!(layout.layout.color, image::ColorType::Rgb8); - assert_eq!( - tiff_decoder.original_color_type(), - image::ExtendedColorType::YCbCr8 - ); - assert_eq!(tiff_decoder.color_type(), image::ColorType::Rgb8); + let (image, meta) = reader.decode()?; + let original_type = meta.attributes().original_color_type; - let mut buffer = vec![0u8; tiff_decoder.total_bytes() as usize]; - tiff_decoder.read_image(&mut buffer)?; - - assert_eq!(buffer.len(), (w * h * 3) as usize); - assert!(buffer.iter().any(|&x| x != 0)); + assert_eq!(original_type, Some(image::ExtendedColorType::YCbCr8)); + assert!(image.as_bytes().iter().any(|&x| x != 0)); Ok(()) } @@ -100,7 +90,11 @@ fn test_decode_8bit_ycbcr_lzw_invalid_coefficients() { let img_path = PathBuf::from("tests/images/tiff/testsuite/ycbcr_lzw_broken.tif"); let data = fs::read(img_path).expect("Test image missing"); - let result = TiffDecoder::new(std::io::Cursor::new(data)); + let result = TiffDecoder::new(std::io::Cursor::new(data)).and_then(|decoder| { + let mut reader = ImageReader::from_decoder(Box::new(decoder)); + reader.peek_layout() + }); + assert!(result.is_err()); } @@ -112,11 +106,13 @@ fn test_decode_8bit_cmyk() -> Result<(), image::ImageError> { let data = fs::read(img_path).expect("Test image missing"); let tiff_decoder = TiffDecoder::new(std::io::Cursor::new(data))?; + let mut reader = ImageReader::from_decoder(Box::new(tiff_decoder)); - assert_eq!(tiff_decoder.color_type(), image::ColorType::Rgb8); + let img = reader.peek_layout()?; + assert_eq!(img.layout.color, image::ColorType::Rgb8); - let mut buffer = vec![0u8; tiff_decoder.total_bytes() as usize]; - tiff_decoder.read_image(&mut buffer)?; + let mut buffer = vec![0u8; img.total_bytes() as usize]; + reader.decode_into(&mut buffer)?; assert_eq!(buffer, vec![190, 190, 190]); Ok(()) @@ -130,11 +126,13 @@ fn test_decode_8bit_cmyk_truncation() -> Result<(), image::ImageError> { let data = fs::read(img_path).expect("Test image missing"); let tiff_decoder = TiffDecoder::new(std::io::Cursor::new(data))?; + let mut reader = ImageReader::from_decoder(Box::new(tiff_decoder)); - assert_eq!(tiff_decoder.color_type(), image::ColorType::Rgb8); + let img = reader.peek_layout()?; + assert_eq!(img.layout.color, image::ColorType::Rgb8); - let mut buffer = vec![0u8; tiff_decoder.total_bytes() as usize]; - tiff_decoder.read_image(&mut buffer)?; + let mut buffer = vec![0u8; img.total_bytes() as usize]; + reader.decode_into(&mut buffer)?; assert_eq!(buffer, vec![126, 126, 126]); Ok(()) diff --git a/tests/limits.rs b/tests/limits.rs index 29bff403d1..4c2833e135 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,19 +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 - 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] @@ -108,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.prepare_image().is_err()); } #[test] @@ -174,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 @@ -183,6 +166,7 @@ fn tiff() { // TiffDecoder let mut decoder = TiffDecoder::new(Cursor::new(&image)).unwrap(); + decoder.prepare_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. } 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 21c623033f..c4b7fcb989 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -25,12 +25,22 @@ fn test_read_xmp_png() -> Result<(), image::ImageError> { const EXPECTED_PNG_METADATA: &str = "\n\n\n\n \n \n \n sunset, mountains, nature\n \n \n \n\n\n"; 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 metadata = png_decoder.xmp_metadata()?; - assert!(metadata.is_some()); - assert_eq!(EXPECTED_PNG_METADATA.as_bytes(), metadata.unwrap()); + + { + let mut png_decoder = PngDecoder::new(std::io::Cursor::new(&data[..])); + png_decoder.prepare_image()?; + let metadata = png_decoder.xmp_metadata()?; + assert!(metadata.is_some()); + assert_eq!(EXPECTED_PNG_METADATA.as_bytes(), metadata.unwrap()); + } + + { + let png_decoder = PngDecoder::new(std::io::Cursor::new(&data[..])); + let mut reader = image::ImageReader::from_decoder(Box::new(png_decoder)); + let (_, mut meta) = reader.decode()?; + assert!(meta.xmp_metadata()?.as_deref() == Some(EXPECTED_PNG_METADATA.as_bytes())); + } Ok(()) } @@ -41,8 +51,8 @@ fn test_read_xmp_webp() -> Result<(), image::ImageError> { const XMP_WEBP_PATH: &str = "tests/images/webp/lossless_images/simple_xmp.webp"; const EXPECTED_METADATA: &str = "\n\n\n\n \n \n \n sunset, mountains, nature\n \n \n \n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n"; let img_path = PathBuf::from_str(XMP_WEBP_PATH).unwrap(); - let data = fs::read(img_path)?; + let mut webp_decoder = WebPDecoder::new(std::io::Cursor::new(data))?; let metadata = webp_decoder.xmp_metadata()?; @@ -110,7 +120,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.prepare_image()?; 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 2888dd11e9..1188f0512c 100644 --- a/tests/reference_images.rs +++ b/tests/reference_images.rs @@ -10,8 +10,8 @@ use std::io::{self, BufWriter}; use std::path::Path; use std::str::FromStr; -use image::ColorType; -use image::{DynamicImage, ImageFormat}; +use image::{ColorType, DynamicImage, ImageFormat}; + use libtest_mimic::{Arguments, Failed, Trial}; use walkdir::WalkDir; @@ -98,9 +98,9 @@ fn main() -> std::process::ExitCode { #[cfg(feature = "gif")] 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 = image::codecs::gif::GifDecoder::new(stream)?; + let decoder = image::ImageReader::from_decoder(Box::new(decoder)); let mut frames = decoder.into_frames().collect_frames()?; // Select a single frame @@ -113,9 +113,9 @@ fn main() -> std::process::ExitCode { #[cfg(feature = "png")] 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 = image::codecs::png::PngDecoder::new(stream)?.apng()?; + let decoder = image::codecs::png::PngDecoder::new(stream).apng()?; + let decoder = image::ImageReader::from_decoder(Box::new(decoder)); let mut frames = decoder.into_frames().collect_frames()?; // Select a single frame diff --git a/tests/regression.rs b/tests/regression.rs index c57415195e..bd3c713a62 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -2,9 +2,6 @@ use std::fs::{self, File}; use std::io::{BufReader, Cursor}; use std::path::PathBuf; -#[cfg(feature = "webp")] -use image::{codecs::webp::WebPDecoder, AnimationDecoder}; - const BASE_PATH: [&str; 2] = [".", "tests"]; const IMAGE_DIR: &str = "images"; const REGRESSION_DIR: &str = "regression"; @@ -47,6 +44,8 @@ fn check_regressions() { #[test] #[cfg(feature = "webp")] fn check_webp_frames_regressions() { + use image::{codecs::webp::WebPDecoder, ImageReader}; + let path: PathBuf = BASE_PATH .iter() .collect::() @@ -60,11 +59,13 @@ fn check_webp_frames_regressions() { let frame_count = image_webp::WebPDecoder::new(cursor.clone()) .unwrap() .num_frames() as usize; + let decoder = WebPDecoder::new(cursor).unwrap(); + let reader = ImageReader::from_decoder(Box::new(decoder)); // The `take` guards against a potentially infinitely running iterator. // Since we take `frame_count + 1`, we can assume that the last iteration already returns `None`. // We then check that each frame has been decoded successfully. - let decoded_frames_count = decoder + let decoded_frames_count = reader .into_frames() .take(frame_count + 1) .enumerate() @@ -157,11 +158,12 @@ fn resizing_with_alpha() { use image::GenericImageView as _; let base: PathBuf = BASE_PATH.iter().collect(); - let image = - image::ImageReader::open(base.join("regression/image/resize-with-alpha-original.png")) - .unwrap() - .decode() - .unwrap(); + let image = image::ImageReaderOptions::open( + base.join("regression/image/resize-with-alpha-original.png"), + ) + .unwrap() + .decode() + .unwrap(); let (w, h) = image.dimensions(); let mut resizable = image.clone(); @@ -190,13 +192,14 @@ fn resizing_with_catmul() { use image::GenericImageView as _; let base: PathBuf = BASE_PATH.iter().collect(); - let image = - image::ImageReader::open(base.join("regression/image/resize-with-alpha-original.png")) - .unwrap() - .decode() - .unwrap(); + let image = image::ImageReaderOptions::open( + base.join("regression/image/resize-with-alpha-original.png"), + ) + .unwrap() + .decode() + .unwrap(); - let expected = image::ImageReader::open( + let expected = image::ImageReaderOptions::open( base.join("regression/image/resize-with-alpha-original-half-size.png"), ) .unwrap() @@ -213,17 +216,17 @@ fn resizing_with_catmul() { #[test] #[cfg(feature = "gif")] fn gif_regressions() { - use image::codecs::gif::GifDecoder; - use image::AnimationDecoder as _; + use image::{codecs::gif::GifDecoder, ImageReader}; let base: PathBuf = BASE_PATH.iter().collect(); let path = base.join("regression/gif/zero-loop-count.gif"); let file = BufReader::new(File::open(path).unwrap()); let decoder = GifDecoder::new(file).expect("Failed to create GIF decoder for regression test"); + let mut reader = ImageReader::from_decoder(Box::new(decoder)); - let _ = decoder.loop_count(); - let mut frames = decoder.into_frames(); + let _ = reader.animation_attributes(); + let mut frames = reader.into_frames(); while let Some(Ok(_frame)) = frames.next() {} }