Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f216daf
Ensure ICO forwards all methods
197g Nov 26, 2025
b8c2b77
Restructure ImageDecoder
197g Nov 22, 2025
9e65eef
Migrate all codecs to init with layout
197g Nov 24, 2025
af6bee8
Split ImageFile off ImageReader
197g Nov 30, 2025
0854e23
Split CMS to private trait interface, icc_profile
197g Dec 3, 2025
c041746
Provide limited metadata decoding with ImageReader
197g Dec 3, 2025
a96e533
Provide ImageReader::skip
197g Dec 9, 2025
682cc88
Rename ImageFile -> ImageReaderOptions
197g Dec 17, 2025
d18c512
Fixup gif testing
197g Dec 17, 2025
1ae3783
Add more_images, ImageAttributes for animation
197g Dec 17, 2025
b4a2fbf
Adjust tests for ImageReader limits usage
197g Dec 21, 2025
0cd6719
Add metadata sequence indicators
197g Dec 26, 2025
e053604
Move metadata hints into decoder attributes
197g Dec 31, 2025
882c076
Remove ImageDecoder::viewbox demonstration
197g Dec 31, 2025
e6a182c
Refactor ImageDecoder::orientation
197g Jan 1, 2026
99da105
Move metadata between peek_layout, read_image
197g Jan 1, 2026
fa092e3
Replace ImageDecoder::{dimensions,color_type}
197g Jan 1, 2026
0990a59
Remove AnimationDecoder trait
197g Jan 17, 2026
6b4cedd
Move original_color_type into ImageLayout
197g Feb 14, 2026
c4e39d4
Rename DecoderAttributes for clarity
197g Feb 15, 2026
10e395b
Add ImageReader metadata access adapter
197g Feb 15, 2026
7d4f279
Move original_color_type to DecodedImageAttributes
197g Mar 8, 2026
ec3a411
Return metadata from ImageReader::decode
197g Mar 9, 2026
2b5413c
Merge remote-tracking branch 'origin/main' into decoder-metadata-inte…
197g Mar 9, 2026
cf98540
Document ImageReader construction / iteration
197g Mar 14, 2026
d4c3e32
Touch-up for ImageDecoder/ImageReader docs
197g Mar 14, 2026
ce98973
Delay all metadata errors
197g Mar 16, 2026
84c3143
Merge remote-tracking branch 'origin/main' into decoder-metadata-inte…
197g Mar 16, 2026
9607caa
Resolve naming and structure nits
197g Mar 23, 2026
278bb47
Turn decode_into to a byte-slice API
197g Mar 23, 2026
90b7601
Rename prepare_layout, as DecoderPreparedImage
197g Mar 29, 2026
9a15ee8
Remove AfterFinish metadata hint
197g Apr 12, 2026
a232c59
Merge remote-tracking branch 'origin/main' into decoder-metadata-inte…
197g Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/fast_blur/main.rs
Original file line number Diff line number Diff line change
@@ -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));

Expand Down
10 changes: 6 additions & 4 deletions fuzz-afl/fuzzers/fuzz_pnm.rs
Original file line number Diff line number Diff line change
@@ -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<DynamicImage> {
let decoder = image::codecs::pnm::PnmDecoder::new(data)?;
let (width, height) = decoder.dimensions();
let mut decoder = image::codecs::pnm::PnmDecoder::new(data)?;
let (width, height) = decoder.peek_layout()?.dimensions();

if width.saturating_mul(height) > 4_000_000 {
return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError)));
return Err(ImageError::Limits(LimitError::from_kind(
LimitErrorKind::DimensionError,
)));
}

DynamicImage::from_decoder(decoder)
Expand Down
10 changes: 6 additions & 4 deletions fuzz-afl/fuzzers/fuzz_webp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DynamicImage> {
let decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(data))?;
let (width, height) = decoder.dimensions();
let mut decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(data))?;
let (width, height) = decoder.peek_layout()?.dimensions();

if width.saturating_mul(height) > 4_000_000 {
return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError)));
return Err(ImageError::Limits(LimitError::from_kind(
LimitErrorKind::DimensionError,
)));
}

DynamicImage::from_decoder(decoder)
Expand Down
4 changes: 2 additions & 2 deletions fuzz-afl/reproducers/reproduce_pnm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ mod utils;

#[inline(always)]
fn pnm_decode(data: &[u8]) -> ImageResult<DynamicImage> {
let decoder = image::codecs::pnm::PnmDecoder::new(data)?;
let (width, height) = decoder.dimensions();
let mut decoder = image::codecs::pnm::PnmDecoder::new(data)?;
let (width, height) = decoder.peek_layout()?.dimensions();

if width.saturating_mul(height) > 4_000_000 {
return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError)));
Expand Down
10 changes: 6 additions & 4 deletions fuzz-afl/reproducers/reproduce_webp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DynamicImage> {
let decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(data))?;
let (width, height) = decoder.dimensions();
let mut decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(data))?;
let (width, height) = decoder.peek_layout()?.dimensions();

if width.saturating_mul(height) > 4_000_000 {
return Err(ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError)));
return Err(ImageError::Limits(LimitError::from_kind(
LimitErrorKind::DimensionError,
)));
}

DynamicImage::from_decoder(decoder)
Expand Down
7 changes: 4 additions & 3 deletions fuzz/fuzzers/fuzzer_script_exr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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<u8>)> {
let mut decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?;
match usize::try_from(decoder.total_bytes()) {
let layout = decoder.peek_layout()?;
match usize::try_from(layout.total_bytes()) {
Ok(decoded_size) if decoded_size <= 256 * 1024 * 1024 => {
decoder.set_limits(Limits::default())?;
let (width, height) = decoder.dimensions();
let (width, height) = layout.dimensions();
let mut buffer = vec![0; decoded_size];
decoder.read_image(buffer.as_mut_slice())?;
Ok((width, height, buffer))
Expand Down
6 changes: 3 additions & 3 deletions fuzz/fuzzers/fuzzer_script_tga.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ fuzz_target!(|data: &[u8]| {

fn decode(data: &[u8]) -> Result<(), image::ImageError> {
use image::ImageDecoder;
let decoder = image::codecs::tga::TgaDecoder::new(std::io::Cursor::new(data))?;
if decoder.total_bytes() > 4_000_000 {
let mut decoder = image::codecs::tga::TgaDecoder::new(std::io::Cursor::new(data))?;
if decoder.peek_layout()?.total_bytes() > 4_000_000 {
return Ok(());
}
let mut buffer = vec![0; decoder.total_bytes() as usize];
let mut buffer = vec![0; decoder.peek_layout()?.total_bytes() as usize];
decoder.read_image(&mut buffer)?;
Ok(())
}
39 changes: 19 additions & 20 deletions src/codecs/avif/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::error::{
DecodingError, ImageFormatHint, LimitError, LimitErrorKind, UnsupportedError,
UnsupportedErrorKind,
};
use crate::io::DecodedImageAttributes;
use crate::metadata::Orientation;
use crate::{ColorType, ImageDecoder, ImageError, ImageFormat, ImageResult};
///
Expand Down Expand Up @@ -369,36 +370,35 @@ fn get_matrix(
}

impl<R: Read> ImageDecoder for AvifDecoder<R> {
fn dimensions(&self) -> (u32, u32) {
(self.picture.width(), self.picture.height())
}

fn color_type(&self) -> ColorType {
if self.picture.bit_depth() == 8 {
fn peek_layout(&mut self) -> ImageResult<crate::ImageLayout> {
let color = if self.picture.bit_depth() == 8 {
ColorType::Rgba8
} else {
ColorType::Rgba16
}
};

Ok(crate::ImageLayout {
width: self.picture.width(),
height: self.picture.height(),
..crate::ImageLayout::empty(color)
})
}

fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> {
Ok(self.icc_profile.clone())
}

fn orientation(&mut self) -> ImageResult<Orientation> {
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<DecodedImageAttributes> {
let layout = self.peek_layout()?;
assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes()));

let bit_depth = self.picture.bit_depth();

// Normally this should never happen,
// if this happens then there is an incorrect implementation somewhere else
assert!(bit_depth == 8 || bit_depth == 10 || bit_depth == 12);

let (width, height) = self.dimensions();
let (width, height) = layout.dimensions();
// This is suspicious if this happens, better fail early
if width == 0 || height == 0 {
return Err(ImageError::Limits(LimitError::from_kind(
Expand Down Expand Up @@ -485,7 +485,7 @@ impl<R: Read> ImageDecoder for AvifDecoder<R> {
}

// 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(),
Expand Down Expand Up @@ -521,11 +521,10 @@ impl<R: Read> ImageDecoder for AvifDecoder<R> {
}
}

Ok(())
}

fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
(*self).read_image(buf)
Ok(DecodedImageAttributes {
orientation: Some(self.orientation),
..DecodedImageAttributes::default()
})
}
}

Expand Down
49 changes: 27 additions & 22 deletions src/codecs/bmp/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,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;
Expand Down Expand Up @@ -2151,31 +2151,31 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
}

impl<R: BufRead + Seek> ImageDecoder for BmpDecoder<R> {
fn dimensions(&self) -> (u32, u32) {
(self.width as u32, self.height as u32)
}

fn color_type(&self) -> ColorType {
if self.indexed_color {
fn peek_layout(&mut self) -> ImageResult<crate::ImageLayout> {
let color = if self.indexed_color {
ColorType::L8
} else if self.add_alpha_channel {
ColorType::Rgba8
} else {
ColorType::Rgb8
}
};

Ok(crate::ImageLayout::new(
self.width as u32,
self.height as u32,
color,
))
}

fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> {
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<Self>, buf: &mut [u8]) -> ImageResult<()> {
(*self).read_image(buf)
fn read_image(&mut self, buf: &mut [u8]) -> ImageResult<DecodedImageAttributes> {
let layout = self.peek_layout()?;
assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes()));
self.read_image_data(buf)?;
Ok(DecodedImageAttributes::default())
}
}

Expand Down Expand Up @@ -2223,8 +2223,9 @@ mod test {
0x4d, 0x00, 0x2a, 0x00,
];

let decoder = BmpDecoder::new(Cursor::new(&data)).unwrap();
let mut buf = vec![0; usize::try_from(decoder.total_bytes()).unwrap()];
let mut decoder = BmpDecoder::new(Cursor::new(&data)).unwrap();
let layout = decoder.peek_layout().unwrap();
let mut buf = vec![0; usize::try_from(layout.total_bytes()).unwrap()];
assert!(decoder.read_image(&mut buf).is_ok());
}

Expand Down Expand Up @@ -2497,7 +2498,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.peek_layout().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();
Expand Down Expand Up @@ -2561,10 +2562,11 @@ mod test {
}

// Verify dimensions are available after metadata
let (width, height) = decoder.dimensions();
let layout = decoder.peek_layout().unwrap();
let (width, height) = 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"
);
Expand Down Expand Up @@ -2702,10 +2704,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.peek_layout().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:?}");
});
Expand Down
6 changes: 3 additions & 3 deletions src/codecs/bmp/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,9 @@ mod tests {
.expect("could not encode image");
}

let decoder = BmpDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode");

let mut buf = vec![0; decoder.total_bytes() as usize];
let mut decoder = BmpDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode");
let layout = decoder.peek_layout().unwrap();
let mut buf = vec![0; layout.total_bytes() as usize];
decoder.read_image(&mut buf).expect("failed to decode");
buf
}
Expand Down
Loading
Loading