From 0ad173239deefc897ae6ecf1e3e40ed3b98d5383 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Sun, 9 May 2021 22:50:43 +0200 Subject: [PATCH 01/45] first ever exr prototype (not even compiled once) --- Cargo.toml | 4 +- src/codecs/openexr.rs | 164 +++++++++++++++++++++++++++++++++++++++ src/image.rs | 18 +++++ src/io/free_functions.rs | 11 ++- src/lib.rs | 2 + 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 src/codecs/openexr.rs diff --git a/Cargo.toml b/Cargo.toml index ef81287517..5e72c1ff46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ rgb = { version = "0.8.25", optional = true } mp4parse = { version = "0.11.5", optional = true } dav1d = { version = "0.6.0", optional = true } dcv-color-primitives = { version = "0.1.16", optional = true } +exr = { version = "1.1.0", optional = true } color_quant = "1.1" [dev-dependencies] @@ -47,7 +48,7 @@ criterion = "0.3" [features] # TODO: Add "avif" to this list while preparing for 0.24.0 -default = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hdr", "dxt", "dds", "farbfeld", "jpeg_rayon"] +default = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hdr", "dxt", "dds", "farbfeld", "jpeg_rayon", "openexr"] ico = ["bmp", "png"] pnm = [] @@ -58,6 +59,7 @@ hdr = ["scoped_threadpool"] dxt = [] dds = ["dxt"] farbfeld = [] +openexr = ["exr"] # Enables multi-threading. # Requires latest stable Rust. diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs new file mode 100644 index 0000000000..c77113a0a0 --- /dev/null +++ b/src/codecs/openexr.rs @@ -0,0 +1,164 @@ +//! Decoding of OpenEXR (.exr) Images +//! +//! OpenEXR is an image format that is widely used, especially in VFX, +//! because it supports lossless and lossy compression for float data. +//! +//! # Related Links +//! * - The OpenEXR reference. + +// # ROADMAP +// - [] ImageDecoder +// - [] ImageEncoder +// - [] GenericImageView? +// - [] GenericImage? +// - [] Progress + +// # MAYBE SOON +// - [] ImageDecoderExt::read_rect_with_progress +// - [] Layers -> Animation? + + +extern crate exr; +use exr::prelude::*; + +use crate::{ImageDecoder, ImageEncoder, ImageResult, ColorType, ExtendedColorType, Progress, image, ImageError, ImageFormat}; +use std::io::{Read, Write, Seek, BufRead, SeekFrom, Cursor}; +use self::exr::image::read::layers::FirstValidLayerReader; +use std::cell::Cell; +use crate::error::{DecodingError, ImageFormatHint}; +use self::exr::meta::header::Header; + +/// An OpenEXR decoder +#[derive(Debug)] +pub struct ExrDecoder { + source: R, + header: exr::meta::header::Header, +} + + + +impl ExrDecoder { + + /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. + pub fn from_buffered(mut source: R) -> ImageResult { + + // read meta data, then go back to the start of the file + let mut meta_data = { + let start_position = source.stream_position()?; + let meta: MetaData = exr::meta::MetaData::read_from_buffered(&mut source, true)?; + source.seek(SeekFrom::Start(start_position))?; + meta + }; + + let header: Header = meta_data.headers.into_iter() + .filter(|header|{ + let has_rgb = ["R","G","B"].iter().all( + |required| header.channels.list.iter().find(|chan| chan.name.eq(required)).is_some() // TODO eq_lowercase only if exrs supports it + ); + + !header.deep && has_rgb + }) + .next() + .ok_or_else(|| ImageError::Decoding(DecodingError::new( + ImageFormatHint::Exact(ImageFormat::Exr), + "image does not contain non-deep rgb channels" + )))?; + + Ok(Self { source, header }) + } +} + + +impl<'a, R: 'a + Read> ImageDecoder<'a> for ExrDecoder { + type Reader = Cursor>; + + fn dimensions(&self) -> (u32, u32) { + let s = self.header.layer_size; + (s.width() as u32, s.height() as u32) + } + + // TODO fn original_color_type(&self) -> ExtendedColorType {} + + fn color_type(&self) -> ColorType { + // TODO adapt to actual exr color type + ColorType::Rgba32 + } + + /// Use `read_image` if possible, + /// as this method creates a whole new buffer to contain the entire image. + fn into_reader(self) -> ImageResult { + Ok(Cursor::new(image::decoder_to_vec(self)?)) + } + + fn scanline_bytes(&self) -> u64 { + // we cannot always read individual scan lines, + // as the tiles or lines in the file could be in random order. + // therefore we currently read all lines at once + // Todo: respect exr.line_order + self.total_bytes() + } + + fn read_image(mut self, target: &mut [u8]) -> ImageResult<()> { + self.read_image_with_progress(target, |_|{}) + } + + fn read_image_with_progress(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> { + // TODO reuse meta-data from earlier + + let result = exr::prelude::read() + .no_deep_data().largest_resolution_level() + .rgba_channels( + // FIXME no alloc+copy, write into target slice directly! + |size, _channels| ( + size, vec![0.0_f32; size.area()*4] + ), + + |&(size, buffer), position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { + // TODO white point chromaticities + srgb/linear conversion? + let first_f32_index = position.flat_index_for_size(size); + buffer[first_f32_index*4 + (first_f32_index + 1)*4].copy_from_slice([r, g, b, a_or_1]); + } + ) + .first_valid_layer().all_attributes() + .on_progress(|progress| progress_callback(Progress::new((progress*100.0) as u64, 100_u64))) + .from_buffered(self.source)?; + + target.copy_from_slice(bytemuck::bytes_of(result.layer_data.channel_data.pixels.1.as_slice())); + // TODO keep meta data? + + Ok(()) + } +} + + + +fn write_image(out: impl Write + Seek, bytes: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { + let pixels: &[f32] = bytemuck::try_from_bytes(bytes).expect("byte buffer must be aligned to f32"); + + match color_type { + ColorType::Rgb32 => { + exr::prelude::Image::from_channels( + (width as usize, height as usize), + SpecificChannels::rgb(|pixel: Vec2| { + let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width as usize, height as usize)); + (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2]) + }) + ).write().to_buffered(out)?; + } + + ColorType::Rgba32 => { + exr::prelude::Image::from_channels( + (width as usize, height as usize), + SpecificChannels::rgba(|pixel: Vec2| { + let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width as usize, height as usize)); + (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2], pixels[pixel_index+3]) + }) + ).write().to_buffered(out)?; + } + + _ => todo!() + } + + Ok(()) +} + diff --git a/src/image.rs b/src/image.rs index 38b70a4a4f..6c46f3c038 100644 --- a/src/image.rs +++ b/src/image.rs @@ -55,6 +55,9 @@ pub enum ImageFormat { /// An Image in Radiance HDR Format Hdr, + /// An Image in OpenEXR Format + Exr, + /// An Image in farbfeld Format Farbfeld, @@ -94,6 +97,7 @@ impl ImageFormat { "bmp" => ImageFormat::Bmp, "ico" => ImageFormat::Ico, "hdr" => ImageFormat::Hdr, + "exr" => ImageFormat::Exr, "pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm, "ff" | "farbfeld" => ImageFormat::Farbfeld, _ => return None, @@ -149,6 +153,7 @@ impl ImageFormat { ImageFormat::Bmp => true, ImageFormat::Ico => true, ImageFormat::Hdr => true, + ImageFormat::Exr => true, ImageFormat::Pnm => true, ImageFormat::Farbfeld => true, ImageFormat::Avif => true, @@ -173,6 +178,7 @@ impl ImageFormat { ImageFormat::Avif => true, ImageFormat::WebP => false, ImageFormat::Hdr => false, + ImageFormat::Exr => true, ImageFormat::Dds => false, ImageFormat::__NonExhaustive(marker) => match marker._private {}, } @@ -200,6 +206,7 @@ impl ImageFormat { ImageFormat::Bmp => &["bmp"], ImageFormat::Ico => &["ico"], ImageFormat::Hdr => &["hdr"], + ImageFormat::Exr => &["exr"], ImageFormat::Farbfeld => &["ff"], // According to: https://aomediacodec.github.io/av1-avif/#mime-registration ImageFormat::Avif => &["avif"], @@ -243,6 +250,10 @@ pub enum ImageOutputFormat { /// An Image in TGA Format Tga, + #[cfg(feature = "openexr")] + /// An Image in OpenEXR Format + Exr, + #[cfg(feature = "tiff")] /// An Image in TIFF Format Tiff, @@ -279,6 +290,8 @@ impl From for ImageOutputFormat { ImageFormat::Farbfeld => ImageOutputFormat::Farbfeld, #[cfg(feature = "tga")] ImageFormat::Tga => ImageOutputFormat::Tga, + #[cfg(feature = "openexr")] + ImageFormat::Exr => ImageOutputFormat::Exr, #[cfg(feature = "tiff")] ImageFormat::Tiff => ImageOutputFormat::Tiff, @@ -511,6 +524,11 @@ pub struct Progress { } impl Progress { + /// Create Progress. + pub fn new(current: u64, total: u64) -> Self { + Self { current, total } + } + /// A measure of completed decoding. pub fn current(self) -> u64 { self.current diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index ea72599424..bf0f1ddd42 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -9,6 +9,8 @@ use crate::codecs::bmp; use crate::codecs::gif; #[cfg(feature = "hdr")] use crate::codecs::hdr; +#[cfg(feature = "openexr")] +use crate::codecs::openexr; #[cfg(feature = "ico")] use crate::codecs::ico; #[cfg(feature = "jpeg")] @@ -81,6 +83,8 @@ pub fn load(r: R, format: ImageFormat) -> ImageResult DynamicImage::from_decoder(ico::IcoDecoder::new(r)?), #[cfg(feature = "hdr")] image::ImageFormat::Hdr => DynamicImage::from_decoder(hdr::HdrAdapter::new(BufReader::new(r))?), + #[cfg(feature = "openexr")] + image::ImageFormat::Exr => DynamicImage::from_decoder(openexr::ExrDecoder::from_buffered(r)?), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(BufReader::new(r))?), #[cfg(feature = "farbfeld")] @@ -129,6 +133,8 @@ pub(crate) fn image_dimensions_with_format_impl(fin: R, forma image::ImageFormat::Ico => ico::IcoDecoder::new(fin)?.dimensions(), #[cfg(feature = "hdr")] image::ImageFormat::Hdr => hdr::HdrAdapter::new(fin)?.dimensions(), + #[cfg(feature = "openexr")] + image::ImageFormat::Exr => exr::HdrAdapter::new(fin)?.dimensions(), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => { pnm::PnmDecoder::new(fin)?.dimensions() @@ -218,6 +224,8 @@ pub(crate) fn write_buffer_impl( ImageOutputFormat::Farbfeld => farbfeld::FarbfeldEncoder::new(fout).write_image(buf, width, height, color), #[cfg(feature = "tga")] ImageOutputFormat::Tga => tga::TgaEncoder::new(fout).write_image(buf, width, height, color), + #[cfg(feature = "openexr")] + ImageOutputFormat::Exr => openexr::write_image(fout, buf, width, height, color), #[cfg(feature = "tiff")] ImageOutputFormat::Tiff => { let mut cursor = std::io::Cursor::new(Vec::new()); @@ -240,7 +248,7 @@ pub(crate) fn write_buffer_impl( } } -static MAGIC_BYTES: [(&[u8], ImageFormat); 20] = [ +static MAGIC_BYTES: [(&[u8], ImageFormat); 21] = [ (b"\x89PNG\r\n\x1a\n", ImageFormat::Png), (&[0xff, 0xd8, 0xff], ImageFormat::Jpeg), (b"GIF89a", ImageFormat::Gif), @@ -261,6 +269,7 @@ static MAGIC_BYTES: [(&[u8], ImageFormat); 20] = [ (b"P7", ImageFormat::Pnm), (b"farbfeld", ImageFormat::Farbfeld), (b"\0\0\0 ftypavif", ImageFormat::Avif), + (&[0x76, 0x2f, 0x31, 0x01], ImageFormat::Exr), // = &exr::meta::magic_number::BYTES ]; /// Guess image format from memory block diff --git a/src/lib.rs b/src/lib.rs index c0e5c1c9ca..2c0915844a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -244,6 +244,8 @@ pub mod codecs { pub mod tiff; #[cfg(feature = "webp")] pub mod webp; + #[cfg(feature = "openexr")] + pub mod openexr; } #[cfg(feature = "avif-encoder")] From 2e4e73a24fa7d76ac5eb757964d33213001055c1 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Sun, 9 May 2021 23:42:29 +0200 Subject: [PATCH 02/45] use u16 for now (can easily be changed back to f32 again) --- src/codecs/openexr.rs | 66 ++++++++++++++++++++++++++-------------- src/io/free_functions.rs | 4 +-- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index c77113a0a0..82c4ee9467 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -21,10 +21,8 @@ extern crate exr; use exr::prelude::*; -use crate::{ImageDecoder, ImageEncoder, ImageResult, ColorType, ExtendedColorType, Progress, image, ImageError, ImageFormat}; -use std::io::{Read, Write, Seek, BufRead, SeekFrom, Cursor}; -use self::exr::image::read::layers::FirstValidLayerReader; -use std::cell::Cell; +use crate::{ImageDecoder, ImageResult, ColorType, Progress, image, ImageError, ImageFormat}; +use std::io::{Write, Seek, BufRead, SeekFrom, Cursor}; use crate::error::{DecodingError, ImageFormatHint}; use self::exr::meta::header::Header; @@ -37,10 +35,10 @@ pub struct ExrDecoder { -impl ExrDecoder { +impl ExrDecoder { /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. - pub fn from_buffered(mut source: R) -> ImageResult { + pub fn read(mut source: R) -> ImageResult { // read meta data, then go back to the start of the file let mut meta_data = { @@ -69,7 +67,7 @@ impl ExrDecoder { } -impl<'a, R: 'a + Read> ImageDecoder<'a> for ExrDecoder { +impl<'a, R: 'a + BufRead + Seek + Send> ImageDecoder<'a> for ExrDecoder { type Reader = Cursor>; fn dimensions(&self) -> (u32, u32) { @@ -81,13 +79,14 @@ impl<'a, R: 'a + Read> ImageDecoder<'a> for ExrDecoder { fn color_type(&self) -> ColorType { // TODO adapt to actual exr color type - ColorType::Rgba32 + // TODO f32 ColorType::Rgba32 + ColorType::Rgba16 } /// Use `read_image` if possible, /// as this method creates a whole new buffer to contain the entire image. fn into_reader(self) -> ImageResult { - Ok(Cursor::new(image::decoder_to_vec(self)?)) + Ok(Cursor::new(image::decoder_to_vec(self)?)) // TODO no vec? } fn scanline_bytes(&self) -> u64 { @@ -110,20 +109,26 @@ impl<'a, R: 'a + Read> ImageDecoder<'a> for ExrDecoder { .rgba_channels( // FIXME no alloc+copy, write into target slice directly! |size, _channels| ( - size, vec![0.0_f32; size.area()*4] + size, vec![0_u16; size.area()*4] ), - |&(size, buffer), position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { + |(size, buffer), position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { // TODO white point chromaticities + srgb/linear conversion? - let first_f32_index = position.flat_index_for_size(size); - buffer[first_f32_index*4 + (first_f32_index + 1)*4].copy_from_slice([r, g, b, a_or_1]); + let first_f32_index = position.flat_index_for_size(*size); + let to_u16 = |v:f32| (v.clamp(0.0, 1.0) * u16::MAX as f32) as u16; // TODO remove, always use 32 + + buffer[first_f32_index*4 .. (first_f32_index + 1)*4] + .copy_from_slice(&[to_u16(r), to_u16(g), to_u16(b), to_u16(a_or_1)]); } ) .first_valid_layer().all_attributes() - .on_progress(|progress| progress_callback(Progress::new((progress*100.0) as u64, 100_u64))) + .on_progress(move |progress| { + let mut prog = progress_callback; + prog(Progress::new((progress*100.0) as u64, 100_u64)); + }) .from_buffered(self.source)?; - target.copy_from_slice(bytemuck::bytes_of(result.layer_data.channel_data.pixels.1.as_slice())); + target.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.1.as_slice())); // TODO keep meta data? Ok(()) @@ -132,33 +137,48 @@ impl<'a, R: 'a + Read> ImageDecoder<'a> for ExrDecoder { -fn write_image(out: impl Write + Seek, bytes: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { - let pixels: &[f32] = bytemuck::try_from_bytes(bytes).expect("byte buffer must be aligned to f32"); +pub fn write_image(mut out: impl Write, bytes: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { + let pixels: &[u16] = bytemuck::try_cast_slice(bytes).expect("byte buffer must be aligned to u16"); + + let mut seekable_write = Cursor::new(Vec::::with_capacity(bytes.len())); // TODO make parameter +seek + let as_f32 = |v:u16| (v as f32) / u16::MAX as f32; // TODO remove, always use f32 match color_type { - ColorType::Rgb32 => { + ColorType::Rgb16 => { exr::prelude::Image::from_channels( (width as usize, height as usize), SpecificChannels::rgb(|pixel: Vec2| { let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width as usize, height as usize)); - (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2]) + (as_f32(pixels[pixel_index]), as_f32(pixels[pixel_index+1]), as_f32(pixels[pixel_index+2])) }) - ).write().to_buffered(out)?; + ).write().to_buffered(&mut seekable_write)?; } - ColorType::Rgba32 => { + ColorType::Rgba16 => { exr::prelude::Image::from_channels( (width as usize, height as usize), SpecificChannels::rgba(|pixel: Vec2| { let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width as usize, height as usize)); - (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2], pixels[pixel_index+3]) + (as_f32(pixels[pixel_index]), as_f32(pixels[pixel_index+1]), as_f32(pixels[pixel_index+2]), as_f32(pixels[pixel_index+3])) }) - ).write().to_buffered(out)?; + ).write().to_buffered(&mut seekable_write)?; } _ => todo!() } + out.write_all(seekable_write.into_inner().as_slice())?; Ok(()) } + +impl From for ImageError { + fn from(exr_error: Error) -> Self { + ImageError::Decoding(DecodingError::new( + ImageFormatHint::Exact(ImageFormat::Exr), + exr_error + )) + } +} + + diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index bf0f1ddd42..8f2733ee58 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -84,7 +84,7 @@ pub fn load(r: R, format: ImageFormat) -> ImageResult DynamicImage::from_decoder(hdr::HdrAdapter::new(BufReader::new(r))?), #[cfg(feature = "openexr")] - image::ImageFormat::Exr => DynamicImage::from_decoder(openexr::ExrDecoder::from_buffered(r)?), + image::ImageFormat::Exr => DynamicImage::from_decoder(openexr::ExrDecoder::read(r)?), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(BufReader::new(r))?), #[cfg(feature = "farbfeld")] @@ -134,7 +134,7 @@ pub(crate) fn image_dimensions_with_format_impl(fin: R, forma #[cfg(feature = "hdr")] image::ImageFormat::Hdr => hdr::HdrAdapter::new(fin)?.dimensions(), #[cfg(feature = "openexr")] - image::ImageFormat::Exr => exr::HdrAdapter::new(fin)?.dimensions(), + image::ImageFormat::Exr => openexr::ExrDecoder::read(fin)?.dimensions(), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => { pnm::PnmDecoder::new(fin)?.dimensions() From 11a8c2a1d15707a0545006fd4bb865f456a0e5a8 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Mon, 10 May 2021 22:52:44 +0200 Subject: [PATCH 03/45] the minorest improvements (minor cleanup) --- src/codecs/openexr.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 82c4ee9467..660e12f9ef 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -33,8 +33,6 @@ pub struct ExrDecoder { header: exr::meta::header::Header, } - - impl ExrDecoder { /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. @@ -51,7 +49,9 @@ impl ExrDecoder { let header: Header = meta_data.headers.into_iter() .filter(|header|{ let has_rgb = ["R","G","B"].iter().all( - |required| header.channels.list.iter().find(|chan| chan.name.eq(required)).is_some() // TODO eq_lowercase only if exrs supports it + // check if r/g/b exists in the channels + |required| header.channels.list.iter() + .find(|chan| chan.name.eq(required)).is_some() // TODO eq_lowercase only if exrs supports it ); !header.deep && has_rgb @@ -71,8 +71,8 @@ impl<'a, R: 'a + BufRead + Seek + Send> ImageDecoder<'a> for ExrDecoder { type Reader = Cursor>; fn dimensions(&self) -> (u32, u32) { - let s = self.header.layer_size; - (s.width() as u32, s.height() as u32) + let size = self.header.layer_size; + (size.width() as u32, size.height() as u32) } // TODO fn original_color_type(&self) -> ExtendedColorType {} @@ -97,10 +97,6 @@ impl<'a, R: 'a + BufRead + Seek + Send> ImageDecoder<'a> for ExrDecoder { self.total_bytes() } - fn read_image(mut self, target: &mut [u8]) -> ImageResult<()> { - self.read_image_with_progress(target, |_|{}) - } - fn read_image_with_progress(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> { // TODO reuse meta-data from earlier @@ -115,7 +111,7 @@ impl<'a, R: 'a + BufRead + Seek + Send> ImageDecoder<'a> for ExrDecoder { |(size, buffer), position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { // TODO white point chromaticities + srgb/linear conversion? let first_f32_index = position.flat_index_for_size(*size); - let to_u16 = |v:f32| (v.clamp(0.0, 1.0) * u16::MAX as f32) as u16; // TODO remove, always use 32 + let to_u16 = |v:f32| (v.clamp(0.0, 1.0) * u16::MAX as f32) as u16; // TODO remove, always use f32 buffer[first_f32_index*4 .. (first_f32_index + 1)*4] .copy_from_slice(&[to_u16(r), to_u16(g), to_u16(b), to_u16(a_or_1)]); @@ -123,8 +119,9 @@ impl<'a, R: 'a + BufRead + Seek + Send> ImageDecoder<'a> for ExrDecoder { ) .first_valid_layer().all_attributes() .on_progress(move |progress| { + let precision = 1000; let mut prog = progress_callback; - prog(Progress::new((progress*100.0) as u64, 100_u64)); + prog(Progress::new((progress*precision as f32) as u64, precision)); }) .from_buffered(self.source)?; @@ -151,7 +148,10 @@ pub fn write_image(mut out: impl Write, bytes: &[u8], width: u32, height: u32, c let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width as usize, height as usize)); (as_f32(pixels[pixel_index]), as_f32(pixels[pixel_index+1]), as_f32(pixels[pixel_index+2])) }) - ).write().to_buffered(&mut seekable_write)?; + ) + .write() + // .on_progress(|progress| todo!()) + .to_buffered(&mut seekable_write)?; } ColorType::Rgba16 => { @@ -161,7 +161,10 @@ pub fn write_image(mut out: impl Write, bytes: &[u8], width: u32, height: u32, c let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width as usize, height as usize)); (as_f32(pixels[pixel_index]), as_f32(pixels[pixel_index+1]), as_f32(pixels[pixel_index+2]), as_f32(pixels[pixel_index+3])) }) - ).write().to_buffered(&mut seekable_write)?; + ) + .write() + // .on_progress(|progress| todo!()) + .to_buffered(&mut seekable_write)?; } _ => todo!() From 2d6cf83fa4a094974f1419ddee762f02e578edf0 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Tue, 1 Jun 2021 23:28:27 +0200 Subject: [PATCH 04/45] use newer exr library version with relaxed reader and writer bounds --- Cargo.toml | 2 +- src/codecs/openexr.rs | 120 ++++++++++++++++++++---------------------- 2 files changed, 58 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e72c1ff46..d8aafd5cdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ rgb = { version = "0.8.25", optional = true } mp4parse = { version = "0.11.5", optional = true } dav1d = { version = "0.6.0", optional = true } dcv-color-primitives = { version = "0.1.16", optional = true } -exr = { version = "1.1.0", optional = true } +exr = { version = "1.2.0", optional = true } color_quant = "1.1" [dev-dependencies] diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 660e12f9ef..7845fd7334 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -7,15 +7,15 @@ //! * - The OpenEXR reference. // # ROADMAP -// - [] ImageDecoder -// - [] ImageEncoder -// - [] GenericImageView? -// - [] GenericImage? -// - [] Progress +// - [x] ImageDecoder +// - [x] ImageEncoder +// - [ ] GenericImageView? +// - [ ] GenericImage? +// - [x] Progress // # MAYBE SOON -// - [] ImageDecoderExt::read_rect_with_progress -// - [] Layers -> Animation? +// - [ ] ImageDecoderExt::read_rect_with_progress +// - [ ] Layers -> Animation? extern crate exr; @@ -29,26 +29,23 @@ use self::exr::meta::header::Header; /// An OpenEXR decoder #[derive(Debug)] pub struct ExrDecoder { - source: R, - header: exr::meta::header::Header, + exr_reader: exr::block::reader::Reader, + + // select a header that is rgb and not deep + header_index: usize, } -impl ExrDecoder { +impl ExrDecoder { /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. pub fn read(mut source: R) -> ImageResult { - // read meta data, then go back to the start of the file - let mut meta_data = { - let start_position = source.stream_position()?; - let meta: MetaData = exr::meta::MetaData::read_from_buffered(&mut source, true)?; - source.seek(SeekFrom::Start(start_position))?; - meta - }; - - let header: Header = meta_data.headers.into_iter() - .filter(|header|{ - let has_rgb = ["R","G","B"].iter().all( + // read meta data, then wait for further instructions, keeping the file open and ready + let exr_reader = exr::block::read(source, false)?; + + let header_index = exr_reader.headers().into_iter() + .position(|header|{ + let has_rgb = ["R","G","B"].iter().all( // alpha will be optional // check if r/g/b exists in the channels |required| header.channels.list.iter() .find(|chan| chan.name.eq(required)).is_some() // TODO eq_lowercase only if exrs supports it @@ -56,22 +53,21 @@ impl ExrDecoder { !header.deep && has_rgb }) - .next() .ok_or_else(|| ImageError::Decoding(DecodingError::new( ImageFormatHint::Exact(ImageFormat::Exr), "image does not contain non-deep rgb channels" )))?; - Ok(Self { source, header }) + Ok(Self { exr_reader, header_index }) } } -impl<'a, R: 'a + BufRead + Seek + Send> ImageDecoder<'a> for ExrDecoder { +impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { type Reader = Cursor>; fn dimensions(&self) -> (u32, u32) { - let size = self.header.layer_size; + let size = self.exr_reader.headers()[self.header_index].layer_size; (size.width() as u32, size.height() as u32) } @@ -79,8 +75,8 @@ impl<'a, R: 'a + BufRead + Seek + Send> ImageDecoder<'a> for ExrDecoder { fn color_type(&self) -> ColorType { // TODO adapt to actual exr color type - // TODO f32 ColorType::Rgba32 - ColorType::Rgba16 + // let _channels = &self.exr_reader.headers()[self.header_index].channels; + ColorType::Rgba32F } /// Use `read_image` if possible, @@ -90,78 +86,77 @@ impl<'a, R: 'a + BufRead + Seek + Send> ImageDecoder<'a> for ExrDecoder { } fn scanline_bytes(&self) -> u64 { - // we cannot always read individual scan lines, - // as the tiles or lines in the file could be in random order. + // we cannot always read individual scan lines for every file, + // as the tiles or lines in the file could be in random or reversed order. // therefore we currently read all lines at once - // Todo: respect exr.line_order + // Todo: optimize for specific exr.line_order? self.total_bytes() } fn read_image_with_progress(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> { - // TODO reuse meta-data from earlier + let blocks_in_header = self.exr_reader.headers()[self.header_index].chunk_count as u64; let result = exr::prelude::read() .no_deep_data().largest_resolution_level() .rgba_channels( - // FIXME no alloc+copy, write into target slice directly! - |size, _channels| ( - size, vec![0_u16; size.area()*4] - ), + |size, _channels| + (size, vec![0_f32; size.area()*4]) + , |(size, buffer), position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { // TODO white point chromaticities + srgb/linear conversion? let first_f32_index = position.flat_index_for_size(*size); - let to_u16 = |v:f32| (v.clamp(0.0, 1.0) * u16::MAX as f32) as u16; // TODO remove, always use f32 - - buffer[first_f32_index*4 .. (first_f32_index + 1)*4] - .copy_from_slice(&[to_u16(r), to_u16(g), to_u16(b), to_u16(a_or_1)]); + buffer[first_f32_index*4 .. (first_f32_index + 1)*4].copy_from_slice(&[r, g, b, a_or_1]); } ) - .first_valid_layer().all_attributes() + .first_valid_layer() // TODO select exact layer by self.header_index? + .all_attributes() .on_progress(move |progress| { - let precision = 1000; let mut prog = progress_callback; - prog(Progress::new((progress*precision as f32) as u64, precision)); + prog(Progress::new((progress*blocks_in_header as f64) as u64, blocks_in_header)); // TODO precision errors? }) - .from_buffered(self.source)?; + .from_chunks(self.exr_reader)?; + // TODO this copy is strictly not necessary, but the exr api is a little too simple for reading into a borrowed target slice target.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.1.as_slice())); - // TODO keep meta data? + // TODO keep meta data? Ok(()) } } -pub fn write_image(mut out: impl Write, bytes: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { - let pixels: &[u16] = bytemuck::try_cast_slice(bytes).expect("byte buffer must be aligned to u16"); +pub fn write_image(mut seekable_write: impl Write + Seek, bytes: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { + let width = width as usize; + let height = height as usize; - let mut seekable_write = Cursor::new(Vec::::with_capacity(bytes.len())); // TODO make parameter +seek - let as_f32 = |v:u16| (v as f32) / u16::MAX as f32; // TODO remove, always use f32 + let pixels: &[f32] = bytemuck::try_cast_slice(bytes).expect("image byte buffer must be aligned to f32"); match color_type { ColorType::Rgb16 => { - exr::prelude::Image::from_channels( - (width as usize, height as usize), - SpecificChannels::rgb(|pixel: Vec2| { - let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width as usize, height as usize)); - (as_f32(pixels[pixel_index]), as_f32(pixels[pixel_index+1]), as_f32(pixels[pixel_index+2])) - }) - ) + exr::prelude::Image + ::from_channels( + (width, height), + SpecificChannels::rgb(|pixel: Vec2| { + let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width, height)); + (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2]) + }) + ) .write() // .on_progress(|progress| todo!()) .to_buffered(&mut seekable_write)?; } ColorType::Rgba16 => { - exr::prelude::Image::from_channels( - (width as usize, height as usize), - SpecificChannels::rgba(|pixel: Vec2| { - let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width as usize, height as usize)); - (as_f32(pixels[pixel_index]), as_f32(pixels[pixel_index+1]), as_f32(pixels[pixel_index+2]), as_f32(pixels[pixel_index+3])) - }) - ) + exr::prelude::Image + ::from_channels( + (width, height), + SpecificChannels::rgba(|pixel: Vec2| { + let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width, height)); + (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2], pixels[pixel_index+3]) + }) + ) .write() // .on_progress(|progress| todo!()) .to_buffered(&mut seekable_write)?; @@ -170,7 +165,6 @@ pub fn write_image(mut out: impl Write, bytes: &[u8], width: u32, height: u32, c _ => todo!() } - out.write_all(seekable_write.into_inner().as_slice())?; Ok(()) } From f0cccc00673ec88b5b4373692c440d54136f722b Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Tue, 1 Jun 2021 23:50:40 +0200 Subject: [PATCH 05/45] update supported formats documentation (add exr, use camel case) --- src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2c0915844a..32712fed8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -184,14 +184,15 @@ pub mod flat; /// | PNG | All supported color types | Same as decoding | /// | JPEG | Baseline and progressive | Baseline JPEG | /// | GIF | Yes | Yes | -/// | BMP | Yes | RGB8, RGBA8, Gray8, GrayA8 | +/// | BMP | Yes | Rgb8, Rgba8, Gray8, GrayA8 | /// | ICO | Yes | Yes | -/// | TIFF | Baseline(no fax support) + LZW + PackBits | RGB8, RGBA8, Gray8 | +/// | TIFF | Baseline(no fax support) + LZW + PackBits | Rgb8, Rgba8, Gray8 | /// | WebP | Lossy(Luma channel only) | No | /// | AVIF | Only 8-bit | Lossy | /// | PNM | PBM, PGM, PPM, standard PAM | Yes | /// | DDS | DXT1, DXT3, DXT5 | No | -/// | TGA | Yes | RGB8, RGBA8, BGR8, BGRA8, Gray8, GrayA8 | +/// | TGA | Yes | Rgb8, Rgba8, Bgr8, Bgra8, Gray8, GrayA8 | +/// | OpenEXR | Rgb32F, Rgba32F (no dwa compression) | Rgb32F, Rgba32F (no dwa compression) | /// | farbfeld | Yes | Yes | /// /// ## A note on format specific features From 307f7be9fc6bffc6751b59007f4e7e07e552fb32 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Wed, 2 Jun 2021 00:33:00 +0200 Subject: [PATCH 06/45] use f32 in writer and invert y --- src/codecs/openexr.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 7845fd7334..c44607f8af 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -95,6 +95,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { fn read_image_with_progress(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> { let blocks_in_header = self.exr_reader.headers()[self.header_index].chunk_count as u64; + let height = self.dimensions().1 as usize; let result = exr::prelude::read() .no_deep_data().largest_resolution_level() @@ -103,8 +104,9 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { (size, vec![0_f32; size.area()*4]) , - |(size, buffer), position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { + |(size, buffer), mut position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { // TODO white point chromaticities + srgb/linear conversion? + position.1 = height - (position.1 + 1); // openexr stores +y upwards, but we need +y downwards let first_f32_index = position.flat_index_for_size(*size); buffer[first_f32_index*4 .. (first_f32_index + 1)*4].copy_from_slice(&[r, g, b, a_or_1]); } @@ -134,32 +136,34 @@ pub fn write_image(mut seekable_write: impl Write + Seek, bytes: &[u8], width: u let pixels: &[f32] = bytemuck::try_cast_slice(bytes).expect("image byte buffer must be aligned to f32"); match color_type { - ColorType::Rgb16 => { + ColorType::Rgb32F => { exr::prelude::Image ::from_channels( (width, height), - SpecificChannels::rgb(|pixel: Vec2| { + SpecificChannels::rgb(|mut pixel: Vec2| { + pixel.1 = height - (pixel.1 + 1); // openexr stores +y upwards, but we need +y downwards let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width, height)); (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2]) }) ) .write() // .on_progress(|progress| todo!()) - .to_buffered(&mut seekable_write)?; + .to_buffered(&mut seekable_write)?; // TODO BufWrite::new()? } - ColorType::Rgba16 => { + ColorType::Rgba32F => { exr::prelude::Image ::from_channels( (width, height), - SpecificChannels::rgba(|pixel: Vec2| { + SpecificChannels::rgba(|mut pixel: Vec2| { + pixel.1 = height - (pixel.1 + 1); // openexr stores +y upwards, but we need +y downwards let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width, height)); (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2], pixels[pixel_index+3]) }) ) .write() // .on_progress(|progress| todo!()) - .to_buffered(&mut seekable_write)?; + .to_buffered(&mut seekable_write)?; // TODO BufWrite::new()? } _ => todo!() From 5e90731962c1fe0edf36a58bd1e51e4e4ca1c5d6 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Thu, 3 Jun 2021 21:50:28 +0200 Subject: [PATCH 07/45] make it compile, with new exr version. also do not require seekable writer for now --- Cargo.toml | 2 +- src/codecs/openexr.rs | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d8aafd5cdf..3944661a9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ rgb = { version = "0.8.25", optional = true } mp4parse = { version = "0.11.5", optional = true } dav1d = { version = "0.6.0", optional = true } dcv-color-primitives = { version = "0.1.16", optional = true } -exr = { version = "1.2.0", optional = true } +exr = { version = "1.3.0", optional = true } color_quant = "1.1" [dev-dependencies] diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index c44607f8af..5f32405c43 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -22,9 +22,8 @@ extern crate exr; use exr::prelude::*; use crate::{ImageDecoder, ImageResult, ColorType, Progress, image, ImageError, ImageFormat}; -use std::io::{Write, Seek, BufRead, SeekFrom, Cursor}; +use std::io::{Write, Seek, BufRead, Cursor}; use crate::error::{DecodingError, ImageFormatHint}; -use self::exr::meta::header::Header; /// An OpenEXR decoder #[derive(Debug)] @@ -38,7 +37,7 @@ pub struct ExrDecoder { impl ExrDecoder { /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. - pub fn read(mut source: R) -> ImageResult { + pub fn read(source: R) -> ImageResult { // read meta data, then wait for further instructions, keeping the file open and ready let exr_reader = exr::block::read(source, false)?; @@ -97,14 +96,14 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { let blocks_in_header = self.exr_reader.headers()[self.header_index].chunk_count as u64; let height = self.dimensions().1 as usize; - let result = exr::prelude::read() + let result = read() .no_deep_data().largest_resolution_level() .rgba_channels( |size, _channels| (size, vec![0_f32; size.area()*4]) , - |(size, buffer), mut position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { + move |(size, buffer), mut position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { // TODO white point chromaticities + srgb/linear conversion? position.1 = height - (position.1 + 1); // openexr stores +y upwards, but we need +y downwards let first_f32_index = position.flat_index_for_size(*size); @@ -113,9 +112,10 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { ) .first_valid_layer() // TODO select exact layer by self.header_index? .all_attributes() - .on_progress(move |progress| { - let mut prog = progress_callback; - prog(Progress::new((progress*blocks_in_header as f64) as u64, blocks_in_header)); // TODO precision errors? + .on_progress(|progress| { + progress_callback(Progress::new( + (progress*blocks_in_header as f64) as u64, blocks_in_header) // TODO precision errors? + ); }) .from_chunks(self.exr_reader)?; @@ -129,11 +129,12 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { -pub fn write_image(mut seekable_write: impl Write + Seek, bytes: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { +pub fn write_image(mut write: impl Write/* + Seek*/, bytes: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { let width = width as usize; let height = height as usize; let pixels: &[f32] = bytemuck::try_cast_slice(bytes).expect("image byte buffer must be aligned to f32"); + let mut seekable_write = Cursor::new(Vec::with_capacity(width*height*3*4)); match color_type { ColorType::Rgb32F => { @@ -169,11 +170,12 @@ pub fn write_image(mut seekable_write: impl Write + Seek, bytes: &[u8], width: u _ => todo!() } + write.write_all(seekable_write.into_inner().as_slice())?; Ok(()) } -impl From for ImageError { +impl From for ImageError { fn from(exr_error: Error) -> Self { ImageError::Decoding(DecodingError::new( ImageFormatHint::Exact(ImageFormat::Exr), From c7a0ef7f36fd5e8995bb7ebc9df0bfb2ad4ae773 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Thu, 3 Jun 2021 21:55:09 +0200 Subject: [PATCH 08/45] fix panic triggering for unsupported color types --- src/codecs/openexr.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 5f32405c43..e39deb8571 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -6,17 +6,7 @@ //! # Related Links //! * - The OpenEXR reference. -// # ROADMAP -// - [x] ImageDecoder -// - [x] ImageEncoder -// - [ ] GenericImageView? -// - [ ] GenericImage? -// - [x] Progress - -// # MAYBE SOON -// - [ ] ImageDecoderExt::read_rect_with_progress -// - [ ] Layers -> Animation? - +// ROADMAP: See https://github.com/image-rs/image/pull/1475 extern crate exr; use exr::prelude::*; @@ -133,16 +123,20 @@ pub fn write_image(mut write: impl Write/* + Seek*/, bytes: &[u8], width: u32, h let width = width as usize; let height = height as usize; - let pixels: &[f32] = bytemuck::try_cast_slice(bytes).expect("image byte buffer must be aligned to f32"); - let mut seekable_write = Cursor::new(Vec::with_capacity(width*height*3*4)); + let mut seekable_write = Cursor::new(Vec::with_capacity(width*height*3*4)); // TODO remove match color_type { ColorType::Rgb32F => { + let pixels: &[f32] = bytemuck::try_cast_slice(bytes) + .expect("image byte buffer must be aligned to f32"); + exr::prelude::Image ::from_channels( (width, height), SpecificChannels::rgb(|mut pixel: Vec2| { - pixel.1 = height - (pixel.1 + 1); // openexr stores +y upwards, but we need +y downwards + debug_assert_ne!(height, 0); + pixel.1 = height - 1 - pixel.1; // openexr stores +y upwards, but we need +y downwards + let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width, height)); (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2]) }) @@ -153,11 +147,16 @@ pub fn write_image(mut write: impl Write/* + Seek*/, bytes: &[u8], width: u32, h } ColorType::Rgba32F => { + let pixels: &[f32] = bytemuck::try_cast_slice(bytes) + .expect("image byte buffer must be aligned to f32"); + exr::prelude::Image ::from_channels( (width, height), SpecificChannels::rgba(|mut pixel: Vec2| { - pixel.1 = height - (pixel.1 + 1); // openexr stores +y upwards, but we need +y downwards + debug_assert_ne!(height, 0); + pixel.1 = height - 1 - pixel.1; // openexr stores +y upwards, but we need +y downwards + let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width, height)); (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2], pixels[pixel_index+3]) }) From 4367914965791a699dbdbf03ddd0adc61e7d8110 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:04:03 +0200 Subject: [PATCH 09/45] add tests to openexr module, load alpha only if present in file or requested, add some convenience functions to read and write f32 buffers using exr --- src/codecs/openexr.rs | 312 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 282 insertions(+), 30 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index e39deb8571..d97b4fa8bb 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -3,6 +3,10 @@ //! OpenEXR is an image format that is widely used, especially in VFX, //! because it supports lossless and lossy compression for float data. //! +//! This decoder only supports RGB and RGBA images. +//! If an image does not contain alpha information, +//! it is defaulted to `1.0`, which means no transparency. +//! //! # Related Links //! * - The OpenEXR reference. @@ -11,22 +15,117 @@ extern crate exr; use exr::prelude::*; -use crate::{ImageDecoder, ImageResult, ColorType, Progress, image, ImageError, ImageFormat}; -use std::io::{Write, Seek, BufRead, Cursor}; -use crate::error::{DecodingError, ImageFormatHint}; +use crate::{ImageDecoder, ImageResult, ColorType, Progress, ImageError, ImageFormat, ImageBuffer, Rgba, Rgb}; +use std::io::{Write, Seek, BufRead, Cursor, BufReader, BufWriter}; +use crate::error::{DecodingError, ImageFormatHint, LimitError, LimitErrorKind, EncodingError}; +use crate::image::decoder_to_vec; +use std::path::Path; + +/// An image buffer for 32-bit float RGB pixels, +/// where the backing container is a flattened vector of floats. +pub type RgbF32Buffer = ImageBuffer, Vec>; + +/// An image buffer for 32-bit float RGBA pixels, +/// where the backing container is a flattened vector of floats. +pub type RgbaF32Buffer = ImageBuffer, Vec>; + + +// TODO this could be a generic function that works for any format +/// Read the file from the specified path into an `RgbaF32Buffer`. +// TODO progress? load rect? +pub fn read_as_rgba_image_from_file(path: impl AsRef) -> ImageResult { + read_as_rgba_image(BufReader::new(std::fs::File::open(path)?)) +} + +// TODO this could be a generic function that works for any format +/// Read an `RgbaF32Buffer`. +/// Assumes the reader is buffered. In most cases, +/// you should wrap your reader in a `BufReader` for best performance. +// TODO progress? load rect? +pub fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult { + ExrDecoder::read(read)?.read_as_rgba_image() +} + +// TODO this could be a generic function that works for any format +/// Read the file from the specified path into an `RgbF32Buffer`. +// TODO progress? load rect? +pub fn read_as_rgb_image_from_file(path: impl AsRef) -> ImageResult { + read_as_rgb_image(BufReader::new(std::fs::File::open(path)?)) +} + +// TODO this could be a generic function that works for any format +/// Read an `RgbF32Buffer`. +/// Assumes the reader is buffered. In most cases, +/// you should wrap your reader in a `BufReader` for best performance. +// TODO progress? load rect? +pub fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult { + ExrDecoder::read(read)?.read_as_rgb_image() +} + + +// TODO this could be a generic function that works for any format +/// Write an `RgbF32Buffer` to the specified file path, replacing any existing file. +// TODO progress? load rect? +pub fn write_rgb_image_to_file(path: impl AsRef, image: &RgbF32Buffer) -> ImageResult<()> { + write_rgb_image(BufWriter::new(std::fs::File::create(path)?), image) +} + +// TODO this could be a generic function that works for any format +/// Write an `RgbF32Buffer`. +/// Assumes the writer is buffered. In most cases, +/// you should wrap your writer in a `BufWriter` for best performance. +// TODO progress? load rect? +pub fn write_rgb_image(write: impl Write/* + Seek*/, image: &RgbF32Buffer) -> ImageResult<()> { + write_buffer( + write, + bytemuck::cast_slice(image.as_raw().as_slice()), + image.width(), image.height(), + ColorType::Rgb32F + ) +} -/// An OpenEXR decoder +// TODO this could be a generic function that works for any format +/// Write an `RgbaF32Buffer` to the specified file path, replacing any existing file. +// TODO progress? load rect? +pub fn write_rgba_image_to_file(path: impl AsRef, image: &RgbaF32Buffer) -> ImageResult<()> { + write_rgba_image(BufWriter::new(std::fs::File::create(path)?), image) +} + +// TODO this could be a generic function that works for any format +/// Write an `RgbaF32Buffer`. +/// Assumes the writer is buffered. In most cases, +/// you should wrap your writer in a `BufWriter` for best performance. +// TODO progress? load rect? +pub fn write_rgba_image(write: impl Write/* + Seek*/, image: &RgbaF32Buffer) -> ImageResult<()> { + write_buffer( + write, + bytemuck::cast_slice(image.as_raw().as_slice()), + image.width(), image.height(), + ColorType::Rgba32F + ) +} + + +/// An OpenEXR decoder. Immediately reads the meta data from the file. #[derive(Debug)] pub struct ExrDecoder { exr_reader: exr::block::reader::Reader, // select a header that is rgb and not deep header_index: usize, + + // decode either rgb or rgba. + // by default, only activated if image contains alpha. + // can be changed to include or discard alpha channels. + insert_alpha: bool, } + impl ExrDecoder { /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. + /// Assumes the reader is buffered. In most cases, + /// you should wrap your reader in a `BufReader` for best performance. pub fn read(source: R) -> ImageResult { // read meta data, then wait for further instructions, keeping the file open and ready @@ -36,10 +135,12 @@ impl ExrDecoder { .position(|header|{ let has_rgb = ["R","G","B"].iter().all( // alpha will be optional // check if r/g/b exists in the channels + // TODO binary search, channels are sorted |required| header.channels.list.iter() .find(|chan| chan.name.eq(required)).is_some() // TODO eq_lowercase only if exrs supports it ); + // we currently dont support deep images, or images with other color spaces than rgb !header.deep && has_rgb }) .ok_or_else(|| ImageError::Decoding(DecodingError::new( @@ -47,7 +148,58 @@ impl ExrDecoder { "image does not contain non-deep rgb channels" )))?; - Ok(Self { exr_reader, header_index }) + // TODO binary search, channels are sorted + let has_alpha = exr_reader.headers()[header_index] + .channels.list + //.binary_search_by_key(b"A", |chan| chan.name.bytes()) + .iter() + .find(|chan| chan.name.eq("A")) // TODO eq_lowercase only if exrs supports it + .is_some(); + + Ok(Self { exr_reader, header_index, insert_alpha: has_alpha }) + } + + // do not leak exrs-specific meta data into public api, just do it for this module + fn selected_exr_header(&self) -> &exr::meta::header::Header { + &self.exr_reader.meta_data().headers[self.header_index] + } + + // TODO does using Rgba with Vec automagically un-flatten the vector?? + /// Read this OpenEXR file as an `RgbaF32Buffer`, + /// including the alpha channel. A default value of `1.0` is used + /// if there is no alpha channel in the image. + /// Any calls to `color_type` or `total_bytes` + /// might not agree with the result of this function. + // TODO progress? load rect? + pub fn read_as_rgba_image(mut self) -> ImageResult { + let (width, height) = self.dimensions(); + + self.insert_alpha = true; + let buffer: Vec = decoder_to_vec(self)?; + + ImageBuffer::from_raw(width, height, buffer) + + // this should be the only reason for the "from raw" call to fail, + // even though such a large allocation would probably cause an error much earlier + .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) + } + + // TODO does using Rgba with Vec automagically un-flatten the vector?? + /// Read this OpenEXR file as an `RgbF32Buffer`, discarding alpha. + /// Any calls to `color_type` or `total_bytes` + /// might not agree with the result of this function. + // TODO progress? load rect? + pub fn read_as_rgb_image(mut self) -> ImageResult { + let (width, height) = self.dimensions(); + + self.insert_alpha = false; + let buffer: Vec = decoder_to_vec(self)?; + + ImageBuffer::from_raw(width, height, buffer) + + // this should be the only reason for the "from raw" call to fail, + // even though such a large allocation would probably cause an error much earlier + .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) } } @@ -56,7 +208,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { type Reader = Cursor>; fn dimensions(&self) -> (u32, u32) { - let size = self.exr_reader.headers()[self.header_index].layer_size; + let size = self.selected_exr_header().layer_size; (size.width() as u32, size.height() as u32) } @@ -65,13 +217,13 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { fn color_type(&self) -> ColorType { // TODO adapt to actual exr color type // let _channels = &self.exr_reader.headers()[self.header_index].channels; - ColorType::Rgba32F + if self.insert_alpha { ColorType::Rgba32F } else { ColorType::Rgb32F } } /// Use `read_image` if possible, /// as this method creates a whole new buffer to contain the entire image. fn into_reader(self) -> ImageResult { - Ok(Cursor::new(image::decoder_to_vec(self)?)) // TODO no vec? + Ok(Cursor::new(decoder_to_vec(self)?)) // TODO no vec? } fn scanline_bytes(&self) -> u64 { @@ -82,22 +234,28 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { self.total_bytes() } + // reads with or without alpha, depending on `self.should_include_alpha` fn read_image_with_progress(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> { - let blocks_in_header = self.exr_reader.headers()[self.header_index].chunk_count as u64; - let height = self.dimensions().1 as usize; + let blocks_in_header = self.selected_exr_header().chunk_count as u64; + + let with_alpha = self.insert_alpha; + let channel_count = if with_alpha { 4 } else { 3 }; let result = read() .no_deep_data().largest_resolution_level() .rgba_channels( - |size, _channels| - (size, vec![0_f32; size.area()*4]) - , + |size, _channels| { + // TODO debug_assert_eq!(self.header_index, header_index); + (size, vec![0_f32; size.area() * channel_count]) + }, - move |(size, buffer), mut position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { - // TODO white point chromaticities + srgb/linear conversion? - position.1 = height - (position.1 + 1); // openexr stores +y upwards, but we need +y downwards + move |(size, buffer), position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { let first_f32_index = position.flat_index_for_size(*size); - buffer[first_f32_index*4 .. (first_f32_index + 1)*4].copy_from_slice(&[r, g, b, a_or_1]); + + buffer[first_f32_index * channel_count .. (first_f32_index + 1) * channel_count] + .copy_from_slice(&[r, g, b, a_or_1][0 .. channel_count]); + + // TODO white point chromaticities + srgb/linear conversion? } ) .first_valid_layer() // TODO select exact layer by self.header_index? @@ -111,15 +269,24 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { // TODO this copy is strictly not necessary, but the exr api is a little too simple for reading into a borrowed target slice target.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.1.as_slice())); - - // TODO keep meta data? Ok(()) } } -pub fn write_image(mut write: impl Write/* + Seek*/, bytes: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { + +/// Write a raw byte buffer of pixels, +/// panicking if the buffer is not aligned to `f32` +/// or if it has an invalid length. +/// +/// Assumes the writer is buffered. In most cases, +/// you should wrap your writer in a `BufWriter` for best performance. +pub fn write_buffer( + mut buffered_write: impl Write/* + Seek*/, bytes: &[u8], + width: u32, height: u32, color_type: ColorType +) -> ImageResult<()> +{ let width = width as usize; let height = height as usize; @@ -130,13 +297,12 @@ pub fn write_image(mut write: impl Write/* + Seek*/, bytes: &[u8], width: u32, h let pixels: &[f32] = bytemuck::try_cast_slice(bytes) .expect("image byte buffer must be aligned to f32"); + debug_assert_eq!(pixels.len(), width * height * 3, "invalid dimensions for byte buffer length"); + exr::prelude::Image ::from_channels( (width, height), - SpecificChannels::rgb(|mut pixel: Vec2| { - debug_assert_ne!(height, 0); - pixel.1 = height - 1 - pixel.1; // openexr stores +y upwards, but we need +y downwards - + SpecificChannels::rgb(|pixel: Vec2| { let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width, height)); (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2]) }) @@ -150,13 +316,12 @@ pub fn write_image(mut write: impl Write/* + Seek*/, bytes: &[u8], width: u32, h let pixels: &[f32] = bytemuck::try_cast_slice(bytes) .expect("image byte buffer must be aligned to f32"); + debug_assert_eq!(pixels.len(), width * height * 4, "invalid dimensions for byte buffer length"); + exr::prelude::Image ::from_channels( (width, height), - SpecificChannels::rgba(|mut pixel: Vec2| { - debug_assert_ne!(height, 0); - pixel.1 = height - 1 - pixel.1; // openexr stores +y upwards, but we need +y downwards - + SpecificChannels::rgba(|pixel: Vec2| { let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width, height)); (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2], pixels[pixel_index+3]) }) @@ -166,10 +331,13 @@ pub fn write_image(mut write: impl Write/* + Seek*/, bytes: &[u8], width: u32, h .to_buffered(&mut seekable_write)?; // TODO BufWrite::new()? } - _ => todo!() + _ => return Err(ImageError::Encoding(EncodingError::new( + ImageFormatHint::Exact(ImageFormat::Exr), + format!("color type {:?} not yet supported", color_type) + ))) } - write.write_all(seekable_write.into_inner().as_slice())?; + buffered_write.write_all(seekable_write.into_inner().as_slice())?; Ok(()) } @@ -184,3 +352,87 @@ impl From for ImageError { } + +#[cfg(test)] +mod test { + use super::*; + use std::path::PathBuf; + + const BASE_PATH: &[&str] = &[".", "tests", "images", "exr"]; + + #[test] + fn check_exr() { + assert!(cfg!(feature = "hdr"), "to run all the openexr tests, activate the hdr feature flag"); + + #[cfg(feature = "hdr")] + { + let folder = BASE_PATH.iter().collect::(); + let reference_path = folder.clone().join("overexposed gradient.hdr"); + let exr_path = folder.clone().join("overexposed gradient - data window equals display window.exr"); + + let hdr: Vec> = crate::codecs::hdr::HdrDecoder::new( + std::io::BufReader::new(std::fs::File::open(&reference_path).unwrap()) + ).unwrap().read_image_hdr().unwrap(); + + let exr_pixels: RgbF32Buffer = read_as_rgb_image_from_file(exr_path).unwrap(); + + // TODO let hdr = ImageBuffer::from_raw(___) + assert_eq!(exr_pixels.dimensions().0 * exr_pixels.dimensions().1, hdr.len() as u32); + + for (expected, found) in hdr.iter().zip(exr_pixels.pixels()){ + for (expected, found) in expected.0.iter().zip(found.0.iter()) { + // the large tolerance seems to be caused by + // the RGBE u8x4 pixel quantization of the hdr image format + assert!((expected - found).abs() < 0.1, "expected {}, found {}", expected, found); + } + } + } + } + + #[test] + fn roundtrip_rgba_exr() { + let mut next_random = vec![ 1.0, 0.0, -1.0, -3.14, 27.0, 11.0, 31.0 ].into_iter().cycle(); + let mut next_random = move || next_random.next().unwrap(); + + let generated_image: RgbaF32Buffer = ImageBuffer::from_fn(9, 31, |_x, _y|{ + Rgba([next_random(), next_random(), next_random(), next_random()]) + }); + + let mut bytes = vec![]; + write_rgba_image(Cursor::new(&mut bytes), &generated_image).unwrap(); + let decoded_image = read_as_rgba_image(Cursor::new(bytes)).unwrap(); + + debug_assert_eq!(generated_image, decoded_image); + } + + #[test] + fn roundtrip_rgb_exr() { + let mut next_random = vec![ 1.0, 0.0, -1.0, -3.14, 27.0, 11.0, 31.0 ].into_iter().cycle(); + let mut next_random = move || next_random.next().unwrap(); + + let generated_image: RgbF32Buffer = ImageBuffer::from_fn(9, 31, |_x, _y|{ + Rgb([next_random(), next_random(), next_random()]) + }); + + let mut bytes = vec![]; + write_rgb_image(Cursor::new(&mut bytes), &generated_image).unwrap(); + let decoded_image = read_as_rgb_image(Cursor::new(bytes)).unwrap(); + + debug_assert_eq!(generated_image, decoded_image); + } + + #[test] + fn compare_rgba_rgb_exr() { + let exr_path = BASE_PATH.iter().collect::() + .join("overexposed gradient - data window equals display window.exr"); + + let rgb: RgbF32Buffer = read_as_rgb_image_from_file(&exr_path).unwrap(); + let rgba: RgbaF32Buffer = read_as_rgba_image_from_file(&exr_path).unwrap(); + + assert_eq!(rgba.dimensions(), rgb.dimensions()); + + for (Rgb(rgb), Rgba(rgba)) in rgb.pixels().zip(rgba.pixels()) { + assert_eq!(rgb, &rgba[..3]); + } + } +} \ No newline at end of file From 57e6d8e0b341696556208da4e73eecff5c89f760 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:04:44 +0200 Subject: [PATCH 10/45] improve documentation: mention that writer argument should be buffered --- src/buffer.rs | 3 +++ src/dynimage.rs | 9 ++++++++- src/io/free_functions.rs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 596fe29258..0bbd78fe09 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -948,6 +948,9 @@ where { /// Writes the buffer to a writer in the specified format. /// + /// Assumes the writer is buffered. In most cases, + /// you should wrap your writer in a `BufWriter` for best performance. + /// /// See [`ImageOutputFormat`](../enum.ImageOutputFormat.html) for /// supported types. /// diff --git a/src/dynimage.rs b/src/dynimage.rs index 80082fe09f..605b8fc3ed 100644 --- a/src/dynimage.rs +++ b/src/dynimage.rs @@ -882,7 +882,11 @@ impl DynamicImage { dynamic_map!(*self, ref p => imageops::rotate270(p)) } - /// Encode this image and write it to ```w``` + /// Encode this image and write it to ```w```. + + /// Assumes the writer is buffered. In most cases, + /// you should wrap your writer in a `BufWriter` for best performance. + /// /// **Note**: TIFF encoding uses buffered writing, /// which can lead to unexpected use of resources pub fn write_to>( @@ -1309,6 +1313,9 @@ where /// See [`ImageOutputFormat`](../enum.ImageOutputFormat.html) for /// supported types. /// +/// Assumes the writer is buffered. In most cases, +/// you should wrap your writer in a `BufWriter` for best performance. + /// /// **Note**: TIFF encoding uses buffered writing, /// which can lead to unexpected use of resources pub fn write_buffer_with_format( diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 8f2733ee58..59dfef0b00 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -225,7 +225,7 @@ pub(crate) fn write_buffer_impl( #[cfg(feature = "tga")] ImageOutputFormat::Tga => tga::TgaEncoder::new(fout).write_image(buf, width, height, color), #[cfg(feature = "openexr")] - ImageOutputFormat::Exr => openexr::write_image(fout, buf, width, height, color), + ImageOutputFormat::Exr => openexr::write_buffer(fout, buf, width, height, color), #[cfg(feature = "tiff")] ImageOutputFormat::Tiff => { let mut cursor = std::io::Cursor::new(Vec::new()); From 5487eb1b9e2f47ffe280d6c037a4175a02c578cd Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:13:06 +0200 Subject: [PATCH 11/45] cosmetic improvements --- src/codecs/openexr.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index d97b4fa8bb..2d16efc207 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -331,9 +331,9 @@ pub fn write_buffer( .to_buffered(&mut seekable_write)?; // TODO BufWrite::new()? } - _ => return Err(ImageError::Encoding(EncodingError::new( + unsupported_color_type => return Err(ImageError::Encoding(EncodingError::new( ImageFormatHint::Exact(ImageFormat::Exr), - format!("color type {:?} not yet supported", color_type) + format!("color type {:?} not yet supported", unsupported_color_type) ))) } @@ -361,7 +361,7 @@ mod test { const BASE_PATH: &[&str] = &[".", "tests", "images", "exr"]; #[test] - fn check_exr() { + fn compare_exr_hdr() { assert!(cfg!(feature = "hdr"), "to run all the openexr tests, activate the hdr feature flag"); #[cfg(feature = "hdr")] @@ -422,7 +422,7 @@ mod test { } #[test] - fn compare_rgba_rgb_exr() { + fn compare_rgba_rgb() { let exr_path = BASE_PATH.iter().collect::() .join("overexposed gradient - data window equals display window.exr"); From 7ea5607c80f63b4a846184ba777d62dbb5248172 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:30:48 +0200 Subject: [PATCH 12/45] try adding an exr fuzzing script, rename exr test functions --- fuzz/Cargo.toml | 4 ++++ fuzz/fuzzers/fuzzer_script_exr.rs | 24 ++++++++++++++++++++++++ src/codecs/openexr.rs | 6 +++--- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 fuzz/fuzzers/fuzzer_script_exr.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 98d1a04a0f..ce695c5125 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -60,3 +60,7 @@ path = "fuzzers/fuzzer_script_ico.rs" [[bin]] name = "fuzzer_script_hdr" path = "fuzzers/fuzzer_script_hdr.rs" + +[[bin]] +name = "fuzzer_script_exr" +path = "fuzzers/fuzzer_script_exr.rs" diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs new file mode 100644 index 0000000000..6dfe521ef8 --- /dev/null +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -0,0 +1,24 @@ +#![no_main] +#[macro_use] extern crate libfuzzer_sys; +extern crate image; + +use crate::ImageResult; +use std::io::Cursor; +use crate::codecs::openexr::*; + +// "just dont panic" +fn roundtrip(bytes: &[u8]) -> ImageResult<()> { + let decoded_image = read_as_rgba_image(Cursor::new(bytes))?; + + let mut bytes = Vec::with_capacity(bytes.len() + 20); + write_rgba_image(Cursor::new(&mut bytes), &decoded_image)?; + let redecoded_image = read_as_rgba_image(Cursor::new(bytes))?; + + // if both images are valid, assert read/write consistency + assert_eq!(decoded_image, redecoded_image, "image was valid but was not reproducible"); + Ok(()) +} + +fuzz_target!(|data: &[u8]| { + let _img = roundtrip(data); // fixme not optimized away? +}); diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 2d16efc207..bbcf1ac995 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -390,7 +390,7 @@ mod test { } #[test] - fn roundtrip_rgba_exr() { + fn roundtrip_rgba() { let mut next_random = vec![ 1.0, 0.0, -1.0, -3.14, 27.0, 11.0, 31.0 ].into_iter().cycle(); let mut next_random = move || next_random.next().unwrap(); @@ -406,7 +406,7 @@ mod test { } #[test] - fn roundtrip_rgb_exr() { + fn roundtrip_rgb() { let mut next_random = vec![ 1.0, 0.0, -1.0, -3.14, 27.0, 11.0, 31.0 ].into_iter().cycle(); let mut next_random = move || next_random.next().unwrap(); @@ -435,4 +435,4 @@ mod test { assert_eq!(rgb, &rgba[..3]); } } -} \ No newline at end of file +} From 9b3f3b7c6b9fe02ecda3ca8edd2b149f1dd8bb9c Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:32:22 +0200 Subject: [PATCH 13/45] add test images (lol forgot that) --- ...dient - data window equals display window.exr | Bin 0 -> 4187 bytes tests/images/exr/overexposed gradient.hdr | Bin 0 -> 933 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/images/exr/overexposed gradient - data window equals display window.exr create mode 100644 tests/images/exr/overexposed gradient.hdr diff --git a/tests/images/exr/overexposed gradient - data window equals display window.exr b/tests/images/exr/overexposed gradient - data window equals display window.exr new file mode 100644 index 0000000000000000000000000000000000000000..fd60a843f93ef127ecddfef6ee06df479fcac4e7 GIT binary patch literal 4187 zcmb`Kc~s6@7sr3)X(Gjy1`+kBE+vZMp=W=8DjJlch=iu6M4~xEw}{ZB2x%fIl?GHA zbSY9oks+Cft3t!QC{oc|_ujSM;r{Xd(^+euv)A6|eD_(Ob=Fzu_|Mc95@s02d4r>; zr@+H!(D88h@nuX0V+)y~%uo~h+YJ5@G5$wn`9C6z^Ont9y#+o#?pr+n%O8^ExH$Sc z+PZtXYzbhTwgl?9Gh>D-{@un6j-v+6#ocGChvT;YJI}-2Q()=sBJgJZ?G067wz>xj zJQn(F6*&7Ycl34N!nk^Dar9+4Lpde{KF;0(f#+ZHh5}FDK>>dqS4RBL^k3b7PO)`& z@!jx05{&hqh5lY4Jv4@3IL)Ivocgw%r0j&t^k7UqsbB1(TYeH)Jxd-sNm|%G&k$0! zwrI5tM8@HL@VXO@uqQ`wdQAp0{c^FS=LT+{ZGeybTXe4)!H#_~pMHMnO)J))AYY3j ziim6`FMm-OkDLJB96emmu>z-UBMJ^2fakh+c-&3F$WytPYH<~=>*}z*yaOBRdtp6N zf*mNI$hL@RBiO|N6A!FLV7D8B5_hAmB^ur)iCDEI2j}a`@m{A6MiZJ*8UGcXCW#&2Y>c5s#iFkTsfXyyp=3R5uQ@;QvPD~B7m z5zkk(V#&N7B+5##CX!=Wk=`lnc2`xlU0RprY$7)8vFWh$)B9*cm-p|X=nuzf_17G# zA9J6wGar-90wGK+oB-94%20tG2ClJq%5{KP#7<-l#KK|l?4TqACxg#JIHDAv7d0YA z_X+$}?(qXpOee7;(=oaXA4(xtNNlyts_%7Yd0Qdgpj=Sm^BT?Gx?nb2z0$Bb)s_&&xB5zoBf#5sh$7vdpu zH5XS`7Ge7OYfzrt3WuBJ{K$HF@{u;8qaR#J`*bwDAAga4dh~`aB#wejf+`*r>tV_( zQ*7Dci5902q4bm*MmKO29Kul`{?E#_HJ%B!99Q7M;P<8P79;N5DY zBmp}KWpFzzASt~PInLg=TDlLBUytFprRi8Mb`jNT_h9YSg5SQiAwNSJ%L?USmZA#( zJUuL`SOleKjtCy_hfnkN!S;0wEV56)qM;PCHdVm9w*f)p-l9gi2W4kCtc!v)+Y&XI zT`Z)@s=k{w?EL&=RH>c2kh~21s69E1(iLMVX;%^Tr`OOL?e`RG*+;uRDj@Fe&p6JT z4<+>_C}h^dD{Tjujwpx*9E9bhbl8~|VOh{s{H~+HUlE{a(HVvpsKMeR_Q6T8l*Yw{FtVx)UGg%x?h`8mWdbLnjcL|W+#))h7u~Ab(NOd8m+EjE3u*Vfiu`7Yritu;m0K`mkaAdWgM! z0=L=^_`^_$75i13z1c6vn!2bBJ3n5C9QnVRNt`G1$S!ylH9LAz+4xABB*-GK-ZHx9 z-biZQZ6t3!0UIr+KxF-FSiL9s>e?gJ&<{$zVF*@%DQbD z^jGD4)OW2c<;$)fOL5*3w4-M&g)Ivqsa=KC6Z(#nw+SQ3eymx%s{H2F$_<*A^*)b z^i)M4qcjd0E2Bz;4^%H0`>Te2$MGh8v*cL5#MV4^4gG8l2rKI0P1H&p%LoEbEdr9M$xxk`jUsL*6$*bMnYJzxo-;r;Cq`qmtpaX3 zO+_r`z_!m4()XRP>8&>eox9O1ei&}6(vijFqAH~V@~Zmb2!5x zi%<>;`bSXLTouZU{+R+s5!t`7A+PnJRI@094)BVp;r=CxDQTh^TrtEw6^D738aD4U z!jD7Cu<)c43YZ}5w~873TQba2^D(8qebdYCR><-{T;cKky5sn>3LE%pW$GkV5k%Mb z9ieHXPmp?G73t&)Uvjb~|pBM4{R_4H2516f&WcMnCExUY;-} zg^Iv>u@tgi%VD0327YTZf~mPRCZDr|ahDfl^dn(^HwNua8F)~23D^2-Q2(?M?M7_~ I-6zcc3)TX_qW}N^ literal 0 HcmV?d00001 diff --git a/tests/images/exr/overexposed gradient.hdr b/tests/images/exr/overexposed gradient.hdr new file mode 100644 index 0000000000000000000000000000000000000000..03f16e7b004d5f0770fe89ba415cb8250289caa1 GIT binary patch literal 933 zcmY$k4{~(zbo6s}<#O{6@^uWcH8#>s$}EX5%1MncN>57V;?j*&Ff>xoj!-Z%;9_E8 z5E0pU=;rgLzS(Q`oPGMep=;W_jk`|V5z({pkFs?3jm{|ToZ%mqm{wNXIY-1bAU@sJ z#V0&5yQbaMD>y7Aqo}#1p`)>*32e~9&ByP3?w&et!=7u;e{@bI%Y54 zyzlDk=B}ypm+w9(BBf>F8k1z_8=R0)-5V61kdas4EMn^96PB6d;2RK~P*CUL6C4qf zo!i#X4l`)Vvd#N0yl9-baLvy1x1YCk&swzZsED|lp{0AYg=unv$&Z%)`D-?a2urCN z+lQsvx&)+USGG?Niq0%96)`Zi^9W2SaB%fYOwOxp^YDvGN^S=l)Cw`ErGN3-?Ux?? zX`8it>yaz38e690rVQwpyswoi#8v)`mCvY;o9xzZoO*kn7&Mei(5>|z{)4m(jh1&xvaj&KQt*% zL{rPi#@#O_&DJR}A||)E#?><5sO)1tL7W z0@6zQPJRjIK7ny*6&*7|qOwH1we`%b-9lost$YI_>p?~}LyUU+uVM1y zMZ1qbYwDT3YR8f5Z-2H;7GdCJ7gtd-cZjlb^ovZ*t?i!?5+~y5qh@St?Gu!4=jKWKn-`>#K*aR`^-KUn0iSsv{y8PwOggI*tUcULcvtNXPnVVNsLC@JMHQU86 zDyO<`N_dQjlZ&Rdp_OY`RAI53Z+Kc^VT*4-Q++2eq~Jz<`QJNp$(HkvJ~d5Vy7s{J z=YQLQMzQh=NvjxogeP0Ng+^!Bb80L1itR{#J2 literal 0 HcmV?d00001 From a9073d18e447df7f258e83b7bc62f3f9835ac972 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:41:02 +0200 Subject: [PATCH 14/45] add openexr feature flag to rust ci workflow --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a0c8e24206..b9f9e0782e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: rust: ["1.34.2", stable, beta, nightly] - features: [ gif, jpeg, png, pnm, tga, dds, tiff, webp, hdr, farbfeld, '' ] + features: [ gif, jpeg, png, pnm, tga, dds, tiff, webp, hdr, openexr, farbfeld, '' ] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 From dc534ace7aa0ef45a2f60e89632f1c5d4ec493d0 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:47:35 +0200 Subject: [PATCH 15/45] try fix fuzzer, update readme format table from lib.rs, --- README.md | 7 ++++--- fuzz/fuzzers/fuzzer_script_exr.rs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c589a78e97..344462ffc7 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,15 @@ All image processing functions provided operate on types that implement the `Gen | PNG | All supported color types | Same as decoding | | JPEG | Baseline and progressive | Baseline JPEG | | GIF | Yes | Yes | -| BMP | Yes | RGB(8), RGBA(8), Gray(8), GrayA(8) | +| BMP | Yes | Rgb8, Rgba8, Gray8, GrayA8 | | ICO | Yes | Yes | -| TIFF | Baseline(no fax support) + LZW + PackBits | RGB(8), RGBA(8), Gray(8) | +| TIFF | Baseline(no fax support) + LZW + PackBits | Rgb8, Rgba8, Gray8 | | WebP | Lossy(Luma channel only) | No | | AVIF | Only 8-bit | Lossy | | PNM | PBM, PGM, PPM, standard PAM | Yes | | DDS | DXT1, DXT3, DXT5 | No | -| TGA | Yes | RGB(8), RGBA(8), BGR(8), BGRA(8), Gray(8), GrayA(8) | +| TGA | Yes | Rgb8, Rgba8, Bgr8, Bgra8, Gray8, GrayA8 | +| OpenEXR | Rgb32F, Rgba32F (no dwa compression) | Rgb32F, Rgba32F (no dwa compression) | | farbfeld | Yes | Yes | ### The [`ImageDecoder`](https://docs.rs/image/*/image/trait.ImageDecoder.html) and [`ImageDecoderExt`](https://docs.rs/image/*/image/trait.ImageDecoderExt.html) Traits diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index 6dfe521ef8..b25bfde992 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -2,9 +2,9 @@ #[macro_use] extern crate libfuzzer_sys; extern crate image; -use crate::ImageResult; use std::io::Cursor; -use crate::codecs::openexr::*; +use image::ImageResult; +use image::codecs::openexr::*; // "just dont panic" fn roundtrip(bytes: &[u8]) -> ImageResult<()> { From dccc0fe88940396a50389b1eb63da908f317450c Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:50:05 +0200 Subject: [PATCH 16/45] add exr to other existing tests --- src/image.rs | 2 +- tests/truncate_images.rs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/image.rs b/src/image.rs index 6c46f3c038..fcb131316b 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1391,7 +1391,7 @@ mod tests { #[test] fn image_formats_are_recognized() { use ImageFormat::*; - const ALL_FORMATS: &'static [ImageFormat] = &[Avif, Png, Jpeg, Gif, WebP, Pnm, Tiff, Tga, Dds, Bmp, Ico, Hdr, Farbfeld]; + const ALL_FORMATS: &'static [ImageFormat] = &[Avif, Png, Jpeg, Gif, WebP, Pnm, Tiff, Tga, Dds, Bmp, Ico, Hdr, Farbfeld, Exr]; for &format in ALL_FORMATS { let mut file = Path::new("file.nothing").to_owned(); for ext in format.extensions_str() { diff --git a/tests/truncate_images.rs b/tests/truncate_images.rs index 8087d11549..ceda45a752 100644 --- a/tests/truncate_images.rs +++ b/tests/truncate_images.rs @@ -15,7 +15,7 @@ where F: Fn(PathBuf), { let base: PathBuf = BASE_PATH.iter().collect(); - let decoders = &["tga", "tiff", "png", "gif", "bmp", "ico", "jpg", "hdr", "farbfeld"]; + let decoders = &["tga", "tiff", "png", "gif", "bmp", "ico", "jpg", "hdr", "farbfeld", "exr"]; for decoder in decoders { let mut path = base.clone(); path.push(dir); @@ -91,3 +91,8 @@ fn truncate_hdr() { fn truncate_farbfeld() { truncate_images("farbfeld"); } + +#[test] +fn truncate_exr() { + truncate_images("exr"); +} From ad151b407297cecfae05650d4af8de30265007f3 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 14:07:00 +0200 Subject: [PATCH 17/45] add exr to even more existing tests and documentation --- Cargo.toml.public-private-dependencies | 4 +++- src/buffer.rs | 2 +- src/dynimage.rs | 4 ++-- src/image.rs | 1 + tests/regression.rs | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Cargo.toml.public-private-dependencies b/Cargo.toml.public-private-dependencies index a9c641a38d..a91044d1af 100644 --- a/Cargo.toml.public-private-dependencies +++ b/Cargo.toml.public-private-dependencies @@ -38,6 +38,7 @@ tiff = { version = "0.6.0", optional = true } ravif = { version = "0.6.0", optional = true } rgb = { version = "0.8.25", optional = true } color_quant = { version = "1.1", public = true } +exr = { version = "1.3.0", optional = true } [dev-dependencies] crc32fast = "1.2.0" @@ -46,7 +47,7 @@ glob = "0.3" quickcheck = "0.9" [features] -default = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hdr", "dxt", "dds", "jpeg_rayon"] +default = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hdr", "dxt", "dds", "jpeg_rayon", "openexr"] ico = ["bmp", "png"] pnm = [] @@ -57,5 +58,6 @@ hdr = ["scoped_threadpool"] dxt = [] dds = ["dxt"] jpeg_rayon = ["jpeg/rayon"] +openexr = ["exr"] benchmarks = [] diff --git a/src/buffer.rs b/src/buffer.rs index 0bbd78fe09..393be750aa 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -896,7 +896,7 @@ where /// Saves the buffer to a file at the path specified. /// /// The image format is derived from the file extension. - /// Currently only jpeg, png, ico, pnm, bmp and + /// Currently only jpeg, png, ico, pnm, bmp, exr and /// tiff files are supported. pub fn save(&self, path: Q) -> ImageResult<()> where diff --git a/src/dynimage.rs b/src/dynimage.rs index 605b8fc3ed..1b3bd910c9 100644 --- a/src/dynimage.rs +++ b/src/dynimage.rs @@ -1265,7 +1265,7 @@ where /// the correct format according to the specified color type. /// This will lead to corrupted files if the buffer contains malformed data. Currently only -/// jpeg, png, ico, pnm, bmp and tiff files are supported. +/// jpeg, png, ico, pnm, bmp, exr and tiff files are supported. pub fn save_buffer

( path: P, buf: &[u8], @@ -1286,7 +1286,7 @@ where /// The buffer is assumed to have the correct format according /// to the specified color type. /// This will lead to corrupted files if the buffer contains -/// malformed data. Currently only jpeg, png, ico, bmp and +/// malformed data. Currently only jpeg, png, ico, bmp, exr and /// tiff files are supported. pub fn save_buffer_with_format

( path: P, diff --git a/src/image.rs b/src/image.rs index fcb131316b..bd7d287faa 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1291,6 +1291,7 @@ mod tests { assert_eq!(from_path("./a.bmp").unwrap(), ImageFormat::Bmp); assert_eq!(from_path("./a.Ico").unwrap(), ImageFormat::Ico); assert_eq!(from_path("./a.hdr").unwrap(), ImageFormat::Hdr); + assert_eq!(from_path("./a.exr").unwrap(), ImageFormat::Exr); assert_eq!(from_path("./a.pbm").unwrap(), ImageFormat::Pnm); assert_eq!(from_path("./a.pAM").unwrap(), ImageFormat::Pnm); assert_eq!(from_path("./a.Ppm").unwrap(), ImageFormat::Pnm); diff --git a/tests/regression.rs b/tests/regression.rs index 94e7a53326..243c7dc0da 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -9,7 +9,7 @@ where F: Fn(PathBuf), { let base: PathBuf = BASE_PATH.iter().collect(); - let decoders = &["tga", "tiff", "png", "gif", "bmp", "ico", "jpg", "hdr", "pbm", "webp"]; + let decoders = &["tga", "tiff", "png", "gif", "bmp", "ico", "jpg", "hdr", "pbm", "webp", "exr"]; for decoder in decoders { let mut path = base.clone(); path.push(dir); From 6d43853b0b5d608b0cd19c702bea219b72ee3ca5 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 14:11:06 +0200 Subject: [PATCH 18/45] remove exr from protected workflows file --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b9f9e0782e..a0c8e24206 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: rust: ["1.34.2", stable, beta, nightly] - features: [ gif, jpeg, png, pnm, tga, dds, tiff, webp, hdr, openexr, farbfeld, '' ] + features: [ gif, jpeg, png, pnm, tga, dds, tiff, webp, hdr, farbfeld, '' ] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 From bf08b3b6926576bc136043d3c5d15cb58520308f Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 14:35:42 +0200 Subject: [PATCH 19/45] remove exr error from public api --- src/codecs/openexr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index bbcf1ac995..0de98934b1 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -346,7 +346,7 @@ impl From for ImageError { fn from(exr_error: Error) -> Self { ImageError::Decoding(DecodingError::new( ImageFormatHint::Exact(ImageFormat::Exr), - exr_error + exr_error.to_string() )) } } From 67121731af45a7a5b14a4fb9855649658dd05644 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 15:00:19 +0200 Subject: [PATCH 20/45] implement original_color_type, add encoder trait implementation, remove exr error from public interface --- src/codecs/openexr.rs | 51 ++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 0de98934b1..b536f3c697 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -15,7 +15,7 @@ extern crate exr; use exr::prelude::*; -use crate::{ImageDecoder, ImageResult, ColorType, Progress, ImageError, ImageFormat, ImageBuffer, Rgba, Rgb}; +use crate::{ImageDecoder, ImageResult, ColorType, Progress, ImageError, ImageFormat, ImageBuffer, Rgba, Rgb, ImageEncoder, ExtendedColorType}; use std::io::{Write, Seek, BufRead, Cursor, BufReader, BufWriter}; use crate::error::{DecodingError, ImageFormatHint, LimitError, LimitErrorKind, EncodingError}; use crate::image::decoder_to_vec; @@ -117,7 +117,10 @@ pub struct ExrDecoder { // decode either rgb or rgba. // by default, only activated if image contains alpha. // can be changed to include or discard alpha channels. - insert_alpha: bool, + alpha_requested: bool, + + // whether the original file contains alpha + alpha_present_in_file: bool, } @@ -129,7 +132,7 @@ impl ExrDecoder { pub fn read(source: R) -> ImageResult { // read meta data, then wait for further instructions, keeping the file open and ready - let exr_reader = exr::block::read(source, false)?; + let exr_reader = exr::block::read(source, false).map_err(to_image_err)?; let header_index = exr_reader.headers().into_iter() .position(|header|{ @@ -156,7 +159,7 @@ impl ExrDecoder { .find(|chan| chan.name.eq("A")) // TODO eq_lowercase only if exrs supports it .is_some(); - Ok(Self { exr_reader, header_index, insert_alpha: has_alpha }) + Ok(Self { exr_reader, header_index, alpha_requested: has_alpha, alpha_present_in_file: has_alpha }) } // do not leak exrs-specific meta data into public api, just do it for this module @@ -174,7 +177,7 @@ impl ExrDecoder { pub fn read_as_rgba_image(mut self) -> ImageResult { let (width, height) = self.dimensions(); - self.insert_alpha = true; + self.alpha_requested = true; let buffer: Vec = decoder_to_vec(self)?; ImageBuffer::from_raw(width, height, buffer) @@ -192,7 +195,7 @@ impl ExrDecoder { pub fn read_as_rgb_image(mut self) -> ImageResult { let (width, height) = self.dimensions(); - self.insert_alpha = false; + self.alpha_requested = false; let buffer: Vec = decoder_to_vec(self)?; ImageBuffer::from_raw(width, height, buffer) @@ -212,12 +215,14 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { (size.width() as u32, size.height() as u32) } - // TODO fn original_color_type(&self) -> ExtendedColorType {} + fn original_color_type(&self) -> ExtendedColorType { + if self.alpha_present_in_file { ExtendedColorType::Rgba32F } else { ExtendedColorType::Rgb32F } + } fn color_type(&self) -> ColorType { // TODO adapt to actual exr color type // let _channels = &self.exr_reader.headers()[self.header_index].channels; - if self.insert_alpha { ColorType::Rgba32F } else { ColorType::Rgb32F } + if self.alpha_requested { ColorType::Rgba32F } else { ColorType::Rgb32F } } /// Use `read_image` if possible, @@ -237,9 +242,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { // reads with or without alpha, depending on `self.should_include_alpha` fn read_image_with_progress(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> { let blocks_in_header = self.selected_exr_header().chunk_count as u64; - - let with_alpha = self.insert_alpha; - let channel_count = if with_alpha { 4 } else { 3 }; + let channel_count = if self.alpha_requested { 4 } else { 3 }; let result = read() .no_deep_data().largest_resolution_level() @@ -265,7 +268,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { (progress*blocks_in_header as f64) as u64, blocks_in_header) // TODO precision errors? ); }) - .from_chunks(self.exr_reader)?; + .from_chunks(self.exr_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 target.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.1.as_slice())); @@ -309,7 +312,7 @@ pub fn write_buffer( ) .write() // .on_progress(|progress| todo!()) - .to_buffered(&mut seekable_write)?; // TODO BufWrite::new()? + .to_buffered(&mut seekable_write).map_err(to_image_err)?; // TODO BufWrite::new()? } ColorType::Rgba32F => { @@ -328,7 +331,7 @@ pub fn write_buffer( ) .write() // .on_progress(|progress| todo!()) - .to_buffered(&mut seekable_write)?; // TODO BufWrite::new()? + .to_buffered(&mut seekable_write).map_err(to_image_err)?; // TODO BufWrite::new()? } unsupported_color_type => return Err(ImageError::Encoding(EncodingError::new( @@ -342,15 +345,23 @@ pub fn write_buffer( } -impl From for ImageError { - fn from(exr_error: Error) -> Self { - ImageError::Decoding(DecodingError::new( - ImageFormatHint::Exact(ImageFormat::Exr), - exr_error.to_string() - )) +// TODO is this struct and trait actually used anywhere? +#[derive(Debug)] +pub struct Encoder (W); +impl Encoder { pub fn new(write: W) -> Self {Self(write)} } +impl ImageEncoder for Encoder where W: Write /*+ Seek*/ { + fn write_image(self, buf: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { + write_buffer(self.0, buf, width, height, color_type) } } +fn to_image_err(exr_error: Error) -> ImageError { + ImageError::Decoding(DecodingError::new( + ImageFormatHint::Exact(ImageFormat::Exr), + exr_error.to_string() + )) +} + #[cfg(test)] From 4df8dc21cd0fa4e5753da66e9c514836ba17b9df Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 15:13:24 +0200 Subject: [PATCH 21/45] use alpha flag in constructor instead of mutating the flag (avoiding inconsistent color_type return values) --- src/codecs/openexr.rs | 58 +++++++++++++++++++++++++++------------- src/io/free_functions.rs | 4 +-- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index b536f3c697..9ad023c8e3 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -43,7 +43,17 @@ pub fn read_as_rgba_image_from_file(path: impl AsRef) -> ImageResult ImageResult { - ExrDecoder::read(read)?.read_as_rgba_image() + let decoder = ExrDecoder::read(read, Some(true))?; + debug_assert_eq!(decoder.color_type(), ColorType::Rgba32F); + + let (width, height) = decoder.dimensions(); + let buffer: Vec = decoder_to_vec(decoder)?; // passing `true` to decoder results in 4 floats per pixel + + ImageBuffer::from_raw(width, height, buffer) + + // this should be the only reason for the "from raw" call to fail, + // even though such a large allocation would probably cause an error much earlier + .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) } // TODO this could be a generic function that works for any format @@ -59,7 +69,17 @@ pub fn read_as_rgb_image_from_file(path: impl AsRef) -> ImageResult ImageResult { - ExrDecoder::read(read)?.read_as_rgb_image() + let decoder = ExrDecoder::read(read, Some(false))?; + debug_assert_eq!(decoder.color_type(), ColorType::Rgb32F); + + let (width, height) = decoder.dimensions(); + let buffer: Vec = decoder_to_vec(decoder)?; // passing `false` to decoder results in 3 floats per pixel + + ImageBuffer::from_raw(width, height, buffer) + + // this should be the only reason for the "from raw" call to fail, + // even though such a large allocation would probably cause an error much earlier + .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) } @@ -117,7 +137,7 @@ pub struct ExrDecoder { // decode either rgb or rgba. // by default, only activated if image contains alpha. // can be changed to include or discard alpha channels. - alpha_requested: bool, + alpha_preference: Option, // whether the original file contains alpha alpha_present_in_file: bool, @@ -129,7 +149,10 @@ impl ExrDecoder { /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. /// Assumes the reader is buffered. In most cases, /// you should wrap your reader in a `BufReader` for best performance. - pub fn read(source: R) -> ImageResult { + /// If alpha preference is specified, an alpha channel will + /// always be present or always be not present in the returned image. + /// If alpha preference is none, the alpha channel will only be returned if it is found in the file. + pub fn read(source: R, alpha_preference: Option) -> ImageResult { // read meta data, then wait for further instructions, keeping the file open and ready let exr_reader = exr::block::read(source, false).map_err(to_image_err)?; @@ -159,7 +182,11 @@ impl ExrDecoder { .find(|chan| chan.name.eq("A")) // TODO eq_lowercase only if exrs supports it .is_some(); - Ok(Self { exr_reader, header_index, alpha_requested: has_alpha, alpha_present_in_file: has_alpha }) + Ok(Self { + exr_reader, header_index, + alpha_preference: alpha_preference, + alpha_present_in_file: has_alpha + }) } // do not leak exrs-specific meta data into public api, just do it for this module @@ -167,7 +194,7 @@ impl ExrDecoder { &self.exr_reader.meta_data().headers[self.header_index] } - // TODO does using Rgba with Vec automagically un-flatten the vector?? + /*// TODO does using Rgba with Vec automagically un-flatten the vector?? /// Read this OpenEXR file as an `RgbaF32Buffer`, /// including the alpha channel. A default value of `1.0` is used /// if there is no alpha channel in the image. @@ -176,8 +203,6 @@ impl ExrDecoder { // TODO progress? load rect? pub fn read_as_rgba_image(mut self) -> ImageResult { let (width, height) = self.dimensions(); - - self.alpha_requested = true; let buffer: Vec = decoder_to_vec(self)?; ImageBuffer::from_raw(width, height, buffer) @@ -194,8 +219,6 @@ impl ExrDecoder { // TODO progress? load rect? pub fn read_as_rgb_image(mut self) -> ImageResult { let (width, height) = self.dimensions(); - - self.alpha_requested = false; let buffer: Vec = decoder_to_vec(self)?; ImageBuffer::from_raw(width, height, buffer) @@ -203,7 +226,7 @@ impl ExrDecoder { // this should be the only reason for the "from raw" call to fail, // even though such a large allocation would probably cause an error much earlier .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) - } + }*/ } @@ -215,14 +238,13 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { (size.width() as u32, size.height() as u32) } - fn original_color_type(&self) -> ExtendedColorType { - if self.alpha_present_in_file { ExtendedColorType::Rgba32F } else { ExtendedColorType::Rgb32F } + fn color_type(&self) -> ColorType { + let returns_alpha = self.alpha_preference.unwrap_or(self.alpha_present_in_file); + if returns_alpha { ColorType::Rgba32F } else { ColorType::Rgb32F } } - fn color_type(&self) -> ColorType { - // TODO adapt to actual exr color type - // let _channels = &self.exr_reader.headers()[self.header_index].channels; - if self.alpha_requested { ColorType::Rgba32F } else { ColorType::Rgb32F } + fn original_color_type(&self) -> ExtendedColorType { + if self.alpha_present_in_file { ExtendedColorType::Rgba32F } else { ExtendedColorType::Rgb32F } } /// Use `read_image` if possible, @@ -242,7 +264,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { // reads with or without alpha, depending on `self.should_include_alpha` fn read_image_with_progress(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> { let blocks_in_header = self.selected_exr_header().chunk_count as u64; - let channel_count = if self.alpha_requested { 4 } else { 3 }; + let channel_count = self.color_type().channel_count() as usize; let result = read() .no_deep_data().largest_resolution_level() diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 59dfef0b00..f4f4ce1a30 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -84,7 +84,7 @@ pub fn load(r: R, format: ImageFormat) -> ImageResult DynamicImage::from_decoder(hdr::HdrAdapter::new(BufReader::new(r))?), #[cfg(feature = "openexr")] - image::ImageFormat::Exr => DynamicImage::from_decoder(openexr::ExrDecoder::read(r)?), + image::ImageFormat::Exr => DynamicImage::from_decoder(openexr::ExrDecoder::read(r, None)?), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(BufReader::new(r))?), #[cfg(feature = "farbfeld")] @@ -134,7 +134,7 @@ pub(crate) fn image_dimensions_with_format_impl(fin: R, forma #[cfg(feature = "hdr")] image::ImageFormat::Hdr => hdr::HdrAdapter::new(fin)?.dimensions(), #[cfg(feature = "openexr")] - image::ImageFormat::Exr => openexr::ExrDecoder::read(fin)?.dimensions(), + image::ImageFormat::Exr => openexr::ExrDecoder::read(fin, None)?.dimensions(), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => { pnm::PnmDecoder::new(fin)?.dimensions() From 9ee062b55ec2c98cd490d341ccabc5d0b2089a43 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 15:18:06 +0200 Subject: [PATCH 22/45] add docs to encoder wrapper --- src/codecs/openexr.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 9ad023c8e3..912b057fdf 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -368,10 +368,16 @@ pub fn write_buffer( // TODO is this struct and trait actually used anywhere? +/// A thin wrapper that implements `ImageEncoder` for OpenEXR images. Will behave like `image::codecs::openexr::write_buffer`. #[derive(Debug)] pub struct Encoder (W); -impl Encoder { pub fn new(write: W) -> Self {Self(write)} } +impl Encoder { + /// Create an `ImageEncoder`. Does not write anything yet. Will behave like `image::codecs::openexr::write_buffer`. + // function for future backwards-compatibility + pub fn new(write: W) -> Self {Self(write)} +} impl ImageEncoder for Encoder where W: Write /*+ Seek*/ { + /// Writes the complete image. Behaves just like `image::codecs::openexr::write_buffer`. fn write_image(self, buf: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { write_buffer(self.0, buf, width, height, color_type) } From 724cc0cd4b5dda2d7613d95d15f6d413416df56f Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 4 Jun 2021 16:08:01 +0200 Subject: [PATCH 23/45] remove commented-out functions --- src/codecs/openexr.rs | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 912b057fdf..e26561004f 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -135,11 +135,9 @@ pub struct ExrDecoder { header_index: usize, // decode either rgb or rgba. - // by default, only activated if image contains alpha. - // can be changed to include or discard alpha channels. + // can be specified to include or discard alpha channels. alpha_preference: Option, - // whether the original file contains alpha alpha_present_in_file: bool, } @@ -193,40 +191,6 @@ impl ExrDecoder { fn selected_exr_header(&self) -> &exr::meta::header::Header { &self.exr_reader.meta_data().headers[self.header_index] } - - /*// TODO does using Rgba with Vec automagically un-flatten the vector?? - /// Read this OpenEXR file as an `RgbaF32Buffer`, - /// including the alpha channel. A default value of `1.0` is used - /// if there is no alpha channel in the image. - /// Any calls to `color_type` or `total_bytes` - /// might not agree with the result of this function. - // TODO progress? load rect? - pub fn read_as_rgba_image(mut self) -> ImageResult { - let (width, height) = self.dimensions(); - let buffer: Vec = decoder_to_vec(self)?; - - ImageBuffer::from_raw(width, height, buffer) - - // this should be the only reason for the "from raw" call to fail, - // even though such a large allocation would probably cause an error much earlier - .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) - } - - // TODO does using Rgba with Vec automagically un-flatten the vector?? - /// Read this OpenEXR file as an `RgbF32Buffer`, discarding alpha. - /// Any calls to `color_type` or `total_bytes` - /// might not agree with the result of this function. - // TODO progress? load rect? - pub fn read_as_rgb_image(mut self) -> ImageResult { - let (width, height) = self.dimensions(); - let buffer: Vec = decoder_to_vec(self)?; - - ImageBuffer::from_raw(width, height, buffer) - - // this should be the only reason for the "from raw" call to fail, - // even though such a large allocation would probably cause an error much earlier - .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) - }*/ } From fa1bf366068bacfa813fa33e411cda326e34617a Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Mon, 7 Jun 2021 00:12:17 +0200 Subject: [PATCH 24/45] documentation improvements --- src/codecs/openexr.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index e26561004f..3c7a387d67 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -5,7 +5,7 @@ //! //! This decoder only supports RGB and RGBA images. //! If an image does not contain alpha information, -//! it is defaulted to `1.0`, which means no transparency. +//! it is defaulted to `1.0` (no transparency). //! //! # Related Links //! * - The OpenEXR reference. @@ -21,6 +21,7 @@ use crate::error::{DecodingError, ImageFormatHint, LimitError, LimitErrorKind, E use crate::image::decoder_to_vec; use std::path::Path; + /// An image buffer for 32-bit float RGB pixels, /// where the backing container is a flattened vector of floats. pub type RgbF32Buffer = ImageBuffer, Vec>; @@ -181,8 +182,8 @@ impl ExrDecoder { .is_some(); Ok(Self { + alpha_preference, exr_reader, header_index, - alpha_preference: alpha_preference, alpha_present_in_file: has_alpha }) } @@ -211,8 +212,8 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { if self.alpha_present_in_file { ExtendedColorType::Rgba32F } else { ExtendedColorType::Rgb32F } } - /// Use `read_image` if possible, - /// as this method creates a whole new buffer to contain the entire image. + /// Use `read_image` instead if possible, + /// as this method creates a whole new buffer just to contain the entire image. fn into_reader(self) -> ImageResult { Ok(Cursor::new(decoder_to_vec(self)?)) // TODO no vec? } @@ -225,7 +226,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { self.total_bytes() } - // reads with or without alpha, depending on `self.should_include_alpha` + // reads with or without alpha, depending on `self.alpha_preference` and `self.alpha_present_in_file` fn read_image_with_progress(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> { let blocks_in_header = self.selected_exr_header().chunk_count as u64; let channel_count = self.color_type().channel_count() as usize; @@ -336,10 +337,11 @@ pub fn write_buffer( #[derive(Debug)] pub struct Encoder (W); impl Encoder { - /// Create an `ImageEncoder`. Does not write anything yet. Will behave like `image::codecs::openexr::write_buffer`. - // function for future backwards-compatibility + /// Create an `ImageEncoder`. Does not write anything yet. Writing later will behave like `image::codecs::openexr::write_buffer`. + // use constructor, not public field, for future backwards-compatibility pub fn new(write: W) -> Self {Self(write)} } + impl ImageEncoder for Encoder where W: Write /*+ Seek*/ { /// Writes the complete image. Behaves just like `image::codecs::openexr::write_buffer`. fn write_image(self, buf: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { @@ -378,8 +380,6 @@ mod test { ).unwrap().read_image_hdr().unwrap(); let exr_pixels: RgbF32Buffer = read_as_rgb_image_from_file(exr_path).unwrap(); - - // TODO let hdr = ImageBuffer::from_raw(___) assert_eq!(exr_pixels.dimensions().0 * exr_pixels.dimensions().1, hdr.len() as u32); for (expected, found) in hdr.iter().zip(exr_pixels.pixels()){ From 072fcfd2483b6d96dd360311ae3a3ce9eeceedb3 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Mon, 7 Jun 2021 00:15:02 +0200 Subject: [PATCH 25/45] simplify imports, clarify reader buffering in documentation and remove double buffering. --- src/io/free_functions.rs | 38 +++++++------------------------------- src/io/reader.rs | 16 +++++++++++----- 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index f4f4ce1a30..8943dd0ecb 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -3,34 +3,7 @@ use std::io::{BufRead, BufReader, BufWriter, Seek}; use std::path::Path; use std::u32; -#[cfg(feature = "bmp")] -use crate::codecs::bmp; -#[cfg(feature = "gif")] -use crate::codecs::gif; -#[cfg(feature = "hdr")] -use crate::codecs::hdr; -#[cfg(feature = "openexr")] -use crate::codecs::openexr; -#[cfg(feature = "ico")] -use crate::codecs::ico; -#[cfg(feature = "jpeg")] -use crate::codecs::jpeg; -#[cfg(feature = "png")] -use crate::codecs::png; -#[cfg(feature = "pnm")] -use crate::codecs::pnm; -#[cfg(feature = "tga")] -use crate::codecs::tga; -#[cfg(feature = "dds")] -use crate::codecs::dds; -#[cfg(feature = "tiff")] -use crate::codecs::tiff; -#[cfg(feature = "webp")] -use crate::codecs::webp; -#[cfg(feature = "farbfeld")] -use crate::codecs::farbfeld; -#[cfg(any(feature = "avif-encoder", feature = "avif-decoder"))] -use crate::codecs::avif; +use crate::codecs::*; use crate::{ImageOutputFormat, color, error::{UnsupportedError, UnsupportedErrorKind}}; use crate::image; @@ -50,7 +23,10 @@ pub(crate) fn open_impl(path: &Path) -> ImageResult { load(fin, ImageFormat::from_path(path)?) } -/// Create a new image from a Reader +/// Create a new image from a Reader. +/// +/// Assumes the reader is already buffered. For optimal performance, +/// consider wrapping the reader with a `BufRead::new()`. /// /// Try [`io::Reader`] for more advanced uses. /// @@ -82,11 +58,11 @@ pub fn load(r: R, format: ImageFormat) -> ImageResult DynamicImage::from_decoder(ico::IcoDecoder::new(r)?), #[cfg(feature = "hdr")] - image::ImageFormat::Hdr => DynamicImage::from_decoder(hdr::HdrAdapter::new(BufReader::new(r))?), + image::ImageFormat::Hdr => DynamicImage::from_decoder(hdr::HdrAdapter::new(r)?), #[cfg(feature = "openexr")] image::ImageFormat::Exr => DynamicImage::from_decoder(openexr::ExrDecoder::read(r, None)?), #[cfg(feature = "pnm")] - image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(BufReader::new(r))?), + image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(r)?), #[cfg(feature = "farbfeld")] image::ImageFormat::Farbfeld => DynamicImage::from_decoder(farbfeld::FarbfeldDecoder::new(r)?), _ => Err(ImageError::Unsupported(ImageFormatHint::Exact(format).into())), diff --git a/src/io/reader.rs b/src/io/reader.rs index b1320fa6f2..58e641bb85 100644 --- a/src/io/reader.rs +++ b/src/io/reader.rs @@ -59,7 +59,7 @@ use super::free_functions; /// [`set_format`]: #method.set_format /// [`ImageDecoder`]: ../trait.ImageDecoder.html pub struct Reader { - /// The reader. + /// The reader. Should be buffered. inner: R, /// The format, if one has been set or deduced. format: Option, @@ -68,22 +68,28 @@ pub struct Reader { impl Reader { /// Create a new image reader without a preset format. /// + /// Assumes the reader is already buffered. For optimal performance, + /// consider wrapping the reader with a `BufRead::new()`. + /// /// It is possible to guess the format based on the content of the read object with /// [`with_guessed_format`], or to set the format directly with [`set_format`]. /// /// [`with_guessed_format`]: #method.with_guessed_format /// [`set_format`]: method.set_format - pub fn new(reader: R) -> Self { + pub fn new(buffered_reader: R) -> Self { Reader { - inner: reader, + inner: buffered_reader, format: None, } } /// Construct a reader with specified format. - pub fn with_format(reader: R, format: ImageFormat) -> Self { + /// + /// Assumes the reader is already buffered. For optimal performance, + /// consider wrapping the reader with a `BufRead::new()`. + pub fn with_format(buffered_reader: R, format: ImageFormat) -> Self { Reader { - inner: reader, + inner: buffered_reader, format: Some(format), } } From a3b2bda9b5043a60a343a468657f0c4916a51989 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Mon, 7 Jun 2021 00:34:16 +0200 Subject: [PATCH 26/45] rename exr to openexr, move ImageBuffer type alias definitions to the existing type aliases --- src/buffer.rs | 11 +++++++- src/codecs/openexr.rs | 58 +++++++++++++++++----------------------- src/image.rs | 18 ++++++------- src/io/free_functions.rs | 8 +++--- 4 files changed, 48 insertions(+), 47 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 393be750aa..6976f3485d 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -896,8 +896,9 @@ where /// Saves the buffer to a file at the path specified. /// /// The image format is derived from the file extension. - /// Currently only jpeg, png, ico, pnm, bmp, exr and + /// Currently only jpeg, png, ico, pnm, bmp and /// tiff files are supported. + // TODO exr supported, but Rgba32F is not yet pub fn save(&self, path: Q) -> ImageResult<()> where Q: AsRef, @@ -1343,6 +1344,14 @@ pub(crate) type Gray16Image = ImageBuffer, Vec>; /// Sendable 16-bit grayscale + alpha channel image buffer pub(crate) type GrayAlpha16Image = ImageBuffer, Vec>; +/// An image buffer for 32-bit float RGB pixels, +/// where the backing container is a flattened vector of floats. +pub(crate) type Rgb32FImage = ImageBuffer, Vec>; + +/// An image buffer for 32-bit float RGBA pixels, +/// where the backing container is a flattened vector of floats. +pub(crate) type Rgba32FImage = ImageBuffer, Vec>; + #[cfg(test)] mod test { use super::{ImageBuffer, RgbImage}; diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 3c7a387d67..5debd5b141 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -20,30 +20,22 @@ use std::io::{Write, Seek, BufRead, Cursor, BufReader, BufWriter}; use crate::error::{DecodingError, ImageFormatHint, LimitError, LimitErrorKind, EncodingError}; use crate::image::decoder_to_vec; use std::path::Path; - - -/// An image buffer for 32-bit float RGB pixels, -/// where the backing container is a flattened vector of floats. -pub type RgbF32Buffer = ImageBuffer, Vec>; - -/// An image buffer for 32-bit float RGBA pixels, -/// where the backing container is a flattened vector of floats. -pub type RgbaF32Buffer = ImageBuffer, Vec>; +use crate::buffer_::{Rgb32FImage, Rgba32FImage}; // TODO this could be a generic function that works for any format -/// Read the file from the specified path into an `RgbaF32Buffer`. +/// Read the file from the specified path into an `Rgba32FImage`. // TODO progress? load rect? -pub fn read_as_rgba_image_from_file(path: impl AsRef) -> ImageResult { +pub fn read_as_rgba_image_from_file(path: impl AsRef) -> ImageResult { read_as_rgba_image(BufReader::new(std::fs::File::open(path)?)) } // TODO this could be a generic function that works for any format -/// Read an `RgbaF32Buffer`. +/// Read an `Rgba32FImage`. /// Assumes the reader is buffered. In most cases, /// you should wrap your reader in a `BufReader` for best performance. // TODO progress? load rect? -pub fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult { +pub fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult { let decoder = ExrDecoder::read(read, Some(true))?; debug_assert_eq!(decoder.color_type(), ColorType::Rgba32F); @@ -58,18 +50,18 @@ pub fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult) -> ImageResult { +pub fn read_as_rgb_image_from_file(path: impl AsRef) -> ImageResult { read_as_rgb_image(BufReader::new(std::fs::File::open(path)?)) } // TODO this could be a generic function that works for any format -/// Read an `RgbF32Buffer`. +/// Read an `Rgb32FImage`. /// Assumes the reader is buffered. In most cases, /// you should wrap your reader in a `BufReader` for best performance. // TODO progress? load rect? -pub fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult { +pub fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult { let decoder = ExrDecoder::read(read, Some(false))?; debug_assert_eq!(decoder.color_type(), ColorType::Rgb32F); @@ -85,18 +77,18 @@ pub fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult // TODO this could be a generic function that works for any format -/// Write an `RgbF32Buffer` to the specified file path, replacing any existing file. +/// Write an `Rgb32FImage` to the specified file path, replacing any existing file. // TODO progress? load rect? -pub fn write_rgb_image_to_file(path: impl AsRef, image: &RgbF32Buffer) -> ImageResult<()> { +pub fn write_rgb_image_to_file(path: impl AsRef, image: &Rgb32FImage) -> ImageResult<()> { write_rgb_image(BufWriter::new(std::fs::File::create(path)?), image) } // TODO this could be a generic function that works for any format -/// Write an `RgbF32Buffer`. +/// Write an `Rgb32FImage`. /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. // TODO progress? load rect? -pub fn write_rgb_image(write: impl Write/* + Seek*/, image: &RgbF32Buffer) -> ImageResult<()> { +pub fn write_rgb_image(write: impl Write/* + Seek*/, image: &Rgb32FImage) -> ImageResult<()> { write_buffer( write, bytemuck::cast_slice(image.as_raw().as_slice()), @@ -106,18 +98,18 @@ pub fn write_rgb_image(write: impl Write/* + Seek*/, image: &RgbF32Buffer) -> Im } // TODO this could be a generic function that works for any format -/// Write an `RgbaF32Buffer` to the specified file path, replacing any existing file. +/// Write an `Rgba32FImage` to the specified file path, replacing any existing file. // TODO progress? load rect? -pub fn write_rgba_image_to_file(path: impl AsRef, image: &RgbaF32Buffer) -> ImageResult<()> { +pub fn write_rgba_image_to_file(path: impl AsRef, image: &Rgba32FImage) -> ImageResult<()> { write_rgba_image(BufWriter::new(std::fs::File::create(path)?), image) } // TODO this could be a generic function that works for any format -/// Write an `RgbaF32Buffer`. +/// Write an `Rgba32FImage`. /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. // TODO progress? load rect? -pub fn write_rgba_image(write: impl Write/* + Seek*/, image: &RgbaF32Buffer) -> ImageResult<()> { +pub fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> ImageResult<()> { write_buffer( write, bytemuck::cast_slice(image.as_raw().as_slice()), @@ -169,7 +161,7 @@ impl ExrDecoder { !header.deep && has_rgb }) .ok_or_else(|| ImageError::Decoding(DecodingError::new( - ImageFormatHint::Exact(ImageFormat::Exr), + ImageFormatHint::Exact(ImageFormat::OpenExr), "image does not contain non-deep rgb channels" )))?; @@ -322,7 +314,7 @@ pub fn write_buffer( } unsupported_color_type => return Err(ImageError::Encoding(EncodingError::new( - ImageFormatHint::Exact(ImageFormat::Exr), + ImageFormatHint::Exact(ImageFormat::OpenExr), format!("color type {:?} not yet supported", unsupported_color_type) ))) } @@ -351,7 +343,7 @@ impl ImageEncoder for Encoder where W: Write /*+ Seek*/ { fn to_image_err(exr_error: Error) -> ImageError { ImageError::Decoding(DecodingError::new( - ImageFormatHint::Exact(ImageFormat::Exr), + ImageFormatHint::Exact(ImageFormat::OpenExr), exr_error.to_string() )) } @@ -379,7 +371,7 @@ mod test { std::io::BufReader::new(std::fs::File::open(&reference_path).unwrap()) ).unwrap().read_image_hdr().unwrap(); - let exr_pixels: RgbF32Buffer = read_as_rgb_image_from_file(exr_path).unwrap(); + let exr_pixels: Rgb32FImage = read_as_rgb_image_from_file(exr_path).unwrap(); assert_eq!(exr_pixels.dimensions().0 * exr_pixels.dimensions().1, hdr.len() as u32); for (expected, found) in hdr.iter().zip(exr_pixels.pixels()){ @@ -397,7 +389,7 @@ mod test { let mut next_random = vec![ 1.0, 0.0, -1.0, -3.14, 27.0, 11.0, 31.0 ].into_iter().cycle(); let mut next_random = move || next_random.next().unwrap(); - let generated_image: RgbaF32Buffer = ImageBuffer::from_fn(9, 31, |_x, _y|{ + let generated_image: Rgba32FImage = ImageBuffer::from_fn(9, 31, |_x, _y|{ Rgba([next_random(), next_random(), next_random(), next_random()]) }); @@ -413,7 +405,7 @@ mod test { let mut next_random = vec![ 1.0, 0.0, -1.0, -3.14, 27.0, 11.0, 31.0 ].into_iter().cycle(); let mut next_random = move || next_random.next().unwrap(); - let generated_image: RgbF32Buffer = ImageBuffer::from_fn(9, 31, |_x, _y|{ + let generated_image: Rgb32FImage = ImageBuffer::from_fn(9, 31, |_x, _y|{ Rgb([next_random(), next_random(), next_random()]) }); @@ -429,8 +421,8 @@ mod test { let exr_path = BASE_PATH.iter().collect::() .join("overexposed gradient - data window equals display window.exr"); - let rgb: RgbF32Buffer = read_as_rgb_image_from_file(&exr_path).unwrap(); - let rgba: RgbaF32Buffer = read_as_rgba_image_from_file(&exr_path).unwrap(); + let rgb: Rgb32FImage = read_as_rgb_image_from_file(&exr_path).unwrap(); + let rgba: Rgba32FImage = read_as_rgba_image_from_file(&exr_path).unwrap(); assert_eq!(rgba.dimensions(), rgb.dimensions()); diff --git a/src/image.rs b/src/image.rs index bd7d287faa..585de1c3f6 100644 --- a/src/image.rs +++ b/src/image.rs @@ -56,7 +56,7 @@ pub enum ImageFormat { Hdr, /// An Image in OpenEXR Format - Exr, + OpenExr, /// An Image in farbfeld Format Farbfeld, @@ -97,7 +97,7 @@ impl ImageFormat { "bmp" => ImageFormat::Bmp, "ico" => ImageFormat::Ico, "hdr" => ImageFormat::Hdr, - "exr" => ImageFormat::Exr, + "exr" => ImageFormat::OpenExr, "pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm, "ff" | "farbfeld" => ImageFormat::Farbfeld, _ => return None, @@ -153,7 +153,7 @@ impl ImageFormat { ImageFormat::Bmp => true, ImageFormat::Ico => true, ImageFormat::Hdr => true, - ImageFormat::Exr => true, + ImageFormat::OpenExr => true, ImageFormat::Pnm => true, ImageFormat::Farbfeld => true, ImageFormat::Avif => true, @@ -178,7 +178,7 @@ impl ImageFormat { ImageFormat::Avif => true, ImageFormat::WebP => false, ImageFormat::Hdr => false, - ImageFormat::Exr => true, + ImageFormat::OpenExr => true, ImageFormat::Dds => false, ImageFormat::__NonExhaustive(marker) => match marker._private {}, } @@ -206,7 +206,7 @@ impl ImageFormat { ImageFormat::Bmp => &["bmp"], ImageFormat::Ico => &["ico"], ImageFormat::Hdr => &["hdr"], - ImageFormat::Exr => &["exr"], + ImageFormat::OpenExr => &["exr"], ImageFormat::Farbfeld => &["ff"], // According to: https://aomediacodec.github.io/av1-avif/#mime-registration ImageFormat::Avif => &["avif"], @@ -252,7 +252,7 @@ pub enum ImageOutputFormat { #[cfg(feature = "openexr")] /// An Image in OpenEXR Format - Exr, + OpenExr, #[cfg(feature = "tiff")] /// An Image in TIFF Format @@ -291,7 +291,7 @@ impl From for ImageOutputFormat { #[cfg(feature = "tga")] ImageFormat::Tga => ImageOutputFormat::Tga, #[cfg(feature = "openexr")] - ImageFormat::Exr => ImageOutputFormat::Exr, + ImageFormat::OpenExr => ImageOutputFormat::OpenExr, #[cfg(feature = "tiff")] ImageFormat::Tiff => ImageOutputFormat::Tiff, @@ -1291,7 +1291,7 @@ mod tests { assert_eq!(from_path("./a.bmp").unwrap(), ImageFormat::Bmp); assert_eq!(from_path("./a.Ico").unwrap(), ImageFormat::Ico); assert_eq!(from_path("./a.hdr").unwrap(), ImageFormat::Hdr); - assert_eq!(from_path("./a.exr").unwrap(), ImageFormat::Exr); + assert_eq!(from_path("./a.exr").unwrap(), ImageFormat::OpenExr); assert_eq!(from_path("./a.pbm").unwrap(), ImageFormat::Pnm); assert_eq!(from_path("./a.pAM").unwrap(), ImageFormat::Pnm); assert_eq!(from_path("./a.Ppm").unwrap(), ImageFormat::Pnm); @@ -1392,7 +1392,7 @@ mod tests { #[test] fn image_formats_are_recognized() { use ImageFormat::*; - const ALL_FORMATS: &'static [ImageFormat] = &[Avif, Png, Jpeg, Gif, WebP, Pnm, Tiff, Tga, Dds, Bmp, Ico, Hdr, Farbfeld, Exr]; + const ALL_FORMATS: &'static [ImageFormat] = &[Avif, Png, Jpeg, Gif, WebP, Pnm, Tiff, Tga, Dds, Bmp, Ico, Hdr, Farbfeld, OpenExr]; for &format in ALL_FORMATS { let mut file = Path::new("file.nothing").to_owned(); for ext in format.extensions_str() { diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 8943dd0ecb..d8a342f024 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -60,7 +60,7 @@ pub fn load(r: R, format: ImageFormat) -> ImageResult DynamicImage::from_decoder(hdr::HdrAdapter::new(r)?), #[cfg(feature = "openexr")] - image::ImageFormat::Exr => DynamicImage::from_decoder(openexr::ExrDecoder::read(r, None)?), + image::ImageFormat::OpenExr => DynamicImage::from_decoder(openexr::ExrDecoder::read(r, None)?), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(r)?), #[cfg(feature = "farbfeld")] @@ -110,7 +110,7 @@ pub(crate) fn image_dimensions_with_format_impl(fin: R, forma #[cfg(feature = "hdr")] image::ImageFormat::Hdr => hdr::HdrAdapter::new(fin)?.dimensions(), #[cfg(feature = "openexr")] - image::ImageFormat::Exr => openexr::ExrDecoder::read(fin, None)?.dimensions(), + image::ImageFormat::OpenExr => openexr::ExrDecoder::read(fin, None)?.dimensions(), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => { pnm::PnmDecoder::new(fin)?.dimensions() @@ -201,7 +201,7 @@ pub(crate) fn write_buffer_impl( #[cfg(feature = "tga")] ImageOutputFormat::Tga => tga::TgaEncoder::new(fout).write_image(buf, width, height, color), #[cfg(feature = "openexr")] - ImageOutputFormat::Exr => openexr::write_buffer(fout, buf, width, height, color), + ImageOutputFormat::OpenExr => openexr::write_buffer(fout, buf, width, height, color), #[cfg(feature = "tiff")] ImageOutputFormat::Tiff => { let mut cursor = std::io::Cursor::new(Vec::new()); @@ -245,7 +245,7 @@ static MAGIC_BYTES: [(&[u8], ImageFormat); 21] = [ (b"P7", ImageFormat::Pnm), (b"farbfeld", ImageFormat::Farbfeld), (b"\0\0\0 ftypavif", ImageFormat::Avif), - (&[0x76, 0x2f, 0x31, 0x01], ImageFormat::Exr), // = &exr::meta::magic_number::BYTES + (&[0x76, 0x2f, 0x31, 0x01], ImageFormat::OpenExr), // = &exr::meta::magic_number::BYTES ]; /// Guess image format from memory block From 465f927562eaebf2bf1675138f3181230cbe4601 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Mon, 7 Jun 2021 00:46:26 +0200 Subject: [PATCH 27/45] remove outdated todo comment --- src/codecs/openexr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 5debd5b141..542f31f7c7 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -291,7 +291,7 @@ pub fn write_buffer( ) .write() // .on_progress(|progress| todo!()) - .to_buffered(&mut seekable_write).map_err(to_image_err)?; // TODO BufWrite::new()? + .to_buffered(&mut seekable_write).map_err(to_image_err)?; } ColorType::Rgba32F => { @@ -310,7 +310,7 @@ pub fn write_buffer( ) .write() // .on_progress(|progress| todo!()) - .to_buffered(&mut seekable_write).map_err(to_image_err)?; // TODO BufWrite::new()? + .to_buffered(&mut seekable_write).map_err(to_image_err)?; } unsupported_color_type => return Err(ImageError::Encoding(EncodingError::new( From 7198c5ad37e07a4b92de6fc1088f82c9791a8dc4 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Mon, 7 Jun 2021 14:43:25 +0200 Subject: [PATCH 28/45] use display window instead of loading only the layer data --- src/codecs/openexr.rs | 69 +++++++++++++++--- ...g - data window differs display window.exr | Bin 0 -> 102452 bytes .../exr/cropping - uncropped original.exr | Bin 0 -> 1049298 bytes 3 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 tests/images/exr/cropping - data window differs display window.exr create mode 100644 tests/images/exr/cropping - uncropped original.exr diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 542f31f7c7..13c09d4013 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -191,7 +191,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { type Reader = Cursor>; fn dimensions(&self) -> (u32, u32) { - let size = self.selected_exr_header().layer_size; + let size = self.selected_exr_header().shared_attributes.display_window.size; (size.width() as u32, size.height() as u32) } @@ -223,21 +223,34 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { let blocks_in_header = self.selected_exr_header().chunk_count as u64; let channel_count = self.color_type().channel_count() as usize; + 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 result = read() .no_deep_data().largest_resolution_level() .rgba_channels( - |size, _channels| { + move |_size, _channels| { // TODO debug_assert_eq!(self.header_index, header_index); - (size, vec![0_f32; size.area() * channel_count]) + vec![0_f32; display_window.size.area() * channel_count] }, - move |(size, buffer), position, (r,g,b,a_or_1): (f32,f32,f32,f32)| { - let first_f32_index = position.flat_index_for_size(*size); + move |buffer, index_in_data_window, (r,g,b,a_or_1): (f32,f32,f32,f32)| { + let index_in_display_window = index_in_data_window.to_i32() + data_window_offset; + + // only keep pixels inside the data window + // TODO filter chunks based on this + if index_in_display_window.x() >= 0 && index_in_display_window.y() >= 0 + && index_in_display_window.x() < display_window.size.width() as i32 + && index_in_display_window.y() < display_window.size.height() as i32 + { + let index_in_display_window = index_in_display_window.to_usize("index bug").unwrap(); + let first_f32_index = index_in_display_window.flat_index_for_size(display_window.size); - buffer[first_f32_index * channel_count .. (first_f32_index + 1) * channel_count] - .copy_from_slice(&[r, g, b, a_or_1][0 .. channel_count]); + buffer[first_f32_index * channel_count .. (first_f32_index + 1) * channel_count] + .copy_from_slice(&[r, g, b, a_or_1][0 .. channel_count]); - // TODO white point chromaticities + srgb/linear conversion? + // TODO white point chromaticities + srgb/linear conversion? + } } ) .first_valid_layer() // TODO select exact layer by self.header_index? @@ -250,7 +263,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { .from_chunks(self.exr_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 - target.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.1.as_slice())); + target.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.as_slice())); Ok(()) } } @@ -281,7 +294,7 @@ pub fn write_buffer( debug_assert_eq!(pixels.len(), width * height * 3, "invalid dimensions for byte buffer length"); - exr::prelude::Image + exr::prelude::Image // TODO compression method zip?? ::from_channels( (width, height), SpecificChannels::rgb(|pixel: Vec2| { @@ -300,7 +313,7 @@ pub fn write_buffer( debug_assert_eq!(pixels.len(), width * height * 4, "invalid dimensions for byte buffer length"); - exr::prelude::Image + exr::prelude::Image // TODO compression method zip?? ::from_channels( (width, height), SpecificChannels::rgba(|pixel: Vec2| { @@ -430,4 +443,38 @@ mod test { assert_eq!(rgb, &rgba[..3]); } } + + #[test] + fn compare_cropped() { + // like in photoshop, exr images may have layers placed anywhere in a canvas. + // we don't want to load the pixels from the layer, but we want to load the pixels from the canvas. + // a layer might be smaller than the canvas, in that case the canvas should be transparent black + // where no layer was covering it. a layer might also be larger than the canvas, + // these pixels should be discarded. + // + // in this test we want to make sure that an + // auto-cropped image will be reproduced to the original. + + let exr_path = BASE_PATH.iter().collect::(); + let original = exr_path.clone().join("cropping - uncropped original.exr"); + let cropped = exr_path.clone().join("cropping - data window differs display window.exr"); + + // smoke-check that the exr files are actually not the same + { + let original_exr = exr::prelude::read_first_flat_layer_from_file(&original).unwrap(); + let cropped_exr = exr::prelude::read_first_flat_layer_from_file(&cropped).unwrap(); + assert_eq!(original_exr.attributes.display_window, cropped_exr.attributes.display_window); + assert_ne!(original_exr.layer_data.attributes.layer_position, cropped_exr.layer_data.attributes.layer_position); + assert_ne!(original_exr.layer_data.size, cropped_exr.layer_data.size); + } + + // check that they result in the same image + let original: Rgba32FImage = read_as_rgba_image_from_file(&original).unwrap(); + let cropped: Rgba32FImage = read_as_rgba_image_from_file(&cropped).unwrap(); + assert_eq!(original.dimensions(), cropped.dimensions()); + + // the following is not a simple assert_eq, as in case of an error, + // the whole image would be printed to the console, which takes forever + assert!(original.pixels().zip(cropped.pixels()).all(|(a,b)| a == b)); + } } diff --git a/tests/images/exr/cropping - data window differs display window.exr b/tests/images/exr/cropping - data window differs display window.exr new file mode 100644 index 0000000000000000000000000000000000000000..9a0a68654bde71f7f59486dfe4eb1f122f4ef1a2 GIT binary patch literal 102452 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

    P%F)v6 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 z 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 ADgXcg literal 0 HcmV?d00001 From 6f3ea90498ca32ee0310003aad1cdf701893f2ed Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Mon, 7 Jun 2021 23:43:28 +0200 Subject: [PATCH 29/45] allow aligned buffers, make progress pub(crate), make image type aliases public, rollback not buffering other formats, --- src/buffer.rs | 4 ++-- src/codecs/openexr.rs | 43 ++++++++++++++++++++++++---------------- src/image.rs | 4 ++-- src/io/free_functions.rs | 4 ++-- src/lib.rs | 41 +++++++++++++++++++++----------------- 5 files changed, 55 insertions(+), 41 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 6976f3485d..fc3d924873 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1346,11 +1346,11 @@ pub(crate) type GrayAlpha16Image = ImageBuffer, Vec>; /// An image buffer for 32-bit float RGB pixels, /// where the backing container is a flattened vector of floats. -pub(crate) type Rgb32FImage = ImageBuffer, Vec>; +pub type Rgb32FImage = ImageBuffer, Vec>; /// An image buffer for 32-bit float RGBA pixels, /// where the backing container is a flattened vector of floats. -pub(crate) type Rgba32FImage = ImageBuffer, Vec>; +pub type Rgba32FImage = ImageBuffer, Vec>; #[cfg(test)] mod test { diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 13c09d4013..db7d59b120 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -21,6 +21,7 @@ use crate::error::{DecodingError, ImageFormatHint, LimitError, LimitErrorKind, E use crate::image::decoder_to_vec; use std::path::Path; use crate::buffer_::{Rgb32FImage, Rgba32FImage}; +use std::convert::TryInto; // TODO this could be a generic function that works for any format @@ -219,7 +220,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { } // reads with or without alpha, depending on `self.alpha_preference` and `self.alpha_present_in_file` - fn read_image_with_progress(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> { + fn read_image_with_progress(self, unaligned_bytes: &mut [u8], progress_callback: F) -> ImageResult<()> { let blocks_in_header = self.selected_exr_header().chunk_count as u64; let channel_count = self.color_type().channel_count() as usize; @@ -263,7 +264,9 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { .from_chunks(self.exr_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 - target.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.as_slice())); + + // this cast is safe and works with any alignment, as bytes are copied, and not f32 values. + unaligned_bytes.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.as_slice())); Ok(()) } } @@ -278,28 +281,36 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. pub fn write_buffer( - mut buffered_write: impl Write/* + Seek*/, bytes: &[u8], + mut buffered_write: impl Write/* + Seek*/, unaligned_bytes: &[u8], width: u32, height: u32, color_type: ColorType ) -> ImageResult<()> { let width = width as usize; let height = height as usize; + if unaligned_bytes.len() < width * height * color_type.bytes_per_pixel() as usize { + return Err(ImageError::Encoding(EncodingError::new( + ImageFormatHint::Exact(ImageFormat::OpenExr), + "byte buffer must have enough bytes for all the f32 pixels" + ))); + } + let mut seekable_write = Cursor::new(Vec::with_capacity(width*height*3*4)); // TODO remove + // bytes might be unaligned so we cannot cast the whole thing, instead lookup each f32 individually + let lookup_f32 = move |f32_index: usize| { + let f32_bytes_slice = &unaligned_bytes[f32_index * 4 .. (f32_index + 1) * 4]; + f32::from_ne_bytes(f32_bytes_slice.try_into().expect("indexing error")) + }; + match color_type { ColorType::Rgb32F => { - let pixels: &[f32] = bytemuck::try_cast_slice(bytes) - .expect("image byte buffer must be aligned to f32"); - - debug_assert_eq!(pixels.len(), width * height * 3, "invalid dimensions for byte buffer length"); - exr::prelude::Image // TODO compression method zip?? ::from_channels( (width, height), SpecificChannels::rgb(|pixel: Vec2| { let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width, height)); - (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2]) + (lookup_f32(pixel_index), lookup_f32(pixel_index+1), lookup_f32(pixel_index+2)) }) ) .write() @@ -308,17 +319,15 @@ pub fn write_buffer( } ColorType::Rgba32F => { - let pixels: &[f32] = bytemuck::try_cast_slice(bytes) - .expect("image byte buffer must be aligned to f32"); - - debug_assert_eq!(pixels.len(), width * height * 4, "invalid dimensions for byte buffer length"); - exr::prelude::Image // TODO compression method zip?? ::from_channels( (width, height), SpecificChannels::rgba(|pixel: Vec2| { let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width, height)); - (pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2], pixels[pixel_index+3]) + ( + lookup_f32(pixel_index), lookup_f32(pixel_index+1), + lookup_f32(pixel_index+2), lookup_f32(pixel_index+3) + ) }) ) .write() @@ -461,8 +470,8 @@ mod test { // smoke-check that the exr files are actually not the same { - let original_exr = exr::prelude::read_first_flat_layer_from_file(&original).unwrap(); - let cropped_exr = exr::prelude::read_first_flat_layer_from_file(&cropped).unwrap(); + let original_exr = read_first_flat_layer_from_file(&original).unwrap(); + let cropped_exr = read_first_flat_layer_from_file(&cropped).unwrap(); assert_eq!(original_exr.attributes.display_window, cropped_exr.attributes.display_window); assert_ne!(original_exr.layer_data.attributes.layer_position, cropped_exr.layer_data.attributes.layer_position); assert_ne!(original_exr.layer_data.size, cropped_exr.layer_data.size); diff --git a/src/image.rs b/src/image.rs index 585de1c3f6..db409edc2b 100644 --- a/src/image.rs +++ b/src/image.rs @@ -524,8 +524,8 @@ pub struct Progress { } impl Progress { - /// Create Progress. - pub fn new(current: u64, total: u64) -> Self { + /// Create Progress. Result in invalid progress if you provide a greater `current` than `total`. + pub(crate) fn new(current: u64, total: u64) -> Self { Self { current, total } } diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index d8a342f024..816e535e58 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -58,11 +58,11 @@ pub fn load(r: R, format: ImageFormat) -> ImageResult DynamicImage::from_decoder(ico::IcoDecoder::new(r)?), #[cfg(feature = "hdr")] - image::ImageFormat::Hdr => DynamicImage::from_decoder(hdr::HdrAdapter::new(r)?), + image::ImageFormat::Hdr => DynamicImage::from_decoder(hdr::HdrAdapter::new(BufReader::new(r))?), #[cfg(feature = "openexr")] image::ImageFormat::OpenExr => DynamicImage::from_decoder(openexr::ExrDecoder::read(r, None)?), #[cfg(feature = "pnm")] - image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(r)?), + image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(BufReader::new(r))?), #[cfg(feature = "farbfeld")] image::ImageFormat::Farbfeld => DynamicImage::from_decoder(farbfeld::FarbfeldDecoder::new(r)?), _ => Err(ImageError::Unsupported(ImageFormatHint::Exact(format).into())), diff --git a/src/lib.rs b/src/lib.rs index 32712fed8b..a42c9ee0b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,26 +109,31 @@ pub use crate::color::{Luma, LumaA, Rgb, Rgba, Bgr, Bgra}; pub use crate::error::{ImageError, ImageResult}; -pub use crate::image::{AnimationDecoder, - GenericImage, - GenericImageView, - ImageDecoder, - ImageDecoderExt, - ImageEncoder, - ImageFormat, - ImageOutputFormat, - Progress, - // Iterators - Pixels, - SubImage}; +pub use crate::image::{ + AnimationDecoder, + GenericImage, + GenericImageView, + ImageDecoder, + ImageDecoderExt, + ImageEncoder, + ImageFormat, + ImageOutputFormat, + Progress, + // Iterators + Pixels, + SubImage +}; pub use crate::buffer_::{ - GrayAlphaImage, - GrayImage, - // Image types - ImageBuffer, - RgbImage, - RgbaImage}; + GrayAlphaImage, + GrayImage, + // Image types + ImageBuffer, + RgbImage, + RgbaImage, + Rgba32FImage, + Rgb32FImage, +}; pub use crate::flat::FlatSamples; From 4276b44b61c801a6a24e6e2e06352fc7d114528b Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Wed, 23 Jun 2021 23:10:59 +0200 Subject: [PATCH 30/45] implement trivial review suggestions --- src/codecs/openexr.rs | 94 ++++++++++++++++++---------------------- src/dynimage.rs | 2 +- src/io/free_functions.rs | 6 +-- 3 files changed, 47 insertions(+), 55 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index db7d59b120..3db629ccaf 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -16,7 +16,7 @@ extern crate exr; use exr::prelude::*; use crate::{ImageDecoder, ImageResult, ColorType, Progress, ImageError, ImageFormat, ImageBuffer, Rgba, Rgb, ImageEncoder, ExtendedColorType}; -use std::io::{Write, Seek, BufRead, Cursor, BufReader, BufWriter}; +use std::io::{Write, Seek, BufRead, Cursor, BufReader}; use crate::error::{DecodingError, ImageFormatHint, LimitError, LimitErrorKind, EncodingError}; use crate::image::decoder_to_vec; use std::path::Path; @@ -24,20 +24,14 @@ use crate::buffer_::{Rgb32FImage, Rgba32FImage}; use std::convert::TryInto; -// TODO this could be a generic function that works for any format -/// Read the file from the specified path into an `Rgba32FImage`. -// TODO progress? load rect? -pub fn read_as_rgba_image_from_file(path: impl AsRef) -> ImageResult { - read_as_rgba_image(BufReader::new(std::fs::File::open(path)?)) -} // TODO this could be a generic function that works for any format /// Read an `Rgba32FImage`. /// Assumes the reader is buffered. In most cases, /// you should wrap your reader in a `BufReader` for best performance. // TODO progress? load rect? -pub fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult { - let decoder = ExrDecoder::read(read, Some(true))?; +fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult { + let decoder = OpenExrDecoder::read(read, Some(true))?; debug_assert_eq!(decoder.color_type(), ColorType::Rgba32F); let (width, height) = decoder.dimensions(); @@ -50,20 +44,14 @@ pub fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult) -> ImageResult { - read_as_rgb_image(BufReader::new(std::fs::File::open(path)?)) -} // TODO this could be a generic function that works for any format /// Read an `Rgb32FImage`. /// Assumes the reader is buffered. In most cases, /// you should wrap your reader in a `BufReader` for best performance. // TODO progress? load rect? -pub fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult { - let decoder = ExrDecoder::read(read, Some(false))?; +fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult { + let decoder = OpenExrDecoder::read(read, Some(false))?; debug_assert_eq!(decoder.color_type(), ColorType::Rgb32F); let (width, height) = decoder.dimensions(); @@ -77,19 +65,13 @@ pub fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult } -// TODO this could be a generic function that works for any format -/// Write an `Rgb32FImage` to the specified file path, replacing any existing file. -// TODO progress? load rect? -pub fn write_rgb_image_to_file(path: impl AsRef, image: &Rgb32FImage) -> ImageResult<()> { - write_rgb_image(BufWriter::new(std::fs::File::create(path)?), image) -} // TODO this could be a generic function that works for any format /// Write an `Rgb32FImage`. /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. // TODO progress? load rect? -pub fn write_rgb_image(write: impl Write/* + Seek*/, image: &Rgb32FImage) -> ImageResult<()> { +fn write_rgb_image(write: impl Write/* + Seek*/, image: &Rgb32FImage) -> ImageResult<()> { write_buffer( write, bytemuck::cast_slice(image.as_raw().as_slice()), @@ -98,19 +80,12 @@ pub fn write_rgb_image(write: impl Write/* + Seek*/, image: &Rgb32FImage) -> Ima ) } -// TODO this could be a generic function that works for any format -/// Write an `Rgba32FImage` to the specified file path, replacing any existing file. -// TODO progress? load rect? -pub fn write_rgba_image_to_file(path: impl AsRef, image: &Rgba32FImage) -> ImageResult<()> { - write_rgba_image(BufWriter::new(std::fs::File::create(path)?), image) -} - // TODO this could be a generic function that works for any format /// Write an `Rgba32FImage`. /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. // TODO progress? load rect? -pub fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> ImageResult<()> { +fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> ImageResult<()> { write_buffer( write, bytemuck::cast_slice(image.as_raw().as_slice()), @@ -122,7 +97,7 @@ pub fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> I /// An OpenEXR decoder. Immediately reads the meta data from the file. #[derive(Debug)] -pub struct ExrDecoder { +pub struct OpenExrDecoder { exr_reader: exr::block::reader::Reader, // select a header that is rgb and not deep @@ -136,7 +111,7 @@ pub struct ExrDecoder { } -impl ExrDecoder { +impl OpenExrDecoder { /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. /// Assumes the reader is buffered. In most cases, @@ -153,9 +128,8 @@ impl ExrDecoder { .position(|header|{ let has_rgb = ["R","G","B"].iter().all( // alpha will be optional // check if r/g/b exists in the channels - // TODO binary search, channels are sorted |required| header.channels.list.iter() - .find(|chan| chan.name.eq(required)).is_some() // TODO eq_lowercase only if exrs supports it + .any(|chan| chan.name.eq(required)) // TODO use search("A") and eq_lowercase later when exrs supports it ); // we currently dont support deep images, or images with other color spaces than rgb @@ -166,13 +140,10 @@ impl ExrDecoder { "image does not contain non-deep rgb channels" )))?; - // TODO binary search, channels are sorted let has_alpha = exr_reader.headers()[header_index] .channels.list - //.binary_search_by_key(b"A", |chan| chan.name.bytes()) .iter() - .find(|chan| chan.name.eq("A")) // TODO eq_lowercase only if exrs supports it - .is_some(); + .any(|chan| chan.name.eq("A")); // TODO use search("A") and eq_lowercase later when exrs supports it Ok(Self { alpha_preference, @@ -188,7 +159,7 @@ impl ExrDecoder { } -impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { +impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for OpenExrDecoder { type Reader = Cursor>; fn dimensions(&self) -> (u32, u32) { @@ -208,7 +179,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { /// Use `read_image` instead if possible, /// as this method creates a whole new buffer just to contain the entire image. fn into_reader(self) -> ImageResult { - Ok(Cursor::new(decoder_to_vec(self)?)) // TODO no vec? + Ok(Cursor::new(decoder_to_vec(self)?)) } fn scanline_bytes(&self) -> u64 { @@ -231,7 +202,6 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { .no_deep_data().largest_resolution_level() .rgba_channels( move |_size, _channels| { - // TODO debug_assert_eq!(self.header_index, header_index); vec![0_f32; display_window.size.area() * channel_count] }, @@ -275,12 +245,13 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder { /// Write a raw byte buffer of pixels, -/// panicking if the buffer is not aligned to `f32` +/// returning an Error if the buffer is not aligned to `f32` /// or if it has an invalid length. /// /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. -pub fn write_buffer( +// private, access via `OpenExrEncoder` +fn write_buffer( mut buffered_write: impl Write/* + Seek*/, unaligned_bytes: &[u8], width: u32, height: u32, color_type: ColorType ) -> ImageResult<()> @@ -291,7 +262,7 @@ pub fn write_buffer( if unaligned_bytes.len() < width * height * color_type.bytes_per_pixel() as usize { return Err(ImageError::Encoding(EncodingError::new( ImageFormatHint::Exact(ImageFormat::OpenExr), - "byte buffer must have enough bytes for all the f32 pixels" + "byte buffer not large enough for the specified dimensions and f32 pixels" ))); } @@ -335,6 +306,7 @@ pub fn write_buffer( .to_buffered(&mut seekable_write).map_err(to_image_err)?; } + // TODO other color types and channel types unsupported_color_type => return Err(ImageError::Encoding(EncodingError::new( ImageFormatHint::Exact(ImageFormat::OpenExr), format!("color type {:?} not yet supported", unsupported_color_type) @@ -349,15 +321,24 @@ pub fn write_buffer( // TODO is this struct and trait actually used anywhere? /// A thin wrapper that implements `ImageEncoder` for OpenEXR images. Will behave like `image::codecs::openexr::write_buffer`. #[derive(Debug)] -pub struct Encoder (W); -impl Encoder { +pub struct OpenExrEncoder (W); + +impl OpenExrEncoder { + /// Create an `ImageEncoder`. Does not write anything yet. Writing later will behave like `image::codecs::openexr::write_buffer`. // use constructor, not public field, for future backwards-compatibility - pub fn new(write: W) -> Self {Self(write)} + pub fn new(write: W) -> Self { Self(write) } } -impl ImageEncoder for Encoder where W: Write /*+ Seek*/ { - /// Writes the complete image. Behaves just like `image::codecs::openexr::write_buffer`. +impl ImageEncoder for OpenExrEncoder where W: Write /*+ Seek*/ { + + /// Writes the complete image. + /// + /// Returns an Error if the buffer is not aligned to `f32` + /// or if it has an invalid length. + /// + /// Assumes the writer is buffered. In most cases, + /// you should wrap your writer in a `BufWriter` for best performance. fn write_image(self, buf: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> { write_buffer(self.0, buf, width, height, color_type) } @@ -379,6 +360,17 @@ mod test { const BASE_PATH: &[&str] = &[".", "tests", "images", "exr"]; + + /// Read the file from the specified path into an `Rgba32FImage`. + fn read_as_rgba_image_from_file(path: impl AsRef) -> ImageResult { + read_as_rgba_image(BufReader::new(std::fs::File::open(path)?)) + } + + /// Read the file from the specified path into an `Rgb32FImage`. + fn read_as_rgb_image_from_file(path: impl AsRef) -> ImageResult { + read_as_rgb_image(BufReader::new(std::fs::File::open(path)?)) + } + #[test] fn compare_exr_hdr() { assert!(cfg!(feature = "hdr"), "to run all the openexr tests, activate the hdr feature flag"); diff --git a/src/dynimage.rs b/src/dynimage.rs index 1b3bd910c9..ad5f0310c9 100644 --- a/src/dynimage.rs +++ b/src/dynimage.rs @@ -1315,7 +1315,7 @@ where /// /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. - /// +/// /// **Note**: TIFF encoding uses buffered writing, /// which can lead to unexpected use of resources pub fn write_buffer_with_format( diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 816e535e58..1e8702df7a 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -60,7 +60,7 @@ pub fn load(r: R, format: ImageFormat) -> ImageResult DynamicImage::from_decoder(hdr::HdrAdapter::new(BufReader::new(r))?), #[cfg(feature = "openexr")] - image::ImageFormat::OpenExr => DynamicImage::from_decoder(openexr::ExrDecoder::read(r, None)?), + image::ImageFormat::OpenExr => DynamicImage::from_decoder(openexr::OpenExrDecoder::read(r, None)?), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(BufReader::new(r))?), #[cfg(feature = "farbfeld")] @@ -110,7 +110,7 @@ pub(crate) fn image_dimensions_with_format_impl(fin: R, forma #[cfg(feature = "hdr")] image::ImageFormat::Hdr => hdr::HdrAdapter::new(fin)?.dimensions(), #[cfg(feature = "openexr")] - image::ImageFormat::OpenExr => openexr::ExrDecoder::read(fin, None)?.dimensions(), + image::ImageFormat::OpenExr => openexr::OpenExrDecoder::read(fin, None)?.dimensions(), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => { pnm::PnmDecoder::new(fin)?.dimensions() @@ -201,7 +201,7 @@ pub(crate) fn write_buffer_impl( #[cfg(feature = "tga")] ImageOutputFormat::Tga => tga::TgaEncoder::new(fout).write_image(buf, width, height, color), #[cfg(feature = "openexr")] - ImageOutputFormat::OpenExr => openexr::write_buffer(fout, buf, width, height, color), + ImageOutputFormat::OpenExr => openexr::OpenExrEncoder::new(fout).write_image(buf, width, height, color), #[cfg(feature = "tiff")] ImageOutputFormat::Tiff => { let mut cursor = std::io::Cursor::new(Vec::new()); From 6d7c80286ffa10e6f02ec136f0ab5bb26b0cb874 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Thu, 24 Jun 2021 21:04:42 +0200 Subject: [PATCH 31/45] bridge disconnected docs sections --- src/dynimage.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dynimage.rs b/src/dynimage.rs index b7cc02e17f..5e7f334a31 100644 --- a/src/dynimage.rs +++ b/src/dynimage.rs @@ -897,7 +897,7 @@ impl DynamicImage { } /// Encode this image and write it to ```w```. - + /// /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. /// @@ -1277,7 +1277,7 @@ where /// /// The image format is derived from the file extension. The buffer is assumed to have /// the correct format according to the specified color type. - +/// /// This will lead to corrupted files if the buffer contains malformed data. Currently only /// jpeg, png, ico, pnm, bmp, exr and tiff files are supported. pub fn save_buffer

    ( From b18339deb0f55619e7cecb59c295f1a6ed987758 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 25 Jun 2021 12:06:55 +0200 Subject: [PATCH 32/45] make helper functions private (plus re-implement the helper methods copy&paste for external fuzzer test scripts) --- fuzz/fuzzers/fuzzer_script_exr.rs | 14 +++ src/codecs/openexr.rs | 148 +++++++++++++++--------------- src/io/free_functions.rs | 4 +- 3 files changed, 90 insertions(+), 76 deletions(-) diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index b25bfde992..9140c7da38 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -8,6 +8,20 @@ use image::codecs::openexr::*; // "just dont panic" fn roundtrip(bytes: &[u8]) -> ImageResult<()> { + + /// 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)?; + + ImageBuffer::from_raw(width, height, buffer) + + // this should be the only reason for the "from raw" call to fail, + // even though such a large allocation would probably cause an error much earlier + .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) + } + let decoded_image = read_as_rgba_image(Cursor::new(bytes))?; let mut bytes = Vec::with_capacity(bytes.len() + 20); diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 3db629ccaf..3b12e0e637 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -9,8 +9,16 @@ //! //! # Related Links //! * - The OpenEXR reference. - -// ROADMAP: See https://github.com/image-rs/image/pull/1475 +//! +//! +//! Current limitations (July 2021): +//! - only non-deep rgb/rgba files supported, no conversion from/to YCbCr or similar +//! - only the first non-deep rgb layer is used +//! - only the largest mip map level is used +//! - pixels outside display window are lost +//! - meta data is lost +//! - dwaa/dwab compressed images not supported yet by the exr library +//! - (chroma) subsampling not supported yet by the exr library extern crate exr; use exr::prelude::*; @@ -25,76 +33,6 @@ use std::convert::TryInto; -// TODO this could be a generic function that works for any format -/// Read an `Rgba32FImage`. -/// Assumes the reader is buffered. In most cases, -/// you should wrap your reader in a `BufReader` for best performance. -// TODO progress? load rect? -fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult { - let decoder = OpenExrDecoder::read(read, Some(true))?; - debug_assert_eq!(decoder.color_type(), ColorType::Rgba32F); - - let (width, height) = decoder.dimensions(); - let buffer: Vec = decoder_to_vec(decoder)?; // passing `true` to decoder results in 4 floats per pixel - - ImageBuffer::from_raw(width, height, buffer) - - // this should be the only reason for the "from raw" call to fail, - // even though such a large allocation would probably cause an error much earlier - .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) -} - - -// TODO this could be a generic function that works for any format -/// Read an `Rgb32FImage`. -/// Assumes the reader is buffered. In most cases, -/// you should wrap your reader in a `BufReader` for best performance. -// TODO progress? load rect? -fn read_as_rgb_image(read: impl BufRead + Seek) -> ImageResult { - let decoder = OpenExrDecoder::read(read, Some(false))?; - debug_assert_eq!(decoder.color_type(), ColorType::Rgb32F); - - let (width, height) = decoder.dimensions(); - let buffer: Vec = decoder_to_vec(decoder)?; // passing `false` to decoder results in 3 floats per pixel - - ImageBuffer::from_raw(width, height, buffer) - - // this should be the only reason for the "from raw" call to fail, - // even though such a large allocation would probably cause an error much earlier - .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) -} - - - -// TODO this could be a generic function that works for any format -/// Write an `Rgb32FImage`. -/// Assumes the writer is buffered. In most cases, -/// you should wrap your writer in a `BufWriter` for best performance. -// TODO progress? load rect? -fn write_rgb_image(write: impl Write/* + Seek*/, image: &Rgb32FImage) -> ImageResult<()> { - write_buffer( - write, - bytemuck::cast_slice(image.as_raw().as_slice()), - image.width(), image.height(), - ColorType::Rgb32F - ) -} - -// TODO this could be a generic function that works for any format -/// Write an `Rgba32FImage`. -/// Assumes the writer is buffered. In most cases, -/// you should wrap your writer in a `BufWriter` for best performance. -// TODO progress? load rect? -fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> ImageResult<()> { - write_buffer( - write, - bytemuck::cast_slice(image.as_raw().as_slice()), - image.width(), image.height(), - ColorType::Rgba32F - ) -} - - /// An OpenEXR decoder. Immediately reads the meta data from the file. #[derive(Debug)] pub struct OpenExrDecoder { @@ -105,6 +43,7 @@ pub struct OpenExrDecoder { // decode either rgb or rgba. // can be specified to include or discard alpha channels. + // if none, the alpha channel will only be allocated where the file contains data for it. alpha_preference: Option, alpha_present_in_file: bool, @@ -113,13 +52,23 @@ pub struct OpenExrDecoder { impl OpenExrDecoder { + + /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. + /// Assumes the reader is buffered. In most cases, + /// you should wrap your reader in a `BufReader` for best performance. + /// Loads an alpha channel if the file has alpha samples. + /// Use `with_alpha_preference` if you want to load or not load alpha unconditionally. + pub fn new(source: R) -> ImageResult { + Self::with_alpha_preference(source, None) + } + /// Create a decoder. Consumes the first few bytes of the source to extract image dimensions. /// Assumes the reader is buffered. In most cases, /// you should wrap your reader in a `BufReader` for best performance. /// If alpha preference is specified, an alpha channel will /// always be present or always be not present in the returned image. /// If alpha preference is none, the alpha channel will only be returned if it is found in the file. - pub fn read(source: R, alpha_preference: Option) -> ImageResult { + pub fn with_alpha_preference(source: R, alpha_preference: Option) -> ImageResult { // read meta data, then wait for further instructions, keeping the file open and ready let exr_reader = exr::block::read(source, false).map_err(to_image_err)?; @@ -152,7 +101,7 @@ impl OpenExrDecoder { }) } - // do not leak exrs-specific meta data into public api, just do it for this module + // 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] } @@ -361,6 +310,31 @@ mod test { const BASE_PATH: &[&str] = &[".", "tests", "images", "exr"]; + + /// Write an `Rgb32FImage`. + /// Assumes the writer is buffered. In most cases, + /// you should wrap your writer in a `BufWriter` for best performance. + fn write_rgb_image(write: impl Write/* + Seek*/, image: &Rgb32FImage) -> ImageResult<()> { + write_buffer( + write, + bytemuck::cast_slice(image.as_raw().as_slice()), + image.width(), image.height(), + ColorType::Rgb32F + ) + } + + /// Write an `Rgba32FImage`. + /// Assumes the writer is buffered. In most cases, + /// you should wrap your writer in a `BufWriter` for best performance. + fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> ImageResult<()> { + write_buffer( + write, + bytemuck::cast_slice(image.as_raw().as_slice()), + image.width(), image.height(), + ColorType::Rgba32F + ) + } + /// Read the file from the specified path into an `Rgba32FImage`. fn read_as_rgba_image_from_file(path: impl AsRef) -> ImageResult { read_as_rgba_image(BufReader::new(std::fs::File::open(path)?)) @@ -371,6 +345,32 @@ mod test { read_as_rgb_image(BufReader::new(std::fs::File::open(path)?)) } + /// 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)?; + + ImageBuffer::from_raw(width, height, buffer) + + // this should be the only reason for the "from raw" call to fail, + // even though such a large allocation would probably cause an error much earlier + .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) + } + + /// 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)?; + + ImageBuffer::from_raw(width, height, buffer) + + // this should be the only reason for the "from raw" call to fail, + // even though such a large allocation would probably cause an error much earlier + .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) + } + #[test] fn compare_exr_hdr() { assert!(cfg!(feature = "hdr"), "to run all the openexr tests, activate the hdr feature flag"); diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 1e8702df7a..d1a7d41530 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -60,7 +60,7 @@ pub fn load(r: R, format: ImageFormat) -> ImageResult DynamicImage::from_decoder(hdr::HdrAdapter::new(BufReader::new(r))?), #[cfg(feature = "openexr")] - image::ImageFormat::OpenExr => DynamicImage::from_decoder(openexr::OpenExrDecoder::read(r, None)?), + image::ImageFormat::OpenExr => DynamicImage::from_decoder(openexr::OpenExrDecoder::new(r)?), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(BufReader::new(r))?), #[cfg(feature = "farbfeld")] @@ -110,7 +110,7 @@ pub(crate) fn image_dimensions_with_format_impl(fin: R, forma #[cfg(feature = "hdr")] image::ImageFormat::Hdr => hdr::HdrAdapter::new(fin)?.dimensions(), #[cfg(feature = "openexr")] - image::ImageFormat::OpenExr => openexr::OpenExrDecoder::read(fin, None)?.dimensions(), + image::ImageFormat::OpenExr => openexr::OpenExrDecoder::new(fin)?.dimensions(), #[cfg(feature = "pnm")] image::ImageFormat::Pnm => { pnm::PnmDecoder::new(fin)?.dimensions() From 495e990beaca3fa89831000ab7363d2c3a8f98b8 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 25 Jun 2021 12:33:48 +0200 Subject: [PATCH 33/45] re-implement more helper methods copy&paste for external fuzzer test script --- fuzz/fuzzers/fuzzer_script_exr.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index 9140c7da38..d475679a67 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -22,6 +22,19 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) } + /// Write an `Rgba32FImage`. + /// Assumes the writer is buffered. In most cases, + /// you should wrap your writer in a `BufWriter` for best performance. + fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> ImageResult<()> { + OpenEXREncoder::new(write).write_image( + bytemuck::cast_slice(image.as_raw().as_slice()), + image.width(), image.height(), + ColorType::Rgba32F + ) + } + + + let decoded_image = read_as_rgba_image(Cursor::new(bytes))?; let mut bytes = Vec::with_capacity(bytes.len() + 20); From b3b349ebb011c921c98f1198afd2e0f9304203a8 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 25 Jun 2021 13:07:19 +0200 Subject: [PATCH 34/45] re-implement even more helper methods copy&paste for external fuzzer test script --- fuzz/fuzzers/fuzzer_script_exr.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index d475679a67..7073642c62 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -1,16 +1,37 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; extern crate image; +extern crate bytemuck; use std::io::Cursor; use image::ImageResult; use image::codecs::openexr::*; +use image::Rgba32FImage; +use image::error::LimitError; +use image::ImageError; +use image::ImageBuffer; +use image::Rgb32FImage; +use std::io::Seek; +use std::io::BufRead; // "just dont panic" fn roundtrip(bytes: &[u8]) -> ImageResult<()> { + use std::io::Write; /// Read the file from the specified path into an `Rgba32FImage`. + // TODO this method should probably already exist in the main image crate fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult { + + // TODO this method should probably already exist in the main image crate + fn decoder_to_vec<'a, T>(decoder: impl ImageDecoder<'a>) -> ImageResult> + where T: crate::traits::Primitive + bytemuck::Pod, + { + let mut buf = vec![num_traits::Zero::zero(); usize::try_from(decoder.total_bytes()).unwrap() / std::mem::size_of::()]; + decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?; + Ok(buf) + } + + let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?; let (width, height) = decoder.dimensions(); let buffer: Vec = decoder_to_vec(decoder)?; @@ -25,6 +46,7 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { /// Write an `Rgba32FImage`. /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. + // TODO this method should probably already exist in the main image crate fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> ImageResult<()> { OpenEXREncoder::new(write).write_image( bytemuck::cast_slice(image.as_raw().as_slice()), From 9838118fc58767473580f8371e556c940138d0c3 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 25 Jun 2021 15:24:45 +0200 Subject: [PATCH 35/45] simplify helper methodsfor external fuzzer test script --- fuzz/fuzzers/fuzzer_script_exr.rs | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index 7073642c62..45b585b336 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -20,27 +20,14 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { /// Read the file from the specified path into an `Rgba32FImage`. // TODO this method should probably already exist in the main image crate - fn read_as_rgba_image(read: impl BufRead + Seek) -> ImageResult { - - // TODO this method should probably already exist in the main image crate - fn decoder_to_vec<'a, T>(decoder: impl ImageDecoder<'a>) -> ImageResult> - where T: crate::traits::Primitive + bytemuck::Pod, - { - let mut buf = vec![num_traits::Zero::zero(); usize::try_from(decoder.total_bytes()).unwrap() / std::mem::size_of::()]; - decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?; - Ok(buf) - } - - + fn read_as_rgba_byte_image(read: impl BufRead + Seek) -> ImageResult<(usize,usize,Vec)> { let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?; let (width, height) = decoder.dimensions(); - let buffer: Vec = decoder_to_vec(decoder)?; - ImageBuffer::from_raw(width, height, buffer) + let mut buffer = vec![0; usize::try_from(decoder.total_bytes()).unwrap()]; + decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?; - // this should be the only reason for the "from raw" call to fail, - // even though such a large allocation would probably cause an error much earlier - .ok_or_else(|| ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))) + Ok((width, height, buffer)) } /// Write an `Rgba32FImage`. @@ -48,7 +35,7 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { /// you should wrap your writer in a `BufWriter` for best performance. // TODO this method should probably already exist in the main image crate fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> ImageResult<()> { - OpenEXREncoder::new(write).write_image( + OpenExrEncoder::new(write).write_image( bytemuck::cast_slice(image.as_raw().as_slice()), image.width(), image.height(), ColorType::Rgba32F @@ -57,11 +44,12 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { - let decoded_image = read_as_rgba_image(Cursor::new(bytes))?; + let decoded_image = read_as_rgba_byte_image(Cursor::new(bytes))?; let mut bytes = Vec::with_capacity(bytes.len() + 20); write_rgba_image(Cursor::new(&mut bytes), &decoded_image)?; - let redecoded_image = read_as_rgba_image(Cursor::new(bytes))?; + + let redecoded_image = read_as_rgba_byte_image(Cursor::new(bytes))?; // if both images are valid, assert read/write consistency assert_eq!(decoded_image, redecoded_image, "image was valid but was not reproducible"); From 5126f1b75d87945fdc20447d948793d0f9bba406 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 25 Jun 2021 15:54:32 +0200 Subject: [PATCH 36/45] fix unused crate import error --- fuzz/fuzzers/fuzzer_script_exr.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index 45b585b336..cdcc5f2fa3 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -1,7 +1,6 @@ #![no_main] #[macro_use] extern crate libfuzzer_sys; extern crate image; -extern crate bytemuck; use std::io::Cursor; use image::ImageResult; From d0c6872456ed43a4e397917181cff2956d8c30ad Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 25 Jun 2021 16:25:55 +0200 Subject: [PATCH 37/45] fix more compiler errrors --- fuzz/fuzzers/fuzzer_script_exr.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index cdcc5f2fa3..7320ecf84e 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -6,12 +6,11 @@ use std::io::Cursor; use image::ImageResult; use image::codecs::openexr::*; use image::Rgba32FImage; -use image::error::LimitError; -use image::ImageError; -use image::ImageBuffer; -use image::Rgb32FImage; use std::io::Seek; use std::io::BufRead; +use std::convert::TryFrom; +use image::ImageDecoder; + // "just dont panic" fn roundtrip(bytes: &[u8]) -> ImageResult<()> { @@ -24,7 +23,7 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { let (width, height) = decoder.dimensions(); let mut buffer = vec![0; usize::try_from(decoder.total_bytes()).unwrap()]; - decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?; + decoder.read_image(buffer.as_mut_slice())?; Ok((width, height, buffer)) } From 83148a3c9d1fca64b28540be7760d17d0f732643 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 25 Jun 2021 16:55:40 +0200 Subject: [PATCH 38/45] fix fuzzer script compile errors --- fuzz/fuzzers/fuzzer_script_exr.rs | 9 ++++----- src/codecs/openexr.rs | 8 +++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index 7320ecf84e..3f07e2c12e 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -10,6 +10,7 @@ use std::io::Seek; use std::io::BufRead; use std::convert::TryFrom; use image::ImageDecoder; +use image::ImageEncoder; // "just dont panic" @@ -18,7 +19,7 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { /// Read the file from the specified path into an `Rgba32FImage`. // TODO this method should probably already exist in the main image crate - fn read_as_rgba_byte_image(read: impl BufRead + Seek) -> ImageResult<(usize,usize,Vec)> { + fn read_as_rgba_byte_image(read: impl BufRead + Seek) -> ImageResult<(u32,u32,Vec)> { let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?; let (width, height) = decoder.dimensions(); @@ -32,16 +33,14 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. // TODO this method should probably already exist in the main image crate - fn write_rgba_image(write: impl Write/* + Seek*/, image: &Rgba32FImage) -> ImageResult<()> { + fn write_rgba_image(write: impl Write/* + Seek*/, (width, height, data): &(u32,u32,Vec)) -> ImageResult<()> { OpenExrEncoder::new(write).write_image( - bytemuck::cast_slice(image.as_raw().as_slice()), - image.width(), image.height(), + data.as_slice(), width, height, ColorType::Rgba32F ) } - let decoded_image = read_as_rgba_byte_image(Cursor::new(bytes))?; let mut bytes = Vec::with_capacity(bytes.len() + 20); diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 3b12e0e637..99b13e4e9a 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -23,12 +23,10 @@ extern crate exr; use exr::prelude::*; -use crate::{ImageDecoder, ImageResult, ColorType, Progress, ImageError, ImageFormat, ImageBuffer, Rgba, Rgb, ImageEncoder, ExtendedColorType}; -use std::io::{Write, Seek, BufRead, Cursor, BufReader}; -use crate::error::{DecodingError, ImageFormatHint, LimitError, LimitErrorKind, EncodingError}; +use crate::{ImageDecoder, ImageResult, ColorType, Progress, ImageError, ImageFormat, ImageEncoder, ExtendedColorType}; +use std::io::{Write, Seek, BufRead, Cursor}; +use crate::error::{DecodingError, ImageFormatHint, EncodingError}; use crate::image::decoder_to_vec; -use std::path::Path; -use crate::buffer_::{Rgb32FImage, Rgba32FImage}; use std::convert::TryInto; From d67a08b1bc33a7ecc2dbdf92e5c0994eb3c29b08 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 25 Jun 2021 17:21:28 +0200 Subject: [PATCH 39/45] fix fuzzer script compile errors --- fuzz/fuzzers/fuzzer_script_exr.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fuzz/fuzzers/fuzzer_script_exr.rs b/fuzz/fuzzers/fuzzer_script_exr.rs index 3f07e2c12e..e4b061aa8e 100644 --- a/fuzz/fuzzers/fuzzer_script_exr.rs +++ b/fuzz/fuzzers/fuzzer_script_exr.rs @@ -5,13 +5,12 @@ extern crate image; use std::io::Cursor; use image::ImageResult; use image::codecs::openexr::*; -use image::Rgba32FImage; use std::io::Seek; use std::io::BufRead; use std::convert::TryFrom; use image::ImageDecoder; use image::ImageEncoder; - +use image::ColorType; // "just dont panic" fn roundtrip(bytes: &[u8]) -> ImageResult<()> { @@ -35,7 +34,7 @@ fn roundtrip(bytes: &[u8]) -> ImageResult<()> { // TODO this method should probably already exist in the main image crate fn write_rgba_image(write: impl Write/* + Seek*/, (width, height, data): &(u32,u32,Vec)) -> ImageResult<()> { OpenExrEncoder::new(write).write_image( - data.as_slice(), width, height, + data.as_slice(), *width, *height, ColorType::Rgba32F ) } From 6d729c9430a1e9298d38a9372531209577358ece Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Fri, 25 Jun 2021 18:20:30 +0200 Subject: [PATCH 40/45] unfix removing "unused imports" --- src/codecs/openexr.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 99b13e4e9a..3b12e0e637 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -23,10 +23,12 @@ extern crate exr; use exr::prelude::*; -use crate::{ImageDecoder, ImageResult, ColorType, Progress, ImageError, ImageFormat, ImageEncoder, ExtendedColorType}; -use std::io::{Write, Seek, BufRead, Cursor}; -use crate::error::{DecodingError, ImageFormatHint, EncodingError}; +use crate::{ImageDecoder, ImageResult, ColorType, Progress, ImageError, ImageFormat, ImageBuffer, Rgba, Rgb, ImageEncoder, ExtendedColorType}; +use std::io::{Write, Seek, BufRead, Cursor, BufReader}; +use crate::error::{DecodingError, ImageFormatHint, LimitError, LimitErrorKind, EncodingError}; use crate::image::decoder_to_vec; +use std::path::Path; +use crate::buffer_::{Rgb32FImage, Rgba32FImage}; use std::convert::TryInto; From 57a83b596bc69c4cf591f2e054118e28db67ccfb Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Wed, 14 Jul 2021 12:01:28 +0200 Subject: [PATCH 41/45] use overflow-safe size checks --- src/codecs/openexr.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 3b12e0e637..e0c3642ba7 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -12,6 +12,7 @@ //! //! //! Current limitations (July 2021): +//! - only pixel type `Rgba32F` and `Rgba16F` are supported //! - only non-deep rgb/rgba files supported, no conversion from/to YCbCr or similar //! - only the first non-deep rgb layer is used //! - only the largest mip map level is used @@ -199,7 +200,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for OpenExrDecoder { /// /// Assumes the writer is buffered. In most cases, /// you should wrap your writer in a `BufWriter` for best performance. -// private, access via `OpenExrEncoder` +// private. access via `OpenExrEncoder` fn write_buffer( mut buffered_write: impl Write/* + Seek*/, unaligned_bytes: &[u8], width: u32, height: u32, color_type: ColorType @@ -208,11 +209,24 @@ fn write_buffer( let width = width as usize; let height = height as usize; - if unaligned_bytes.len() < width * height * color_type.bytes_per_pixel() as usize { - return Err(ImageError::Encoding(EncodingError::new( - ImageFormatHint::Exact(ImageFormat::OpenExr), - "byte buffer not large enough for the specified dimensions and f32 pixels" - ))); + { // check whether dimensions match the buffer size + let expected_byte_count = width.checked_mul(height) + .and_then(|size| size.checked_mul(color_type.bytes_per_pixel() as usize)); + + // if the width and height does not match the length of the bytes, the arguments are invalid + let has_invalid_size_or_overflowed = expected_byte_count + .map(|expected_byte_count | unaligned_bytes.len() < expected_byte_count) + + // otherwise, size calculation overflowed, is bigger than memory, + // therefore data is too small, so it is invalid. + .unwrap_or(true); + + if has_invalid_size_or_overflowed { + return Err(ImageError::Encoding(EncodingError::new( + ImageFormatHint::Exact(ImageFormat::OpenExr), + "byte buffer not large enough for the specified dimensions and f32 pixels" + ))); + } } let mut seekable_write = Cursor::new(Vec::with_capacity(width*height*3*4)); // TODO remove From ab811e5eeee40d441bf4a1f855125e514d51f593 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Wed, 14 Jul 2021 13:05:18 +0200 Subject: [PATCH 42/45] also check buffer dimensions in decoder, not only encoder --- src/codecs/openexr.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index e0c3642ba7..fbcd2276d3 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -148,6 +148,28 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for OpenExrDecoder { 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; + { // 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 expected_byte_count = (width as usize).checked_mul(height as usize) + .and_then(|size| size.checked_mul(bytes_per_pixel)); + + // if the width and height does not match the length of the bytes, the arguments are invalid + let has_invalid_size_or_overflowed = expected_byte_count + .map(|expected_byte_count | unaligned_bytes.len() < expected_byte_count) + + // otherwise, size calculation overflowed, is bigger than memory, + // therefore data is too small, so it is invalid. + .unwrap_or(true); + + if has_invalid_size_or_overflowed { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormatHint::Exact(ImageFormat::OpenExr), + "byte buffer not large enough for the specified dimensions and f32 pixels" + ))); + } + } + let result = read() .no_deep_data().largest_resolution_level() .rgba_channels( @@ -209,7 +231,7 @@ fn write_buffer( let width = width as usize; let height = height as usize; - { // check whether dimensions match the buffer size + { // check whether the buffer is large enough for the specified dimensions let expected_byte_count = width.checked_mul(height) .and_then(|size| size.checked_mul(color_type.bytes_per_pixel() as usize)); From 9c6d68f21f436e6e582d4e2af5bd5b4e616a70ce Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Wed, 14 Jul 2021 13:08:12 +0200 Subject: [PATCH 43/45] panic in decoder instead of returning an error --- src/codecs/openexr.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index fbcd2276d3..d4273d5c42 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -163,10 +163,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for OpenExrDecoder { .unwrap_or(true); if has_invalid_size_or_overflowed { - return Err(ImageError::Decoding(DecodingError::new( - ImageFormatHint::Exact(ImageFormat::OpenExr), - "byte buffer not large enough for the specified dimensions and f32 pixels" - ))); + panic!("byte buffer not large enough for the specified dimensions and f32 pixels"); } } From 4710fd7db6122d2fa83bcd8075ec255ec16eb158 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Wed, 14 Jul 2021 13:11:49 +0200 Subject: [PATCH 44/45] when checking decoder buffer size, use strict equality, as declared in the documentation --- src/codecs/openexr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index d4273d5c42..10dcc83a5b 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -156,7 +156,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for OpenExrDecoder { // if the width and height does not match the length of the bytes, the arguments are invalid let has_invalid_size_or_overflowed = expected_byte_count - .map(|expected_byte_count | unaligned_bytes.len() < expected_byte_count) + .map(|expected_byte_count | unaligned_bytes.len() != expected_byte_count) // otherwise, size calculation overflowed, is bigger than memory, // therefore data is too small, so it is invalid. From 899431fc9d7890f1bae2a5dcdfb86ab3b50443e3 Mon Sep 17 00:00:00 2001 From: Johannes Vollmer <32042925+johannesvollmer@users.noreply.github.com> Date: Wed, 14 Jul 2021 13:28:27 +0200 Subject: [PATCH 45/45] re-trigger CI --- src/codecs/openexr.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 10dcc83a5b..ac0967bc83 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -205,6 +205,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for OpenExrDecoder { // TODO this copy is strictly not necessary, but the exr api is a little too simple for reading into a borrowed target slice // this cast is safe and works with any alignment, as bytes are copied, and not f32 values. + // note: buffer slice length is checked in the beginning of this function and will be correct at this point unaligned_bytes.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.as_slice())); Ok(()) }