Skip to main content

icon_to_image/
color.rs

1//! Color parsing and representation.
2//!
3//! Supports hex colors (#RGB, #RGBA, #RRGGBB, #RRGGBBAA) and RGB tuples.
4
5use crate::error::{IconFontError, Result};
6
7/// RGBA color with values 0-255.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct Color {
10    pub r: u8,
11    pub g: u8,
12    pub b: u8,
13    pub a: u8,
14}
15
16impl Color {
17    #[inline]
18    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
19        Self { r, g, b, a: 255 }
20    }
21
22    #[inline]
23    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
24        Self { r, g, b, a }
25    }
26
27    #[inline]
28    pub const fn transparent() -> Self {
29        Self {
30            r: 0,
31            g: 0,
32            b: 0,
33            a: 0,
34        }
35    }
36
37    #[inline]
38    pub const fn black() -> Self {
39        Self::rgb(0, 0, 0)
40    }
41
42    #[inline]
43    pub const fn white() -> Self {
44        Self::rgb(255, 255, 255)
45    }
46
47    /// Parse a color from a hex string (with or without '#').
48    ///
49    /// # Examples
50    ///
51    /// ```
52    /// use icon_to_image::Color;
53    /// let red = Color::from_hex("#FF0000").unwrap();
54    /// let blue = Color::from_hex("0000FF").unwrap();
55    /// let semi_transparent = Color::from_hex("#FF000080").unwrap();
56    /// ```
57    pub fn from_hex(hex: &str) -> Result<Self> {
58        let hex = hex.strip_prefix('#').unwrap_or(hex);
59
60        match hex.len() {
61            3 => {
62                let r = parse_hex_digit(hex.chars().next().unwrap())?;
63                let g = parse_hex_digit(hex.chars().nth(1).unwrap())?;
64                let b = parse_hex_digit(hex.chars().nth(2).unwrap())?;
65                Ok(Self::rgb(r * 17, g * 17, b * 17))
66            }
67            4 => {
68                let r = parse_hex_digit(hex.chars().next().unwrap())?;
69                let g = parse_hex_digit(hex.chars().nth(1).unwrap())?;
70                let b = parse_hex_digit(hex.chars().nth(2).unwrap())?;
71                let a = parse_hex_digit(hex.chars().nth(3).unwrap())?;
72                Ok(Self::rgba(r * 17, g * 17, b * 17, a * 17))
73            }
74            6 => {
75                let r = u8::from_str_radix(&hex[0..2], 16)
76                    .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
77                let g = u8::from_str_radix(&hex[2..4], 16)
78                    .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
79                let b = u8::from_str_radix(&hex[4..6], 16)
80                    .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
81                Ok(Self::rgb(r, g, b))
82            }
83            8 => {
84                let r = u8::from_str_radix(&hex[0..2], 16)
85                    .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
86                let g = u8::from_str_radix(&hex[2..4], 16)
87                    .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
88                let b = u8::from_str_radix(&hex[4..6], 16)
89                    .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
90                let a = u8::from_str_radix(&hex[6..8], 16)
91                    .map_err(|_| IconFontError::InvalidColor(hex.to_string()))?;
92                Ok(Self::rgba(r, g, b, a))
93            }
94            _ => Err(IconFontError::InvalidColor(format!(
95                "Invalid hex length: {}. Expected 3, 4, 6, or 8 characters.",
96                hex.len()
97            ))),
98        }
99    }
100
101    #[inline]
102    pub const fn to_rgba(&self) -> [u8; 4] {
103        [self.r, self.g, self.b, self.a]
104    }
105
106    #[inline]
107    pub const fn is_transparent(&self) -> bool {
108        self.a == 0
109    }
110}
111
112impl Default for Color {
113    fn default() -> Self {
114        Self::black()
115    }
116}
117
118fn parse_hex_digit(c: char) -> Result<u8> {
119    match c {
120        '0'..='9' => Ok(c as u8 - b'0'),
121        'a'..='f' => Ok(c as u8 - b'a' + 10),
122        'A'..='F' => Ok(c as u8 - b'A' + 10),
123        _ => Err(IconFontError::InvalidColor(format!(
124            "Invalid hex digit: {}",
125            c
126        ))),
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_hex_parsing() {
136        assert_eq!(Color::from_hex("#FF0000").unwrap(), Color::rgb(255, 0, 0));
137        assert_eq!(Color::from_hex("00FF00").unwrap(), Color::rgb(0, 255, 0));
138        assert_eq!(Color::from_hex("#F00").unwrap(), Color::rgb(255, 0, 0));
139        assert_eq!(
140            Color::from_hex("#FF000080").unwrap(),
141            Color::rgba(255, 0, 0, 128)
142        );
143    }
144
145    #[test]
146    fn test_rgb_constructor() {
147        let c = Color::rgb(100, 150, 200);
148        assert_eq!(c.r, 100);
149        assert_eq!(c.g, 150);
150        assert_eq!(c.b, 200);
151        assert_eq!(c.a, 255);
152    }
153}