gimp_palette/
lib.rs

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    /// Creates a new named Palette from a non-zero-length collection of Colors.
30    ///
31    /// Passing a zero-length collection will result in an Err(NewPaletteError::NoColors)
32    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    /// Creates a new Palette from a file path.
45    /// TODO: Decide how to handle the 'Columns' header value.
46    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 color_count = 0;
57
58        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                // TODO: Handle this value.
94                // - Should we only read `color_count` colors, or
95                // - Err if color_count != (line count - 3 - comments), or
96                // - Explicitly ignore this value
97                //color_count = line[4..].trim().to_string();
98                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}