From ededdd75c97588855b822349c53a22cabbd070f4 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 19:51:49 +0000 Subject: [PATCH] gif: expose first-frame data byte count --- crates/gif/src/lib.rs | 23 +++++++++++++++++++++++ crates/gif/src/tests.rs | 9 +++++++++ 2 files changed, 32 insertions(+) diff --git a/crates/gif/src/lib.rs b/crates/gif/src/lib.rs index 56b6767fb..1cec284ca 100644 --- a/crates/gif/src/lib.rs +++ b/crates/gif/src/lib.rs @@ -253,6 +253,29 @@ pub fn first_frame_decoded_pixels(bytes: &[u8]) -> Option { Some(pixels) } +/// Counts compressed image-data bytes in the first frame's sub-blocks, excluding its LZW code-size +/// byte and all later blocks. A truncated final sub-block contributes only the bytes present. +pub fn first_frame_compressed_bytes(bytes: &[u8]) -> Option { + let parsed = scan(bytes, 1)?; + let frame = parsed.frames.first()?; + let packed = *bytes.get(frame.offset.checked_add(9)?)?; + let mut pos = frame.offset.checked_add(10)?.checked_add(table_len(packed))?.checked_add(1)?; + let mut total = 0usize; + loop { + let length = usize::from(*bytes.get(pos)?); + pos = pos.checked_add(1)?; + if length == 0 { + return Some(total); + } + let end = pos.checked_add(length)?; + total = total.checked_add(end.min(bytes.len()).saturating_sub(pos))?; + if end > bytes.len() { + return Some(total); + } + pos = end; + } +} + /// Whether a file of `file_len` bytes can plausibly hold `declared_bytes` of decoded picture: at /// most `max_ratio` decoded bytes for each file byte, plus 1 MiB. A decoder that sets up the whole /// logical screen before it reads a frame should be given only files that pass this. diff --git a/crates/gif/src/tests.rs b/crates/gif/src/tests.rs index a75743b19..6a776eb50 100644 --- a/crates/gif/src/tests.rs +++ b/crates/gif/src/tests.rs @@ -54,6 +54,15 @@ fn animated(delays_cs: &[u32], repeat: Option) -> Ve out } +#[test] +fn first_frame_compressed_bytes_exclude_extensions_after_the_image_data() { + let mut bytes = gif(&[[1, 2, 3], [4, 5, 6]], None, None); + assert_eq!(first_frame_compressed_bytes(&bytes), Some(2)); + bytes.splice(bytes.len() - 1..bytes.len() - 1, [0x21, 0xFE, 1, b'x', 0]); + assert_eq!(first_frame_compressed_bytes(&bytes), Some(2)); + assert_eq!(first_frame_compressed_bytes(b"not a gif"), None); +} + #[test] fn the_first_frames_table_and_transparent_entry() { let g = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 255]];