pixie_anim_lib/
gif.rs

1//! GIF89a Structure and Writing.
2
3use crate::error::Result;
4use crate::lzw::LzwEncoder;
5use std::io::Write;
6
7/// Options for configuring the GIF output.
8pub struct GifOptions {
9    /// Width of the logical screen.
10    pub width: u16,
11    /// Height of the logical screen.
12    pub height: u16,
13    /// Whether to include a global color table.
14    pub has_global_palette: bool,
15    /// Size of the palette as a power of 2 (2^(n+1)).
16    pub palette_size: u8,
17}
18
19/// Descriptor for a single image frame within the GIF.
20pub struct ImageDescriptor {
21    /// X offset from the left edge of the logical screen.
22    pub x: u16,
23    /// Y offset from the top edge of the logical screen.
24    pub y: u16,
25    /// Width of the image.
26    pub width: u16,
27    /// Height of the image.
28    pub height: u16,
29    /// LZW minimum code size.
30    pub lzw_min_code_size: u8,
31}
32
33/// A writer for creating GIF89a formatted data.
34pub struct GifWriter<W: Write> {
35    writer: W,
36}
37
38impl<W: Write> GifWriter<W> {
39    /// Creates a new GifWriter wrapping the provided output stream.
40    pub fn new(writer: W) -> Self {
41        Self { writer }
42    }
43
44    /// Writes the GIF89a header.
45    pub fn write_header(&mut self) -> Result<()> {
46        self.writer.write_all(b"GIF89a")?;
47        Ok(())
48    }
49
50    /// Writes the Logical Screen Descriptor block.
51    pub fn write_logical_screen_descriptor(&mut self, options: &GifOptions) -> Result<()> {
52        self.writer.write_all(&options.width.to_le_bytes())?;
53        self.writer.write_all(&options.height.to_le_bytes())?;
54
55        let mut packed = 0u8;
56        if options.has_global_palette {
57            packed |= 0x80;
58            packed |= (options.palette_size - 1) & 0x07;
59            packed |= 0x70; // 8-bit color resolution
60        }
61
62        self.writer.write_all(&[packed, 0, 0])?; // packed, background, pixel aspect ratio
63        Ok(())
64    }
65
66    /// Writes the Netscape Application Block for infinite looping.
67    pub fn write_netscape_loop_block(&mut self) -> Result<()> {
68        self.writer.write_all(&[0x21, 0xFF, 0x0B])?; // extension introducer, application label, block size
69        self.writer.write_all(b"NETSCAPE2.0")?;
70        self.writer.write_all(&[0x03, 0x01])?; // sub-block size, loop sub-block id
71        self.writer.write_all(&0u16.to_le_bytes())?; // loop count (0 = infinite)
72        self.writer.write_all(&[0])?; // block terminator
73        Ok(())
74    }
75
76    /// Writes the global palette data.
77    pub fn write_global_palette(&mut self, palette: &[u8]) -> Result<()> {
78        self.writer.write_all(palette)?;
79        Ok(())
80    }
81
82    /// Writes a Graphic Control Extension block for a frame.
83    pub fn write_graphic_control_extension(
84        &mut self,
85        delay: u16,
86        transparent_idx: Option<u8>,
87    ) -> Result<()> {
88        self.writer.write_all(&[0x21, 0xF9, 0x04])?; // introducer, label, size
89
90        let mut packed = 0x04; // Disposal Method: 1 (Do not dispose)
91        if transparent_idx.is_some() {
92            packed |= 0x01;
93        }
94        self.writer.write_all(&[packed])?;
95        self.writer.write_all(&delay.to_le_bytes())?;
96        self.writer.write_all(&[transparent_idx.unwrap_or(0), 0])?; // transparent index, terminator
97        Ok(())
98    }
99
100    /// Writes encoded image data sub-blocks.
101    pub fn write_image_data(
102        &mut self,
103        descriptor: &ImageDescriptor,
104        indices: &[u8],
105        encoder: &mut LzwEncoder,
106    ) -> Result<()> {
107        // Image Descriptor
108        self.writer.write_all(&[0x2C])?; // separator
109        self.writer.write_all(&descriptor.x.to_le_bytes())?;
110        self.writer.write_all(&descriptor.y.to_le_bytes())?;
111        self.writer.write_all(&descriptor.width.to_le_bytes())?;
112        self.writer.write_all(&descriptor.height.to_le_bytes())?;
113        self.writer.write_all(&[0])?; // packed: no local palette
114
115        // LZW Minimum Code Size
116        self.writer.write_all(&[descriptor.lzw_min_code_size])?;
117
118        // Encode data into sub-blocks
119        let mut lzw_data = Vec::new();
120        encoder.encode(indices, &mut lzw_data)?;
121
122        // GIF requires data in sub-blocks (max 255 bytes)
123        for chunk in lzw_data.chunks(255) {
124            self.writer.write_all(&[chunk.len() as u8])?;
125            self.writer.write_all(chunk)?;
126        }
127        self.writer.write_all(&[0])?; // block terminator
128
129        Ok(())
130    }
131
132    /// Writes the GIF trailer byte.
133    pub fn write_trailer(&mut self) -> Result<()> {
134        self.writer.write_all(&[0x3B])?;
135        Ok(())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_write_minimal_gif() {
145        let mut buffer = Vec::new();
146        {
147            let mut writer = GifWriter::new(&mut buffer);
148            let options = GifOptions {
149                width: 1,
150                height: 1,
151                has_global_palette: true,
152                palette_size: 1, // 2 colors
153            };
154
155            writer.write_header().unwrap();
156            writer.write_logical_screen_descriptor(&options).unwrap();
157
158            let mut palette = vec![0u8; 6]; // 2 RGB colors
159            palette[0] = 255; // Red
160            writer.write_global_palette(&palette).unwrap();
161            
162            let mut encoder = LzwEncoder::new(2);
163            let descriptor = ImageDescriptor {
164                x: 0,
165                y: 0,
166                width: 1,
167                height: 1,
168                lzw_min_code_size: 2,
169            };
170            writer.write_image_data(&descriptor, &[0], &mut encoder).unwrap();
171            writer.write_trailer().unwrap();
172        }
173
174        assert!(buffer.starts_with(b"GIF89a"));
175        assert_eq!(*buffer.last().unwrap(), 0x3B);
176    }
177}