image.rs

8.0 kB · rust · 233 lines

1use super::codec;2use super::colors::Color;3use super::errors::{value_error, MrlyError, Result};4use super::resample::{self, Filter};5use serde::{Deserialize, Serialize};67/// A paletted image: rows of palette indices and the palette they point into, hex strings in json.8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]9#[serde(try_from = "Parts")]10pub struct Image {11    /// The width in pixels.12    pub width: usize,13    /// The height in pixels.14    pub height: usize,15    /// The palette index of every pixel, row by row.16    pub rows: Vec<Vec<usize>>,17    /// The colors the rows index.18    pub palette: Vec<Color>,19}2021impl Image {22    /// Builds an image from its four parts.23    pub fn new(width: usize, height: usize, rows: Vec<Vec<usize>>, palette: Vec<Color>) -> Image {24        Image {25            width,26            height,27            rows,28            palette,29        }30    }31    /// Builds a paletted image from raw rgba pixels, growing the palette as new colors appear.32    pub fn from_pixels(width: usize, height: usize, pixels: &[[u8; 4]]) -> Image {33        let mut palette: Vec<Color> = Vec::new();34        let mut rows = Vec::with_capacity(height);35        for y in 0..height {36            let mut row = Vec::with_capacity(width);37            for x in 0..width {38                let px = pixels.get(y * width + x).copied().unwrap_or([0, 0, 0, 0]);39                let color = Color::rgba(px[0], px[1], px[2], px[3]);40                let id = match palette.iter().position(|&c| c == color) {41                    Some(id) => id,42                    None => {43                        palette.push(color);44                        palette.len() - 145                    }46                };47                row.push(id);48            }49            rows.push(row);50        }51        Image::new(width, height, rows, palette)52    }53    /// Returns the flat rgba pixels, transparent wherever an index misses the palette.54    pub fn colors(&self) -> Vec<[u8; 4]> {55        let mut out = Vec::with_capacity(self.width * self.height);56        for row in &self.rows {57            for &id in row {58                let c = self59                    .palette60                    .get(id)61                    .copied()62                    .unwrap_or(Color::rgba(0, 0, 0, 0));63                out.push([c.r, c.g, c.b, c.a]);64            }65        }66        out67    }68    /// Encodes the image as a png at the given scale.69    pub fn png(&self, scale: usize) -> Result<Vec<u8>> {70        codec::png(&self.colors(), self.width, self.height, scale)71    }72    /// Resamples the image to a new size, its palette rebuilt from the blended pixels.73    pub fn resample(&self, width: usize, height: usize, filter: Filter) -> Result<Image> {74        let pixels = resample::resample(75            &self.colors(),76            self.width,77            self.height,78            width,79            height,80            filter,81        )?;82        Ok(Image::from_pixels(width, height, &pixels))83    }84}8586/// Encodes indexed frames sharing one size and palette as an animated gif.87pub fn gif(frames: &[Image], scale: usize, delay: usize) -> Result<Vec<u8>> {88    let first = match frames.first() {89        Some(first) => first,90        None => return Err(MrlyError::Value("gif needs at least one frame.".into())),91    };92    let palette: Vec<[u8; 4]> = first.palette.iter().map(|c| [c.r, c.g, c.b, c.a]).collect();93    let mut indices = Vec::with_capacity(frames.len());94    for frame in frames {95        if (frame.width, frame.height) != (first.width, first.height) {96            return Err(MrlyError::Value("every frame must share one size.".into()));97        }98        if frame.palette != first.palette {99            return Err(MrlyError::Value(100                "every frame must share one palette.".into(),101            ));102        }103        let ids = frame.rows.iter().flat_map(|row| row.iter());104        indices.push(105            ids.map(|&id| u8::try_from(id).or_else(|_| value_error("palette index above 255.")))106                .collect::<Result<Vec<u8>>>()?,107        );108    }109    let views: Vec<&[u8]> = indices.iter().map(|f| f.as_slice()).collect();110    codec::gif(&views, &palette, first.width, first.height, scale, delay)111}112113#[derive(Deserialize)]114struct Parts {115    width: usize,116    height: usize,117    rows: Vec<Vec<usize>>,118    palette: Vec<Color>,119}120121impl TryFrom<Parts> for Image {122    type Error = MrlyError;123124    fn try_from(parts: Parts) -> Result<Image> {125        let Parts {126            width,127            height,128            rows,129            palette,130        } = parts;131        if rows.len() != height || rows.iter().any(|row| row.len() != width) {132            return value_error("rows must fill the image's width and height.");133        }134        if rows.iter().flatten().any(|&id| id >= palette.len()) {135            return value_error("rows must index the palette.");136        }137        Ok(Image::new(width, height, rows, palette))138    }139}140141#[cfg(test)]142mod tests {143    use super::*;144    use crate::json;145146    fn sample() -> Image {147        Image::new(148            2,149            2,150            vec![vec![0, 1], vec![1, 2]],151            vec![152                Color::rgb(255, 0, 0),153                Color::rgb(0, 0, 0),154                Color::rgba(0, 140, 255, 128),155            ],156        )157    }158159    #[test]160    fn json_round_trips() {161        let image = sample();162        let json = serde_json::to_value(&image).unwrap();163        assert_eq!(json["palette"][0], "#ff0000");164        assert_eq!(json["palette"][2], "#008cff80");165        let back: Image = serde_json::from_value(json).unwrap();166        assert_eq!(image, back);167    }168169    #[test]170    fn from_json_rejects_garbage() {171        let read = |value| serde_json::from_value::<Image>(value);172        assert!(read(json!(null)).is_err());173        assert!(read(json!({ "width": 1, "height": 1 })).is_err());174        let ragged = json!({ "width": 2, "height": 2, "rows": [[0]], "palette": ["#ffffff"] });175        assert!(read(ragged).is_err());176        let short = json!({ "width": 1, "height": 2, "rows": [[0]], "palette": ["#ffffff"] });177        assert!(read(short).is_err());178        let loose = json!({ "width": 1, "height": 1, "rows": [[9]], "palette": ["#ffffff"] });179        assert!(read(loose).is_err());180        let murky = json!({ "width": 1, "height": 1, "rows": [[0]], "palette": ["soup"] });181        assert!(read(murky).is_err());182    }183184    #[test]185    fn pixels_round_trip() {186        let pixels = vec![187            [255, 0, 0, 255],188            [0, 0, 0, 255],189            [0, 0, 0, 255],190            [255, 0, 0, 255],191        ];192        let image = Image::from_pixels(2, 2, &pixels);193        assert_eq!(image.rows, vec![vec![0, 1], vec![1, 0]]);194        assert_eq!(image.palette.len(), 2);195        assert_eq!(image.colors(), pixels);196    }197198    #[test]199    fn png_delegates_to_the_codec() {200        let bytes = sample().png(4).unwrap();201        assert_eq!(&bytes[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);202        assert!(sample().png(0).is_err());203    }204205    #[test]206    fn resample_keeps_the_palette_on_a_nearest_upscale() {207        let image = sample().resample(4, 4, Filter::Nearest).unwrap();208        assert_eq!((image.width, image.height), (4, 4));209        assert_eq!(image.palette.len(), 3);210        assert_eq!(image.rows[0], vec![0, 0, 1, 1]);211        assert_eq!(image.rows[3], vec![1, 1, 2, 2]);212        assert!(sample().resample(0, 4, Filter::Nearest).is_err());213    }214215    #[test]216    fn gif_encodes_frames_sharing_one_palette() {217        let first = sample();218        let mut second = sample();219        second.rows = vec![vec![2, 1], vec![1, 0]];220        let bytes = gif(&[first.clone(), second], 2, 5).unwrap();221        assert_eq!(&bytes[0..6], b"GIF89a");222        assert_eq!(&bytes[6..8], &4u16.to_le_bytes());223        let mut odd = sample();224        odd.palette.pop();225        odd.rows = vec![vec![0, 1], vec![1, 0]];226        assert!(gif(&[first.clone(), odd], 1, 5).is_err());227        let mut wide = first.clone();228        wide.width = 4;229        wide.rows = vec![vec![0, 1, 0, 1], vec![1, 2, 1, 2]];230        assert!(gif(&[first, wide], 1, 5).is_err());231        assert!(gif(&[], 1, 5).is_err());232    }233}