gif.rs
7.7 kB · rust · 199 lines
1use crate::errors::{value_error, MrlyError, Result};2use crate::resample::block;3use gif::{DisposalMethod, Encoder, Frame, Repeat};4use std::borrow::Cow;56impl From<gif::EncodingError> for MrlyError {7 fn from(error: gif::EncodingError) -> MrlyError {8 MrlyError::Value(error.to_string())9 }10}1112/// Encodes indexed frames as an animated gif89a, each source pixel a scale by scale block.13///14/// The frames index one shared palette, the delay is in hundredths of a second, the15/// animation loops forever, and the first fully transparent palette entry becomes the16/// frame's transparent color.17///18/// ```19/// let still = [0u8, 1, 1, 0];20/// let flip = [1u8, 0, 0, 1];21/// let palette = [[0, 0, 0, 255], [255, 255, 255, 255]];22/// let bytes = mrlycore::gif(&[&still[..], &flip[..]], &palette, 2, 2, 3, 8).unwrap();23/// assert_eq!(&bytes[0..6], b"GIF89a");24/// assert_eq!(bytes[bytes.len() - 1], 0x3b);25/// ```26pub fn gif(27 frames: &[&[u8]],28 palette: &[[u8; 4]],29 width: usize,30 height: usize,31 scale: usize,32 delay: usize,33) -> Result<Vec<u8>> {34 if scale < 1 {35 return value_error("scale must be at least 1.");36 }37 if width == 0 || height == 0 {38 return value_error("width and height must be at least 1.");39 }40 if frames.is_empty() {41 return value_error("gif needs at least one frame.");42 }43 if palette.is_empty() || palette.len() > 256 {44 return value_error("palette must hold 1 to 256 colors.");45 }46 let (out_w, out_h) = (width * scale, height * scale);47 if out_w > u16::MAX as usize || out_h > u16::MAX as usize {48 return value_error("gif size must fit in 16 bits.");49 }50 for frame in frames {51 if frame.len() != width * height {52 return value_error("every frame must hold width * height indices.");53 }54 if frame.iter().any(|&i| i as usize >= palette.len()) {55 return value_error("frame index out of palette range.");56 }57 }58 let table: Vec<u8> = palette59 .iter()60 .flat_map(|c| c[..3].iter().copied())61 .collect();62 let transparent = palette.iter().position(|c| c[3] == 0).map(|i| i as u8);63 let dispose = match transparent {64 Some(_) => DisposalMethod::Background,65 None => DisposalMethod::Keep,66 };67 let out = Vec::with_capacity(frames.len() * out_w * out_h / 2 + 1024);68 let mut encoder = Encoder::new(out, out_w as u16, out_h as u16, &table)?;69 encoder.set_repeat(Repeat::Infinite)?;70 for frame in frames {71 encoder.write_frame(&Frame {72 delay: delay.min(u16::MAX as usize) as u16,73 dispose,74 transparent,75 width: out_w as u16,76 height: out_h as u16,77 buffer: Cow::Owned(block(frame, width, height, scale)),78 ..Frame::default()79 })?;80 }81 Ok(encoder.into_inner()?)82}8384#[cfg(test)]85mod tests {86 use super::*;87 use gif::{ColorOutput, DecodeOptions};88 struct Gif {89 width: usize,90 height: usize,91 palette: Vec<[u8; 3]>,92 repeat: Repeat,93 frames: Vec<Frame<'static>>,94 }95 fn ungif(bytes: &[u8]) -> Gif {96 assert_eq!(&bytes[0..6], b"GIF89a");97 assert_eq!(bytes[bytes.len() - 1], 0x3b);98 let mut options = DecodeOptions::new();99 options.set_color_output(ColorOutput::Indexed);100 let decoder = options.read_info(bytes).unwrap();101 let palette = decoder102 .global_palette()103 .unwrap()104 .chunks(3)105 .map(|c| [c[0], c[1], c[2]])106 .collect();107 Gif {108 width: decoder.width() as usize,109 height: decoder.height() as usize,110 palette,111 repeat: decoder.repeat(),112 frames: decoder.into_iter().map(|f| f.unwrap()).collect(),113 }114 }115 #[test]116 fn gif_round_trips_indexed_frames() {117 let first = [0u8, 1, 2, 1, 2, 0, 1, 0, 1, 1, 0, 2];118 let second = [2u8, 2, 0, 0, 1, 2, 1, 2, 0, 1, 1, 0];119 let palette = [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 0]];120 let bytes = gif(&[&first[..], &second[..]], &palette, 4, 3, 1, 5).unwrap();121 let out = ungif(&bytes);122 assert_eq!((out.width, out.height), (4, 3));123 assert_eq!(out.palette.len(), 4);124 assert_eq!(&out.palette[..3], &[[255, 0, 0], [0, 255, 0], [0, 0, 255]]);125 assert_eq!(out.frames.len(), 2);126 assert_eq!(&out.frames[0].buffer[..], &first);127 assert_eq!(&out.frames[1].buffer[..], &second);128 }129 #[test]130 fn gif_scales_every_pixel_into_a_block() {131 let frame = [0u8, 1, 1, 0];132 let palette = [[0, 0, 0, 255], [255, 255, 255, 255]];133 let bytes = gif(&[&frame[..]], &palette, 2, 2, 3, 0).unwrap();134 let out = ungif(&bytes);135 assert_eq!((out.width, out.height), (6, 6));136 assert_eq!(out.frames.len(), 1);137 assert_eq!(out.frames[0].buffer.len(), 36);138 for y in 0..6 {139 for x in 0..6 {140 let want = frame[(y / 3) * 2 + x / 3];141 assert_eq!(out.frames[0].buffer[y * 6 + x], want, "pixel {x},{y}");142 }143 }144 }145 #[test]146 fn gif_carries_delay_loop_and_transparency() {147 let frame = [0u8, 1];148 let palette = [[1, 2, 3, 255], [4, 5, 6, 0]];149 let bytes = gif(&[&frame[..], &frame[..]], &palette, 2, 1, 1, 7).unwrap();150 let out = ungif(&bytes);151 assert_eq!(out.palette, [[1, 2, 3], [4, 5, 6]]);152 assert_eq!(out.repeat, Repeat::Infinite);153 assert_eq!(out.frames.len(), 2);154 for frame in &out.frames {155 assert_eq!(frame.delay, 7);156 assert_eq!(frame.transparent, Some(1));157 assert_eq!(frame.dispose, DisposalMethod::Background);158 assert_eq!((frame.width, frame.height), (2, 1));159 }160 let opaque = gif(&[&frame[..]], &[[1, 2, 3, 255], [4, 5, 6, 255]], 2, 1, 1, 7).unwrap();161 let out = ungif(&opaque);162 assert_eq!(out.frames[0].transparent, None);163 assert_eq!(out.frames[0].dispose, DisposalMethod::Keep);164 let slow = gif(&[&frame[..]], &palette, 2, 1, 1, 1 << 20).unwrap();165 assert_eq!(ungif(&slow).frames[0].delay, u16::MAX);166 }167 #[test]168 fn gif_round_trips_a_full_palette() {169 let mut random = crate::chacha::ChaCha8::from_u64(11);170 let palette: Vec<[u8; 4]> = (0..256).map(|i| [i as u8, 9, 9, 255]).collect();171 let frame: Vec<u8> = (0..128 * 128).map(|_| random.next_u32() as u8).collect();172 let bytes = gif(&[&frame[..]], &palette, 128, 128, 1, 4).unwrap();173 let out = ungif(&bytes);174 assert_eq!((out.width, out.height), (128, 128));175 assert_eq!(out.palette.len(), 256);176 assert_eq!(out.palette[200], [200, 9, 9]);177 assert_eq!(&out.frames[0].buffer[..], &frame[..]);178 }179 #[test]180 fn gif_compresses_flat_frames() {181 let frame = vec![3u8; 256 * 256];182 let palette: Vec<[u8; 4]> = (0..8).map(|i| [i as u8 * 30, 0, 0, 255]).collect();183 let bytes = gif(&[&frame[..]], &palette, 256, 256, 1, 4).unwrap();184 assert!(bytes.len() < 2000, "flat frame took {} bytes", bytes.len());185 assert_eq!(&ungif(&bytes).frames[0].buffer[..], &frame[..]);186 }187 #[test]188 fn gif_rejects_bad_inputs() {189 let frame = [0u8, 1, 1, 0];190 let palette = [[0, 0, 0, 255], [255, 255, 255, 255]];191 assert!(gif(&[&frame[..]], &palette, 2, 2, 0, 5).is_err());192 assert!(gif(&[], &palette, 2, 2, 1, 5).is_err());193 assert!(gif(&[&frame[..]], &[], 2, 2, 1, 5).is_err());194 assert!(gif(&[&frame[..]], &palette, 3, 2, 1, 5).is_err());195 assert!(gif(&[&frame[..]], &palette, 2, 0, 1, 5).is_err());196 assert!(gif(&[&[0u8, 1, 2, 0][..]], &palette, 2, 2, 1, 5).is_err());197 assert!(gif(&[&frame[..]], &palette, 2, 2, 40000, 5).is_err());198 }199}