1use std::path::Path;
2use std::io::{BufRead, BufReader, Write};
3use std::fs::File;
4
5pub struct Color {
6 pub r: u8,
7 pub g: u8,
8 pub b: u8,
9}
10
11pub struct Palette {
12 _name: String,
13 _colors: Vec<Color>,
14}
15
16pub enum NewPaletteError {
17 NoColors,
18 InvalidData { line_num: usize, val: String },
19 IoErr(std::io::Error),
20}
21
22impl From<std::io::Error> for NewPaletteError {
23 fn from(e: std::io::Error) -> Self {
24 NewPaletteError::IoErr(e)
25 }
26}
27
28impl Palette {
29 pub fn new<S: ToString>(name: S, colors: Vec<Color>) -> Result<Self, NewPaletteError> {
33 match colors.len() {
34 0 => Err(NewPaletteError::NoColors),
35 _ => {
36 Ok(Palette {
37 _name: name.to_string(),
38 _colors: colors,
39 })
40 }
41 }
42 }
43
44 pub fn read_from_file<P: AsRef<Path>>(file_path: P) -> Result<Self, NewPaletteError> {
47 let f = File::open(&file_path)?;
48 let reader = BufReader::new(f);
49
50 fn is_comment(s: &str) -> bool {
51 s.chars().skip_while(|c| c.is_whitespace()).next() == Some('#')
52 }
53
54 let mut colors = vec![];
55 let mut name = String::new();
56 let mut lines = reader.lines().enumerate();
59 while let Some((index, Ok(line))) = lines.next() {
60 if is_comment(&line) || line.trim().len() == 0 {
61 continue;
62 }
63
64 let line_num = index + 1;
65
66 if line_num == 1 {
67 if line != "GIMP Palette" {
68 return Err(NewPaletteError::InvalidData {
69 line_num,
70 val: line,
71 });
72 }
73
74 continue;
75 } else if line_num == 2 {
76 if !line.starts_with("Name:") {
77 return Err(NewPaletteError::InvalidData {
78 line_num,
79 val: line,
80 });
81 }
82
83 name = line[4..].trim().to_string();
84 continue;
85 } else if line_num == 3 {
86 if !line.starts_with("Columns:") {
87 return Err(NewPaletteError::InvalidData {
88 line_num,
89 val: line,
90 });
91 }
92
93 continue;
99 }
100
101 let mut split = line.split_whitespace();
102 match (split.next(), split.next(), split.next()) {
103 (Some(r_str), Some(g_str), Some(b_str)) => {
104 let r =
105 r_str
106 .parse::<u8>()
107 .expect(&format!("Failed to parse line {}'s r into a byte", line_num));
108 let g =
109 g_str
110 .parse::<u8>()
111 .expect(&format!("Failed to parse line {}'s g into a byte", line_num));
112 let b =
113 b_str
114 .parse::<u8>()
115 .expect(&format!("Failed to parse line {}'s b into a byte", line_num));
116 colors.push(Color { r, g, b });
117 }
118 _ => {
119 return Err(NewPaletteError::InvalidData {
120 line_num,
121 val: line.clone(),
122 })
123 }
124 }
125 }
126
127 Ok(Self {
128 _name: name,
129 _colors: colors,
130 })
131 }
132
133 pub fn get_name<'a>(&'a self) -> &'a str {
134 &self._name
135 }
136
137 pub fn get_colors<'a>(&'a self) -> &'a [Color] {
138 &self._colors
139 }
140
141 pub fn write_to_file<P: AsRef<Path>>(&self, file_path: P) -> Result<(), std::io::Error> {
142 let header = format!("GIMP Palette\nName: {}\nColumns: {}\n",
143 self._name,
144 self._colors.len());
145 let colors_string = create_string_from_colors(&self._colors);
146 let mut f = std::fs::OpenOptions::new()
147 .write(true)
148 .create(true)
149 .open(file_path)?;
150
151 let final_string = header + &colors_string;
152 f.write_all(final_string.as_bytes())
153 }
154}
155
156impl ToString for Color {
157 fn to_string(&self) -> String {
158 format!("{:3} {:3} {:3}", self.r, self.g, self.b)
159 }
160}
161
162fn create_string_from_colors(colors: &[Color]) -> String {
163 colors
164 .iter()
165 .map(|c| c.to_string() + "\n")
166 .collect::<String>()
167}
168
169#[cfg(test)]
170mod tests {
171 #[test]
172 fn colorless_palette_err() {
173 assert!(super::Palette::new("Failure", vec![]).is_err());
174 }
175
176 #[test]
177 fn colors_1() {
178 let colors = vec![super::Color { r: 255, g: 0, b: 0 }];
179
180 assert_eq!(super::create_string_from_colors(&colors), "255 0 0\n");
181 }
182
183 #[test]
184 fn colors_3() {
185 let colors = vec![super::Color { r: 255, g: 0, b: 0 },
186 super::Color { r: 0, g: 255, b: 0 },
187 super::Color { r: 0, g: 0, b: 255 }];
188
189 assert_eq!(super::create_string_from_colors(&colors),
190 "255 0 0\n 0 255 0\n 0 0 255\n");
191 }
192}