use std::fs::read_to_string; use std::path::PathBuf; use serde_derive::{Serialize, Deserialize}; use crate::colour::Colour; use image::{GenericImageView, Rgba, DynamicImage, GenericImage}; use image::io::Reader as ImageReader; /// todo enumerate original format #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Palette { pub name: String, pub colours: Vec, } impl Palette { /// if trying to get an out-of-bounds index, colours will wrap around /// so if palette has 8 colours (0-7) and index is 8, will return 0 pub fn get_colour(&self, index: &u8) -> Option<&Colour> { self.colours.get((index % self.colours.len() as u8) as usize) } /// colour 0 is transparent pub fn get_colour_rgba8(&self, index: &u8) -> Vec { match index { 0 => vec![0,0,0,0], _ => { let colour = self.get_colour(index).unwrap(); vec![colour.red, colour.green, colour.blue, 255] } } } /// todo Result pub fn from_file(path: PathBuf) -> Self { match path.extension().unwrap().to_str().unwrap() { "gpl" => Self::from_gpl(path), "hex" => Self::from_hex(path), "pal" => Self::from_jasc(path), "png" => Self::from_png(path), "txt" => Self::from_txt(path), _ => panic!("Bad palette") } } /// JASC .pal format (Paint Shop Pro) pub fn from_jasc(path: PathBuf) -> Self { let name = path.file_stem().unwrap().to_str().unwrap().into(); let mut colours = Vec::new(); for (index, line) in read_to_string(&path).unwrap().lines().enumerate() { // ignore the first 3 lines if index > 2 { let mut values: Vec = Vec::new(); for value in line.split_whitespace() { values.push(value.parse().unwrap()); } colours.push(Colour::from(values)); } } Palette { name, colours } } /// JASC .pal format (Paint Shop Pro) pub fn to_jasc(&self) -> String { let colours: Vec = self.colours.iter().map(|colour| format!("{} {} {}", colour.red, colour.green, colour.blue) ).collect(); format!("JASC-PAL\r\n0100\r\n{}\r\n{}\r\n", self.colours.len(), colours.join("\r\n")) } /// GIMP .gpl format pub fn from_gpl(path: PathBuf) -> Self { let name = path.file_stem().unwrap().to_str().unwrap().into(); let mut colours = Vec::new(); for (index, line) in read_to_string(&path).unwrap().lines().enumerate() { // ignore header and comments if index > 0 && !line.starts_with("#") { let parts = line.split_whitespace(); colours.push(Colour::from(parts.last().unwrap())); } } Self { name, colours } } /// GIMP .gpl format pub fn to_gpl(&self) -> String { let colours: Vec = self.colours.iter().map(|colour| { format!( "{}\t{}\t{}\t{}", colour.red, colour.green, colour.blue, colour.to_string().replace('#', "") ) }).collect(); // todo fix palette description? does it matter? format!( "GIMP Palette\r\n#Palette Name: {}\r\n#Colors: {}\r\n{}\r\n", self.name, colours.len(), colours.join("\r\n") ) } /// Paint.net .txt format pub fn from_txt(path: PathBuf) -> Self { let name = path.file_stem().unwrap().to_str().unwrap().into(); let mut colours = Vec::new(); for line in read_to_string(&path).unwrap().lines() { // header and comments begin with ; if !line.starts_with(";") { // colour starts with FF colours.push(Colour::from(&line[2..])); } } Self { name, colours } } pub fn to_txt(&self) -> String { let colours: Vec = self.colours.iter().map(|colour| colour.to_string().replace('#', "FF") ).collect(); // todo re-insert original comments format!( ";paint.net Palette File\r\n;Colors: {}\r\n{}\r\n", self.colours.len(), colours.join("\r\n") ) } /// simple file format. one hexadecimal colour per line pub fn from_hex(path: PathBuf) -> Self { let name = path.file_stem().unwrap().to_str().unwrap().into(); let colours = read_to_string(&path).unwrap().lines().map(|line| Colour::from(line) ).collect(); Self { name, colours } } /// simple file format. one hexadecimal colour per line pub fn to_hex(&self) -> String { let colours: Vec = self.colours.iter().map(|colour| colour.to_string().replace('#', "") ).collect(); format!("{}\r\n", colours.join("\r\n")) } pub fn from_png(path: PathBuf) -> Self { let name = path.file_stem().unwrap().to_str().unwrap().into(); let image = ImageReader::open(path).unwrap().decode().unwrap(); let mut colours: Vec> = Vec::new(); for (_x, _y, pixel) in image.pixels() { if !colours.contains(&pixel) { colours.push(pixel); } } let colours = colours.iter().map(|colour| Colour { red: colour.0[0], green: colour.0[1], blue: colour.0[2] } ).collect(); // todo preserve original image? Self { name, colours } } /// todo maybe this should be Into pub fn to_png(&self) -> DynamicImage { let mut image = DynamicImage::new_rgb8( self.colours.len() as u32, 1 as u32 ); for (x, colour) in self.colours.iter().enumerate() { let pixel = Rgba::from([colour.red, colour.green, colour.blue, 255]); image.put_pixel(x as u32, 0, pixel); } image } } #[cfg(test)] mod test { use std::path::PathBuf; use crate::Palette; #[test] fn palette_from_jasc() { let path = PathBuf::from("src/test-resources/palettes/soup11.pal"); let output = Palette::from_jasc(path); let expected = crate::mock::palette::soup11(); assert_eq!(output, expected); } #[test] fn palette_to_jasc() { let output = crate::mock::palette::soup11().to_jasc(); let expected = include_str!("test-resources/palettes/soup11.pal"); assert_eq!(output, expected); } #[test] fn palette_from_gpl() { let path = PathBuf::from("src/test-resources/palettes/soup11.gpl"); let output = Palette::from_gpl(path); let expected = crate::mock::palette::soup11(); assert_eq!(output, expected); } #[test] fn palette_to_gpl() { let output = crate::mock::palette::soup11().to_gpl(); let expected = include_str!("test-resources/palettes/soup11.gpl"); assert_eq!(output, expected); } #[test] fn palette_from_txt() { let path = PathBuf::from("src/test-resources/palettes/soup11.txt"); let output = Palette::from_txt(path); let expected = crate::mock::palette::soup11(); assert_eq!(output, expected); } #[test] fn palette_to_txt() { let output = crate::mock::palette::soup11().to_txt(); let expected = include_str!("test-resources/palettes/soup11.txt"); assert_eq!(output, expected); } #[test] fn palette_from_hex() { let path = PathBuf::from("src/test-resources/palettes/soup11.hex"); let output = Palette::from_hex(path); let expected = crate::mock::palette::soup11(); assert_eq!(output, expected); } #[test] fn palette_to_hex() { let output = crate::mock::palette::soup11().to_hex(); let expected = include_str!("test-resources/palettes/soup11.hex"); assert_eq!(output, expected); } #[test] fn palette_from_png() { let path = PathBuf::from("src/test-resources/palettes/soup11.png"); let output = Palette::from_png(path); let expected = crate::mock::palette::soup11(); assert_eq!(output, expected); } #[test] fn palette_to_png() { use image::io::Reader as ImageReader; let output = crate::mock::palette::soup11().to_png(); let path = PathBuf::from("src/test-resources/palettes/soup11.png"); let expected = ImageReader::open(path).unwrap().decode().unwrap(); assert_eq!(output, expected); } }