we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement GIF decoder (GIF87a/GIF89a)

Pure Rust GIF decoder in the image crate supporting:
- GIF87a and GIF89a format parsing
- LZW decompression with variable-width codes, clear/EOI codes
- Global and local color tables
- Graphic Control Extension (transparency, delay, disposal methods)
- Multi-frame animated GIFs with canvas compositing
- Interlaced image support (4-pass Adam7-style deinterlacing)
- Sub-frame positioning (frames smaller than logical screen)
- Application, comment, and plain text extension skipping
- API: decode_gif() for first frame, decode_gif_frames() for all frames

31 unit tests covering LZW decompression, deinterlacing, frame decoding,
transparency, animation, local color tables, extensions, and error cases.

No external dependencies, no unsafe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

authored by

Pierre Le Fevre
Claude Opus 4.6
and committed by tangled.org 4adef4d8 3e2d2105

+1880
+1879
crates/image/src/gif.rs
··· 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 + 7 + use crate::pixel::{self, Image, ImageError}; 8 + 9 + // --------------------------------------------------------------------------- 10 + // Constants 11 + // --------------------------------------------------------------------------- 12 + 13 + const GIF87A: &[u8; 6] = b"GIF87a"; 14 + const GIF89A: &[u8; 6] = b"GIF89a"; 15 + 16 + const IMAGE_DESCRIPTOR: u8 = 0x2C; 17 + const EXTENSION_INTRODUCER: u8 = 0x21; 18 + const TRAILER: u8 = 0x3B; 19 + 20 + const GRAPHIC_CONTROL_EXT: u8 = 0xF9; 21 + const APPLICATION_EXT: u8 = 0xFF; 22 + const COMMENT_EXT: u8 = 0xFE; 23 + const 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)] 31 + pub 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 + 42 + impl 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)] 60 + pub 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. 77 + struct 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 + 88 + impl 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 we've filled the current capacity 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 + 232 + struct LzwBitReader<'a> { 233 + data: &'a [u8], 234 + pos: usize, 235 + bit_buf: u32, 236 + bits_in_buf: u8, 237 + } 238 + 239 + impl<'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 + 270 + struct GifReader<'a> { 271 + data: &'a [u8], 272 + pos: usize, 273 + } 274 + 275 + impl<'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 + 346 + struct GraphicControl { 347 + disposal: DisposalMethod, 348 + transparent_color: Option<u8>, 349 + delay_cs: u16, 350 + } 351 + 352 + // --------------------------------------------------------------------------- 353 + // Logical Screen Descriptor 354 + // --------------------------------------------------------------------------- 355 + 356 + struct ScreenDescriptor { 357 + width: u16, 358 + height: u16, 359 + global_color_table: Option<Vec<u8>>, 360 + background_index: u8, 361 + } 362 + 363 + fn 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 409 + const INTERLACE_PASSES: [(usize, usize); 4] = [(0, 8), (4, 8), (2, 4), (1, 2)]; 410 + 411 + fn 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 + 437 + struct 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 + 450 + fn 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. 523 + fn 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 + 569 + fn 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 + 610 + fn 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 + 647 + fn 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. 714 + pub 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. 727 + pub 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)] 840 + mod 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 + // Using 3-bit codes (min_code_size=2, initial code_size=3) 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 860 + (1, 3), // literal 1 861 + (5, 3), // eoi 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) — all 3-bit codes 1260 + // 9 codes * 3 bits = 18 bits = 3 bytes (with padding) 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, 3), (5, 3)] { 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 + }
+1
crates/image/src/lib.rs
··· 1 1 //! Image decoders — PNG (DEFLATE), JPEG, GIF, WebP — pure Rust. 2 2 3 3 pub mod deflate; 4 + pub mod gif; 4 5 pub mod pixel; 5 6 pub mod png; 6 7 pub mod zlib;