initials_revamped/
color.rs1use 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 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 pub fn find_ratio(&self, other: &RgbColor) -> f32 {
27 self.calculate_luminance() / other.calculate_luminance()
28 }
29
30 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
40impl 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 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}