image.rs

6.1 kB · rust · 187 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#[derive(Deserialize)]87struct Parts {88    width: usize,89    height: usize,90    rows: Vec<Vec<usize>>,91    palette: Vec<Color>,92}9394impl TryFrom<Parts> for Image {95    type Error = MrlyError;9697    fn try_from(parts: Parts) -> Result<Image> {98        let Parts {99            width,100            height,101            rows,102            palette,103        } = parts;104        if rows.len() != height || rows.iter().any(|row| row.len() != width) {105            return value_error("rows must fill the image's width and height.");106        }107        if rows.iter().flatten().any(|&id| id >= palette.len()) {108            return value_error("rows must index the palette.");109        }110        Ok(Image::new(width, height, rows, palette))111    }112}113114#[cfg(test)]115mod tests {116    use super::*;117    use crate::core::json;118119    fn sample() -> Image {120        Image::new(121            2,122            2,123            vec![vec![0, 1], vec![1, 2]],124            vec![125                Color::rgb(255, 0, 0),126                Color::rgb(0, 0, 0),127                Color::rgba(0, 140, 255, 128),128            ],129        )130    }131132    #[test]133    fn json_round_trips() {134        let image = sample();135        let json = serde_json::to_value(&image).unwrap();136        assert_eq!(json["palette"][0], "#ff0000");137        assert_eq!(json["palette"][2], "#008cff80");138        let back: Image = serde_json::from_value(json).unwrap();139        assert_eq!(image, back);140    }141142    #[test]143    fn from_json_rejects_garbage() {144        let read = |value| serde_json::from_value::<Image>(value);145        assert!(read(json!(null)).is_err());146        assert!(read(json!({ "width": 1, "height": 1 })).is_err());147        let ragged = json!({ "width": 2, "height": 2, "rows": [[0]], "palette": ["#ffffff"] });148        assert!(read(ragged).is_err());149        let short = json!({ "width": 1, "height": 2, "rows": [[0]], "palette": ["#ffffff"] });150        assert!(read(short).is_err());151        let loose = json!({ "width": 1, "height": 1, "rows": [[9]], "palette": ["#ffffff"] });152        assert!(read(loose).is_err());153        let murky = json!({ "width": 1, "height": 1, "rows": [[0]], "palette": ["soup"] });154        assert!(read(murky).is_err());155    }156157    #[test]158    fn pixels_round_trip() {159        let pixels = vec![160            [255, 0, 0, 255],161            [0, 0, 0, 255],162            [0, 0, 0, 255],163            [255, 0, 0, 255],164        ];165        let image = Image::from_pixels(2, 2, &pixels);166        assert_eq!(image.rows, vec![vec![0, 1], vec![1, 0]]);167        assert_eq!(image.palette.len(), 2);168        assert_eq!(image.colors(), pixels);169    }170171    #[test]172    fn png_delegates_to_the_codec() {173        let bytes = sample().png(4).unwrap();174        assert_eq!(&bytes[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);175        assert!(sample().png(0).is_err());176    }177178    #[test]179    fn resample_keeps_the_palette_on_a_nearest_upscale() {180        let image = sample().resample(4, 4, Filter::Nearest).unwrap();181        assert_eq!((image.width, image.height), (4, 4));182        assert_eq!(image.palette.len(), 3);183        assert_eq!(image.rows[0], vec![0, 0, 1, 1]);184        assert_eq!(image.rows[3], vec![1, 1, 2, 2]);185        assert!(sample().resample(0, 4, Filter::Nearest).is_err());186    }187}