color_parser/
lib.rs

1//! A utility library for parsing and converting colors between different formats.
2//!
3//! Supports conversion between:
4//! - Hexadecimal (`#RRGGBB`, `#RRGGBBAA`, `#RGB`, `#RGBA`) and `Rgba`
5//! - `Rgba` to `Hsl`, `Hsv`, and `Cmyk`
6//!
7//! # Example
8//! ```rust
9//! use color_parser::*;
10//!
11//! let rgba = parse_hex_to_rgba("#ff8800").unwrap();
12//! let hsl = parse_rgb_to_hsl(&rgba).unwrap();
13//! let hsv = parse_rgb_to_hsv(&rgba).unwrap();
14//! let cmyk = parse_rgb_to_cmyk(&rgba).unwrap();
15//! ```
16
17/// Represents a color in the RGBA color space.
18#[derive(Debug, PartialEq, Eq)]
19pub struct Rgba {
20    /// Red channel (0–255)
21    pub red: u8,
22    /// Green channel (0–255)
23    pub green: u8,
24    /// Blue channel (0–255)
25    pub blue: u8,
26    /// Alpha channel (0–255), where 255 is fully opaque
27    pub alpha: u8,
28}
29
30/// Represents a color in the HSL color space.
31pub struct Hsl {
32    /// Hue in degrees [0–360)
33    pub hue: f64,
34    /// Saturation as percentage [0–100]
35    pub saturation: f64,
36    /// Lightness as percentage [0–100]
37    pub lightness: f64,
38}
39
40/// Represents a color in the HSV color space.
41pub struct Hsv {
42    /// Hue in degrees [0–360)
43    pub hue: f64,
44    /// Saturation as percentage [0–100]
45    pub saturation: f64,
46    /// Value (brightness) as percentage [0–100]
47    pub value: f64,
48}
49
50/// Represents a color in the CMYK color space.
51pub struct Cmyk {
52    /// Cyan channel as percentage [0–100]
53    pub cyan: f64,
54    /// Magenta channel as percentage [0–100]
55    pub magenta: f64,
56    /// Yellow channel as percentage [0–100]
57    pub yellow: f64,
58    /// Black (Key) channel as percentage [0–100]
59    pub black: f64,
60}
61
62/// Errors that can occur during color parsing or conversion.
63#[derive(Debug)]
64pub enum ColorParserError {
65    /// Invalid hex string length (must be 3, 4, 6, or 8 characters)
66    InvalidLength,
67    /// Invalid character in hex string
68    InvalidCharacter,
69    /// RGB values must be in the 0–255 range
70    InvalidRgbValue,
71}
72
73impl std::fmt::Display for ColorParserError {
74    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
75        match self {
76            ColorParserError::InvalidLength => write!(f, "Hex color must be 6 character long"),
77            ColorParserError::InvalidCharacter => write!(f, "Invalid character in hex color"),
78            ColorParserError::InvalidRgbValue => write!(f, "RGB value must be between 0 and 255"),
79        }
80    }
81}
82
83impl std::error::Error for ColorParserError {}
84
85/// Parses a hexadecimal color string into an `Rgba` struct.
86///
87/// Accepts the following formats:
88/// - `#RRGGBB`
89/// - `#RRGGBBAA`
90/// - `#RGB`
91/// - `#RGBA`
92///
93/// # Errors
94/// Returns `ColorParserError` if the format or characters are invalid.
95///
96/// # Examples
97/// ```rust
98/// use color_parser::{parse_hex_to_rgba};
99///
100/// let color = parse_hex_to_rgba("#ff8800").unwrap();
101/// assert_eq!(color.red, 255);
102/// ```
103pub fn parse_hex_to_rgba(hex: &str) -> Result<Rgba, ColorParserError> {
104    let hex = hex.trim_start_matches('#');
105
106    // Handle different hex color formats and ensure valid length
107    let expanded = match hex.len() {
108        8 => hex.to_string(),      // Full RGBA
109        6 => format!("{}FF", hex), // default alpha = 255
110        4 => {
111            // Expands #RGBA => #RRGGBBAA
112            let mut s = String::with_capacity(8);
113            for ch in hex.chars() {
114                s.push(ch);
115                s.push(ch);
116            }
117            s
118        }
119        3 => {
120            // Expands #RGB => #RRGGBB + FF
121            let mut s = String::with_capacity(8);
122            for ch in hex.chars() {
123                s.push(ch);
124                s.push(ch);
125            }
126            s.push_str("FF"); // Default alpha
127            s
128        }
129        _ => return Err(ColorParserError::InvalidLength),
130    };
131
132    let red =
133        u8::from_str_radix(&expanded[0..2], 16).map_err(|_| ColorParserError::InvalidCharacter)?;
134    let green =
135        u8::from_str_radix(&expanded[2..4], 16).map_err(|_| ColorParserError::InvalidCharacter)?;
136    let blue =
137        u8::from_str_radix(&expanded[4..6], 16).map_err(|_| ColorParserError::InvalidCharacter)?;
138    let alpha =
139        u8::from_str_radix(&expanded[6..8], 16).map_err(|_| ColorParserError::InvalidCharacter)?;
140
141    // Check that RGBA values are within valid range
142    if !(0..=255).contains(&red)
143        || !(0..=255).contains(&green)
144        || !(0..=255).contains(&blue)
145        || !(0..=255).contains(&alpha)
146    {
147        return Err(ColorParserError::InvalidRgbValue);
148    }
149
150    Ok(Rgba {
151        red,
152        green,
153        blue,
154        alpha,
155    })
156}
157
158/// Converts an `Rgba` color to the HSL color space.
159///
160/// # Errors
161/// Returns `InvalidRgbValue` if RGB values are outside the 0–255 range.
162/// This should never occur with valid `u8` values.
163///
164/// # Examples
165/// ```rust
166/// use color_parser::{Rgba, parse_rgb_to_hsl};
167///
168/// let rgba = Rgba { red: 255, green: 0, blue: 0, alpha: 255 };
169/// let hsl = parse_rgb_to_hsl(&rgba).unwrap();
170/// assert_eq!(hsl.hue.round(), 0.0);
171/// ```
172pub fn parse_rgb_to_hsl(color: &Rgba) -> Result<Hsl, ColorParserError> {
173    // Check that RGBA values are within valid range
174    if !(0..=255).contains(&color.red)
175        || !(0..=255).contains(&color.green)
176        || !(0..=255).contains(&color.blue)
177    {
178        return Err(ColorParserError::InvalidRgbValue);
179    }
180
181    // Convert r, g, b [0, 255] range to [0, 1]
182    let r = color.red as f64 / 255.0;
183    let g = color.green as f64 / 255.0;
184    let b = color.blue as f64 / 255.0;
185
186    // Find min and max among the r, g, b values
187    let max = r.max(g).max(b);
188    let min = r.min(g).min(b);
189    let delta = max - min;
190
191    // Calculate lightness
192    let lightness = (max + min) / 2.0;
193
194    // Calculate saturation
195    let saturation = if delta == 0.0 {
196        0.0
197    } else {
198        delta / (1.0 - (2.0 * lightness - 1.0).abs())
199    };
200
201    // Calculate hue
202    let hue = if delta == 0.0 {
203        0.0
204    } else if max == r {
205        (g - b) / delta + (if g < b { 6.0 } else { 0.0 })
206    } else if max == g {
207        (b - r) / delta + 2.0 // 120° on the color wheel
208    } else {
209        (r - g) / delta + 4.0 // 240° on the color wheel
210    };
211
212    Ok(Hsl {
213        hue: (hue * 60.0) % 360.0, // Normalize the hue to [0°, 350°]
214        saturation: saturation * 100.0,
215        lightness: lightness * 100.0,
216    })
217}
218
219/// Converts an `Rgba` color to the HSV color space.
220///
221/// # Errors
222/// Returns `InvalidRgbValue` if RGB values are outside the 0–255 range.
223///
224/// # Examples
225/// ```rust
226/// use color_parser::{Rgba, parse_rgb_to_hsv};
227///
228/// let rgba = Rgba { red: 0, green: 255, blue: 0, alpha: 255 };
229/// let hsv = parse_rgb_to_hsv(&rgba).unwrap();
230/// assert_eq!(hsv.hue.round(), 120.0);
231/// ```
232pub fn parse_rgb_to_hsv(color: &Rgba) -> Result<Hsv, ColorParserError> {
233    // Check that RGBA values are within valid range
234    if !(0..=255).contains(&color.red)
235        || !(0..=255).contains(&color.green)
236        || !(0..=255).contains(&color.blue)
237    {
238        return Err(ColorParserError::InvalidRgbValue);
239    }
240
241    // Convert r, g, b [0, 255] range to [0, 1]
242    let r = color.red as f64 / 255.0;
243    let g = color.green as f64 / 255.0;
244    let b = color.blue as f64 / 255.0;
245
246    // Find min and max among the r, g, b values
247    let max = r.max(g).max(b);
248    let min = r.min(g).min(b);
249    let delta = max - min;
250
251    // Calculate the value
252    let value = max;
253
254    // Calculate the saturation
255    let saturation = if delta == 0.0 { 0.0 } else { delta / value };
256
257    // Calculate the hue
258    let hue = if delta == 0.0 {
259        0.0
260    } else if max == r {
261        (g - b) / delta + (if g < b { 6.0 } else { 0.0 })
262    } else if max == g {
263        (b - r) / delta + 2.0
264    } else {
265        (r - g) / delta + 4.0
266    };
267
268    Ok(Hsv {
269        hue: (hue * 60.0) % 360.0,
270        saturation: saturation * 100.0,
271        value: value * 100.0,
272    })
273}
274
275/// Converts an `Rgba` color to the CMYK color space.
276///
277/// # Errors
278/// Returns `InvalidRgbValue` if RGB values are outside the 0–255 range.
279///
280/// # Examples
281/// ```rust
282/// use color_parser::{Rgba, parse_rgb_to_cmyk};
283///
284/// let rgba = Rgba { red: 0, green: 0, blue: 0, alpha: 255 };
285/// let cmyk = parse_rgb_to_cmyk(&rgba).unwrap();
286/// assert_eq!(cmyk.black, 100.0);
287/// ```
288pub fn parse_rgb_to_cmyk(color: &Rgba) -> Result<Cmyk, ColorParserError> {
289    // Check that RGBA values are within valid range
290    if !(0..=255).contains(&color.red)
291        || !(0..=255).contains(&color.green)
292        || !(0..=255).contains(&color.blue)
293    {
294        return Err(ColorParserError::InvalidRgbValue);
295    }
296
297    // Convert r, g, b [0, 255] range to [0, 1]
298    let r = color.red as f64 / 255.0;
299    let g = color.green as f64 / 255.0;
300    let b = color.blue as f64 / 255.0;
301
302    // Calculate the black key color
303    let k = 1.0 - r.max(g).max(b);
304
305    // If RGB is key (black), set CMY to 0
306    if k == 1.0 {
307        return Ok(Cmyk {
308            cyan: 0.0 * 100.0,
309            magenta: 0.0 * 100.0,
310            yellow: 0.0 * 100.0,
311            black: 100.0,
312        });
313    }
314    // Calculate cyan, magenta and yellow color
315    let c = (1.0 - r - k) / (1.0 - k);
316    let m = (1.0 - g - k) / (1.0 - k);
317    let y = (1.0 - b - k) / (1.0 - k);
318
319    Ok(Cmyk {
320        cyan: c * 100.0,
321        magenta: m * 100.0,
322        yellow: y * 100.0,
323        black: k * 100.0,
324    })
325}