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}