From e202b74b6d06389102abbf8ce2bd50c3d831d1ff Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 14 Apr 2026 11:29:54 +0000 Subject: [PATCH 1/3] add support for truncated images This commit adds a dedicated error variant for truncation and ensures that the unused part of the output buffer is zeroed. This allows users to display incomplete images, matching the functionality of giflib. --- src/reader/converter.rs | 43 ++++++++--- src/reader/decoder.rs | 4 + src/reader/mod.rs | 6 +- tests/truncated.rs | 88 ++++++++++++++++++++++ tests/truncated/interlaced-truncated.png | Bin 0 -> 7726 bytes tests/truncated/moon_impact-truncated.png | Bin 0 -> 4594 bytes 6 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 tests/truncated.rs create mode 100644 tests/truncated/interlaced-truncated.png create mode 100644 tests/truncated/moon_impact-truncated.png diff --git a/src/reader/converter.rs b/src/reader/converter.rs index 9833765..b4e0ea5 100644 --- a/src/reader/converter.rs +++ b/src/reader/converter.rs @@ -103,7 +103,8 @@ impl PixelConverter { current_frame: &Frame<'_>, mut buf: &mut [u8], data_callback: FillBufferCallback<'_>, - ) -> Result { + ) -> Result { + let original_len = buf.len(); loop { let decode_into = match self.color_output { // When decoding indexed data, LZW can write the pixels directly @@ -121,9 +122,9 @@ impl PixelConverter { &mut self.buffer[..buffer_size] } }; - match data_callback(&mut OutputBuffer::Slice(decode_into))? { - 0 => return Ok(false), - bytes_decoded => { + match data_callback(&mut OutputBuffer::Slice(decode_into)) { + Ok(0) => return Ok(original_len - buf.len()), + Ok(bytes_decoded) => { match self.color_output { ColorOutput::RGBA => { let transparent = current_frame.transparent; @@ -160,9 +161,13 @@ impl PixelConverter { } } if buf.is_empty() { - return Ok(true); + return Ok(original_len); } } + Err(DecodingError::UnexpectedEof) => { + return Ok(original_len - buf.len()); + } + Err(e) => return Err(e), } } } @@ -190,11 +195,12 @@ impl PixelConverter { ) -> Result<(), DecodingError> { if frame.interlaced { let width = self.line_length(frame); - for row in (InterlaceIterator { + let mut row_iter = InterlaceIterator { len: frame.height, next: 0, pass: 0, - }) { + }; + for row in &mut row_iter { // this can't overflow 32-bit, because row never equals (maximum) height let start = row * width; // Handle a too-small buffer and 32-bit usize overflow without panicking @@ -202,8 +208,19 @@ impl PixelConverter { .get_mut(start..) .and_then(|b| b.get_mut(..width)) .ok_or_else(|| DecodingError::format("buffer too small"))?; - if !self.fill_buffer(frame, line, data_callback)? { - return Err(DecodingError::format("image truncated")); + let filled = self.fill_buffer(frame, line, data_callback)?; + if filled < line.len() { + // Once MSRV is >= 1.95: + // core::hint::cold_path(); + line[filled..].fill(0); + // Zero out remaining rows + for rem_row in row_iter { + let start = rem_row * width; + if let Some(rem_line) = buf.get_mut(start..start + width) { + rem_line.fill(0); + } + } + return Err(DecodingError::Truncated); } } } else { @@ -211,8 +228,12 @@ impl PixelConverter { .buffer_size(frame) .and_then(|buffer_size| buf.get_mut(..buffer_size)) .ok_or_else(|| DecodingError::format("buffer too small"))?; - if !self.fill_buffer(frame, buf, data_callback)? { - return Err(DecodingError::format("image truncated")); + let filled = self.fill_buffer(frame, buf, data_callback)?; + if filled < buf.len() { + // Once MSRV is >= 1.95: + // core::hint::cold_path(); + buf[filled..].fill(0); + return Err(DecodingError::Truncated); } }; Ok(()) diff --git a/src/reader/decoder.rs b/src/reader/decoder.rs index bd86cbb..d58fd3a 100644 --- a/src/reader/decoder.rs +++ b/src/reader/decoder.rs @@ -57,6 +57,8 @@ pub enum DecodingError { LzwError(LzwError), /// Returned if the image is found to be malformed. Format(DecodingFormatError), + /// Image truncated. The unfilled output buffer has been zeroed and is safe to use. + Truncated, /// Wraps `std::io::Error`. Io(io::Error), } @@ -79,6 +81,7 @@ impl fmt::Display for DecodingError { Self::DecoderNotFound => fmt.write_str("Decoder Not Found"), Self::EndCodeNotFound => fmt.write_str("End-Code Not Found"), Self::UnexpectedEof => fmt.write_str("Unexpected End of File"), + Self::Truncated => fmt.write_str("Image truncated"), Self::LzwError(ref err) => err.fmt(fmt), Self::Format(ref d) => d.fmt(fmt), Self::Io(ref err) => err.fmt(fmt), @@ -98,6 +101,7 @@ impl error::Error for DecodingError { Self::LzwError(ref err) => Some(err), Self::Format(ref err) => Some(err), Self::Io(ref err) => Some(err), + Self::Truncated => None, } } } diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 2f6eaf6..96a9ff5 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -563,10 +563,12 @@ where /// `Self::next_frame_info` needs to be called beforehand. Returns `true` if the supplied /// buffer could be filled completely. Should not be called after `false` had been returned. pub fn fill_buffer(&mut self, buf: &mut [u8]) -> Result { - self.pixel_converter + let filled = self + .pixel_converter .fill_buffer(&self.current_frame, buf, &mut |out| { self.decoder.decode_next_bytes(out) - }) + })?; + Ok(filled == buf.len()) } /// Output buffer size diff --git a/tests/truncated.rs b/tests/truncated.rs new file mode 100644 index 0000000..37ed380 --- /dev/null +++ b/tests/truncated.rs @@ -0,0 +1,88 @@ +#![cfg(feature = "std")] +use gif::{ColorOutput, DecodeOptions, DecodingError}; +use std::fs::File; +use std::io::{BufWriter, Read}; +use std::path::Path; + +fn test_truncation(gif_path: &str, png_path: &str, truncate_len: usize) { + let mut file = File::open(gif_path).expect("Failed to open GIF"); + let mut data = Vec::new(); + file.read_to_end(&mut data).expect("Failed to read GIF"); + + data.truncate(truncate_len); + + let mut options = DecodeOptions::new(); + options.set_color_output(ColorOutput::RGBA); + + let mut decoder = options.read_info(&data[..]).expect("Failed to read info"); + + let mut hit_truncated = false; + let mut buf = Vec::new(); + + while let Ok(Some(_)) = decoder.next_frame_info() { + buf.resize(decoder.buffer_size(), 0); + match decoder.read_into_buffer(&mut buf) { + Ok(()) => { + println!("Decoded a frame!"); + } + Err(DecodingError::Truncated) => { + println!("Hit Truncated error!"); + hit_truncated = true; + break; + } + Err(e) => panic!("Unexpected error: {:?}", e), + } + } + + assert!(hit_truncated); + + // Save PNG if it hits truncated, for verification! + if !Path::new(png_path).exists() { + let width = decoder.width() as u32; + let height = decoder.height() as u32; + let file = File::create(png_path).expect("Failed to create PNG"); + let ref mut w = BufWriter::new(file); + let mut encoder = png::Encoder::new(w, width, height); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().expect("Failed to write header"); + writer + .write_image_data(&buf) + .expect("Failed to write image data"); + println!("Generated expected PNG: {}", png_path); + } else { + println!("Comparing against existing PNG: {}", png_path); + // Read expected PNG and compare + let file = File::open(png_path).expect("Failed to open PNG"); + let decoder = png::Decoder::new(std::io::BufReader::new(file)); + let mut reader = decoder.read_info().expect("Failed to read PNG info"); + let mut expected_buf = vec![ + 0; + reader + .output_buffer_size() + .expect("Failed to get output buffer size") + ]; + reader + .next_frame(&mut expected_buf) + .expect("Failed to read PNG frame"); + assert_eq!(buf, expected_buf); + } +} + +#[test] +fn test_truncated_non_interlaced() { + test_truncation( + "tests/samples/moon_impact.gif", + "tests/truncated/moon_impact-truncated.png", + 5000, + ); +} + +#[test] +fn test_truncated_interlaced() { + test_truncation( + "tests/samples/interlaced.gif", + "tests/truncated/interlaced-truncated.png", + 5000, + ); +} diff --git a/tests/truncated/interlaced-truncated.png b/tests/truncated/interlaced-truncated.png new file mode 100644 index 0000000000000000000000000000000000000000..cbe1855143a5267a96a7f86eb9b6cddc3ca5f71c GIT binary patch literal 7726 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 I{`u?w1$NPmo&W#< literal 0 HcmV?d00001 diff --git a/tests/truncated/moon_impact-truncated.png b/tests/truncated/moon_impact-truncated.png new file mode 100644 index 0000000000000000000000000000000000000000..8d386c354b5c7a17788a6e2569b7c7744ad72544 GIT binary patch literal 4594 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 Date: Tue, 14 Apr 2026 12:19:10 +0000 Subject: [PATCH 2/3] improve rendering of truncated interlaced GIFs --- src/reader/converter.rs | 65 +++++++++++++++++------ tests/truncated/interlaced-truncated.png | Bin 7726 -> 9011 bytes 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/reader/converter.rs b/src/reader/converter.rs index b4e0ea5..00222b7 100644 --- a/src/reader/converter.rs +++ b/src/reader/converter.rs @@ -200,7 +200,8 @@ impl PixelConverter { next: 0, pass: 0, }; - for row in &mut row_iter { + let mut truncated = false; + for (row, pass) in &mut row_iter { // this can't overflow 32-bit, because row never equals (maximum) height let start = row * width; // Handle a too-small buffer and 32-bit usize overflow without panicking @@ -210,19 +211,19 @@ impl PixelConverter { .ok_or_else(|| DecodingError::format("buffer too small"))?; let filled = self.fill_buffer(frame, line, data_callback)?; if filled < line.len() { - // Once MSRV is >= 1.95: - // core::hint::cold_path(); - line[filled..].fill(0); - // Zero out remaining rows - for rem_row in row_iter { - let start = rem_row * width; - if let Some(rem_line) = buf.get_mut(start..start + width) { - rem_line.fill(0); - } - } - return Err(DecodingError::Truncated); + truncated = true; + match pass { + 0 => line[filled..].fill(0), + 1 => buf.copy_within((row - 4) * width..(row - 4) * width + width, start), + 2 => buf.copy_within((row - 2) * width..(row - 2) * width + width, start), + 3 => buf.copy_within((row - 1) * width..(row - 1) * width + width, start), + _ => unreachable!(), + }; } } + if truncated { + return Err(DecodingError::Truncated); + } } else { let buf = self .buffer_size(frame) @@ -247,13 +248,14 @@ struct InterlaceIterator { } impl iter::Iterator for InterlaceIterator { - type Item = usize; + type Item = (usize, usize); #[inline] fn next(&mut self) -> Option { if self.len == 0 { return None; } + let current_pass = self.pass; // although the pass never goes out of bounds thanks to len==0, // the optimizer doesn't see it. get()? avoids costlier panicking code. let mut next = self.next + *[8, 8, 4, 2].get(self.pass)?; @@ -263,7 +265,7 @@ impl iter::Iterator for InterlaceIterator { self.pass += 1; } mem::swap(&mut next, &mut self.next); - Some(next) + Some((next, current_pass)) } } @@ -275,7 +277,7 @@ mod test { #[rustfmt::skip] #[test] - fn test_interlace_iterator() { + fn test_interlace_iterator_row() { for &(len, expect) in &[ (0, &[][..]), (1, &[0][..]), @@ -297,11 +299,40 @@ mod test { (17, &[0, 8, 16, 4, 12, 2, 6, 10, 14, 1, 3, 5, 7, 9, 11, 13, 15][..]), ] { let iter = InterlaceIterator { len, next: 0, pass: 0 }; - let lines = iter.collect::>(); + let lines = iter.map(|(r, _)| r).collect::>(); assert_eq!(lines, expect); } } + #[rustfmt::skip] + #[test] + fn test_interlace_iterator_pass() { + for &(len, expect) in &[ + (0, &[][..]), + (1, &[0][..]), + (2, &[0, 3][..]), + (3, &[0, 2, 3][..]), + (4, &[0, 2, 3, 3][..]), + (5, &[0, 1, 2, 3, 3][..]), + (6, &[0, 1, 2, 3, 3, 3][..]), + (7, &[0, 1, 2, 2, 3, 3, 3][..]), + (8, &[0, 1, 2, 2, 3, 3, 3, 3][..]), + (9, &[0, 0, 1, 2, 2, 3, 3, 3, 3][..]), + (10, &[0, 0, 1, 2, 2, 3, 3, 3, 3, 3][..]), + (11, &[0, 0, 1, 2, 2, 2, 3, 3, 3, 3, 3][..]), + (12, &[0, 0, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3][..]), + (13, &[0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3][..]), + (14, &[0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3][..]), + (15, &[0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3][..]), + (16, &[0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3][..]), + (17, &[0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3][..]), + ] { + let iter = InterlaceIterator { len, next: 0, pass: 0 }; + let passes = iter.map(|(_, p)| p).collect::>(); + assert_eq!(passes, expect); + } + } + #[test] fn interlace_max() { let iter = InterlaceIterator { @@ -309,6 +340,6 @@ mod test { next: 0, pass: 0, }; - assert_eq!(65533, iter.last().unwrap()); + assert_eq!((65533, 3), iter.last().unwrap()); } } diff --git a/tests/truncated/interlaced-truncated.png b/tests/truncated/interlaced-truncated.png index cbe1855143a5267a96a7f86eb9b6cddc3ca5f71c..139525530f1357006a6f4e0ef379ea927b3f9f2c 100644 GIT binary patch literal 9011 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 B?92cF literal 7726 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 I{`u?w1$NPmo&W#< From b9c789ac4760295430aa2058109fba9c9dc7c9a4 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 29 Apr 2026 14:44:29 +0000 Subject: [PATCH 3/3] add newtype for `Pass` --- src/reader/converter.rs | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/reader/converter.rs b/src/reader/converter.rs index 00222b7..0f9480a 100644 --- a/src/reader/converter.rs +++ b/src/reader/converter.rs @@ -198,7 +198,7 @@ impl PixelConverter { let mut row_iter = InterlaceIterator { len: frame.height, next: 0, - pass: 0, + pass: Pass(0), }; let mut truncated = false; for (row, pass) in &mut row_iter { @@ -212,7 +212,7 @@ impl PixelConverter { let filled = self.fill_buffer(frame, line, data_callback)?; if filled < line.len() { truncated = true; - match pass { + match pass.0 { 0 => line[filled..].fill(0), 1 => buf.copy_within((row - 4) * width..(row - 4) * width + width, start), 2 => buf.copy_within((row - 2) * width..(row - 2) * width + width, start), @@ -241,14 +241,24 @@ impl PixelConverter { } } +/// Represents one of the four GIF interlace passes. +/// +/// GIF interlacing works in four passes: +/// - Pass 0: every 8th row, starting from row 0 +/// - Pass 1: every 8th row, starting from row 4 +/// - Pass 2: every 4th row, starting from row 2 +/// - Pass 3: every 2nd row, starting from row 1 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Pass(pub usize); + struct InterlaceIterator { len: u16, next: usize, - pass: usize, + pass: Pass, } impl iter::Iterator for InterlaceIterator { - type Item = (usize, usize); + type Item = (usize, Pass); #[inline] fn next(&mut self) -> Option { @@ -258,11 +268,11 @@ impl iter::Iterator for InterlaceIterator { let current_pass = self.pass; // although the pass never goes out of bounds thanks to len==0, // the optimizer doesn't see it. get()? avoids costlier panicking code. - let mut next = self.next + *[8, 8, 4, 2].get(self.pass)?; + let mut next = self.next + *[8, 8, 4, 2].get(self.pass.0)?; while next >= self.len as usize { - debug_assert!(self.pass < 4); - next = *[4, 2, 1, 0].get(self.pass)?; - self.pass += 1; + debug_assert!(self.pass.0 < 4); + next = *[4, 2, 1, 0].get(self.pass.0)?; + self.pass.0 += 1; } mem::swap(&mut next, &mut self.next); Some((next, current_pass)) @@ -273,7 +283,7 @@ impl iter::Iterator for InterlaceIterator { mod test { use alloc::vec::Vec; - use super::InterlaceIterator; + use super::{InterlaceIterator, Pass}; #[rustfmt::skip] #[test] @@ -298,7 +308,7 @@ mod test { (16, &[0, 8, 4, 12, 2, 6, 10, 14, 1, 3, 5, 7, 9, 11, 13, 15][..]), (17, &[0, 8, 16, 4, 12, 2, 6, 10, 14, 1, 3, 5, 7, 9, 11, 13, 15][..]), ] { - let iter = InterlaceIterator { len, next: 0, pass: 0 }; + let iter = InterlaceIterator { len, next: 0, pass: Pass(0) }; let lines = iter.map(|(r, _)| r).collect::>(); assert_eq!(lines, expect); } @@ -327,8 +337,8 @@ mod test { (16, &[0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3][..]), (17, &[0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3][..]), ] { - let iter = InterlaceIterator { len, next: 0, pass: 0 }; - let passes = iter.map(|(_, p)| p).collect::>(); + let iter = InterlaceIterator { len, next: 0, pass: Pass(0) }; + let passes = iter.map(|(_, p)| p.0).collect::>(); assert_eq!(passes, expect); } } @@ -338,8 +348,8 @@ mod test { let iter = InterlaceIterator { len: 0xFFFF, next: 0, - pass: 0, + pass: Pass(0), }; - assert_eq!((65533, 3), iter.last().unwrap()); + assert_eq!((65533, Pass(3)), iter.last().unwrap()); } }