gif.rs
12.7 kB · rust · 379 lines
1use crate::board::Board;2use crate::ink;3use crate::out;4use mrlycore::errors::{value_error, MrlyError, Result};5use mrlycore::Color;6use std::collections::HashMap;7use std::path::PathBuf;89// REEL1011/// A strip of frames over one palette, printed as a gif that loops forever.12pub struct Reel {13 /// The width in pixels.14 pub width: usize,15 /// The height in pixels.16 pub height: usize,17 /// The colors every frame indexes, at most 256.18 pub palette: Vec<Color>,19 /// The frames, each the palette index of every pixel and a delay in centiseconds.20 pub frames: Vec<(Vec<u8>, u16)>,21}2223impl Reel {24 /// Opens an empty reel of the given size.25 pub fn new(width: usize, height: usize) -> Reel {26 Reel {27 width,28 height,29 palette: Vec::new(),30 frames: Vec::new(),31 }32 }33 /// Indexes the board exactly against the palette, growing it, and holds it for a delay in centiseconds.34 pub fn add(&mut self, board: &Board, delay: u16) -> Result<()> {35 if board.width != self.width || board.height != self.height {36 return value_error(format!(37 "a {}x{} board does not fit a {}x{} reel.",38 board.width, board.height, self.width, self.height39 ));40 }41 let mut palette = self.palette.clone();42 let mut seen: HashMap<[u8; 3], u8> = palette43 .iter()44 .enumerate()45 .map(|(id, color)| ([color.r, color.g, color.b], id as u8))46 .collect();47 let mut indices = Vec::with_capacity(board.pixels.len());48 for pixel in &board.pixels {49 let rgb = [pixel[0], pixel[1], pixel[2]];50 let id = match seen.get(&rgb) {51 Some(&id) => id,52 None => {53 if palette.len() == 256 {54 return value_error("a reel holds at most 256 colors.");55 }56 let id = palette.len() as u8;57 palette.push(Color::rgb(rgb[0], rgb[1], rgb[2]));58 seen.insert(rgb, id);59 id60 }61 };62 indices.push(id);63 }64 self.palette = palette;65 self.frames.push((indices, delay));66 Ok(())67 }68 /// Encodes the reel as a gif89a: one global color table, one graphic control per frame, looping forever.69 pub fn bytes(&self) -> Result<Vec<u8>> {70 let (width, height) = match (u16::try_from(self.width), u16::try_from(self.height)) {71 (Ok(width), Ok(height)) => (width, height),72 _ => return value_error("a reel is at most 65535 by 65535 pixels."),73 };74 if self.palette.len() > 256 {75 return value_error("a reel holds at most 256 colors.");76 }77 let slots = self.palette.len().max(2).next_power_of_two();78 let bits = slots.trailing_zeros();79 let mut out = Vec::new();80 out.extend_from_slice(b"GIF89a");81 out.extend_from_slice(&width.to_le_bytes());82 out.extend_from_slice(&height.to_le_bytes());83 out.extend_from_slice(&[0xF0 | (bits as u8 - 1), 0x00, 0x00]);84 for slot in 0..slots {85 let color = self86 .palette87 .get(slot)88 .copied()89 .unwrap_or(Color::rgb(0, 0, 0));90 out.extend_from_slice(&[color.r, color.g, color.b]);91 }92 out.extend_from_slice(&[0x21, 0xFF, 0x0B]);93 out.extend_from_slice(b"NETSCAPE2.0");94 out.extend_from_slice(&[0x03, 0x01, 0x00, 0x00, 0x00]);95 for (indices, delay) in &self.frames {96 out.extend_from_slice(&[0x21, 0xF9, 0x04, 0x00]);97 out.extend_from_slice(&delay.to_le_bytes());98 out.extend_from_slice(&[0x00, 0x00]);99 out.extend_from_slice(&[0x2C, 0x00, 0x00, 0x00, 0x00]);100 out.extend_from_slice(&width.to_le_bytes());101 out.extend_from_slice(&height.to_le_bytes());102 out.push(0x00);103 let min = bits.max(2);104 out.push(min as u8);105 for block in lzw(indices, min).chunks(255) {106 out.push(block.len() as u8);107 out.extend_from_slice(block);108 }109 out.push(0x00);110 }111 out.push(0x3B);112 Ok(out)113 }114 /// Writes the reel to files/figures/<name>-<theme>.gif and announces the one line it printed.115 pub fn save(&self, name: &str) -> Result<PathBuf> {116 let name = format!("{name}-{}", ink::name());117 let folder = out::root().join("files").join("figures");118 std::fs::create_dir_all(&folder)119 .map_err(|e| MrlyError::Value(format!("cannot make {folder:?}: {e}")))?;120 let path = folder.join(format!("{name}.gif"));121 let bytes = self.bytes()?;122 std::fs::write(&path, bytes)123 .map_err(|e| MrlyError::Value(format!("cannot write {path:?}: {e}")))?;124 println!(125 "reel {name} {}x{} {} frames",126 self.width,127 self.height,128 self.frames.len()129 );130 Ok(path)131 }132}133134// LZW135136struct Stream {137 out: Vec<u8>,138 acc: u32,139 used: u32,140 width: u32,141}142143impl Stream {144 fn emit(&mut self, code: u16, next: u16) {145 self.acc |= (code as u32) << self.used;146 self.used += self.width;147 while self.used >= 8 {148 self.out.push((self.acc & 0xFF) as u8);149 self.acc >>= 8;150 self.used -= 8;151 }152 if u32::from(next) >= 1 << self.width && self.width < 12 {153 self.width += 1;154 }155 }156 fn flush(&mut self) {157 if self.used > 0 {158 self.out.push((self.acc & 0xFF) as u8);159 }160 }161}162163fn lzw(indices: &[u8], min: u32) -> Vec<u8> {164 let clear = 1u16 << min;165 let end = clear + 1;166 let mut stream = Stream {167 out: Vec::new(),168 acc: 0,169 used: 0,170 width: min + 1,171 };172 let mut table: HashMap<(u16, u8), u16> = HashMap::new();173 let mut next = clear + 2;174 stream.emit(clear, next);175 let mut prefix = match indices.first() {176 Some(&first) => u16::from(first),177 None => {178 stream.emit(end, next);179 stream.flush();180 return stream.out;181 }182 };183 for &index in &indices[1..] {184 if let Some(&code) = table.get(&(prefix, index)) {185 prefix = code;186 continue;187 }188 stream.emit(prefix, next);189 if next < 4096 {190 table.insert((prefix, index), next);191 next += 1;192 } else {193 stream.emit(clear, next);194 table.clear();195 next = clear + 2;196 stream.width = min + 1;197 }198 prefix = u16::from(index);199 }200 stream.emit(prefix, next);201 stream.emit(end, next);202 stream.flush();203 stream.out204}205206#[cfg(test)]207mod tests {208 use super::*;209210 fn painted(pattern: &[u8], colors: &[Color]) -> Board {211 let mut board = Board::new(4, 4, colors[0]);212 for (at, &id) in pattern.iter().enumerate() {213 let color = colors[id as usize];214 board.pixels[at] = [color.r, color.g, color.b, color.a];215 }216 board217 }218219 struct Reader<'a> {220 bytes: &'a [u8],221 at: usize,222 }223224 impl Reader<'_> {225 fn byte(&mut self) -> u8 {226 self.at += 1;227 self.bytes[self.at - 1]228 }229 fn word(&mut self) -> u16 {230 u16::from_le_bytes([self.byte(), self.byte()])231 }232 fn blocks(&mut self) -> Vec<u8> {233 let mut out = Vec::new();234 loop {235 let len = self.byte() as usize;236 if len == 0 {237 return out;238 }239 out.extend_from_slice(&self.bytes[self.at..self.at + len]);240 self.at += len;241 }242 }243 }244245 fn inflate(data: &[u8], min: u32) -> Vec<u8> {246 let clear = 1usize << min;247 let roots = || {248 (0..clear + 2)249 .map(|id| vec![id as u8])250 .collect::<Vec<Vec<u8>>>()251 };252 let mut table = roots();253 let mut width = min + 1;254 let mut at = 0usize;255 let mut prev: Option<Vec<u8>> = None;256 let mut out = Vec::new();257 while at + width as usize <= data.len() * 8 {258 let mut code = 0usize;259 for bit in 0..width as usize {260 let pos = at + bit;261 code |= usize::from((data[pos / 8] >> (pos % 8)) & 1) << bit;262 }263 at += width as usize;264 if code == clear {265 table = roots();266 width = min + 1;267 prev = None;268 continue;269 }270 if code == clear + 1 {271 break;272 }273 let entry = match table.get(code) {274 Some(entry) => entry.clone(),275 None => {276 let mut grown = prev.clone().unwrap();277 grown.push(grown[0]);278 grown279 }280 };281 out.extend_from_slice(&entry);282 if let Some(mut grown) = prev {283 grown.push(entry[0]);284 table.push(grown);285 if table.len() == 1 << width && width < 12 {286 width += 1;287 }288 }289 prev = Some(entry);290 }291 out292 }293294 fn decode(bytes: &[u8]) -> (Vec<Color>, Vec<(Vec<u8>, u16)>) {295 assert_eq!(&bytes[0..6], b"GIF89a");296 let mut read = Reader { bytes, at: 6 };297 let size = (read.word(), read.word());298 let packed = read.byte();299 read.byte();300 read.byte();301 let mut palette = Vec::new();302 for _ in 0..(2usize << (packed & 7)) {303 palette.push(Color::rgb(read.byte(), read.byte(), read.byte()));304 }305 let mut frames = Vec::new();306 let mut delay = 0u16;307 loop {308 match read.byte() {309 0x21 => {310 let label = read.byte();311 let data = read.blocks();312 if label == 0xF9 {313 delay = u16::from_le_bytes([data[1], data[2]]);314 }315 }316 0x2C => {317 assert_eq!((read.word(), read.word()), (0, 0));318 assert_eq!((read.word(), read.word()), size);319 assert_eq!(read.byte(), 0x00);320 let min = u32::from(read.byte());321 let indices = inflate(&read.blocks(), min);322 assert_eq!(indices.len(), size.0 as usize * size.1 as usize);323 frames.push((indices, delay));324 }325 0x3B => return (palette, frames),326 other => panic!("stray block {other:#x}"),327 }328 }329 }330331 #[test]332 fn one_frame_encodes_to_the_bytes_of_the_spec() {333 let mut board = Board::new(3, 2, Color::rgb(255, 0, 0));334 board.pixels[2] = [0, 0, 255, 255];335 board.pixels[3] = [0, 0, 255, 255];336 let mut reel = Reel::new(3, 2);337 reel.add(&board, 7).unwrap();338 let mut want = Vec::new();339 want.extend_from_slice(b"GIF89a");340 want.extend_from_slice(&[0x03, 0x00, 0x02, 0x00, 0xF0, 0x00, 0x00]);341 want.extend_from_slice(&[0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF]);342 want.extend_from_slice(&[0x21, 0xFF, 0x0B]);343 want.extend_from_slice(b"NETSCAPE2.0");344 want.extend_from_slice(&[0x03, 0x01, 0x00, 0x00, 0x00]);345 want.extend_from_slice(&[0x21, 0xF9, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00]);346 want.extend_from_slice(&[0x2C, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00]);347 want.extend_from_slice(&[0x02, 0x03, 0x04, 0x12, 0x56, 0x00]);348 want.push(0x3B);349 assert_eq!(reel.bytes().unwrap(), want);350 }351352 #[test]353 fn a_two_frame_reel_round_trips_through_a_decoder() {354 let colors = [355 Color::rgb(9, 9, 9),356 Color::rgb(200, 30, 40),357 Color::rgb(0, 80, 255),358 ];359 let first: Vec<u8> = (0..16u8).map(|at| at % 3).collect();360 let second: Vec<u8> = (0..16u8).map(|at| (at / 2) % 3).collect();361 let mut reel = Reel::new(4, 4);362 reel.add(&painted(&first, &colors), 4).unwrap();363 reel.add(&painted(&second, &colors), 11).unwrap();364 let (palette, frames) = decode(&reel.bytes().unwrap());365 assert_eq!(&palette[..3], &colors[..]);366 assert_eq!(frames, vec![(first, 4), (second, 11)]);367 }368369 #[test]370 fn the_palette_refuses_a_two_hundred_and_fifty_seventh_color() {371 let mut board = Board::new(257, 1, Color::rgb(0, 0, 0));372 for at in 0..257 {373 board.pixels[at] = [(at / 256) as u8, (at % 256) as u8, 0, 255];374 }375 let mut reel = Reel::new(257, 1);376 assert!(reel.add(&board, 1).is_err());377 assert!(reel.palette.is_empty());378 }379}