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}