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}