//! Animated GIF (GIF89a) from RGBA frames, for motion animations. Every frame shares one palette, //! so a colour keeps its value from frame to frame, and each frame after the first stores only //! the rectangle that changed. The palette splits the frames' colours where that removes the //! most squared colour error, so a small, strongly coloured part (a red part, a thin trace line) //! keeps its colour instead of being averaged into the shading around it. No dithering: what //! stands still stays still. use std::collections::HashMap; /// Colours a GIF palette holds. const PALETTE: usize = 256; /// Frames (straight RGBA, `width × height`, rows top to bottom; alpha is ignored) as a GIF that /// loops forever and shows each frame for `delay` hundredths of a second. `None` without frames, /// for a frame of the wrong size, or a side GIF can't store (above 65535). pub fn gif(width: usize, height: usize, frames: &[Vec], delay: u16) -> Option> { let (w, h) = (u16::try_from(width).ok().filter(|w| *w > 0)?, u16::try_from(height).ok().filter(|h| *h > 0)?); let bytes = width.checked_mul(height)?.checked_mul(4)?; if frames.is_empty() || frames.iter().any(|f| f.len() != bytes) { return None; } let (palette, index) = palette(frames); let mut out = Vec::with_capacity(1024); out.extend_from_slice(b"GIF89a"); out.extend_from_slice(&w.to_le_bytes()); out.extend_from_slice(&h.to_le_bytes()); // A global table of 256 colours (8 bits each), background colour 0, square pixels. out.extend_from_slice(&[0xF7, 0, 0]); for k in 0..PALETTE { out.extend_from_slice(palette.get(k).unwrap_or(&[0, 0, 0])); } // Loop forever (the NETSCAPE2.0 extension every viewer reads). out.extend_from_slice(&[0x21, 0xFF, 0x0B]); out.extend_from_slice(b"NETSCAPE2.0"); out.extend_from_slice(&[0x03, 0x01, 0x00, 0x00, 0x00]); let mut previous: Option> = None; for frame in frames { let now = indices(frame, &index); let (x0, y0, x1, y1) = previous.as_deref().map_or(Some((0, 0, width - 1, height - 1)), |p| changed(p, &now, width)).unwrap_or((0, 0, 0, 0)); let mut rect = Vec::with_capacity((x1 - x0 + 1) * (y1 - y0 + 1)); for y in y0..=y1 { rect.extend_from_slice(now.get(y * width + x0..=y * width + x1)?); } // Graphic control: kept on screen under the next frame (disposal 1), shown `delay`. out.extend_from_slice(&[0x21, 0xF9, 0x04, 0x04]); out.extend_from_slice(&delay.to_le_bytes()); out.extend_from_slice(&[0, 0]); out.push(0x2C); for v in [x0, y0, x1 - x0 + 1, y1 - y0 + 1] { out.extend_from_slice(&u16::try_from(v).ok()?.to_le_bytes()); } out.push(0); out.push(8); let data = weezl::encode::Encoder::new(weezl::BitOrder::Lsb, 8).encode(&rect).ok()?; for block in data.chunks(255) { out.push(block.len() as u8); out.extend_from_slice(block); } out.push(0); previous = Some(now); } out.push(0x3B); Some(out) } /// The pixels' colours, `0xRRGGBB`. fn colours(frame: &[u8]) -> impl Iterator + '_ { frame.as_chunks::<4>().0.iter().map(|&[r, g, b, _]| (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b)) } /// A frame's palette indices. fn indices(frame: &[u8], index: &HashMap) -> Vec { let mut last = (u32::MAX, 0); colours(frame) .map(|k| { if k != last.0 { last = (k, index.get(&k).copied().unwrap_or(0)); } last.1 }) .collect() } /// The smallest rectangle (x0, y0, x1, y1, inclusive) holding every pixel that differs, or `None` /// when none does. fn changed(before: &[u8], now: &[u8], width: usize) -> Option<(usize, usize, usize, usize)> { let mut r: Option<(usize, usize, usize, usize)> = None; for (i, _) in before.iter().zip(now).enumerate().filter(|(_, (a, b))| a != b) { let (x, y) = (i % width, i / width); r = Some(r.map_or((x, y, x, y), |(x0, y0, x1, y1)| (x0.min(x), y0.min(y), x1.max(x), y1.max(y)))); } r } /// How many pixels have a colour, and the sums its mean and squared error follow from. #[derive(Clone, Copy, Default)] struct Sums { n: f64, s: [f64; 3], q: f64, } impl Sums { fn of(rgb: u32, n: u64) -> Sums { let n = n as f64; let c = [f64::from((rgb >> 16) as u8), f64::from((rgb >> 8) as u8), f64::from(rgb as u8)]; Sums { n, s: c.map(|v| n * v), q: n * c.iter().map(|v| v * v).sum::() } } fn add(&mut self, o: &Sums) { self.n += o.n; self.q += o.q; self.s.iter_mut().zip(o.s).for_each(|(a, b)| *a += b); } fn minus(&self, o: &Sums) -> Sums { let mut s = *self; s.n -= o.n; s.q -= o.q; s.s.iter_mut().zip(o.s).for_each(|(a, b)| *a -= b); s } /// The squared distance of the pixels from their mean colour. fn error(&self) -> f64 { if self.n <= 0.0 { 0.0 } else { self.q - self.s.iter().map(|s| s * s).sum::() / self.n } } fn mean(&self) -> [u8; 3] { self.s.map(|s| if self.n > 0.0 { (s / self.n).round().clamp(0.0, 255.0) as u8 } else { 0 }) } } /// A set of the frames' colours that becomes one palette entry, and its best split: (error /// removed, channel, the largest value of that channel on the low side). struct Group { colours: Vec<(u32, Sums)>, sums: Sums, split: Option<(f64, u32, u8)>, } impl Group { fn new(colours: Vec<(u32, Sums)>) -> Group { let mut sums = Sums::default(); colours.iter().for_each(|(_, s)| sums.add(s)); let mut best: Option<(f64, u32, u8)> = None; for channel in [16u32, 8, 0] { let mut bins = [Sums::default(); 256]; for (k, s) in &colours { if let Some(b) = bins.get_mut(usize::from((k >> channel) as u8)) { b.add(s); } } let mut low = Sums::default(); for (v, b) in bins.iter().enumerate() { low.add(b); if low.n <= 0.0 || low.n >= sums.n { continue; } let gain = sums.error() - low.error() - sums.minus(&low).error(); if best.is_none_or(|(g, _, _)| gain > g) { best = Some((gain, channel, v as u8)); } } } Group { colours, sums, split: best.filter(|(g, _, _)| *g > 0.0) } } } /// One palette for all frames (up to 256 colours) and each colour's index in it. Every colour /// gets its own entry while there are 256 or fewer. fn palette(frames: &[Vec]) -> (Vec<[u8; 3]>, HashMap) { let mut counts: HashMap = HashMap::new(); for frame in frames { // Runs of one colour (the background) are counted once per run. let mut run = (u32::MAX, 0u64); for k in colours(frame) { if k != run.0 { if run.1 > 0 { *counts.entry(run.0).or_default() += run.1; } run = (k, 0); } run.1 += 1; } if run.1 > 0 { *counts.entry(run.0).or_default() += run.1; } } let mut groups = vec![Group::new(counts.into_iter().map(|(k, n)| (k, Sums::of(k, n))).collect())]; while groups.len() < PALETTE { let Some(i) = groups.iter().enumerate().filter_map(|(i, g)| g.split.map(|s| (i, s.0))).max_by(|a, b| a.1.total_cmp(&b.1)).map(|(i, _)| i) else { break; }; let g = groups.swap_remove(i); let Some((_, channel, at)) = g.split else { break }; let (low, high) = g.colours.into_iter().partition(|(k, _)| (k >> channel) as u8 <= at); groups.push(Group::new(low)); groups.push(Group::new(high)); } let mut index = HashMap::new(); for (i, g) in groups.iter().enumerate() { for (k, _) in &g.colours { index.insert(*k, i as u8); } } (groups.iter().map(|g| g.sums.mean()).collect(), index) } #[cfg(test)] mod tests { use super::*; /// The frames of a GIF this module wrote, as RGBA (each frame drawn over the one before). fn decode(gif: &[u8]) -> (usize, usize, Vec>, Vec) { let u16_at = |i: usize| usize::from(u16::from_le_bytes([gif[i], gif[i + 1]])); assert_eq!(&gif[..6], b"GIF89a"); let (w, h) = (u16_at(6), u16_at(8)); let table = &gif[13..13 + 768]; let mut at = 13 + 768; let (mut frames, mut delays) = (Vec::new(), Vec::new()); let mut canvas = vec![0u8; w * h * 4]; loop { match gif[at] { 0x3B => break, 0x21 => { if gif[at + 1] == 0xF9 { delays.push(u16_at(at + 4) as u16); } at += 2; while gif[at] != 0 { at += usize::from(gif[at]) + 1; } at += 1; } 0x2C => { let (x0, y0, fw, fh) = (u16_at(at + 1), u16_at(at + 3), u16_at(at + 5), u16_at(at + 7)); at += 11; let mut data = Vec::new(); while gif[at] != 0 { data.extend_from_slice(&gif[at + 1..at + 1 + usize::from(gif[at])]); at += usize::from(gif[at]) + 1; } at += 1; let pixels = weezl::decode::Decoder::new(weezl::BitOrder::Lsb, 8).decode(&data).unwrap(); assert_eq!(pixels.len(), fw * fh); for (i, p) in pixels.iter().enumerate() { let o = ((y0 + i / fw) * w + x0 + i % fw) * 4; let c = usize::from(*p) * 3; canvas[o..o + 4].copy_from_slice(&[table[c], table[c + 1], table[c + 2], 255]); } frames.push(canvas.clone()); } b => panic!("unexpected block {b:#x} at {at}"), } } (w, h, frames, delays) } fn frame(w: usize, h: usize, f: impl Fn(usize, usize) -> [u8; 3]) -> Vec { (0..w * h).flat_map(|i| f(i % w, i / w).into_iter().chain([255])).collect() } #[test] fn few_colours_come_back_exactly_and_frames_keep_their_size() { let (w, h) = (37, 23); let a = frame(w, h, |x, y| { if x < 10 { [250, 30, 30] } else if y > 15 { [20, 200, 60] } else { [220, 224, 230] } }); // The second frame moves the red band: only that part changes. let b = frame(w, h, |x, y| { if (5..15).contains(&x) { [250, 30, 30] } else if y > 15 { [20, 200, 60] } else { [220, 224, 230] } }); let bytes = gif(w, h, &[a.clone(), b.clone(), b.clone()], 5).unwrap(); let (dw, dh, frames, delays) = decode(&bytes); assert_eq!((dw, dh, delays), (w, h, vec![5, 5, 5])); assert_eq!(frames, vec![a, b.clone(), b]); } #[test] fn a_thin_saturated_line_keeps_its_colour_among_many_shades() { let (w, h) = (300, 200); // A grey shading with far more than 256 shades, a red part and a 1-pixel green line. let f = frame(w, h, |x, y| { if y == 100 { [30, 220, 40] } else if (40..80).contains(&x) && (40..80).contains(&y) { [200 + (y as u8 % 40), 30, 30] } else { let g = 60 + ((x * 180) / w) as u8; [g, g.saturating_add((y / 4) as u8), g.saturating_add(10)] } }); let (_, _, frames, _) = decode(&gif(w, h, std::slice::from_ref(&f), 4).unwrap()); let px = |img: &[u8], x: usize, y: usize| img[(y * w + x) * 4..(y * w + x) * 4 + 3].to_vec(); assert_eq!(px(&frames[0], 150, 100), vec![30, 220, 40], "the line keeps its colour"); let red = px(&frames[0], 60, 60); assert!(red[0] > 180 && red[1] < 60 && red[2] < 60, "the part stays red: {red:?}"); // Elsewhere each channel is close to the original. let worst = f.chunks(4).zip(frames[0].chunks(4)).map(|(a, b)| (0..3).map(|c| a[c].abs_diff(b[c])).max().unwrap()).max().unwrap(); assert!(worst <= 12, "worst channel error {worst}"); } #[test] fn bad_input_is_none_not_a_crash() { assert!(gif(4, 4, &[], 5).is_none()); assert!(gif(4, 4, &[vec![0; 63]], 5).is_none()); assert!(gif(0, 4, &[vec![]], 5).is_none()); assert!(gif(70_000, 1, &[vec![0; 280_000]], 5).is_none()); assert!(gif(usize::MAX, 2, &[vec![0; 8]], 5).is_none()); } }