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}