Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.85.0", stable, beta, nightly]
rust: ["1.88.0", stable, beta, nightly]
features: ["", "std", "color_quant"]
steps:
- uses: actions/checkout@v4
Expand All @@ -27,7 +27,7 @@ jobs:
- name: test
run: >
cargo test --tests --benches --no-default-features --features "$FEATURES"
if: ${{ matrix.rust != '1.85.0' }}
if: ${{ matrix.rust != '1.88.0' }}
env:
FEATURES: ${{ matrix.features }}
rustfmt:
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ documentation = "https://docs.rs/gif"
edition = "2024"
resolver = "2"
include = ["src/**", "LICENSE-*", "README.md", "benches/*.rs"]
rust-version = "1.85"
rust-version = "1.88"

[lib]
bench = false

[dependencies]
weezl = "0.1.10"
weezl = "0.2.1"
color_quant = { version = "1.1", optional = true }

[dev-dependencies]
Expand Down
74 changes: 74 additions & 0 deletions tests/small_interlaced.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#![cfg(feature = "std")]
//! 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