we (web engine): Experimental web browser project to understand the limits of Claude
1//! GIF decoder (GIF87a / GIF89a) — pure Rust.
2//!
3//! Supports single-frame and animated GIFs, global and local color tables,
4//! LZW decompression, transparency via Graphic Control Extension, interlacing,
5//! and frame disposal methods.
6
7use crate::pixel::{self, Image, ImageError};
8
9// ---------------------------------------------------------------------------
10// Constants
11// ---------------------------------------------------------------------------
12
13const GIF87A: &[u8; 6] = b"GIF87a";
14const GIF89A: &[u8; 6] = b"GIF89a";
15
16const IMAGE_DESCRIPTOR: u8 = 0x2C;
17const EXTENSION_INTRODUCER: u8 = 0x21;
18const TRAILER: u8 = 0x3B;
19
20const GRAPHIC_CONTROL_EXT: u8 = 0xF9;
21const APPLICATION_EXT: u8 = 0xFF;
22const COMMENT_EXT: u8 = 0xFE;
23const PLAIN_TEXT_EXT: u8 = 0x01;
24
25// ---------------------------------------------------------------------------
26// Disposal method
27// ---------------------------------------------------------------------------
28
29/// How the decoder should handle the frame's area before drawing the next frame.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DisposalMethod {
32 /// No disposal specified — leave the frame in place.
33 None,
34 /// Do not dispose — leave the frame in place (same as None).
35 DoNotDispose,
36 /// Restore the area to the background color.
37 RestoreBackground,
38 /// Restore the area to the previous frame's content.
39 RestorePrevious,
40}
41
42impl DisposalMethod {
43 fn from_byte(b: u8) -> Self {
44 match b {
45 0 => Self::None,
46 1 => Self::DoNotDispose,
47 2 => Self::RestoreBackground,
48 3 => Self::RestorePrevious,
49 _ => Self::None,
50 }
51 }
52}
53
54// ---------------------------------------------------------------------------
55// GifFrame
56// ---------------------------------------------------------------------------
57
58/// A single frame from an animated GIF.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct GifFrame {
61 /// The frame image in RGBA8.
62 pub image: Image,
63 /// Delay time in hundredths of a second (centiseconds).
64 pub delay_cs: u16,
65 /// Disposal method for this frame.
66 pub disposal: DisposalMethod,
67}
68
69// ---------------------------------------------------------------------------
70// LZW decompressor
71// ---------------------------------------------------------------------------
72
73/// LZW decompressor for GIF image data.
74///
75/// GIF uses a variable-width LZW scheme with clear and EOI codes. The minimum
76/// code size is specified per sub-image. Codes are packed LSB-first.
77struct LzwDecoder {
78 min_code_size: u8,
79 clear_code: u16,
80 eoi_code: u16,
81 /// Current entries in the code table. Each entry is stored as
82 /// (prefix_code, suffix_byte). Single-byte entries have prefix = u16::MAX.
83 table: Vec<(u16, u8)>,
84 code_size: u8,
85 next_code: u16,
86}
87
88impl LzwDecoder {
89 fn new(min_code_size: u8) -> Self {
90 let clear_code = 1u16 << min_code_size;
91 let eoi_code = clear_code + 1;
92 Self {
93 min_code_size,
94 clear_code,
95 eoi_code,
96 table: Vec::new(),
97 code_size: min_code_size + 1,
98 next_code: 0,
99 }
100 }
101
102 fn reset(&mut self) {
103 self.table.clear();
104 let initial_entries = (1u16 << self.min_code_size) + 2;
105 self.table.reserve(initial_entries as usize);
106 for i in 0..self.clear_code {
107 self.table.push((u16::MAX, i as u8));
108 }
109 // Clear code and EOI code entries (not used for output, but occupy slots)
110 self.table.push((u16::MAX, 0)); // clear code
111 self.table.push((u16::MAX, 0)); // eoi code
112 self.code_size = self.min_code_size + 1;
113 self.next_code = self.eoi_code + 1;
114 }
115
116 /// Emit the string for a code into `output`. Returns the first byte of the string.
117 fn emit(&self, code: u16, output: &mut Vec<u8>) -> u8 {
118 // Walk the chain to find the string length, then write in reverse.
119 let start = output.len();
120 let mut c = code;
121 loop {
122 let (prefix, suffix) = self.table[c as usize];
123 output.push(suffix);
124 if prefix == u16::MAX {
125 break;
126 }
127 c = prefix;
128 }
129 // Reverse the appended portion
130 output[start..].reverse();
131 output[start]
132 }
133
134 /// Get the first byte of the string for a code.
135 fn first_byte(&self, code: u16) -> u8 {
136 let mut c = code;
137 loop {
138 let (prefix, suffix) = self.table[c as usize];
139 if prefix == u16::MAX {
140 return suffix;
141 }
142 c = prefix;
143 }
144 }
145
146 fn add_entry(&mut self, prefix: u16, suffix: u8) {
147 if self.next_code < 4096 {
148 self.table.push((prefix, suffix));
149 self.next_code += 1;
150 // Increase code size when all codes at the current size are used (early change)
151 if self.next_code >= (1 << self.code_size) && self.code_size < 12 {
152 self.code_size += 1;
153 }
154 }
155 }
156
157 /// Decompress LZW data from packed sub-blocks.
158 fn decompress(&mut self, data: &[u8]) -> Result<Vec<u8>, ImageError> {
159 self.reset();
160 let mut reader = LzwBitReader::new(data);
161 let mut output = Vec::new();
162
163 // First code must be a clear code
164 let first = reader.read_bits(self.code_size)?;
165 if first != self.clear_code {
166 return Err(ImageError::Decode(
167 "GIF LZW: expected clear code at start".into(),
168 ));
169 }
170
171 // Read the first data code after clear
172 let mut prev_code = reader.read_bits(self.code_size)?;
173 if prev_code == self.eoi_code {
174 return Ok(output);
175 }
176 if prev_code >= self.next_code {
177 return Err(ImageError::Decode("GIF LZW: invalid first code".into()));
178 }
179 self.emit(prev_code, &mut output);
180
181 while let Ok(code) = reader.read_bits(self.code_size) {
182 if code == self.eoi_code {
183 break;
184 }
185
186 if code == self.clear_code {
187 self.reset();
188 prev_code = match reader.read_bits(self.code_size) {
189 Ok(c) => c,
190 Err(_) => break,
191 };
192 if prev_code == self.eoi_code {
193 break;
194 }
195 if prev_code >= self.next_code {
196 return Err(ImageError::Decode(
197 "GIF LZW: invalid code after clear".into(),
198 ));
199 }
200 self.emit(prev_code, &mut output);
201 continue;
202 }
203
204 if code < self.next_code {
205 // Code is in the table
206 let first = self.first_byte(code);
207 self.add_entry(prev_code, first);
208 self.emit(code, &mut output);
209 } else if code == self.next_code {
210 // Special case: code is not yet in table
211 let first = self.first_byte(prev_code);
212 self.add_entry(prev_code, first);
213 self.emit(code, &mut output);
214 } else {
215 return Err(ImageError::Decode(format!(
216 "GIF LZW: code {code} out of range (next={next})",
217 next = self.next_code
218 )));
219 }
220
221 prev_code = code;
222 }
223
224 Ok(output)
225 }
226}
227
228// ---------------------------------------------------------------------------
229// Bit reader for LZW (LSB-first, packed)
230// ---------------------------------------------------------------------------
231
232struct LzwBitReader<'a> {
233 data: &'a [u8],
234 pos: usize,
235 bit_buf: u32,
236 bits_in_buf: u8,
237}
238
239impl<'a> LzwBitReader<'a> {
240 fn new(data: &'a [u8]) -> Self {
241 Self {
242 data,
243 pos: 0,
244 bit_buf: 0,
245 bits_in_buf: 0,
246 }
247 }
248
249 fn read_bits(&mut self, count: u8) -> Result<u16, ImageError> {
250 while self.bits_in_buf < count {
251 if self.pos >= self.data.len() {
252 return Err(ImageError::Decode("GIF LZW: unexpected end of data".into()));
253 }
254 self.bit_buf |= (self.data[self.pos] as u32) << self.bits_in_buf;
255 self.pos += 1;
256 self.bits_in_buf += 8;
257 }
258 let mask = (1u32 << count) - 1;
259 let value = (self.bit_buf & mask) as u16;
260 self.bit_buf >>= count;
261 self.bits_in_buf -= count;
262 Ok(value)
263 }
264}
265
266// ---------------------------------------------------------------------------
267// GIF parser internals
268// ---------------------------------------------------------------------------
269
270struct GifReader<'a> {
271 data: &'a [u8],
272 pos: usize,
273}
274
275impl<'a> GifReader<'a> {
276 fn new(data: &'a [u8]) -> Self {
277 Self { data, pos: 0 }
278 }
279
280 fn remaining(&self) -> usize {
281 self.data.len().saturating_sub(self.pos)
282 }
283
284 fn read_byte(&mut self) -> Result<u8, ImageError> {
285 if self.pos >= self.data.len() {
286 return Err(ImageError::Decode("GIF: unexpected end of data".into()));
287 }
288 let b = self.data[self.pos];
289 self.pos += 1;
290 Ok(b)
291 }
292
293 fn read_u16_le(&mut self) -> Result<u16, ImageError> {
294 if self.pos + 2 > self.data.len() {
295 return Err(ImageError::Decode("GIF: unexpected end of data".into()));
296 }
297 let lo = self.data[self.pos] as u16;
298 let hi = self.data[self.pos + 1] as u16;
299 self.pos += 2;
300 Ok(lo | (hi << 8))
301 }
302
303 fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], ImageError> {
304 if self.pos + n > self.data.len() {
305 return Err(ImageError::Decode("GIF: unexpected end of data".into()));
306 }
307 let slice = &self.data[self.pos..self.pos + n];
308 self.pos += n;
309 Ok(slice)
310 }
311
312 /// Read concatenated sub-blocks (size-prefixed, terminated by a zero-length block).
313 fn read_sub_blocks(&mut self) -> Result<Vec<u8>, ImageError> {
314 let mut result = Vec::new();
315 loop {
316 let block_size = self.read_byte()? as usize;
317 if block_size == 0 {
318 break;
319 }
320 let block = self.read_bytes(block_size)?;
321 result.extend_from_slice(block);
322 }
323 Ok(result)
324 }
325
326 /// Skip sub-blocks without collecting data.
327 fn skip_sub_blocks(&mut self) -> Result<(), ImageError> {
328 loop {
329 let block_size = self.read_byte()? as usize;
330 if block_size == 0 {
331 break;
332 }
333 if self.pos + block_size > self.data.len() {
334 return Err(ImageError::Decode("GIF: unexpected end of data".into()));
335 }
336 self.pos += block_size;
337 }
338 Ok(())
339 }
340}
341
342// ---------------------------------------------------------------------------
343// Graphic Control Extension
344// ---------------------------------------------------------------------------
345
346struct GraphicControl {
347 disposal: DisposalMethod,
348 transparent_color: Option<u8>,
349 delay_cs: u16,
350}
351
352// ---------------------------------------------------------------------------
353// Logical Screen Descriptor
354// ---------------------------------------------------------------------------
355
356struct ScreenDescriptor {
357 width: u16,
358 height: u16,
359 global_color_table: Option<Vec<u8>>,
360 background_index: u8,
361}
362
363fn parse_header_and_screen(reader: &mut GifReader<'_>) -> Result<ScreenDescriptor, ImageError> {
364 // Validate signature
365 if reader.remaining() < 6 {
366 return Err(ImageError::Decode("GIF: data too short for header".into()));
367 }
368 let sig = reader.read_bytes(6)?;
369 if sig != GIF87A.as_slice() && sig != GIF89A.as_slice() {
370 return Err(ImageError::Decode("GIF: invalid signature".into()));
371 }
372
373 // Logical screen descriptor (7 bytes)
374 let width = reader.read_u16_le()?;
375 let height = reader.read_u16_le()?;
376 let packed = reader.read_byte()?;
377 let background_index = reader.read_byte()?;
378 let _pixel_aspect_ratio = reader.read_byte()?;
379
380 let has_gct = (packed & 0x80) != 0;
381 let gct_size_field = packed & 0x07;
382
383 let global_color_table = if has_gct {
384 let num_entries = 1usize << (gct_size_field as usize + 1);
385 let table_bytes = num_entries * 3;
386 let table = reader.read_bytes(table_bytes)?;
387 Some(table.to_vec())
388 } else {
389 None
390 };
391
392 Ok(ScreenDescriptor {
393 width,
394 height,
395 global_color_table,
396 background_index,
397 })
398}
399
400// ---------------------------------------------------------------------------
401// GIF interlace pass reordering
402// ---------------------------------------------------------------------------
403
404/// GIF interlaced images use 4 passes with these parameters:
405/// Pass 1: start row 0, step 8
406/// Pass 2: start row 4, step 8
407/// Pass 3: start row 2, step 4
408/// Pass 4: start row 1, step 2
409const INTERLACE_PASSES: [(usize, usize); 4] = [(0, 8), (4, 8), (2, 4), (1, 2)];
410
411fn deinterlace(data: &[u8], width: usize, height: usize) -> Vec<u8> {
412 let row_bytes = width;
413 let mut output = vec![0u8; width * height];
414 let mut src_row = 0usize;
415
416 for &(start, step) in &INTERLACE_PASSES {
417 let mut y = start;
418 while y < height {
419 let src_offset = src_row * row_bytes;
420 let dst_offset = y * row_bytes;
421 if src_offset + row_bytes <= data.len() {
422 output[dst_offset..dst_offset + row_bytes]
423 .copy_from_slice(&data[src_offset..src_offset + row_bytes]);
424 }
425 src_row += 1;
426 y += step;
427 }
428 }
429
430 output
431}
432
433// ---------------------------------------------------------------------------
434// Frame decoding
435// ---------------------------------------------------------------------------
436
437struct RawFrame {
438 left: u16,
439 top: u16,
440 width: u16,
441 height: u16,
442 color_table: Vec<u8>,
443 transparent_color: Option<u8>,
444 disposal: DisposalMethod,
445 delay_cs: u16,
446 /// Decompressed indexed pixel data.
447 indices: Vec<u8>,
448}
449
450fn decode_frame(
451 reader: &mut GifReader<'_>,
452 global_ct: &Option<Vec<u8>>,
453 gce: Option<GraphicControl>,
454) -> Result<RawFrame, ImageError> {
455 // Image descriptor
456 let left = reader.read_u16_le()?;
457 let top = reader.read_u16_le()?;
458 let width = reader.read_u16_le()?;
459 let height = reader.read_u16_le()?;
460 let packed = reader.read_byte()?;
461
462 let has_lct = (packed & 0x80) != 0;
463 let interlaced = (packed & 0x40) != 0;
464 let lct_size_field = packed & 0x07;
465
466 let local_color_table = if has_lct {
467 let num_entries = 1usize << (lct_size_field as usize + 1);
468 let table_bytes = num_entries * 3;
469 let table = reader.read_bytes(table_bytes)?;
470 table.to_vec()
471 } else {
472 Vec::new()
473 };
474
475 // Determine which color table to use
476 let color_table = if !local_color_table.is_empty() {
477 local_color_table
478 } else if let Some(ref gct) = global_ct {
479 gct.clone()
480 } else {
481 return Err(ImageError::Decode("GIF: no color table available".into()));
482 };
483
484 // LZW minimum code size
485 let min_code_size = reader.read_byte()?;
486 if min_code_size > 11 {
487 return Err(ImageError::Decode(format!(
488 "GIF: invalid LZW minimum code size {min_code_size}"
489 )));
490 }
491
492 // Read LZW sub-blocks
493 let lzw_data = reader.read_sub_blocks()?;
494
495 // Decompress
496 let mut decoder = LzwDecoder::new(min_code_size);
497 let mut indices = decoder.decompress(&lzw_data)?;
498
499 // Deinterlace if needed
500 if interlaced && width > 0 && height > 0 {
501 indices = deinterlace(&indices, width as usize, height as usize);
502 }
503
504 let (disposal, transparent_color, delay_cs) = match gce {
505 Some(gc) => (gc.disposal, gc.transparent_color, gc.delay_cs),
506 None => (DisposalMethod::None, None, 0),
507 };
508
509 Ok(RawFrame {
510 left,
511 top,
512 width,
513 height,
514 color_table,
515 transparent_color,
516 disposal,
517 delay_cs,
518 indices,
519 })
520}
521
522/// Render a raw frame's indices into an RGBA8 image.
523fn render_frame(frame: &RawFrame) -> Result<Image, ImageError> {
524 let w = frame.width as u32;
525 let h = frame.height as u32;
526 if w == 0 || h == 0 {
527 return Err(ImageError::Decode("GIF: frame has zero dimension".into()));
528 }
529
530 let pixel_count = w as usize * h as usize;
531 let palette = &frame.color_table;
532 let palette_len = palette.len() / 3;
533
534 // Truncate indices to expected pixel count (some GIFs have extra data)
535 let indices = if frame.indices.len() >= pixel_count {
536 &frame.indices[..pixel_count]
537 } else {
538 // Pad with zeros if data is short
539 return render_frame_padded(frame, pixel_count, palette, palette_len);
540 };
541
542 match frame.transparent_color {
543 Some(tc) => {
544 let mut data = Vec::with_capacity(pixel_count * 4);
545 for &idx in indices {
546 if idx == tc {
547 data.extend_from_slice(&[0, 0, 0, 0]);
548 } else {
549 let i = idx as usize;
550 if i >= palette_len {
551 return Err(ImageError::PaletteIndexOutOfBounds {
552 index: idx,
553 palette_len,
554 });
555 }
556 let offset = i * 3;
557 data.push(palette[offset]);
558 data.push(palette[offset + 1]);
559 data.push(palette[offset + 2]);
560 data.push(255);
561 }
562 }
563 Image::new(w, h, data)
564 }
565 None => pixel::from_indexed(w, h, palette, indices),
566 }
567}
568
569fn render_frame_padded(
570 frame: &RawFrame,
571 pixel_count: usize,
572 palette: &[u8],
573 palette_len: usize,
574) -> Result<Image, ImageError> {
575 let w = frame.width as u32;
576 let h = frame.height as u32;
577 let mut data = Vec::with_capacity(pixel_count * 4);
578 for i in 0..pixel_count {
579 let idx = if i < frame.indices.len() {
580 frame.indices[i]
581 } else {
582 0
583 };
584 if let Some(tc) = frame.transparent_color {
585 if idx == tc {
586 data.extend_from_slice(&[0, 0, 0, 0]);
587 continue;
588 }
589 }
590 let ci = idx as usize;
591 if ci >= palette_len {
592 return Err(ImageError::PaletteIndexOutOfBounds {
593 index: idx,
594 palette_len,
595 });
596 }
597 let offset = ci * 3;
598 data.push(palette[offset]);
599 data.push(palette[offset + 1]);
600 data.push(palette[offset + 2]);
601 data.push(255);
602 }
603 Image::new(w, h, data)
604}
605
606// ---------------------------------------------------------------------------
607// Canvas compositing for animated GIFs
608// ---------------------------------------------------------------------------
609
610fn composite_frame(
611 canvas: &mut [u8],
612 canvas_width: u32,
613 canvas_height: u32,
614 frame: &RawFrame,
615 frame_image: &Image,
616) {
617 let cx = frame.left as usize;
618 let cy = frame.top as usize;
619 let fw = frame.width as usize;
620 let fh = frame.height as usize;
621 let cw = canvas_width as usize;
622 let ch = canvas_height as usize;
623
624 for row in 0..fh {
625 let dy = cy + row;
626 if dy >= ch {
627 break;
628 }
629 for col in 0..fw {
630 let dx = cx + col;
631 if dx >= cw {
632 break;
633 }
634 let src_off = (row * fw + col) * 4;
635 let dst_off = (dy * cw + dx) * 4;
636 let a = frame_image.data[src_off + 3];
637 if a > 0 {
638 canvas[dst_off] = frame_image.data[src_off];
639 canvas[dst_off + 1] = frame_image.data[src_off + 1];
640 canvas[dst_off + 2] = frame_image.data[src_off + 2];
641 canvas[dst_off + 3] = frame_image.data[src_off + 3];
642 }
643 }
644 }
645}
646
647fn apply_disposal(
648 canvas: &mut [u8],
649 prev_canvas: &[u8],
650 frame: &RawFrame,
651 canvas_width: u32,
652 canvas_height: u32,
653 bg_index: u8,
654 color_table: &Option<Vec<u8>>,
655) {
656 match frame.disposal {
657 DisposalMethod::RestoreBackground => {
658 let cx = frame.left as usize;
659 let cy = frame.top as usize;
660 let fw = frame.width as usize;
661 let fh = frame.height as usize;
662 let cw = canvas_width as usize;
663 let ch = canvas_height as usize;
664
665 // Background color from the global color table
666 let (br, bg, bb) = if let Some(ref gct) = color_table {
667 let i = bg_index as usize;
668 let palette_len = gct.len() / 3;
669 if i < palette_len {
670 let off = i * 3;
671 (gct[off], gct[off + 1], gct[off + 2])
672 } else {
673 (0, 0, 0)
674 }
675 } else {
676 (0, 0, 0)
677 };
678
679 for row in 0..fh {
680 let dy = cy + row;
681 if dy >= ch {
682 break;
683 }
684 for col in 0..fw {
685 let dx = cx + col;
686 if dx >= cw {
687 break;
688 }
689 let off = (dy * cw + dx) * 4;
690 canvas[off] = br;
691 canvas[off + 1] = bg;
692 canvas[off + 2] = bb;
693 canvas[off + 3] = 0; // transparent background
694 }
695 }
696 }
697 DisposalMethod::RestorePrevious => {
698 canvas.copy_from_slice(prev_canvas);
699 }
700 DisposalMethod::None | DisposalMethod::DoNotDispose => {
701 // Leave canvas as-is
702 }
703 }
704}
705
706// ---------------------------------------------------------------------------
707// Public API
708// ---------------------------------------------------------------------------
709
710/// Decode a GIF image, returning the first frame as an RGBA8 image.
711///
712/// For single-frame GIFs this returns the only frame. For animated GIFs this
713/// returns the composited first frame at the logical screen dimensions.
714pub fn decode_gif(data: &[u8]) -> Result<Image, ImageError> {
715 let frames = decode_gif_frames(data)?;
716 if frames.is_empty() {
717 return Err(ImageError::Decode("GIF: no frames found".into()));
718 }
719 Ok(frames.into_iter().next().unwrap().image)
720}
721
722/// Decode all frames from a GIF image.
723///
724/// Each frame is composited onto the logical screen canvas according to the
725/// frame's position, disposal method, and transparency settings. The returned
726/// `Image` in each `GifFrame` has the logical screen dimensions.
727pub fn decode_gif_frames(data: &[u8]) -> Result<Vec<GifFrame>, ImageError> {
728 let mut reader = GifReader::new(data);
729 let screen = parse_header_and_screen(&mut reader)?;
730
731 let sw = screen.width as u32;
732 let sh = screen.height as u32;
733 if sw == 0 || sh == 0 {
734 return Err(ImageError::Decode(
735 "GIF: logical screen has zero dimension".into(),
736 ));
737 }
738
739 let canvas_size = sw as usize * sh as usize * 4;
740 let mut canvas = vec![0u8; canvas_size];
741 let mut prev_canvas = vec![0u8; canvas_size];
742 let mut frames = Vec::new();
743 let mut current_gce: Option<GraphicControl> = None;
744
745 loop {
746 if reader.remaining() == 0 {
747 break;
748 }
749 let block_type = reader.read_byte()?;
750
751 match block_type {
752 TRAILER => break,
753
754 EXTENSION_INTRODUCER => {
755 let label = reader.read_byte()?;
756 match label {
757 GRAPHIC_CONTROL_EXT => {
758 let block_size = reader.read_byte()?;
759 if block_size != 4 {
760 return Err(ImageError::Decode(
761 "GIF: invalid graphic control extension size".into(),
762 ));
763 }
764 let packed = reader.read_byte()?;
765 let delay_cs = reader.read_u16_le()?;
766 let transparent_index = reader.read_byte()?;
767 let _terminator = reader.read_byte()?;
768
769 let disposal = DisposalMethod::from_byte((packed >> 2) & 0x07);
770 let has_transparency = (packed & 0x01) != 0;
771
772 current_gce = Some(GraphicControl {
773 disposal,
774 transparent_color: if has_transparency {
775 Some(transparent_index)
776 } else {
777 None
778 },
779 delay_cs,
780 });
781 }
782 APPLICATION_EXT | COMMENT_EXT | PLAIN_TEXT_EXT => {
783 // Skip the block size and sub-blocks
784 reader.skip_sub_blocks()?;
785 }
786 _ => {
787 // Unknown extension — skip sub-blocks
788 reader.skip_sub_blocks()?;
789 }
790 }
791 }
792
793 IMAGE_DESCRIPTOR => {
794 let gce = current_gce.take();
795 let raw = decode_frame(&mut reader, &screen.global_color_table, gce)?;
796 let frame_image = render_frame(&raw)?;
797
798 // Save canvas state before compositing (for RestorePrevious)
799 prev_canvas.copy_from_slice(&canvas);
800
801 // Composite frame onto canvas
802 composite_frame(&mut canvas, sw, sh, &raw, &frame_image);
803
804 // Output the composited canvas as this frame
805 let output_image = Image::new(sw, sh, canvas.clone())?;
806 frames.push(GifFrame {
807 image: output_image,
808 delay_cs: raw.delay_cs,
809 disposal: raw.disposal,
810 });
811
812 // Apply disposal for next frame
813 apply_disposal(
814 &mut canvas,
815 &prev_canvas,
816 &raw,
817 sw,
818 sh,
819 screen.background_index,
820 &screen.global_color_table,
821 );
822 }
823
824 _ => {
825 // Unknown block type — try to skip. In practice this shouldn't happen
826 // in well-formed GIFs but we don't want to fail hard.
827 break;
828 }
829 }
830 }
831
832 Ok(frames)
833}
834
835// ---------------------------------------------------------------------------
836// Tests
837// ---------------------------------------------------------------------------
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842
843 // -----------------------------------------------------------------------
844 // LZW decompressor tests
845 // -----------------------------------------------------------------------
846
847 #[test]
848 fn lzw_simple_sequence() {
849 // LZW with min code size 2 (codes 0-3 are literal, 4=clear, 5=eoi)
850 // Encode a simple sequence: clear, 0, 1, 0, 1, eoi
851 // initial code_size=3, bumps to 4 after next_code reaches 8
852 let mut bits: u64 = 0;
853 let mut bit_pos = 0u32;
854
855 let codes: &[(u16, u8)] = &[
856 (4, 3), // clear code
857 (0, 3), // literal 0
858 (1, 3), // literal 1
859 (0, 3), // literal 0 → add_entry makes next_code=8, code_size→4
860 (1, 4), // literal 1 (now 4 bits)
861 (5, 4), // eoi (4 bits)
862 ];
863
864 for &(code, nbits) in codes {
865 bits |= (code as u64) << bit_pos;
866 bit_pos += nbits as u32;
867 }
868
869 let byte_count = (bit_pos + 7) / 8;
870 let data: Vec<u8> = (0..byte_count)
871 .map(|i| ((bits >> (i * 8)) & 0xFF) as u8)
872 .collect();
873
874 let mut decoder = LzwDecoder::new(2);
875 let result = decoder.decompress(&data).unwrap();
876 assert_eq!(result, vec![0, 1, 0, 1]);
877 }
878
879 #[test]
880 fn lzw_repeated_pattern() {
881 // Encode: clear, 1, 1, 1, 1, eoi with min_code_size=2
882 // After clear: table has 0,1,2,3, clear=4, eoi=5, next=6
883 // Code size = 3
884 // Read 1 -> output [1], prev=1
885 // Read 1 -> in table, first_byte(1)=1, add (1,1) as code 6 -> output [1,1]
886 // next=7
887 // Read 6 -> in table, first_byte(6)=1, add (1,1) as code 7 -> output [1,1,1,1]
888 // next=8 -> code_size becomes 4
889 // Read 5 (eoi, now 4 bits) -> done
890
891 let mut bits: u64 = 0;
892 let mut bit_pos = 0u32;
893
894 let codes: &[(u16, u8)] = &[
895 (4, 3), // clear
896 (1, 3), // literal 1
897 (1, 3), // literal 1
898 (6, 3), // code 6 = {1,1}
899 (5, 4), // eoi (now 4 bits since next_code=8 bumped code_size)
900 ];
901
902 for &(code, nbits) in codes {
903 bits |= (code as u64) << bit_pos;
904 bit_pos += nbits as u32;
905 }
906
907 let byte_count = (bit_pos + 7) / 8;
908 let data: Vec<u8> = (0..byte_count)
909 .map(|i| ((bits >> (i * 8)) & 0xFF) as u8)
910 .collect();
911
912 let mut decoder = LzwDecoder::new(2);
913 let result = decoder.decompress(&data).unwrap();
914 assert_eq!(result, vec![1, 1, 1, 1]);
915 }
916
917 #[test]
918 fn lzw_code_not_in_table() {
919 // Test the special case where code == next_code
920 // min_code_size=2, clear=4, eoi=5, next=6
921 // Sequence: clear, 0, 6(=next), eoi
922 // After 0: prev=0
923 // Read 6: code == next_code, first_byte(prev=0)=0, add(0,0) as 6
924 // emit(6) = [0,0]
925 // Total output: [0, 0, 0]
926
927 let mut bits: u64 = 0;
928 let mut bit_pos = 0u32;
929
930 let codes: &[(u16, u8)] = &[
931 (4, 3), // clear
932 (0, 3), // literal 0
933 (6, 3), // code 6 = next_code (special case)
934 (5, 3), // eoi
935 ];
936
937 for &(code, nbits) in codes {
938 bits |= (code as u64) << bit_pos;
939 bit_pos += nbits as u32;
940 }
941
942 let byte_count = (bit_pos + 7) / 8;
943 let data: Vec<u8> = (0..byte_count)
944 .map(|i| ((bits >> (i * 8)) & 0xFF) as u8)
945 .collect();
946
947 let mut decoder = LzwDecoder::new(2);
948 let result = decoder.decompress(&data).unwrap();
949 assert_eq!(result, vec![0, 0, 0]);
950 }
951
952 #[test]
953 fn lzw_clear_code_reset() {
954 // Test that clear code resets the table
955 // min_code_size=2, clear=4, eoi=5
956 // Sequence: clear, 0, 1, clear, 2, 3, eoi
957
958 let mut bits: u64 = 0;
959 let mut bit_pos = 0u32;
960
961 let codes: &[(u16, u8)] = &[
962 (4, 3), // clear
963 (0, 3), // literal 0
964 (1, 3), // literal 1
965 (4, 3), // clear (reset)
966 (2, 3), // literal 2
967 (3, 3), // literal 3
968 (5, 3), // eoi
969 ];
970
971 for &(code, nbits) in codes {
972 bits |= (code as u64) << bit_pos;
973 bit_pos += nbits as u32;
974 }
975
976 let byte_count = (bit_pos + 7) / 8;
977 let data: Vec<u8> = (0..byte_count)
978 .map(|i| ((bits >> (i * 8)) & 0xFF) as u8)
979 .collect();
980
981 let mut decoder = LzwDecoder::new(2);
982 let result = decoder.decompress(&data).unwrap();
983 assert_eq!(result, vec![0, 1, 2, 3]);
984 }
985
986 #[test]
987 fn lzw_empty_after_clear_eoi() {
988 // clear then immediate eoi
989 let mut bits: u64 = 0;
990 let mut bit_pos = 0u32;
991
992 let codes: &[(u16, u8)] = &[
993 (4, 3), // clear
994 (5, 3), // eoi
995 ];
996
997 for &(code, nbits) in codes {
998 bits |= (code as u64) << bit_pos;
999 bit_pos += nbits as u32;
1000 }
1001
1002 let byte_count = (bit_pos + 7) / 8;
1003 let data: Vec<u8> = (0..byte_count)
1004 .map(|i| ((bits >> (i * 8)) & 0xFF) as u8)
1005 .collect();
1006
1007 let mut decoder = LzwDecoder::new(2);
1008 let result = decoder.decompress(&data).unwrap();
1009 assert!(result.is_empty());
1010 }
1011
1012 #[test]
1013 fn lzw_min_code_size_1() {
1014 // min_code_size=1: literals 0,1; clear=2, eoi=3
1015 // code_size starts at 2
1016 // After reading 0, 1: add_entry(0,1)=code4, next=5 > 4=1<<2 → code_size=3
1017 let mut bits: u64 = 0;
1018 let mut bit_pos = 0u32;
1019
1020 let codes: &[(u16, u8)] = &[
1021 (2, 2), // clear (code_size=2)
1022 (0, 2), // literal 0
1023 (1, 2), // literal 1 → after this, code_size bumps to 3
1024 (0, 3), // literal 0 (now 3 bits)
1025 (3, 3), // eoi (3 bits)
1026 ];
1027
1028 for &(code, nbits) in codes {
1029 bits |= (code as u64) << bit_pos;
1030 bit_pos += nbits as u32;
1031 }
1032
1033 let byte_count = (bit_pos + 7) / 8;
1034 let data: Vec<u8> = (0..byte_count)
1035 .map(|i| ((bits >> (i * 8)) & 0xFF) as u8)
1036 .collect();
1037
1038 let mut decoder = LzwDecoder::new(1);
1039 let result = decoder.decompress(&data).unwrap();
1040 assert_eq!(result, vec![0, 1, 0]);
1041 }
1042
1043 #[test]
1044 fn lzw_min_code_size_8() {
1045 // min_code_size=8: literals 0-255; clear=256, eoi=257
1046 // code_size starts at 9
1047 let mut bits: u128 = 0;
1048 let mut bit_pos = 0u32;
1049
1050 let codes: &[(u16, u8)] = &[
1051 (256, 9), // clear
1052 (0, 9), // literal 0
1053 (255, 9), // literal 255
1054 (128, 9), // literal 128
1055 (257, 9), // eoi
1056 ];
1057
1058 for &(code, nbits) in codes {
1059 bits |= (code as u128) << bit_pos;
1060 bit_pos += nbits as u32;
1061 }
1062
1063 let byte_count = (bit_pos + 7) / 8;
1064 let data: Vec<u8> = (0..byte_count)
1065 .map(|i| ((bits >> (i * 8)) & 0xFF) as u8)
1066 .collect();
1067
1068 let mut decoder = LzwDecoder::new(8);
1069 let result = decoder.decompress(&data).unwrap();
1070 assert_eq!(result, vec![0, 255, 128]);
1071 }
1072
1073 // -----------------------------------------------------------------------
1074 // Deinterlace tests
1075 // -----------------------------------------------------------------------
1076
1077 #[test]
1078 fn deinterlace_8_rows() {
1079 // With 8 rows and width=1:
1080 // Pass 1 (start=0, step=8): row 0
1081 // Pass 2 (start=4, step=8): row 4
1082 // Pass 3 (start=2, step=4): rows 2, 6
1083 // Pass 4 (start=1, step=2): rows 1, 3, 5, 7
1084 // Interlaced order: [0, 4, 2, 6, 1, 3, 5, 7]
1085 let interlaced: Vec<u8> = vec![0, 4, 2, 6, 1, 3, 5, 7];
1086 let result = deinterlace(&interlaced, 1, 8);
1087 assert_eq!(result, vec![0, 1, 2, 3, 4, 5, 6, 7]);
1088 }
1089
1090 #[test]
1091 fn deinterlace_4_rows() {
1092 // 4 rows:
1093 // Pass 1 (start=0, step=8): row 0
1094 // Pass 2 (start=4, step=8): (none)
1095 // Pass 3 (start=2, step=4): row 2
1096 // Pass 4 (start=1, step=2): rows 1, 3
1097 // Interlaced order: [0, 2, 1, 3]
1098 let interlaced: Vec<u8> = vec![0, 2, 1, 3];
1099 let result = deinterlace(&interlaced, 1, 4);
1100 assert_eq!(result, vec![0, 1, 2, 3]);
1101 }
1102
1103 #[test]
1104 fn deinterlace_1_row() {
1105 let interlaced: Vec<u8> = vec![42];
1106 let result = deinterlace(&interlaced, 1, 1);
1107 assert_eq!(result, vec![42]);
1108 }
1109
1110 #[test]
1111 fn deinterlace_wider() {
1112 // 4 rows, width=2
1113 // Interlaced row order: 0, 2, 1, 3
1114 let interlaced: Vec<u8> = vec![
1115 10, 11, // row 0
1116 30, 31, // row 2
1117 20, 21, // row 1
1118 40, 41, // row 3
1119 ];
1120 let result = deinterlace(&interlaced, 2, 4);
1121 assert_eq!(
1122 result,
1123 vec![
1124 10, 11, // row 0
1125 20, 21, // row 1
1126 30, 31, // row 2
1127 40, 41, // row 3
1128 ]
1129 );
1130 }
1131
1132 // -----------------------------------------------------------------------
1133 // Disposal method tests
1134 // -----------------------------------------------------------------------
1135
1136 #[test]
1137 fn disposal_from_byte() {
1138 assert_eq!(DisposalMethod::from_byte(0), DisposalMethod::None);
1139 assert_eq!(DisposalMethod::from_byte(1), DisposalMethod::DoNotDispose);
1140 assert_eq!(
1141 DisposalMethod::from_byte(2),
1142 DisposalMethod::RestoreBackground
1143 );
1144 assert_eq!(
1145 DisposalMethod::from_byte(3),
1146 DisposalMethod::RestorePrevious
1147 );
1148 assert_eq!(DisposalMethod::from_byte(4), DisposalMethod::None);
1149 assert_eq!(DisposalMethod::from_byte(255), DisposalMethod::None);
1150 }
1151
1152 // -----------------------------------------------------------------------
1153 // Full GIF decode tests (hand-crafted minimal GIFs)
1154 // -----------------------------------------------------------------------
1155
1156 /// Build a minimal valid GIF87a with a single 1x1 red pixel.
1157 fn build_1x1_red_gif() -> Vec<u8> {
1158 let mut gif = Vec::new();
1159
1160 // Header
1161 gif.extend_from_slice(b"GIF87a");
1162
1163 // Logical screen descriptor: 1x1, GCT flag set, 1 bit color resolution
1164 // packed: 0x80 (has GCT) | 0x00 (color res = 0+1=1) | 0x00 (sort=0) | 0x00 (GCT size=0 -> 2 entries)
1165 gif.push(1);
1166 gif.push(0); // width=1
1167 gif.push(1);
1168 gif.push(0); // height=1
1169 gif.push(0x80); // packed
1170 gif.push(0); // background index
1171 gif.push(0); // pixel aspect ratio
1172
1173 // GCT: 2 entries (2^(0+1) = 2)
1174 gif.extend_from_slice(&[255, 0, 0]); // entry 0: red
1175 gif.extend_from_slice(&[0, 0, 0]); // entry 1: black
1176
1177 // Image descriptor
1178 gif.push(0x2C);
1179 gif.extend_from_slice(&[0, 0]); // left
1180 gif.extend_from_slice(&[0, 0]); // top
1181 gif.push(1);
1182 gif.push(0); // width=1
1183 gif.push(1);
1184 gif.push(0); // height=1
1185 gif.push(0); // packed (no LCT, no interlace)
1186
1187 // LZW min code size = 2 (must be >= 2 for GIF)
1188 gif.push(2);
1189
1190 // LZW data: clear(4), 0, eoi(5) — all in 3-bit codes
1191 // Bits: 100 000 101 = 0b101_000_100 packed LSB first
1192 // Byte 0: bits 0-7 = 00100 000 = 0b00000100 (reversed reading)
1193 // Let me construct this properly:
1194 // code 4 (clear) = 100 (3 bits)
1195 // code 0 = 000 (3 bits)
1196 // code 5 (eoi) = 101 (3 bits)
1197 // Total: 9 bits packed LSB first
1198 // bits[0..3] = 100 = 4 (clear)
1199 // bits[3..6] = 000 = 0 (literal 0)
1200 // bits[6..9] = 101 = 5 (eoi)
1201 // Byte 0 (bits 0-7): bit0=0, bit1=0, bit2=1, bit3=0, bit4=0, bit5=0, bit6=1, bit7=0
1202 // = 0b01000100 = 0x44
1203 // Byte 1 (bits 8): bit8=1
1204 // = 0b00000001 = 0x01
1205 gif.push(2); // sub-block size = 2 bytes
1206 gif.push(0x44);
1207 gif.push(0x01);
1208 gif.push(0); // sub-block terminator
1209
1210 // Trailer
1211 gif.push(0x3B);
1212
1213 gif
1214 }
1215
1216 #[test]
1217 fn decode_1x1_red() {
1218 let data = build_1x1_red_gif();
1219 let img = decode_gif(&data).unwrap();
1220 assert_eq!(img.width, 1);
1221 assert_eq!(img.height, 1);
1222 assert_eq!(img.data, vec![255, 0, 0, 255]);
1223 }
1224
1225 /// Build a 2x2 GIF with 4 colors.
1226 fn build_2x2_gif() -> Vec<u8> {
1227 let mut gif = Vec::new();
1228
1229 // Header
1230 gif.extend_from_slice(b"GIF89a");
1231
1232 // Screen: 2x2, GCT with 4 entries (size field = 1 -> 2^(1+1) = 4)
1233 gif.push(2);
1234 gif.push(0);
1235 gif.push(2);
1236 gif.push(0);
1237 gif.push(0x81); // has GCT, GCT size=1 (4 entries)
1238 gif.push(0); // bg index
1239 gif.push(0); // aspect
1240
1241 // GCT: 4 entries
1242 gif.extend_from_slice(&[255, 0, 0]); // 0: red
1243 gif.extend_from_slice(&[0, 255, 0]); // 1: green
1244 gif.extend_from_slice(&[0, 0, 255]); // 2: blue
1245 gif.extend_from_slice(&[255, 255, 0]); // 3: yellow
1246
1247 // Image descriptor
1248 gif.push(0x2C);
1249 gif.extend_from_slice(&[0, 0, 0, 0]); // left=0, top=0
1250 gif.push(2);
1251 gif.push(0);
1252 gif.push(2);
1253 gif.push(0); // 2x2
1254 gif.push(0); // no LCT
1255
1256 // LZW min code size = 2 (4 literal codes: 0,1,2,3; clear=4, eoi=5)
1257 gif.push(2);
1258
1259 // Encode: clear(4), 0, 1, 2, 3, eoi(5)
1260 // code_size starts at 3, bumps to 4 after code 2 (next_code reaches 8)
1261 let mut bits: u64 = 0;
1262 let mut bp = 0u32;
1263 for &(code, nbits) in &[(4u16, 3u8), (0, 3), (1, 3), (2, 3), (3, 4), (5, 4)] {
1264 bits |= (code as u64) << bp;
1265 bp += nbits as u32;
1266 }
1267 let byte_count = ((bp + 7) / 8) as usize;
1268 let lzw_bytes: Vec<u8> = (0..byte_count)
1269 .map(|i| ((bits >> (i as u32 * 8)) & 0xFF) as u8)
1270 .collect();
1271
1272 gif.push(lzw_bytes.len() as u8);
1273 gif.extend_from_slice(&lzw_bytes);
1274 gif.push(0); // terminator
1275
1276 gif.push(0x3B); // trailer
1277 gif
1278 }
1279
1280 #[test]
1281 fn decode_2x2_four_colors() {
1282 let data = build_2x2_gif();
1283 let img = decode_gif(&data).unwrap();
1284 assert_eq!(img.width, 2);
1285 assert_eq!(img.height, 2);
1286 assert_eq!(
1287 img.data,
1288 vec![
1289 255, 0, 0, 255, // red
1290 0, 255, 0, 255, // green
1291 0, 0, 255, 255, // blue
1292 255, 255, 0, 255, // yellow
1293 ]
1294 );
1295 }
1296
1297 #[test]
1298 fn decode_frames_single() {
1299 let data = build_2x2_gif();
1300 let frames = decode_gif_frames(&data).unwrap();
1301 assert_eq!(frames.len(), 1);
1302 assert_eq!(frames[0].delay_cs, 0);
1303 assert_eq!(frames[0].disposal, DisposalMethod::None);
1304 }
1305
1306 /// Build a GIF89a with transparency.
1307 fn build_transparent_gif() -> Vec<u8> {
1308 let mut gif = Vec::new();
1309
1310 gif.extend_from_slice(b"GIF89a");
1311
1312 // Screen: 2x1
1313 gif.push(2);
1314 gif.push(0);
1315 gif.push(1);
1316 gif.push(0);
1317 gif.push(0x80); // has GCT, size=0 (2 entries)
1318 gif.push(0);
1319 gif.push(0);
1320
1321 // GCT: 2 entries
1322 gif.extend_from_slice(&[255, 0, 0]); // 0: red
1323 gif.extend_from_slice(&[0, 255, 0]); // 1: green
1324
1325 // Graphic Control Extension
1326 gif.push(0x21); // extension introducer
1327 gif.push(0xF9); // GCE label
1328 gif.push(4); // block size
1329 gif.push(0x01); // packed: disposal=0, transparent=true
1330 gif.push(10);
1331 gif.push(0); // delay = 10cs
1332 gif.push(1); // transparent color index = 1
1333 gif.push(0); // block terminator
1334
1335 // Image descriptor
1336 gif.push(0x2C);
1337 gif.extend_from_slice(&[0, 0, 0, 0]);
1338 gif.push(2);
1339 gif.push(0);
1340 gif.push(1);
1341 gif.push(0);
1342 gif.push(0);
1343
1344 // LZW min code size = 2
1345 gif.push(2);
1346
1347 // Encode: clear(4), 0, 1, eoi(5)
1348 let mut bits: u64 = 0;
1349 let mut bp = 0u32;
1350 for &(code, nbits) in &[(4u16, 3u8), (0, 3), (1, 3), (5, 3)] {
1351 bits |= (code as u64) << bp;
1352 bp += nbits as u32;
1353 }
1354 let byte_count = ((bp + 7) / 8) as usize;
1355 let lzw_bytes: Vec<u8> = (0..byte_count)
1356 .map(|i| ((bits >> (i as u32 * 8)) & 0xFF) as u8)
1357 .collect();
1358
1359 gif.push(lzw_bytes.len() as u8);
1360 gif.extend_from_slice(&lzw_bytes);
1361 gif.push(0);
1362
1363 gif.push(0x3B);
1364 gif
1365 }
1366
1367 #[test]
1368 fn decode_with_transparency() {
1369 let data = build_transparent_gif();
1370 let img = decode_gif(&data).unwrap();
1371 assert_eq!(img.width, 2);
1372 assert_eq!(img.height, 1);
1373 // Pixel 0: index 0 = red (opaque)
1374 // Pixel 1: index 1 = transparent
1375 assert_eq!(
1376 img.data,
1377 vec![
1378 255, 0, 0, 255, // red, opaque
1379 0, 0, 0, 0, // transparent
1380 ]
1381 );
1382 }
1383
1384 #[test]
1385 fn decode_transparency_delay() {
1386 let data = build_transparent_gif();
1387 let frames = decode_gif_frames(&data).unwrap();
1388 assert_eq!(frames.len(), 1);
1389 assert_eq!(frames[0].delay_cs, 10);
1390 }
1391
1392 /// Build a 2-frame animated GIF.
1393 fn build_animated_gif() -> Vec<u8> {
1394 let mut gif = Vec::new();
1395
1396 gif.extend_from_slice(b"GIF89a");
1397
1398 // Screen: 2x1
1399 gif.push(2);
1400 gif.push(0);
1401 gif.push(1);
1402 gif.push(0);
1403 gif.push(0x80); // GCT, size=0 (2 entries)
1404 gif.push(0);
1405 gif.push(0);
1406
1407 // GCT
1408 gif.extend_from_slice(&[255, 0, 0]); // 0: red
1409 gif.extend_from_slice(&[0, 0, 255]); // 1: blue
1410
1411 // --- Frame 1 ---
1412 // GCE: disposal=none, delay=5
1413 gif.push(0x21);
1414 gif.push(0xF9);
1415 gif.push(4);
1416 gif.push(0x00); // disposal=0(none), no transparency
1417 gif.push(5);
1418 gif.push(0); // delay=5
1419 gif.push(0); // no transparent
1420 gif.push(0);
1421
1422 // Image descriptor: full 2x1
1423 gif.push(0x2C);
1424 gif.extend_from_slice(&[0, 0, 0, 0]);
1425 gif.push(2);
1426 gif.push(0);
1427 gif.push(1);
1428 gif.push(0);
1429 gif.push(0);
1430
1431 gif.push(2); // LZW min code size
1432
1433 // Encode: clear(4), 0, 1, eoi(5)
1434 {
1435 let mut bits: u64 = 0;
1436 let mut bp = 0u32;
1437 for &(code, nbits) in &[(4u16, 3u8), (0, 3), (1, 3), (5, 3)] {
1438 bits |= (code as u64) << bp;
1439 bp += nbits as u32;
1440 }
1441 let byte_count = ((bp + 7) / 8) as usize;
1442 let lzw_bytes: Vec<u8> = (0..byte_count)
1443 .map(|i| ((bits >> (i as u32 * 8)) & 0xFF) as u8)
1444 .collect();
1445 gif.push(lzw_bytes.len() as u8);
1446 gif.extend_from_slice(&lzw_bytes);
1447 gif.push(0);
1448 }
1449
1450 // --- Frame 2 ---
1451 // GCE: disposal=restore_bg, delay=10
1452 gif.push(0x21);
1453 gif.push(0xF9);
1454 gif.push(4);
1455 gif.push(0x08); // disposal=2(restore bg), no transparency
1456 gif.push(10);
1457 gif.push(0); // delay=10
1458 gif.push(0);
1459 gif.push(0);
1460
1461 // Image descriptor: full 2x1
1462 gif.push(0x2C);
1463 gif.extend_from_slice(&[0, 0, 0, 0]);
1464 gif.push(2);
1465 gif.push(0);
1466 gif.push(1);
1467 gif.push(0);
1468 gif.push(0);
1469
1470 gif.push(2);
1471
1472 // Encode: clear(4), 1, 0, eoi(5)
1473 {
1474 let mut bits: u64 = 0;
1475 let mut bp = 0u32;
1476 for &(code, nbits) in &[(4u16, 3u8), (1, 3), (0, 3), (5, 3)] {
1477 bits |= (code as u64) << bp;
1478 bp += nbits as u32;
1479 }
1480 let byte_count = ((bp + 7) / 8) as usize;
1481 let lzw_bytes: Vec<u8> = (0..byte_count)
1482 .map(|i| ((bits >> (i as u32 * 8)) & 0xFF) as u8)
1483 .collect();
1484 gif.push(lzw_bytes.len() as u8);
1485 gif.extend_from_slice(&lzw_bytes);
1486 gif.push(0);
1487 }
1488
1489 gif.push(0x3B);
1490 gif
1491 }
1492
1493 #[test]
1494 fn decode_animated_two_frames() {
1495 let data = build_animated_gif();
1496 let frames = decode_gif_frames(&data).unwrap();
1497 assert_eq!(frames.len(), 2);
1498
1499 // Frame 1: [red, blue]
1500 assert_eq!(frames[0].delay_cs, 5);
1501 assert_eq!(frames[0].disposal, DisposalMethod::None);
1502 assert_eq!(frames[0].image.data, vec![255, 0, 0, 255, 0, 0, 255, 255]);
1503
1504 // Frame 2: [blue, red] (composited on canvas)
1505 assert_eq!(frames[1].delay_cs, 10);
1506 assert_eq!(frames[1].disposal, DisposalMethod::RestoreBackground);
1507 assert_eq!(frames[1].image.data, vec![0, 0, 255, 255, 255, 0, 0, 255]);
1508 }
1509
1510 #[test]
1511 fn decode_animated_first_frame_only() {
1512 let data = build_animated_gif();
1513 let img = decode_gif(&data).unwrap();
1514 // decode_gif returns the first frame
1515 assert_eq!(img.data, vec![255, 0, 0, 255, 0, 0, 255, 255]);
1516 }
1517
1518 /// Build a GIF with a local color table.
1519 fn build_local_ct_gif() -> Vec<u8> {
1520 let mut gif = Vec::new();
1521
1522 gif.extend_from_slice(b"GIF87a");
1523
1524 // Screen: 1x1, NO GCT
1525 gif.push(1);
1526 gif.push(0);
1527 gif.push(1);
1528 gif.push(0);
1529 gif.push(0x00); // no GCT
1530 gif.push(0);
1531 gif.push(0);
1532
1533 // Image descriptor with LCT
1534 gif.push(0x2C);
1535 gif.extend_from_slice(&[0, 0, 0, 0]);
1536 gif.push(1);
1537 gif.push(0);
1538 gif.push(1);
1539 gif.push(0);
1540 gif.push(0x80); // has LCT, size=0 (2 entries)
1541
1542 // LCT
1543 gif.extend_from_slice(&[0, 128, 255]); // 0: teal-ish
1544 gif.extend_from_slice(&[0, 0, 0]); // 1: black
1545
1546 gif.push(2); // LZW min code size
1547
1548 // Encode: clear(4), 0, eoi(5)
1549 let mut bits: u64 = 0;
1550 let mut bp = 0u32;
1551 for &(code, nbits) in &[(4u16, 3u8), (0, 3), (5, 3)] {
1552 bits |= (code as u64) << bp;
1553 bp += nbits as u32;
1554 }
1555 let byte_count = ((bp + 7) / 8) as usize;
1556 let lzw_bytes: Vec<u8> = (0..byte_count)
1557 .map(|i| ((bits >> (i as u32 * 8)) & 0xFF) as u8)
1558 .collect();
1559 gif.push(lzw_bytes.len() as u8);
1560 gif.extend_from_slice(&lzw_bytes);
1561 gif.push(0);
1562
1563 gif.push(0x3B);
1564 gif
1565 }
1566
1567 #[test]
1568 fn decode_local_color_table() {
1569 let data = build_local_ct_gif();
1570 let img = decode_gif(&data).unwrap();
1571 assert_eq!(img.width, 1);
1572 assert_eq!(img.height, 1);
1573 assert_eq!(img.data, vec![0, 128, 255, 255]);
1574 }
1575
1576 // -----------------------------------------------------------------------
1577 // Error case tests
1578 // -----------------------------------------------------------------------
1579
1580 #[test]
1581 fn invalid_signature() {
1582 let data = b"NOT_GIF";
1583 assert!(matches!(
1584 decode_gif(data),
1585 Err(ImageError::Decode(ref msg)) if msg.contains("invalid signature")
1586 ));
1587 }
1588
1589 #[test]
1590 fn too_short() {
1591 let data = b"GIF8";
1592 assert!(matches!(decode_gif(data), Err(ImageError::Decode(_))));
1593 }
1594
1595 #[test]
1596 fn no_frames() {
1597 let mut gif = Vec::new();
1598 gif.extend_from_slice(b"GIF87a");
1599 gif.push(1);
1600 gif.push(0);
1601 gif.push(1);
1602 gif.push(0);
1603 gif.push(0x00);
1604 gif.push(0);
1605 gif.push(0);
1606 gif.push(0x3B);
1607 assert!(matches!(
1608 decode_gif(&gif),
1609 Err(ImageError::Decode(ref msg)) if msg.contains("no frames")
1610 ));
1611 }
1612
1613 #[test]
1614 fn zero_dimension_screen() {
1615 let mut gif = Vec::new();
1616 gif.extend_from_slice(b"GIF87a");
1617 gif.push(0);
1618 gif.push(0); // width=0
1619 gif.push(1);
1620 gif.push(0); // height=1
1621 gif.push(0x00);
1622 gif.push(0);
1623 gif.push(0);
1624 assert!(matches!(
1625 decode_gif(&gif),
1626 Err(ImageError::Decode(ref msg)) if msg.contains("zero dimension")
1627 ));
1628 }
1629
1630 #[test]
1631 fn invalid_lzw_min_code_size() {
1632 let mut gif = Vec::new();
1633 gif.extend_from_slice(b"GIF87a");
1634 gif.push(1);
1635 gif.push(0);
1636 gif.push(1);
1637 gif.push(0);
1638 gif.push(0x80);
1639 gif.push(0);
1640 gif.push(0);
1641 gif.extend_from_slice(&[0; 6]); // GCT (2 entries)
1642
1643 gif.push(0x2C); // image descriptor
1644 gif.extend_from_slice(&[0, 0, 0, 0, 1, 0, 1, 0, 0]);
1645 gif.push(12); // invalid LZW min code size (>11)
1646
1647 assert!(matches!(
1648 decode_gif(&gif),
1649 Err(ImageError::Decode(ref msg)) if msg.contains("LZW minimum code size")
1650 ));
1651 }
1652
1653 // -----------------------------------------------------------------------
1654 // Sub-frame positioning test
1655 // -----------------------------------------------------------------------
1656
1657 /// Build a GIF where the frame is smaller than the logical screen.
1658 fn build_subframe_gif() -> Vec<u8> {
1659 let mut gif = Vec::new();
1660
1661 gif.extend_from_slice(b"GIF89a");
1662
1663 // Screen: 3x3
1664 gif.push(3);
1665 gif.push(0);
1666 gif.push(3);
1667 gif.push(0);
1668 gif.push(0x80); // GCT, size=0 (2 entries)
1669 gif.push(0);
1670 gif.push(0);
1671
1672 // GCT
1673 gif.extend_from_slice(&[0, 0, 0]); // 0: black
1674 gif.extend_from_slice(&[255, 255, 255]); // 1: white
1675
1676 // Image descriptor: 1x1 at position (1,1)
1677 gif.push(0x2C);
1678 gif.push(1);
1679 gif.push(0); // left=1
1680 gif.push(1);
1681 gif.push(0); // top=1
1682 gif.push(1);
1683 gif.push(0); // width=1
1684 gif.push(1);
1685 gif.push(0); // height=1
1686 gif.push(0);
1687
1688 gif.push(2); // LZW min
1689
1690 // Encode: clear(4), 1, eoi(5)
1691 let mut bits: u64 = 0;
1692 let mut bp = 0u32;
1693 for &(code, nbits) in &[(4u16, 3u8), (1, 3), (5, 3)] {
1694 bits |= (code as u64) << bp;
1695 bp += nbits as u32;
1696 }
1697 let byte_count = ((bp + 7) / 8) as usize;
1698 let lzw_bytes: Vec<u8> = (0..byte_count)
1699 .map(|i| ((bits >> (i as u32 * 8)) & 0xFF) as u8)
1700 .collect();
1701 gif.push(lzw_bytes.len() as u8);
1702 gif.extend_from_slice(&lzw_bytes);
1703 gif.push(0);
1704
1705 gif.push(0x3B);
1706 gif
1707 }
1708
1709 #[test]
1710 fn decode_subframe_positioning() {
1711 let data = build_subframe_gif();
1712 let img = decode_gif(&data).unwrap();
1713 assert_eq!(img.width, 3);
1714 assert_eq!(img.height, 3);
1715 // Canvas starts all black (0,0,0,0 = transparent)
1716 // The 1x1 white pixel is composited at position (1,1)
1717 let pixel = |x: usize, y: usize| -> &[u8] {
1718 let off = (y * 3 + x) * 4;
1719 &img.data[off..off + 4]
1720 };
1721 // All pixels except (1,1) should be transparent black
1722 assert_eq!(pixel(0, 0), &[0, 0, 0, 0]);
1723 assert_eq!(pixel(1, 0), &[0, 0, 0, 0]);
1724 assert_eq!(pixel(2, 0), &[0, 0, 0, 0]);
1725 assert_eq!(pixel(0, 1), &[0, 0, 0, 0]);
1726 assert_eq!(pixel(1, 1), &[255, 255, 255, 255]); // white
1727 assert_eq!(pixel(2, 1), &[0, 0, 0, 0]);
1728 assert_eq!(pixel(0, 2), &[0, 0, 0, 0]);
1729 assert_eq!(pixel(1, 2), &[0, 0, 0, 0]);
1730 assert_eq!(pixel(2, 2), &[0, 0, 0, 0]);
1731 }
1732
1733 // -----------------------------------------------------------------------
1734 // Application extension skip test
1735 // -----------------------------------------------------------------------
1736
1737 #[test]
1738 fn skip_application_extension() {
1739 let mut gif = Vec::new();
1740
1741 gif.extend_from_slice(b"GIF89a");
1742 gif.push(1);
1743 gif.push(0);
1744 gif.push(1);
1745 gif.push(0);
1746 gif.push(0x80);
1747 gif.push(0);
1748 gif.push(0);
1749 gif.extend_from_slice(&[255, 0, 0, 0, 0, 0]); // GCT
1750
1751 // Application extension (NETSCAPE2.0 for looping)
1752 gif.push(0x21);
1753 gif.push(0xFF);
1754 // Sub-blocks for the extension
1755 gif.push(11); // sub-block size
1756 gif.extend_from_slice(b"NETSCAPE2.0");
1757 gif.push(3); // sub-block size
1758 gif.push(1); // sub-block id
1759 gif.push(0);
1760 gif.push(0); // loop count
1761 gif.push(0); // terminator
1762
1763 // Image
1764 gif.push(0x2C);
1765 gif.extend_from_slice(&[0, 0, 0, 0, 1, 0, 1, 0, 0]);
1766 gif.push(2);
1767
1768 let mut bits: u64 = 0;
1769 let mut bp = 0u32;
1770 for &(code, nbits) in &[(4u16, 3u8), (0, 3), (5, 3)] {
1771 bits |= (code as u64) << bp;
1772 bp += nbits as u32;
1773 }
1774 let byte_count = ((bp + 7) / 8) as usize;
1775 let lzw_bytes: Vec<u8> = (0..byte_count)
1776 .map(|i| ((bits >> (i as u32 * 8)) & 0xFF) as u8)
1777 .collect();
1778 gif.push(lzw_bytes.len() as u8);
1779 gif.extend_from_slice(&lzw_bytes);
1780 gif.push(0);
1781
1782 gif.push(0x3B);
1783
1784 let img = decode_gif(&gif).unwrap();
1785 assert_eq!(img.width, 1);
1786 assert_eq!(img.height, 1);
1787 assert_eq!(img.data, vec![255, 0, 0, 255]);
1788 }
1789
1790 // -----------------------------------------------------------------------
1791 // Comment extension skip test
1792 // -----------------------------------------------------------------------
1793
1794 #[test]
1795 fn skip_comment_extension() {
1796 let mut gif = Vec::new();
1797
1798 gif.extend_from_slice(b"GIF89a");
1799 gif.push(1);
1800 gif.push(0);
1801 gif.push(1);
1802 gif.push(0);
1803 gif.push(0x80);
1804 gif.push(0);
1805 gif.push(0);
1806 gif.extend_from_slice(&[0, 255, 0, 0, 0, 0]); // GCT
1807
1808 // Comment extension
1809 gif.push(0x21);
1810 gif.push(0xFE);
1811 gif.push(5);
1812 gif.extend_from_slice(b"hello");
1813 gif.push(0);
1814
1815 // Image
1816 gif.push(0x2C);
1817 gif.extend_from_slice(&[0, 0, 0, 0, 1, 0, 1, 0, 0]);
1818 gif.push(2);
1819
1820 let mut bits: u64 = 0;
1821 let mut bp = 0u32;
1822 for &(code, nbits) in &[(4u16, 3u8), (0, 3), (5, 3)] {
1823 bits |= (code as u64) << bp;
1824 bp += nbits as u32;
1825 }
1826 let byte_count = ((bp + 7) / 8) as usize;
1827 let lzw_bytes: Vec<u8> = (0..byte_count)
1828 .map(|i| ((bits >> (i as u32 * 8)) & 0xFF) as u8)
1829 .collect();
1830 gif.push(lzw_bytes.len() as u8);
1831 gif.extend_from_slice(&lzw_bytes);
1832 gif.push(0);
1833
1834 gif.push(0x3B);
1835
1836 let img = decode_gif(&gif).unwrap();
1837 assert_eq!(img.data, vec![0, 255, 0, 255]);
1838 }
1839
1840 // -----------------------------------------------------------------------
1841 // GIF87a compatibility test
1842 // -----------------------------------------------------------------------
1843
1844 #[test]
1845 fn gif87a_is_accepted() {
1846 let data = build_1x1_red_gif();
1847 // Already uses GIF87a
1848 assert!(data.starts_with(b"GIF87a"));
1849 let img = decode_gif(&data).unwrap();
1850 assert_eq!(img.data, vec![255, 0, 0, 255]);
1851 }
1852
1853 // -----------------------------------------------------------------------
1854 // Bit reader tests
1855 // -----------------------------------------------------------------------
1856
1857 #[test]
1858 fn bit_reader_basic() {
1859 // Byte 0: 0b10110100 — bits[0..8] = 0,0,1,0,1,1,0,1
1860 // Byte 1: 0b00000001 — bits[8..16] = 1,0,0,0,0,0,0,0
1861 let data = [0b10110100u8, 0b00000001];
1862 let mut reader = LzwBitReader::new(&data);
1863
1864 // Read 3 bits [0,1,2]: 0*1+0*2+1*4 = 4
1865 assert_eq!(reader.read_bits(3).unwrap(), 4);
1866 // Read 3 bits [3,4,5]: 0*1+1*2+1*4 = 6
1867 assert_eq!(reader.read_bits(3).unwrap(), 6);
1868 // Read 3 bits [6,7,8]: 0*1+1*2+1*4 = 6
1869 assert_eq!(reader.read_bits(3).unwrap(), 6);
1870 }
1871
1872 #[test]
1873 fn bit_reader_eof() {
1874 let data = [0xFF];
1875 let mut reader = LzwBitReader::new(&data);
1876 assert!(reader.read_bits(8).is_ok());
1877 assert!(reader.read_bits(1).is_err());
1878 }
1879}