initials_revamped/
color.rs

1//! Color module that helps generating and operating on rgb colors
2use error::Error;
3use image::Rgba;
4use std::iter::FromIterator;
5use std::str::FromStr;
6use std::u8;
7
8#[derive(Debug, PartialEq, Copy, Clone)]
9pub struct RgbColor(u8, u8, u8);
10
11impl RgbColor {
12    pub fn new(red: u8, green: u8, blue: u8) -> RgbColor {
13        RgbColor(red, green, blue)
14    }
15
16    /// Generate a random rgb color
17    pub fn random() -> Self {
18        use rand::prelude::*;
19
20        let mut rng = thread_rng();
21
22        RgbColor(rng.gen(), rng.gen(), rng.gen())
23    }
24
25    /// Calculate the contrast ratio between colors
26    pub fn find_ratio(&self, other: &RgbColor) -> f32 {
27        self.calculate_luminance() / other.calculate_luminance()
28    }
29
30    /// Convert to rgba (including transparency) for image creation
31    pub fn to_rgba(self, alpha: u8) -> Rgba<u8> {
32        Rgba([self.0, self.1, self.2, alpha])
33    }
34
35    fn calculate_luminance(&self) -> f32 {
36        0.299 * f32::from(self.0) + 0.587 * f32::from(self.1) + 0.114 * f32::from(self.2) + 0.05
37    }
38}
39
40/// Parse hex code and generate RGB vector accordingly.
41impl FromStr for RgbColor {
42    type Err = Error;
43
44    fn from_str(hex: &str) -> Result<RgbColor, Error> {
45        if hex.is_empty() {
46            return Err(Error::InvalidHexFormat {
47                expected: String::from("Color hex code must not be empty"),
48                actual: String::from("Color hex was empty"),
49            });
50        }
51
52        if !hex.starts_with('#') {
53            return Err(Error::InvalidHexFormat {
54                expected: String::from("Color hex code must start with `#`"),
55                actual: format!("Color hex starts with `{}`", hex.chars().nth(0).unwrap()),
56            });
57        }
58
59        if hex.len() != 7 {
60            return Err(Error::InvalidHexFormat {
61                expected: String::from("Hex code must be `7` characters long. Example: `#00FF00`"),
62                actual: format!("Hex code is `{}` characters long!", hex.len()),
63            });
64        }
65
66        // collect characters from the String
67        let raw: Vec<char> = hex.chars().collect();
68
69        Ok(RgbColor(
70            u8::from_str_radix(&String::from_iter(&raw[1..3]), 16)?,
71            u8::from_str_radix(&String::from_iter(&raw[3..5]), 16)?,
72            u8::from_str_radix(&String::from_iter(&raw[5..7]), 16)?,
73        ))
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_invalid_hex_empty_str() {
83        let res: Result<RgbColor, Error> = "".parse();
84        assert!(res.is_err());
85        assert_eq!(
86            format!("{}", res.unwrap_err()), 
87            "unexpected hex color format: expected(Color hex code must not be empty), got(Color hex was empty)"
88        );
89    }
90
91    #[test]
92    fn test_invalid_hex_with_missing_prefix() {
93        let res: Result<RgbColor, Error> = "00ff00".parse();
94        assert!(res.is_err());
95        assert_eq!(
96            format!("{}", res.unwrap_err()), 
97            "unexpected hex color format: expected(Color hex code must start with `#`), got(Color hex starts with `0`)"
98        );
99    }
100
101    #[test]
102    fn test_invalid_hex_with_wrong_size() {
103        let res: Result<RgbColor, Error> = "#ff00".parse();
104        assert!(res.is_err());
105        assert_eq!(
106            format!("{}", res.unwrap_err()), 
107            "unexpected hex color format: expected(Hex code must be `7` characters long. Example: `#00FF00`), got(Hex code is `5` characters long!)"
108        );
109    }
110
111    #[test]
112    fn test_invalid_hex_with_parse_error() {
113        let res: Result<RgbColor, Error> = "#0qfd00".parse();
114        assert!(res.is_err());
115        assert_eq!(
116            format!("{}", res.unwrap_err()),
117            "couldn't parse hex value: invalid digit found in string"
118        );
119    }
120
121    #[test]
122    fn test_valid_hex() {
123        let res: Result<RgbColor, Error> = "#00ff00".parse();
124        assert!(res.is_ok());
125        assert_eq!(res.unwrap(), RgbColor(0, 255, 0));
126    }
127
128    #[test]
129    fn test_contrast_ratio() {
130        let rgb_white = RgbColor(255, 255, 255);
131        let rgb_yellow = RgbColor(255, 255, 0);
132        assert_eq!(rgb_white.find_ratio(&rgb_yellow).floor(), 1.);
133        let rgb_blue = RgbColor(0, 0, 255);
134        assert_eq!(rgb_white.find_ratio(&rgb_blue).floor(), 8.);
135    }
136}