From 6faaf1fbf94472c9c17f88bff8ab0d7c8dca717c Mon Sep 17 00:00:00 2001 From: Charles Samborski Date: Tue, 5 Jul 2022 21:24:26 +0200 Subject: [PATCH] feature: Implement `load_lossy` Add support for lossy image loading. The new `load_lossy` function adds error recovery. If loading reaches the step where it allocates the pixel buffer, then it always succeeds: any error following this step will result in a partially filled buffer where the missing pixels will use their default "zero" value. This allows for example to read truncated files or other minor issues. - Closes image-rs/image#1752 --- src/codecs/bmp/decoder.rs | 2 +- src/codecs/gif.rs | 2 +- src/codecs/hdr/decoder.rs | 2 +- src/codecs/ico/decoder.rs | 2 +- src/codecs/openexr.rs | 6 ++--- src/codecs/pnm/decoder.rs | 2 +- src/dynimage.rs | 51 ++++++++++++++++++++++++++++++--------- src/image.rs | 20 +++++++++------ src/io/free_functions.rs | 39 ++++++++++++++++++++++++++---- src/io/reader.rs | 3 ++- src/lib.rs | 2 +- 11 files changed, 97 insertions(+), 34 deletions(-) diff --git a/src/codecs/bmp/decoder.rs b/src/codecs/bmp/decoder.rs index fce2057f6c..7a882dfe43 100644 --- a/src/codecs/bmp/decoder.rs +++ b/src/codecs/bmp/decoder.rs @@ -1514,7 +1514,7 @@ impl<'a, R: 'a + Read + Seek> ImageDecoder<'a> for BmpDecoder { fn into_reader(self) -> ImageResult { Ok(BmpReader( - Cursor::new(image::decoder_to_vec(self)?), + Cursor::new(image::decoder_to_vec(self).map_err(|(_, e)| e)?), PhantomData, )) } diff --git a/src/codecs/gif.rs b/src/codecs/gif.rs index 7bd620181c..d7f7e7b137 100644 --- a/src/codecs/gif.rs +++ b/src/codecs/gif.rs @@ -109,7 +109,7 @@ impl<'a, R: 'a + Read> ImageDecoder<'a> for GifDecoder { fn into_reader(self) -> ImageResult { Ok(GifReader( - Cursor::new(image::decoder_to_vec(self)?), + Cursor::new(image::decoder_to_vec(self).map_err(|(_, e)| e)?), PhantomData, )) } diff --git a/src/codecs/hdr/decoder.rs b/src/codecs/hdr/decoder.rs index ac37edce7a..ef00c18a29 100644 --- a/src/codecs/hdr/decoder.rs +++ b/src/codecs/hdr/decoder.rs @@ -201,7 +201,7 @@ impl<'a, R: 'a + BufRead> ImageDecoder<'a> for HdrAdapter { fn into_reader(self) -> ImageResult { Ok(HdrReader( - Cursor::new(image::decoder_to_vec(self)?), + Cursor::new(image::decoder_to_vec(self).map_err(|(_, e)| e)?), PhantomData, )) } diff --git a/src/codecs/ico/decoder.rs b/src/codecs/ico/decoder.rs index 97a025a1b8..635068627c 100644 --- a/src/codecs/ico/decoder.rs +++ b/src/codecs/ico/decoder.rs @@ -296,7 +296,7 @@ impl<'a, R: 'a + Read + Seek> ImageDecoder<'a> for IcoDecoder { fn into_reader(self) -> ImageResult { Ok(IcoReader( - Cursor::new(image::decoder_to_vec(self)?), + Cursor::new(image::decoder_to_vec(self).map_err(|(_, e)| e)?), PhantomData, )) } diff --git a/src/codecs/openexr.rs b/src/codecs/openexr.rs index 16ed94547a..0d996a998e 100644 --- a/src/codecs/openexr.rs +++ b/src/codecs/openexr.rs @@ -138,7 +138,7 @@ impl<'a, R: 'a + Read + Seek> ImageDecoder<'a> for OpenExrDecoder { /// 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)?)) + Ok(Cursor::new(decoder_to_vec(self).map_err(|(_, e)| e)?)) } fn scanline_bytes(&self) -> u64 { @@ -429,7 +429,7 @@ mod test { fn read_as_rgb_image(read: impl Read + Seek) -> ImageResult { let decoder = OpenExrDecoder::with_alpha_preference(read, Some(false))?; let (width, height) = decoder.dimensions(); - let buffer: Vec = decoder_to_vec(decoder)?; + let buffer: Vec = decoder_to_vec(decoder).map_err(|(_, e)| e)?; ImageBuffer::from_raw(width, height, buffer) // this should be the only reason for the "from raw" call to fail, @@ -443,7 +443,7 @@ mod test { fn read_as_rgba_image(read: impl Read + Seek) -> ImageResult { let decoder = OpenExrDecoder::with_alpha_preference(read, Some(true))?; let (width, height) = decoder.dimensions(); - let buffer: Vec = decoder_to_vec(decoder)?; + let buffer: Vec = decoder_to_vec(decoder).map_err(|(_, e)| e)?; ImageBuffer::from_raw(width, height, buffer) // this should be the only reason for the "from raw" call to fail, diff --git a/src/codecs/pnm/decoder.rs b/src/codecs/pnm/decoder.rs index 8d91ba001a..e49ebaa4da 100644 --- a/src/codecs/pnm/decoder.rs +++ b/src/codecs/pnm/decoder.rs @@ -615,7 +615,7 @@ impl<'a, R: 'a + Read> ImageDecoder<'a> for PnmDecoder { fn into_reader(self) -> ImageResult { Ok(PnmReader( - Cursor::new(image::decoder_to_vec(self)?), + Cursor::new(image::decoder_to_vec(self).map_err(|(_, e)| e)?), PhantomData, )) } diff --git a/src/dynimage.rs b/src/dynimage.rs index 53a71da191..6473e4d097 100644 --- a/src/dynimage.rs +++ b/src/dynimage.rs @@ -26,6 +26,7 @@ use crate::math::resize_dimensions; use crate::traits::Pixel; use crate::{image, Luma, LumaA}; use crate::{Rgb32FImage, Rgba32FImage}; +use crate::io::free_functions::LoadErrorHandling; /// A Dynamic Image /// @@ -172,7 +173,20 @@ impl DynamicImage { /// Decodes an encoded image into a dynamic image. pub fn from_decoder<'a>(decoder: impl ImageDecoder<'a>) -> ImageResult { - decoder_to_image(decoder) + Self::from_decoder_inner(decoder, LoadErrorHandling::Strict) + } + + /// Decodes an encoded image into a dynamic image, even if there are some errors. + /// + /// If an error is encountered while reading pixel data, use a default pixel + /// value (e.g. transparent) instead of failing the whole operation. + pub fn from_decoder_lossy<'a>(decoder: impl ImageDecoder<'a>) -> ImageResult { + Self::from_decoder_inner(decoder, LoadErrorHandling::Lossy) + } + + /// Decodes an encoded image into a dynamic image, with the supplied error handling mode. + pub(crate) fn from_decoder_inner<'a>(decoder: impl ImageDecoder<'a>, error_handling: LoadErrorHandling) -> ImageResult { + decoder_to_image(decoder, error_handling) } /// Returns a copy of this image as an RGB image. @@ -1016,58 +1030,71 @@ impl Default for DynamicImage { } /// Decodes an image and stores it into a dynamic image -fn decoder_to_image<'a, I: ImageDecoder<'a>>(decoder: I) -> ImageResult { +fn decoder_to_image<'a, I: ImageDecoder<'a>>(decoder: I, error_handling: LoadErrorHandling) -> ImageResult { let (w, h) = decoder.dimensions(); let color_type = decoder.color_type(); + fn handle_result(result: Result, (Option>, ImageError)>, error_handling: LoadErrorHandling) -> ImageResult> { + match result { + Ok(buf) => Ok(buf), + Err((None, e)) => Err(e), + Err((Some(buf), e)) => { + match error_handling { + LoadErrorHandling::Strict => Err(e), + LoadErrorHandling::Lossy => Ok(buf), + } + } + } + } + let image = match color_type { color::ColorType::Rgb8 => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgb8) } color::ColorType::Rgba8 => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgba8) } color::ColorType::L8 => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageLuma8) } color::ColorType::La8 => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageLumaA8) } color::ColorType::Rgb16 => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgb16) } color::ColorType::Rgba16 => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgba16) } color::ColorType::Rgb32F => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgb32F) } color::ColorType::Rgba32F => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageRgba32F) } color::ColorType::L16 => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageLuma16) } color::ColorType::La16 => { - let buf = image::decoder_to_vec(decoder)?; + let buf = handle_result(image::decoder_to_vec(decoder), error_handling)?; ImageBuffer::from_raw(w, h, buf).map(DynamicImage::ImageLumaA16) } diff --git a/src/image.rs b/src/image.rs index cbedcd7153..9ccf55caf3 100644 --- a/src/image.rs +++ b/src/image.rs @@ -572,20 +572,26 @@ where /// of the output buffer is guaranteed. /// /// Panics if there isn't enough memory to decode the image. -pub(crate) fn decoder_to_vec<'a, T>(decoder: impl ImageDecoder<'a>) -> ImageResult> +/// +/// If an error occurs after the pixel buffer was created, it is returned. Note +/// however that it may be incomplete. Pixels that were not read use their +/// default "zero" value (the memory is initialized and safe to access). +pub(crate) fn decoder_to_vec<'a, T>(decoder: impl ImageDecoder<'a>) -> Result, (Option>, ImageError)> where T: crate::traits::Primitive + bytemuck::Pod, { let total_bytes = usize::try_from(decoder.total_bytes()); if total_bytes.is_err() || total_bytes.unwrap() > isize::max_value() as usize { - return Err(ImageError::Limits(LimitError::from_kind( + return Err((None, ImageError::Limits(LimitError::from_kind( LimitErrorKind::InsufficientMemory, - ))); + )))); } let mut buf = vec![num_traits::Zero::zero(); total_bytes.unwrap() / std::mem::size_of::()]; - decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?; - Ok(buf) + match decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice())) { + Ok(()) => Ok(buf), + Err(e) => Err((Some(buf), e)) + } } /// Represents the progress of an image operation. @@ -1306,7 +1312,7 @@ mod tests { }; use crate::color::Rgba; use crate::math::Rect; - use crate::{GrayImage, ImageBuffer}; + use crate::{GrayImage, ImageBuffer, ImageError}; #[test] #[allow(deprecated)] @@ -1835,7 +1841,7 @@ mod tests { } assert_eq!(D.total_bytes(), u64::max_value()); - let v: ImageResult> = super::decoder_to_vec(D); + let v: Result, (Option>, ImageError)> = super::decoder_to_vec(D); assert!(v.is_err()); } } diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 190ada7474..5cddd19046 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -23,6 +23,14 @@ pub(crate) fn open_impl(path: &Path) -> ImageResult { load(buffered_read, ImageFormat::from_path(path)?) } +/// Enum describing how to handle load errors +pub(crate) enum LoadErrorHandling { + /// Fail the whole load if an error is encountered + Strict, + /// Ignore errors while reading pixel data (use a default value) + Lossy, +} + /// Create a new image from a Reader. /// /// Assumes the reader is already buffered. For optimal performance, @@ -34,7 +42,27 @@ pub(crate) fn open_impl(path: &Path) -> ImageResult { #[allow(unused_variables)] // r is unused if no features are supported. pub fn load(r: R, format: ImageFormat) -> ImageResult { - load_inner(r, super::Limits::default(), format) + load_inner(r, super::Limits::default(), format, LoadErrorHandling::Strict) +} + +/// Create a new image from a Reader, even if there are some errors. +/// +/// This is a lenient version of the image loader. If an error is encountered +/// when reading pixel data, it will use a default pixel value (e.g. +/// `transparent`) for those pixels instead of failing the whole read. +/// +/// Use [`load`] to only accept well-formed images. +/// +/// Assumes the reader is already buffered. For optimal performance, +/// consider wrapping the reader with a `BufReader::new()`. +/// +/// Try [`io::Reader`] for more advanced uses. +/// +/// [`io::Reader`]: io/struct.Reader.html +#[allow(unused_variables)] +// r is unused if no features are supported. +pub fn load_lossy(r: R, format: ImageFormat) -> ImageResult { + load_inner(r, super::Limits::default(), format, LoadErrorHandling::Lossy) } pub(crate) trait DecoderVisitor { @@ -89,8 +117,9 @@ pub(crate) fn load_inner( r: R, limits: super::Limits, format: ImageFormat, + error_handling: LoadErrorHandling, ) -> ImageResult { - struct LoadVisitor(super::Limits); + struct LoadVisitor(super::Limits, LoadErrorHandling); impl DecoderVisitor for LoadVisitor { type Result = DynamicImage; @@ -99,16 +128,16 @@ pub(crate) fn load_inner( self, mut decoder: D, ) -> ImageResult { - let mut limits = self.0; + let LoadVisitor(mut limits, error_handling) = self; // Check that we do not allocate a bigger buffer than we are allowed to // FIXME: should this rather go in `DynamicImage::from_decoder` somehow? limits.reserve(decoder.total_bytes())?; decoder.set_limits(limits)?; - DynamicImage::from_decoder(decoder) + DynamicImage::from_decoder_inner(decoder, error_handling) } } - load_decoder(r, format, LoadVisitor(limits)) + load_decoder(r, format, LoadVisitor(limits, error_handling)) } pub(crate) fn image_dimensions_impl(path: &Path) -> ImageResult<(u32, u32)> { diff --git a/src/io/reader.rs b/src/io/reader.rs index 7721a79311..62376580d1 100644 --- a/src/io/reader.rs +++ b/src/io/reader.rs @@ -6,6 +6,7 @@ use crate::dynimage::DynamicImage; use crate::error::{ImageFormatHint, UnsupportedError, UnsupportedErrorKind}; use crate::image::ImageFormat; use crate::{ImageError, ImageResult}; +use crate::io::free_functions::LoadErrorHandling; use super::free_functions; @@ -225,7 +226,7 @@ impl Reader { /// If no format was determined, returns an `ImageError::Unsupported`. pub fn decode(mut self) -> ImageResult { let format = self.require_format()?; - free_functions::load_inner(self.inner, self.limits, format) + free_functions::load_inner(self.inner, self.limits, format, LoadErrorHandling::Strict) } fn require_format(&mut self) -> ImageResult { diff --git a/src/lib.rs b/src/lib.rs index 963ec020f3..39a8ca6d94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,7 +143,7 @@ pub use crate::dynimage::{ image_dimensions, load_from_memory, load_from_memory_with_format, open, save_buffer, save_buffer_with_format, write_buffer_with_format, }; -pub use crate::io::free_functions::{guess_format, load}; +pub use crate::io::free_functions::{guess_format, load, load_lossy}; pub use crate::dynimage::DynamicImage;