Skip to main content

oxihuman_core/
image_gif.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! GIF89a encoder and decoder with proper LZW compression.
5//!
6//! This module implements a complete GIF89a codec in pure Rust:
7//! - Encoder: RGB pixels → GIF89a bytes, using median-cut color quantization
8//!   (≤256 colors) and variable-width LSB-first LZW compression.
9//! - Decoder: GIF89a / GIF87a bytes → RGB pixels, handling extension blocks,
10//!   local color tables, and the standard LZW special cases.
11
12use super::image_codec::RawDecodeResult;
13use std::collections::HashMap;
14
15// ─────────────────────────────────────────────────────────────────────────────
16// Error type
17// ─────────────────────────────────────────────────────────────────────────────
18
19/// Errors that can occur during GIF encode / decode operations.
20#[derive(Debug, thiserror::Error)]
21pub enum GifError {
22    /// The data is structurally invalid.
23    #[error("Invalid GIF: {0}")]
24    Invalid(String),
25    /// The byte stream ended prematurely.
26    #[error("Truncated GIF data")]
27    Truncated,
28    /// An error in the LZW layer.
29    #[error("LZW error: {0}")]
30    LzwError(String),
31}
32
33// ─────────────────────────────────────────────────────────────────────────────
34// Colour quantisation — median-cut algorithm
35// ─────────────────────────────────────────────────────────────────────────────
36
37/// One bucket of pixels used during median-cut quantisation.
38struct Bucket {
39    /// Flat list of (R, G, B) triples belonging to this bucket.
40    pixels: Vec<[u8; 3]>,
41}
42
43impl Bucket {
44    fn new(pixels: Vec<[u8; 3]>) -> Self {
45        Self { pixels }
46    }
47
48    /// Range of each channel.
49    fn channel_ranges(&self) -> ([u8; 3], [u8; 3]) {
50        let mut min = [255u8; 3];
51        let mut max = [0u8; 3];
52        for p in &self.pixels {
53            for c in 0..3 {
54                if p[c] < min[c] {
55                    min[c] = p[c];
56                }
57                if p[c] > max[c] {
58                    max[c] = p[c];
59                }
60            }
61        }
62        (min, max)
63    }
64
65    /// Widest channel index.
66    fn widest_channel(&self) -> usize {
67        let (min, max) = self.channel_ranges();
68        let ranges = [
69            max[0].saturating_sub(min[0]),
70            max[1].saturating_sub(min[1]),
71            max[2].saturating_sub(min[2]),
72        ];
73        if ranges[0] >= ranges[1] && ranges[0] >= ranges[2] {
74            0
75        } else if ranges[1] >= ranges[2] {
76            1
77        } else {
78            2
79        }
80    }
81
82    /// Split the bucket along the median of the widest channel.
83    /// Returns `None` if the bucket cannot be split (single pixel or all same).
84    fn split(mut self) -> Option<(Bucket, Bucket)> {
85        if self.pixels.len() < 2 {
86            return None;
87        }
88        let ch = self.widest_channel();
89        self.pixels.sort_unstable_by_key(|p| p[ch]);
90        let mid = self.pixels.len() / 2;
91        // Require that the two halves actually differ in the split channel,
92        // otherwise there is no real division to be made.
93        if self.pixels[0][ch] == self.pixels[self.pixels.len() - 1][ch] {
94            return None;
95        }
96        let right = self.pixels.split_off(mid);
97        Some((Bucket::new(self.pixels), Bucket::new(right)))
98    }
99
100    /// Representative colour: the unweighted average of all pixels.
101    fn representative(&self) -> [u8; 3] {
102        if self.pixels.is_empty() {
103            return [0, 0, 0];
104        }
105        let mut sum = [0u64; 3];
106        for p in &self.pixels {
107            sum[0] += p[0] as u64;
108            sum[1] += p[1] as u64;
109            sum[2] += p[2] as u64;
110        }
111        let n = self.pixels.len() as u64;
112        [(sum[0] / n) as u8, (sum[1] / n) as u8, (sum[2] / n) as u8]
113    }
114}
115
116/// Quantise `pixels` (tightly-packed RGB) to at most `max_colors` palette entries.
117///
118/// Returns `(palette, index_map)` where `palette` is the list of representative
119/// colours and `index_map[i]` gives the palette index for pixel `i`.
120fn median_cut_quantize(pixels: &[[u8; 3]], max_colors: usize) -> (Vec<[u8; 3]>, Vec<u8>) {
121    if pixels.is_empty() {
122        return (vec![[0, 0, 0]], vec![]);
123    }
124
125    // Seed with one bucket holding every pixel.
126    let mut buckets: Vec<Bucket> = vec![Bucket::new(pixels.to_vec())];
127
128    // Split until we reach max_colors or no more splits are possible.
129    while buckets.len() < max_colors {
130        // Pick the bucket with the largest range (most "spreadable").
131        let pick = buckets
132            .iter()
133            .enumerate()
134            .map(|(i, b)| {
135                let (mn, mx) = b.channel_ranges();
136                let spread = (mx[0].saturating_sub(mn[0]) as u32)
137                    + (mx[1].saturating_sub(mn[1]) as u32)
138                    + (mx[2].saturating_sub(mn[2]) as u32);
139                (spread, i)
140            })
141            .max_by_key(|&(s, _)| s);
142
143        let idx = match pick {
144            Some((0, _)) | None => break, // No splittable bucket.
145            Some((_, i)) => i,
146        };
147
148        let bucket = buckets.remove(idx);
149        match bucket.split() {
150            Some((a, b)) => {
151                buckets.push(a);
152                buckets.push(b);
153            }
154            None => {
155                // Put it back — it can't be split.
156                buckets.insert(idx, Bucket::new(vec![]));
157                break;
158            }
159        }
160    }
161
162    // Build the palette from bucket representatives.
163    let palette: Vec<[u8; 3]> = buckets.iter().map(|b| b.representative()).collect();
164
165    // Assign each pixel to the nearest palette entry (Euclidean² distance).
166    let index_map: Vec<u8> = pixels
167        .iter()
168        .map(|p| nearest_palette_index(p, &palette))
169        .collect();
170
171    (palette, index_map)
172}
173
174/// Return the index of the palette entry closest to `pixel` in Euclidean² space.
175fn nearest_palette_index(pixel: &[u8; 3], palette: &[[u8; 3]]) -> u8 {
176    let mut best_idx = 0usize;
177    let mut best_dist = u32::MAX;
178    for (i, entry) in palette.iter().enumerate() {
179        let dr = pixel[0] as i32 - entry[0] as i32;
180        let dg = pixel[1] as i32 - entry[1] as i32;
181        let db = pixel[2] as i32 - entry[2] as i32;
182        let dist = (dr * dr + dg * dg + db * db) as u32;
183        if dist < best_dist {
184            best_dist = dist;
185            best_idx = i;
186            if dist == 0 {
187                break;
188            }
189        }
190    }
191    best_idx as u8
192}
193
194// ─────────────────────────────────────────────────────────────────────────────
195// LZW compressor (GIF variant — LSB-first bit packing)
196// ─────────────────────────────────────────────────────────────────────────────
197
198/// Compress `indices` using GIF LZW with the given `min_code_size`.
199///
200/// Returns the raw bit-stream bytes (not yet wrapped in sub-blocks).
201fn lzw_compress(indices: &[u8], min_code_size: u8) -> Vec<u8> {
202    let clear_code = 1u32 << min_code_size;
203    let end_code = clear_code + 1;
204    let initial_next = end_code + 1;
205
206    // The code table maps (prefix_code, suffix_byte) → new_code.
207    let mut table: HashMap<(u32, u8), u32> = HashMap::new();
208    let mut next_code = initial_next;
209    let mut code_width = min_code_size as u32 + 1;
210    // Threshold at which we increase code_width.
211    let mut next_threshold = 1u32 << code_width;
212
213    let mut bit_buf = 0u64; // Bit accumulator (LSB-first).
214    let mut bit_len = 0u32; // Number of valid bits in bit_buf.
215    let mut out: Vec<u8> = Vec::new();
216
217    /// Flush `bits` bits from bit_buf into `out` (LSB first).
218    fn emit(code: u32, bits: u32, buf: &mut u64, len: &mut u32, out: &mut Vec<u8>) {
219        *buf |= (code as u64) << *len;
220        *len += bits;
221        while *len >= 8 {
222            out.push((*buf & 0xFF) as u8);
223            *buf >>= 8;
224            *len -= 8;
225        }
226    }
227
228    // Helper to reset the code table to initial state.
229    macro_rules! reset_table {
230        () => {{
231            table.clear();
232            next_code = initial_next;
233            code_width = min_code_size as u32 + 1;
234            next_threshold = 1u32 << code_width;
235        }};
236    }
237
238    // Emit the initial Clear code.
239    emit(clear_code, code_width, &mut bit_buf, &mut bit_len, &mut out);
240
241    if indices.is_empty() {
242        emit(end_code, code_width, &mut bit_buf, &mut bit_len, &mut out);
243        // Flush remaining bits.
244        if bit_len > 0 {
245            out.push((bit_buf & 0xFF) as u8);
246        }
247        return out;
248    }
249
250    let mut string_code = indices[0] as u32;
251
252    for &byte in &indices[1..] {
253        let key = (string_code, byte);
254        match table.get(&key) {
255            Some(&existing) => {
256                string_code = existing;
257            }
258            None => {
259                // Emit the current string code.
260                emit(
261                    string_code,
262                    code_width,
263                    &mut bit_buf,
264                    &mut bit_len,
265                    &mut out,
266                );
267
268                if next_code < 4096 {
269                    table.insert(key, next_code);
270                    next_code += 1;
271                    if next_code > next_threshold && code_width < 12 {
272                        code_width += 1;
273                        next_threshold = 1u32 << code_width;
274                    }
275                } else {
276                    // Table full: emit Clear and reset.
277                    emit(clear_code, code_width, &mut bit_buf, &mut bit_len, &mut out);
278                    reset_table!();
279                }
280
281                string_code = byte as u32;
282            }
283        }
284    }
285
286    // Emit the remaining string.
287    emit(
288        string_code,
289        code_width,
290        &mut bit_buf,
291        &mut bit_len,
292        &mut out,
293    );
294    // Emit End code.
295    emit(end_code, code_width, &mut bit_buf, &mut bit_len, &mut out);
296    // Flush any remaining bits.
297    if bit_len > 0 {
298        out.push((bit_buf & 0xFF) as u8);
299    }
300
301    out
302}
303
304/// Wrap `lzw_data` in GIF sub-blocks (length-prefixed chunks of ≤255 bytes).
305fn wrap_in_sub_blocks(lzw_data: &[u8]) -> Vec<u8> {
306    let mut out = Vec::new();
307    for chunk in lzw_data.chunks(255) {
308        out.push(chunk.len() as u8);
309        out.extend_from_slice(chunk);
310    }
311    out.push(0); // Block terminator.
312    out
313}
314
315// ─────────────────────────────────────────────────────────────────────────────
316// Public encoder
317// ─────────────────────────────────────────────────────────────────────────────
318
319/// Encode a raw RGB pixel buffer as a GIF89a byte stream.
320///
321/// `pixels` must contain exactly `width * height * 3` bytes in row-major,
322/// top-to-bottom, R-G-B order.
323pub fn gif_encode_rgb(width: u32, height: u32, pixels: &[u8]) -> Result<Vec<u8>, GifError> {
324    let pixel_count = (width as usize) * (height as usize);
325    if pixels.len() < pixel_count * 3 {
326        return Err(GifError::Invalid(format!(
327            "pixel buffer too short: expected {} bytes, got {}",
328            pixel_count * 3,
329            pixels.len()
330        )));
331    }
332    if width == 0 || height == 0 {
333        return Err(GifError::Invalid("width and height must be > 0".into()));
334    }
335
336    // Reinterpret the flat u8 slice as an array of RGB triples.
337    let rgb_pixels: Vec<[u8; 3]> = pixels
338        .chunks_exact(3)
339        .take(pixel_count)
340        .map(|c| [c[0], c[1], c[2]])
341        .collect();
342
343    // Quantise to ≤256 colours.
344    let max_colors = 256usize;
345    let (mut palette, index_map) = median_cut_quantize(&rgb_pixels, max_colors);
346
347    // GIF requires the palette length to be a power of two.  Find the smallest
348    // power-of-two size ≥ palette.len() that is also ≥ 2.
349    let palette_len = palette.len().max(2);
350    let depth = {
351        let mut d = 1u8;
352        while (1usize << d) < palette_len {
353            d += 1;
354        }
355        d // actual bit depth; palette contains 2^depth entries
356    };
357    // Pad palette to exactly 2^depth entries.
358    while palette.len() < (1 << depth) {
359        palette.push([0, 0, 0]);
360    }
361
362    let gct_size_field = depth - 1; // field value n means 2^(n+1) entries
363    let packed_byte: u8 = 0x80 |                       // Global Color Table flag
364        ((depth - 1) << 4) |         // colour resolution (depth - 1)
365        gct_size_field; // GCT size field
366
367    let min_code_size = depth.max(2); // GIF spec: minimum is 2
368
369    let mut out: Vec<u8> = Vec::new();
370
371    // ── Header ────────────────────────────────────────────────────────────────
372    out.extend_from_slice(b"GIF89a");
373
374    // ── Logical Screen Descriptor ─────────────────────────────────────────────
375    out.extend_from_slice(&(width as u16).to_le_bytes());
376    out.extend_from_slice(&(height as u16).to_le_bytes());
377    out.push(packed_byte);
378    out.push(0); // Background colour index.
379    out.push(0); // Pixel aspect ratio.
380
381    // ── Global Color Table ────────────────────────────────────────────────────
382    for entry in &palette {
383        out.push(entry[0]);
384        out.push(entry[1]);
385        out.push(entry[2]);
386    }
387
388    // ── Image Descriptor ──────────────────────────────────────────────────────
389    out.push(0x2C); // Image Separator.
390    out.extend_from_slice(&0u16.to_le_bytes()); // Left.
391    out.extend_from_slice(&0u16.to_le_bytes()); // Top.
392    out.extend_from_slice(&(width as u16).to_le_bytes());
393    out.extend_from_slice(&(height as u16).to_le_bytes());
394    out.push(0x00); // Packed: no local table, not interlaced.
395
396    // ── LZW Minimum Code Size ─────────────────────────────────────────────────
397    out.push(min_code_size);
398
399    // ── Image Data ────────────────────────────────────────────────────────────
400    let lzw_bytes = lzw_compress(&index_map, min_code_size);
401    let sub_blocks = wrap_in_sub_blocks(&lzw_bytes);
402    out.extend_from_slice(&sub_blocks);
403
404    // ── Trailer ───────────────────────────────────────────────────────────────
405    out.push(0x3B);
406
407    Ok(out)
408}
409
410// ─────────────────────────────────────────────────────────────────────────────
411// LZW decompressor (GIF variant — LSB-first bit reading)
412// ─────────────────────────────────────────────────────────────────────────────
413
414/// Bit-level reader that extracts variable-width codes (LSB-first) from a byte
415/// slice.
416struct BitReader<'a> {
417    data: &'a [u8],
418    pos: usize, // byte position
419    bit_buf: u32,
420    bit_len: u32,
421}
422
423impl<'a> BitReader<'a> {
424    fn new(data: &'a [u8]) -> Self {
425        Self {
426            data,
427            pos: 0,
428            bit_buf: 0,
429            bit_len: 0,
430        }
431    }
432
433    /// Read `n` bits LSB-first.  Returns `None` if the stream is exhausted.
434    fn read_bits(&mut self, n: u32) -> Option<u32> {
435        while self.bit_len < n {
436            if self.pos >= self.data.len() {
437                return None;
438            }
439            self.bit_buf |= (self.data[self.pos] as u32) << self.bit_len;
440            self.pos += 1;
441            self.bit_len += 8;
442        }
443        let mask = (1u32 << n) - 1;
444        let val = self.bit_buf & mask;
445        self.bit_buf >>= n;
446        self.bit_len -= n;
447        Some(val)
448    }
449}
450
451/// Concatenate all sub-block data bytes from a GIF image data section.
452///
453/// `bytes` must point to the start of the first sub-block (after the LZW
454/// minimum code size byte).  Returns the concatenated payload and the number
455/// of source bytes consumed (including the trailing zero-length block).
456fn read_sub_blocks(bytes: &[u8]) -> Result<(Vec<u8>, usize), GifError> {
457    let mut out: Vec<u8> = Vec::new();
458    let mut pos = 0usize;
459    loop {
460        if pos >= bytes.len() {
461            return Err(GifError::Truncated);
462        }
463        let block_len = bytes[pos] as usize;
464        pos += 1;
465        if block_len == 0 {
466            break;
467        }
468        if pos + block_len > bytes.len() {
469            return Err(GifError::Truncated);
470        }
471        out.extend_from_slice(&bytes[pos..pos + block_len]);
472        pos += block_len;
473    }
474    Ok((out, pos))
475}
476
477/// GIF LZW decompressor.
478///
479/// Decompresses `data` using `min_code_size` and returns the index stream.
480///
481/// The code table uses a linked-list representation: each entry stores
482/// (prefix_code, suffix_byte).  For the initial leaf codes prefix_code is
483/// `u16::MAX` (sentinel meaning "no prefix").  Strings are reconstructed by
484/// walking the chain backwards and then reversing.
485///
486/// Code-width growth rule (GIF spec / Netscape interpretation):
487///   After emitting a code that fills the current range — i.e. after the table
488///   size reaches `2^code_width` — the *next* code read uses `code_width + 1`
489///   bits (up to a maximum of 12).
490fn lzw_decompress(data: &[u8], min_code_size: u8) -> Result<Vec<u8>, GifError> {
491    let clear_code = 1u16 << min_code_size;
492    let end_code = clear_code + 1;
493    let table_initial_size = (end_code + 1) as usize;
494
495    // prefix[i] == u16::MAX  →  leaf (no prefix)
496    let mut prefix: Vec<u16> = Vec::with_capacity(4096);
497    let mut suffix: Vec<u8> = Vec::with_capacity(4096);
498
499    // Seed: single-byte leaf codes 0..clear_code.
500    for i in 0..(clear_code as usize) {
501        prefix.push(u16::MAX);
502        suffix.push(i as u8);
503    }
504    // clear_code slot
505    prefix.push(u16::MAX);
506    suffix.push(0);
507    // end_code slot
508    prefix.push(u16::MAX);
509    suffix.push(0);
510
511    let mut code_width = min_code_size as u32 + 1;
512
513    let mut reader = BitReader::new(data);
514    let mut output: Vec<u8> = Vec::new();
515
516    // Reusable scratch buffer for string reconstruction.
517    let mut scratch: Vec<u8> = Vec::new();
518
519    // Walk the chain for `code` and write the string (reversed) into `scratch`.
520    let walk_chain =
521        |code: u16, pfx: &[u16], sfx: &[u8], scratch: &mut Vec<u8>| -> Result<(), GifError> {
522            scratch.clear();
523            let mut c = code;
524            let mut depth = 0usize;
525            loop {
526                if (c as usize) >= pfx.len() {
527                    return Err(GifError::LzwError(format!("code {} not in table", c)));
528                }
529                scratch.push(sfx[c as usize]);
530                let p = pfx[c as usize];
531                if p == u16::MAX {
532                    break;
533                }
534                c = p;
535                depth += 1;
536                if depth > 4096 {
537                    return Err(GifError::LzwError("cycle in LZW chain".into()));
538                }
539            }
540            Ok(())
541        };
542
543    // Returns the first (root) byte for `code`.
544    let first_byte = |code: u16, pfx: &[u16], sfx: &[u8]| -> u8 {
545        let mut c = code;
546        loop {
547            let p = pfx[c as usize];
548            if p == u16::MAX {
549                return sfx[c as usize];
550            }
551            c = p;
552        }
553    };
554
555    let mut prev_code: Option<u16> = None;
556    // Track whether we are waiting for the first non-clear code.
557    let mut after_clear = true;
558
559    loop {
560        let raw = reader.read_bits(code_width).ok_or(GifError::Truncated)?;
561        let code = raw as u16;
562
563        if code == end_code {
564            break;
565        }
566
567        if code == clear_code {
568            prefix.truncate(table_initial_size);
569            suffix.truncate(table_initial_size);
570            code_width = min_code_size as u32 + 1;
571            prev_code = None;
572            after_clear = true;
573            continue;
574        }
575
576        // ── First code after a clear ──────────────────────────────────────────
577        if after_clear {
578            // The first code after Clear must be in the initial table.
579            if (code as usize) >= table_initial_size {
580                return Err(GifError::LzwError(format!(
581                    "first code after clear ({}) exceeds initial table size ({})",
582                    code, table_initial_size
583                )));
584            }
585            output.push(suffix[code as usize]);
586            prev_code = Some(code);
587            after_clear = false;
588            // No new entry is added for the first code.
589            continue;
590        }
591
592        let prev = prev_code.ok_or_else(|| GifError::LzwError("no prev code".into()))?;
593        let next_code = prefix.len() as u16;
594
595        if (code as usize) < prefix.len() {
596            // ── Known code path ───────────────────────────────────────────────
597            // Decode the string for `code`.
598            walk_chain(code, &prefix, &suffix, &mut scratch)?;
599            scratch.reverse();
600
601            // New table entry: prev + first_byte(code).
602            let fb = scratch[0];
603            if next_code < 4096 {
604                prefix.push(prev);
605                suffix.push(fb);
606            }
607
608            output.extend_from_slice(&scratch);
609        } else if code == next_code {
610            // ── Special case: code == next table slot ─────────────────────────
611            // The string is prev_string + prev_string[0].
612            let fb = first_byte(prev, &prefix, &suffix);
613
614            if next_code < 4096 {
615                prefix.push(prev);
616                suffix.push(fb);
617            }
618
619            // Now decode the newly-added entry (which equals `prev` + `fb`).
620            walk_chain(prev, &prefix, &suffix, &mut scratch)?;
621            scratch.reverse();
622            scratch.push(fb);
623
624            output.extend_from_slice(&scratch);
625        } else {
626            return Err(GifError::LzwError(format!(
627                "code {} is beyond next expected entry {}",
628                code, next_code
629            )));
630        }
631
632        prev_code = Some(code);
633
634        // Grow code_width once the table has filled the current slot range.
635        // The threshold is: when table size == 2^code_width (i.e. when the
636        // table is exactly full), the *next* read uses code_width + 1.
637        if code_width < 12 && prefix.len() == (1 << code_width) {
638            code_width += 1;
639        }
640    }
641
642    Ok(output)
643}
644
645// ─────────────────────────────────────────────────────────────────────────────
646// Public decoder
647// ─────────────────────────────────────────────────────────────────────────────
648
649/// Decode a GIF87a or GIF89a byte stream into raw RGB pixels.
650///
651/// Only the first frame is decoded.  The result always uses 3 bytes per pixel
652/// (R, G, B) regardless of the original colour depth.
653pub fn gif_decode(bytes: &[u8]) -> Result<RawDecodeResult, GifError> {
654    if bytes.len() < 6 {
655        return Err(GifError::Truncated);
656    }
657
658    // ── Header ────────────────────────────────────────────────────────────────
659    if &bytes[..6] != b"GIF89a" && &bytes[..6] != b"GIF87a" {
660        return Err(GifError::Invalid(format!(
661            "bad GIF signature: {:?}",
662            &bytes[..6.min(bytes.len())]
663        )));
664    }
665
666    if bytes.len() < 13 {
667        return Err(GifError::Truncated);
668    }
669
670    // ── Logical Screen Descriptor ─────────────────────────────────────────────
671    let canvas_width = u16::from_le_bytes([bytes[6], bytes[7]]) as usize;
672    let canvas_height = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
673    let lsd_packed = bytes[10];
674    let has_gct = (lsd_packed & 0x80) != 0;
675    let gct_size_field = lsd_packed & 0x07;
676    let gct_entries = 1usize << (gct_size_field as usize + 1);
677
678    let mut pos = 13usize; // Skip LSD (7 bytes past header).
679
680    // ── Global Colour Table ───────────────────────────────────────────────────
681    let mut global_palette: Vec<[u8; 3]> = Vec::new();
682    if has_gct {
683        let gct_bytes = gct_entries * 3;
684        if pos + gct_bytes > bytes.len() {
685            return Err(GifError::Truncated);
686        }
687        for i in 0..gct_entries {
688            global_palette.push([
689                bytes[pos + i * 3],
690                bytes[pos + i * 3 + 1],
691                bytes[pos + i * 3 + 2],
692            ]);
693        }
694        pos += gct_bytes;
695    }
696
697    // ── Walk blocks until we hit an Image Descriptor ──────────────────────────
698    loop {
699        if pos >= bytes.len() {
700            return Err(GifError::Truncated);
701        }
702        match bytes[pos] {
703            0x3B => {
704                // Trailer before we found an image.
705                return Err(GifError::Invalid("GIF contains no image frames".into()));
706            }
707            0x21 => {
708                // Extension block.
709                pos += 1;
710                if pos >= bytes.len() {
711                    return Err(GifError::Truncated);
712                }
713                pos += 1; // Skip extension label.
714                          // Skip all sub-blocks.
715                loop {
716                    if pos >= bytes.len() {
717                        return Err(GifError::Truncated);
718                    }
719                    let block_len = bytes[pos] as usize;
720                    pos += 1;
721                    if block_len == 0 {
722                        break;
723                    }
724                    pos += block_len;
725                    if pos > bytes.len() {
726                        return Err(GifError::Truncated);
727                    }
728                }
729            }
730            0x2C => {
731                // Image Descriptor.
732                break;
733            }
734            other => {
735                return Err(GifError::Invalid(format!(
736                    "unexpected block byte 0x{:02X} at offset {}",
737                    other, pos
738                )));
739            }
740        }
741    }
742
743    // ── Image Descriptor ──────────────────────────────────────────────────────
744    // pos now points at 0x2C.
745    if pos + 10 > bytes.len() {
746        return Err(GifError::Truncated);
747    }
748    let img_width = u16::from_le_bytes([bytes[pos + 5], bytes[pos + 6]]) as usize;
749    let img_height = u16::from_le_bytes([bytes[pos + 7], bytes[pos + 8]]) as usize;
750    let img_packed = bytes[pos + 9];
751    let has_lct = (img_packed & 0x80) != 0;
752    let lct_size_field = img_packed & 0x07;
753    pos += 10; // Consume Image Descriptor.
754
755    // ── Local Colour Table (if present) ───────────────────────────────────────
756    let active_palette: Vec<[u8; 3]> = if has_lct {
757        let lct_entries = 1usize << (lct_size_field as usize + 1);
758        let lct_bytes = lct_entries * 3;
759        if pos + lct_bytes > bytes.len() {
760            return Err(GifError::Truncated);
761        }
762        let mut lct = Vec::with_capacity(lct_entries);
763        for i in 0..lct_entries {
764            lct.push([
765                bytes[pos + i * 3],
766                bytes[pos + i * 3 + 1],
767                bytes[pos + i * 3 + 2],
768            ]);
769        }
770        pos += lct_bytes;
771        lct
772    } else {
773        global_palette
774    };
775
776    if active_palette.is_empty() {
777        return Err(GifError::Invalid("no colour table available".into()));
778    }
779
780    // ── LZW Minimum Code Size ─────────────────────────────────────────────────
781    if pos >= bytes.len() {
782        return Err(GifError::Truncated);
783    }
784    let min_code_size = bytes[pos];
785    pos += 1;
786
787    if !(2..=11).contains(&min_code_size) {
788        return Err(GifError::Invalid(format!(
789            "invalid LZW minimum code size: {}",
790            min_code_size
791        )));
792    }
793
794    // ── Image Sub-blocks ──────────────────────────────────────────────────────
795    if pos >= bytes.len() {
796        return Err(GifError::Truncated);
797    }
798    let (lzw_data, _consumed) = read_sub_blocks(&bytes[pos..])?;
799
800    // ── LZW Decompress ────────────────────────────────────────────────────────
801    let indices = lzw_decompress(&lzw_data, min_code_size)?;
802
803    // Use canvas dimensions if image dimensions are 0 (degenerate case).
804    let out_width = if img_width == 0 {
805        canvas_width
806    } else {
807        img_width
808    };
809    let out_height = if img_height == 0 {
810        canvas_height
811    } else {
812        img_height
813    };
814    let pixel_count = out_width * out_height;
815
816    // Map indices → RGB.
817    let mut pixels: Vec<u8> = Vec::with_capacity(pixel_count * 3);
818    for i in 0..pixel_count {
819        let idx = if i < indices.len() {
820            indices[i] as usize
821        } else {
822            0
823        };
824        let color = if idx < active_palette.len() {
825            active_palette[idx]
826        } else {
827            [0, 0, 0]
828        };
829        pixels.push(color[0]);
830        pixels.push(color[1]);
831        pixels.push(color[2]);
832    }
833
834    Ok(RawDecodeResult {
835        width: out_width,
836        height: out_height,
837        pixels,
838    })
839}
840
841// ─────────────────────────────────────────────────────────────────────────────
842// Tests
843// ─────────────────────────────────────────────────────────────────────────────
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848
849    /// Build a solid-colour `width × height` RGB pixel buffer.
850    fn solid_rgb(width: u32, height: u32, r: u8, g: u8, b: u8) -> Vec<u8> {
851        let n = (width * height) as usize;
852        let mut buf = Vec::with_capacity(n * 3);
853        for _ in 0..n {
854            buf.push(r);
855            buf.push(g);
856            buf.push(b);
857        }
858        buf
859    }
860
861    /// Build a simple gradient `width × height` RGB pixel buffer.
862    fn gradient_rgb(width: u32, height: u32) -> Vec<u8> {
863        let mut buf = Vec::new();
864        for y in 0..height {
865            for x in 0..width {
866                buf.push(((x * 255) / width.max(1)) as u8);
867                buf.push(((y * 255) / height.max(1)) as u8);
868                buf.push(128u8);
869            }
870        }
871        buf
872    }
873
874    #[test]
875    fn test_gif_header() {
876        let pixels = solid_rgb(4, 4, 200, 10, 10);
877        let gif = gif_encode_rgb(4, 4, &pixels).expect("encode");
878        assert!(
879            gif.starts_with(b"GIF89a"),
880            "GIF must start with GIF89a signature"
881        );
882    }
883
884    #[test]
885    fn test_gif_trailer() {
886        let pixels = solid_rgb(4, 4, 10, 200, 10);
887        let gif = gif_encode_rgb(4, 4, &pixels).expect("encode");
888        assert_eq!(
889            *gif.last().expect("non-empty"),
890            0x3Bu8,
891            "GIF must end with 0x3B trailer"
892        );
893    }
894
895    #[test]
896    fn test_gif_roundtrip_solid_color() {
897        let pixels = solid_rgb(4, 4, 220, 20, 20);
898        let gif = gif_encode_rgb(4, 4, &pixels).expect("encode");
899        let result = gif_decode(&gif).expect("decode");
900        // At least half the pixels should be "reddish".
901        let reddish = result
902            .pixels
903            .chunks_exact(3)
904            .filter(|p| p[0] >= 200 && p[1] <= 50 && p[2] <= 50)
905            .count();
906        let total = result.width * result.height;
907        assert!(
908            reddish * 2 >= total,
909            "expected reddish pixels: got {}/{} reddish",
910            reddish,
911            total
912        );
913    }
914
915    #[test]
916    fn test_gif_roundtrip_size() {
917        let pixels = gradient_rgb(8, 8);
918        let gif = gif_encode_rgb(8, 8, &pixels).expect("encode");
919        let result = gif_decode(&gif).expect("decode");
920        assert_eq!(result.width, 8);
921        assert_eq!(result.height, 8);
922    }
923
924    #[test]
925    fn test_gif_lzw_clear_code() {
926        // The LZW stream for min_code_size=8 starts with the clear code (256 = 0x100).
927        // In LSB-first 9-bit packing the first 9 bits are:
928        //   0x100 = 0b1_0000_0000 → byte[0] = 0x00, bits [0..1] of byte[1] = 0b1
929        // The stream is wrapped in sub-blocks; the first sub-block byte is the
930        // length, then the data starts at byte[1] of the encoded output.
931        let pixels = solid_rgb(2, 2, 100, 100, 100);
932        let gif = gif_encode_rgb(2, 2, &pixels).expect("encode");
933
934        // Locate the LZW minimum code size byte.
935        // Offset: 6 (header) + 7 (LSD) + palette_bytes + 10 (image descriptor) + 1 (min code).
936        // We search for 0x2C (image separator) to locate the image descriptor.
937        let sep_pos = gif
938            .iter()
939            .position(|&b| b == 0x2C)
940            .expect("image separator");
941        let min_code_size_pos = sep_pos + 10;
942        assert!(
943            min_code_size_pos + 2 < gif.len(),
944            "GIF too short for LZW data"
945        );
946        let min_code_size = gif[min_code_size_pos];
947        let clear_code = 1u32 << min_code_size;
948
949        // The first sub-block starts right after the min_code_size byte.
950        let sub_block_start = min_code_size_pos + 1;
951        assert!(
952            sub_block_start < gif.len(),
953            "No sub-block after min_code_size"
954        );
955        let sub_block_len = gif[sub_block_start] as usize;
956        assert!(sub_block_len > 0, "First sub-block must be non-empty");
957
958        // Extract the first sub-block's data bytes.
959        let data_start = sub_block_start + 1;
960        assert!(
961            data_start + sub_block_len <= gif.len(),
962            "Sub-block data truncated"
963        );
964        let lzw_data = &gif[data_start..data_start + sub_block_len];
965
966        // Read the first `min_code_size + 1` bits LSB-first and verify they
967        // equal the clear code.
968        let code_width = min_code_size as u32 + 1;
969        let mut bit_buf = 0u32;
970        for (i, &byte) in lzw_data.iter().take(4).enumerate() {
971            bit_buf |= (byte as u32) << (i * 8);
972        }
973        let mask = (1u32 << code_width) - 1;
974        let first_code = bit_buf & mask;
975        assert_eq!(
976            first_code, clear_code,
977            "First LZW code must be the clear code ({}); got {}",
978            clear_code, first_code
979        );
980    }
981
982    #[test]
983    fn test_gif_decode_invalid_returns_error() {
984        let bad = b"NOT_GIF";
985        let result = gif_decode(bad);
986        assert!(result.is_err(), "Expected error for invalid GIF magic");
987    }
988
989    #[test]
990    fn test_gif_decode_empty_returns_error() {
991        let result = gif_decode(&[]);
992        assert!(result.is_err(), "Expected error for empty input");
993    }
994
995    #[test]
996    fn test_gif_encode_dimensions_preserved() {
997        let pixels = gradient_rgb(10, 6);
998        let gif = gif_encode_rgb(10, 6, &pixels).expect("encode");
999        // Logical Screen Descriptor width at bytes 6-7, height at bytes 8-9.
1000        let w = u16::from_le_bytes([gif[6], gif[7]]) as u32;
1001        let h = u16::from_le_bytes([gif[8], gif[9]]) as u32;
1002        assert_eq!(w, 10, "encoded width mismatch");
1003        assert_eq!(h, 6, "encoded height mismatch");
1004    }
1005}