Files
peachy/src/palette.rs
2021-11-14 18:05:09 +00:00

282 lines
8.5 KiB
Rust

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<Colour>,
}
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<u8> {
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<Palette>
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<u8> = 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<String> = 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<String> = 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<String> = 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<String> = 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<Rgba<u8>> = 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<DynamicImage>
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);
}
}