Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 76 additions & 11 deletions src/reader/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,20 +809,49 @@ impl StreamingDecoder {
return goto!(n, DecodeSubBlock(left - n), emit Decoded::Nothing);
}

let (mut consumed, bytes_len, status) =
let (consumed, bytes_len, status) =
self.lzw_reader.decode_bytes(&buf[..n], write_into)?;

// skip if can't make progress (decode would fail if check_for_end_code was set)
if matches!(status, LzwStatus::NoProgress) {
consumed = n;
}

let decoded = if let Some(bytes_len) = NonZeroUsize::new(bytes_len) {
Decoded::BytesDecoded(bytes_len)
if matches!(status, LzwStatus::NoProgress)
&& consumed == 0
&& bytes_len == 0
&& !self.lzw_reader.has_ended()
{
// weezl returned NoProgress because the output buffer is too
// small for the next LZW code entry. The input data is valid
// and must NOT be skipped. Drain one byte through a temp buffer
// to unstick the decoder, then append to the caller's output.
// See: https://github.com/image-rs/weezl/pull/72
let (written, _retry_status) = Self::lzw_drain_one(
&mut self.lzw_reader,
write_into,
&self.memory_limit,
)?;
if written > 0 {
goto!(0, DecodeSubBlock(left), emit Decoded::BytesDecoded(
NonZeroUsize::new(written).unwrap()
))
} else {
// Truly stuck; skip remaining input
goto!(n, DecodeSubBlock(left - n), emit Decoded::Nothing)
}
} else if matches!(status, LzwStatus::NoProgress) {
// Had some consumed_in or consumed_out but still NoProgress.
let consumed = n;
let decoded = if let Some(bytes_len) = NonZeroUsize::new(bytes_len) {
Decoded::BytesDecoded(bytes_len)
} else {
Decoded::Nothing
};
goto!(consumed, DecodeSubBlock(left - consumed), emit decoded)
} else {
Decoded::Nothing
};
goto!(consumed, DecodeSubBlock(left - consumed), emit decoded)
let decoded = if let Some(bytes_len) = NonZeroUsize::new(bytes_len) {
Decoded::BytesDecoded(bytes_len)
} else {
Decoded::Nothing
};
goto!(consumed, DecodeSubBlock(left - consumed), emit decoded)
}
} else if b != 0 {
// decode next sub-block
goto!(DecodeSubBlock(b as usize))
Expand All @@ -835,6 +864,23 @@ impl StreamingDecoder {
goto!(0, DecodeSubBlock(0), emit Decoded::Nothing)
} else if matches!(status, LzwStatus::Done) {
goto!(0, FrameDecoded)
} else if !self.lzw_reader.has_ended() {
// weezl stalled during flush but hasn't ended. Drain one
// byte to unstick before treating as end-of-frame.
let (written, retry_status) = Self::lzw_drain_one(
&mut self.lzw_reader,
write_into,
&self.memory_limit,
)?;
if written > 0 {
goto!(0, DecodeSubBlock(0), emit Decoded::BytesDecoded(
NonZeroUsize::new(written).unwrap()
))
} else if matches!(retry_status, LzwStatus::Done) {
goto!(0, FrameDecoded)
} else {
goto!(0, FrameDecoded)
}
} else {
goto!(0, FrameDecoded)
}
Expand All @@ -850,6 +896,25 @@ impl StreamingDecoder {
}
}

/// Retry LZW decode with a 1-byte temp buffer to unstick weezl when the
/// caller's output buffer is too small for the decoder's internal state
/// machine to advance. Returns the number of bytes written to `write_into`.
fn lzw_drain_one(
lzw_reader: &mut LzwReader,
write_into: &mut OutputBuffer<'_>,
memory_limit: &MemoryLimit,
) -> Result<(usize, LzwStatus), DecodingError> {
let mut tmp = [0u8];
let (_, retry_len, retry_status) =
lzw_reader.decode_bytes(&[], &mut OutputBuffer::Slice(&mut tmp))?;
if retry_len > 0 {
let (_, copied) = write_into.append(&tmp[..retry_len], memory_limit)?;
Ok((copied, retry_status))
} else {
Ok((0, retry_status))
}
}

fn read_control_extension(&mut self) -> Result<(), DecodingError> {
if self.ext.data.len() != 4 {
return Err(DecodingError::format("control extension has wrong length"));
Expand Down
73 changes: 73 additions & 0 deletions tests/small_interlaced.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Regression tests for small interlaced GIF decoding.
//!
//! These GIFs were produced by gifsicle 1.95 and are valid per the GIF89a spec
//! (verified with giftopnm from netpbm). The gif crate incorrectly rejects them
//! as "image truncated" due to how it handles weezl's NoProgress status when
//! decoding interlaced frames with small per-row output buffers.
//!
//! Related: https://github.com/image-rs/weezl/pull/72 (yield_on_full fixes)

// 39 bytes: 7x7 interlaced grayscale GIF (2-color palette) from gifsicle 1.95
const SMALL_INTERLACED_7X7: &[u8] = &[
0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x07, 0x00, 0x07, 0x00, 0xf0, 0x00, 0x00, 0xad, 0xad, 0xad,
0xff, 0xff, 0xff, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x40, 0x02, 0x06, 0x84,
0x8f, 0xa9, 0xcb, 0x8d, 0x05, 0x00, 0x3b,
];

// 71 bytes: 9x9 interlaced bilevel GIF from gifsicle 1.95
const SMALL_INTERLACED_9X9: &[u8] = &[
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x09, 0x00, 0x09, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0x21, 0xff, 0x0b, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x61, 0x67, 0x69, 0x63,
0x6b, 0x0e, 0x67, 0x61, 0x6d, 0x6d, 0x61, 0x3d, 0x30, 0x2e, 0x34, 0x35, 0x34, 0x35, 0x34, 0x35,
0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x09, 0x00, 0x40, 0x02, 0x08, 0x84, 0x8f, 0xa9,
0xcb, 0xed, 0x0f, 0x4f, 0x01, 0x00, 0x3b,
];

// 149 bytes: 47x63 interlaced grayscale GIF from gifsicle 1.95
const SMALL_INTERLACED_47X63: &[u8] = &[
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x2f, 0x00, 0x3f, 0x00, 0xf0, 0x00, 0x00, 0x23, 0x23, 0x23,
0xdc, 0xdc, 0xdc, 0x21, 0xff, 0x0b, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x61, 0x67, 0x69, 0x63,
0x6b, 0x0e, 0x67, 0x61, 0x6d, 0x6d, 0x61, 0x3d, 0x30, 0x2e, 0x34, 0x35, 0x34, 0x35, 0x34, 0x35,
0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x3f, 0x00, 0x40, 0x02, 0x56, 0x8c, 0x8f, 0xa9,
0xcb, 0xed, 0x0f, 0xa3, 0x9c, 0xb4, 0xda, 0x0a, 0xb2, 0xde, 0xbc, 0xfb, 0x0f, 0x86, 0xe2, 0x48,
0x6a, 0xd7, 0xc9, 0x94, 0xea, 0xca, 0xb6, 0xee, 0x0b, 0xc7, 0xf2, 0x4c, 0xd7, 0xf6, 0x8d, 0xe7,
0xfa, 0xce, 0xf7, 0xfe, 0x0f, 0x0c, 0x0a, 0x87, 0xc4, 0xa2, 0xf1, 0x88, 0x4c, 0x2a, 0x97, 0xcc,
0x66, 0x10, 0x05, 0x45, 0x28, 0xa3, 0x54, 0xa7, 0xf5, 0x49, 0x85, 0x5e, 0xb7, 0xbd, 0xac, 0x96,
0x0b, 0xc6, 0x79, 0x51, 0xe1, 0x72, 0x6d, 0x7c, 0x32, 0xab, 0x63, 0xe8, 0xcb, 0xfa, 0x0d, 0x8f,
0xcb, 0x4b, 0x05, 0x00, 0x3b,
];

/// Decode a GIF using read_next_frame and return the frame pixels + dimensions.
fn decode_gif(data: &[u8]) -> Result<(Vec<u8>, u16, u16), gif::DecodingError> {
let mut opts = gif::DecodeOptions::new();
opts.set_color_output(gif::ColorOutput::Indexed);
let mut decoder = opts.read_info(data)?;
let frame = decoder
.read_next_frame()?
.expect("expected at least one frame");
Ok((frame.buffer.to_vec(), frame.width, frame.height))
}

#[test]
fn small_interlaced_7x7_decodes() {
let (pixels, w, h) =
decode_gif(SMALL_INTERLACED_7X7).expect("7x7 interlaced GIF should decode");
assert_eq!((w, h), (7, 7));
assert_eq!(pixels.len(), 49); // 7*7 indexed pixels
}

#[test]
fn small_interlaced_9x9_decodes() {
let (pixels, w, h) =
decode_gif(SMALL_INTERLACED_9X9).expect("9x9 interlaced GIF should decode");
assert_eq!((w, h), (9, 9));
assert_eq!(pixels.len(), 81); // 9*9 indexed pixels
}

#[test]
fn small_interlaced_47x63_decodes() {
let (pixels, w, h) =
decode_gif(SMALL_INTERLACED_47X63).expect("47x63 interlaced GIF should decode");
assert_eq!((w, h), (47, 63));
assert_eq!(pixels.len(), 2961); // 47*63 indexed pixels
}
Loading