use crate::lzw::compress;
pub struct Gif {
width: u16,
height: u16,
palette_bits: u8,
out: Vec<u8>,
}
impl Gif {
pub fn new(width: u16, height: u16, palette: &[[u8; 3]]) -> Gif {
assert!(!palette.is_empty() && palette.len() <= 256, "palette size");
let palette_bits = (usize::BITS - (palette.len() - 1).leading_zeros()).clamp(1, 8) as u8;
let entries = 1usize << palette_bits;
let mut out = Vec::new();
out.extend_from_slice(b"GIF89a");
out.extend_from_slice(&width.to_le_bytes());
out.extend_from_slice(&height.to_le_bytes());
out.push(0b1000_0000 | (palette_bits - 1));
out.push(0); out.push(0); for index in 0..entries {
let color = palette.get(index).copied().unwrap_or([0, 0, 0]);
out.extend_from_slice(&color);
}
out.extend_from_slice(b"\x21\xFF\x0BNETSCAPE2.0\x03\x01\x00\x00\x00");
Gif {
width,
height,
palette_bits,
out,
}
}
pub fn add_frame(&mut self, pixels: &[u8], delay: u16) {
assert_eq!(
pixels.len(),
usize::from(self.width) * usize::from(self.height),
"frame is not the GIF's size"
);
self.out.extend_from_slice(&[0x21, 0xF9, 0x04, 0x00]);
self.out.extend_from_slice(&delay.to_le_bytes());
self.out.extend_from_slice(&[0x00, 0x00]);
self.out.push(0x2C);
self.out.extend_from_slice(&0u16.to_le_bytes());
self.out.extend_from_slice(&0u16.to_le_bytes());
self.out.extend_from_slice(&self.width.to_le_bytes());
self.out.extend_from_slice(&self.height.to_le_bytes());
self.out.push(0);
let min_code_bits = self.palette_bits.max(2);
self.out.push(min_code_bits);
let compressed = compress(pixels, min_code_bits);
for chunk in compressed.chunks(255) {
self.out.push(chunk.len() as u8);
self.out.extend_from_slice(chunk);
}
self.out.push(0);
}
pub fn finish(mut self) -> Vec<u8> {
self.out.push(0x3B); self.out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_file_is_shaped_like_a_gif() {
let palette = [[0, 0, 0], [255, 0, 0], [0, 255, 0]];
let mut gif = Gif::new(4, 2, &palette);
gif.add_frame(&[0, 1, 2, 0, 2, 1, 0, 1], 10);
gif.add_frame(&[1; 8], 10);
let bytes = gif.finish();
assert_eq!(&bytes[..6], b"GIF89a");
assert_eq!(&bytes[6..10], &[4, 0, 2, 0], "size, little-endian");
assert_eq!(bytes[10] & 0b0000_0111, 1);
assert_eq!(*bytes.last().expect("trailer"), 0x3B);
assert_eq!(
bytes.iter().filter(|&&b| b == 0x2C).count(),
2,
"one image descriptor per frame"
);
}
}