1use std::{error, fmt};
2
3use crate::utils::remap;
4use crate::Color;
5
6#[cfg(feature = "named-colors")]
7use crate::NAMED_COLORS;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum ParseColorError {
12 InvalidHex,
14 InvalidRgb,
16 InvalidHsl,
18 InvalidHwb,
20 InvalidHsv,
22 #[cfg(feature = "lab")]
24 InvalidLab,
25 #[cfg(feature = "lab")]
27 InvalidLch,
28 InvalidOklab,
30 InvalidOklch,
32 InvalidFunction,
34 InvalidUnknown,
36}
37
38impl fmt::Display for ParseColorError {
39 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40 match *self {
41 Self::InvalidHex => f.write_str("invalid hex format"),
42 Self::InvalidRgb => f.write_str("invalid rgb format"),
43 Self::InvalidHsl => f.write_str("invalid hsl format"),
44 Self::InvalidHwb => f.write_str("invalid hwb format"),
45 Self::InvalidHsv => f.write_str("invalid hsv format"),
46 #[cfg(feature = "lab")]
47 Self::InvalidLab => f.write_str("invalid lab format"),
48 #[cfg(feature = "lab")]
49 Self::InvalidLch => f.write_str("invalid lch format"),
50 Self::InvalidOklab => f.write_str("invalid oklab format"),
51 Self::InvalidOklch => f.write_str("invalid oklch format"),
52 Self::InvalidFunction => f.write_str("invalid color function"),
53 Self::InvalidUnknown => f.write_str("invalid unknown format"),
54 }
55 }
56}
57
58impl error::Error for ParseColorError {}
59
60pub fn parse(s: &str) -> Result<Color, ParseColorError> {
90 let s = s.trim();
91
92 if s.eq_ignore_ascii_case("transparent") {
93 return Ok(Color::new(0.0, 0.0, 0.0, 0.0));
94 }
95
96 if let Some(s) = s.strip_prefix('#') {
98 return parse_hex(s);
99 }
100
101 if let (Some(idx), Some(s)) = (s.find('('), s.strip_suffix(')')) {
102 let fname = &s[..idx].trim_end();
103 let mut params = s[idx + 1..]
104 .split(&[',', '/'])
105 .flat_map(str::split_ascii_whitespace);
106
107 let (Some(val0), Some(val1), Some(val2)) = (params.next(), params.next(), params.next())
108 else {
109 return Err(ParseColorError::InvalidFunction);
110 };
111
112 let alpha = if let Some(a) = params.next() {
113 if let Some((v, _)) = parse_percent_or_float(a) {
114 v.clamp(0.0, 1.0)
115 } else {
116 return Err(ParseColorError::InvalidFunction);
117 }
118 } else {
119 1.0
120 };
121
122 if params.next().is_some() {
123 return Err(ParseColorError::InvalidFunction);
124 }
125
126 if fname.eq_ignore_ascii_case("rgb") || fname.eq_ignore_ascii_case("rgba") {
127 if let (Some((r, r_fmt)), Some((g, g_fmt)), Some((b, b_fmt))) = (
128 parse_percent_or_255(val0),
130 parse_percent_or_255(val1),
132 parse_percent_or_255(val2),
134 ) {
135 if r_fmt == g_fmt && g_fmt == b_fmt {
136 return Ok(Color {
137 r: r.clamp(0.0, 1.0),
138 g: g.clamp(0.0, 1.0),
139 b: b.clamp(0.0, 1.0),
140 a: alpha,
141 });
142 }
143 }
144
145 return Err(ParseColorError::InvalidRgb);
146 } else if fname.eq_ignore_ascii_case("hsl") || fname.eq_ignore_ascii_case("hsla") {
147 if let (Some(h), Some((s, s_fmt)), Some((l, l_fmt))) = (
148 parse_angle(val0),
150 parse_percent_or_float(val1),
152 parse_percent_or_float(val2),
154 ) {
155 if s_fmt == l_fmt {
156 return Ok(Color::from_hsla(h, s, l, alpha));
157 }
158 }
159
160 return Err(ParseColorError::InvalidHsl);
161 } else if fname.eq_ignore_ascii_case("hwb") || fname.eq_ignore_ascii_case("hwba") {
162 if let (Some(h), Some((w, w_fmt)), Some((b, b_fmt))) = (
163 parse_angle(val0),
165 parse_percent_or_float(val1),
167 parse_percent_or_float(val2),
169 ) {
170 if w_fmt == b_fmt {
171 return Ok(Color::from_hwba(h, w, b, alpha));
172 }
173 }
174
175 return Err(ParseColorError::InvalidHwb);
176 } else if fname.eq_ignore_ascii_case("hsv") || fname.eq_ignore_ascii_case("hsva") {
177 if let (Some(h), Some((s, s_fmt)), Some((v, v_fmt))) = (
178 parse_angle(val0),
180 parse_percent_or_float(val1),
182 parse_percent_or_float(val2),
184 ) {
185 if s_fmt == v_fmt {
186 return Ok(Color::from_hsva(h, s, v, alpha));
187 }
188 }
189
190 return Err(ParseColorError::InvalidHsv);
191 } else if fname.eq_ignore_ascii_case("lab") {
192 #[cfg(feature = "lab")]
193 if let (Some((l, l_fmt)), Some((a, a_fmt)), Some((b, b_fmt))) = (
194 parse_percent_or_float(val0),
196 parse_percent_or_float(val1),
198 parse_percent_or_float(val2),
200 ) {
201 let l = if l_fmt { l * 100.0 } else { l };
202 let a = if a_fmt {
203 remap(a, -1.0, 1.0, -125.0, 125.0)
204 } else {
205 a
206 };
207 let b = if b_fmt {
208 remap(b, -1.0, 1.0, -125.0, 125.0)
209 } else {
210 b
211 };
212 return Ok(Color::from_laba(l.max(0.0), a, b, alpha));
213 } else {
214 return Err(ParseColorError::InvalidLab);
215 }
216 } else if fname.eq_ignore_ascii_case("lch") {
217 #[cfg(feature = "lab")]
218 if let (Some((l, l_fmt)), Some((c, c_fmt)), Some(h)) = (
219 parse_percent_or_float(val0),
221 parse_percent_or_float(val1),
223 parse_angle(val2),
225 ) {
226 let l = if l_fmt { l * 100.0 } else { l };
227 let c = if c_fmt { c * 150.0 } else { c };
228 return Ok(Color::from_lcha(
229 l.max(0.0),
230 c.max(0.0),
231 h.to_radians(),
232 alpha,
233 ));
234 } else {
235 return Err(ParseColorError::InvalidLch);
236 }
237 } else if fname.eq_ignore_ascii_case("oklab") {
238 if let (Some((l, _)), Some((a, a_fmt)), Some((b, b_fmt))) = (
239 parse_percent_or_float(val0),
241 parse_percent_or_float(val1),
243 parse_percent_or_float(val2),
245 ) {
246 let a = if a_fmt {
247 remap(a, -1.0, 1.0, -0.4, 0.4)
248 } else {
249 a
250 };
251 let b = if b_fmt {
252 remap(b, -1.0, 1.0, -0.4, 0.4)
253 } else {
254 b
255 };
256 return Ok(Color::from_oklaba(l.max(0.0), a, b, alpha));
257 }
258
259 return Err(ParseColorError::InvalidOklab);
260 } else if fname.eq_ignore_ascii_case("oklch") {
261 if let (Some((l, _)), Some((c, c_fmt)), Some(h)) = (
262 parse_percent_or_float(val0),
264 parse_percent_or_float(val1),
266 parse_angle(val2),
268 ) {
269 let c = if c_fmt { c * 0.4 } else { c };
270 return Ok(Color::from_oklcha(
271 l.max(0.0),
272 c.max(0.0),
273 h.to_radians(),
274 alpha,
275 ));
276 }
277
278 return Err(ParseColorError::InvalidOklch);
279 }
280
281 return Err(ParseColorError::InvalidFunction);
282 }
283
284 if let Ok(c) = parse_hex(s) {
286 return Ok(c);
287 }
288
289 #[cfg(feature = "named-colors")]
291 if s.len() > 2 && s.len() < 21 {
292 let s = s.to_ascii_lowercase();
293 if let Some([r, g, b]) = NAMED_COLORS.get(&s) {
294 return Ok(Color::from_rgba8(*r, *g, *b, 255));
295 }
296 }
297
298 Err(ParseColorError::InvalidUnknown)
299}
300
301fn parse_hex(s: &str) -> Result<Color, ParseColorError> {
302 if !s.is_ascii() {
303 return Err(ParseColorError::InvalidHex);
304 }
305
306 let n = s.len();
307
308 fn parse_single_digit(digit: &str) -> Result<u8, ParseColorError> {
309 u8::from_str_radix(digit, 16)
310 .map(|n| (n << 4) | n)
311 .map_err(|_| ParseColorError::InvalidHex)
312 }
313
314 if n == 3 || n == 4 {
315 let r = parse_single_digit(&s[0..1])?;
316 let g = parse_single_digit(&s[1..2])?;
317 let b = parse_single_digit(&s[2..3])?;
318
319 let a = if n == 4 {
320 parse_single_digit(&s[3..4])?
321 } else {
322 255
323 };
324
325 Ok(Color::from_rgba8(r, g, b, a))
326 } else if n == 6 || n == 8 {
327 let r = u8::from_str_radix(&s[0..2], 16).map_err(|_| ParseColorError::InvalidHex)?;
328 let g = u8::from_str_radix(&s[2..4], 16).map_err(|_| ParseColorError::InvalidHex)?;
329 let b = u8::from_str_radix(&s[4..6], 16).map_err(|_| ParseColorError::InvalidHex)?;
330
331 let a = if n == 8 {
332 u8::from_str_radix(&s[6..8], 16).map_err(|_| ParseColorError::InvalidHex)?
333 } else {
334 255
335 };
336
337 Ok(Color::from_rgba8(r, g, b, a))
338 } else {
339 Err(ParseColorError::InvalidHex)
340 }
341}
342
343fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> {
345 if suffix.len() > s.len() {
346 return None;
347 }
348 let s_end = &s[s.len() - suffix.len()..];
349 if s_end.eq_ignore_ascii_case(suffix) {
350 Some(&s[..s.len() - suffix.len()])
351 } else {
352 None
353 }
354}
355
356fn parse_percent_or_float(s: &str) -> Option<(f32, bool)> {
357 s.strip_suffix('%')
358 .and_then(|s| s.parse().ok().map(|t: f32| (t / 100.0, true)))
359 .or_else(|| s.parse().ok().map(|t| (t, false)))
360}
361
362fn parse_percent_or_255(s: &str) -> Option<(f32, bool)> {
363 s.strip_suffix('%')
364 .and_then(|s| s.parse().ok().map(|t: f32| (t / 100.0, true)))
365 .or_else(|| s.parse().ok().map(|t: f32| (t / 255.0, false)))
366}
367
368fn parse_angle(s: &str) -> Option<f32> {
369 strip_suffix(s, "deg")
370 .and_then(|s| s.parse().ok())
371 .or_else(|| {
372 strip_suffix(s, "grad")
373 .and_then(|s| s.parse().ok())
374 .map(|t: f32| t * 360.0 / 400.0)
375 })
376 .or_else(|| {
377 strip_suffix(s, "rad")
378 .and_then(|s| s.parse().ok())
379 .map(|t: f32| t.to_degrees())
380 })
381 .or_else(|| {
382 strip_suffix(s, "turn")
383 .and_then(|s| s.parse().ok())
384 .map(|t: f32| t * 360.0)
385 })
386 .or_else(|| s.parse().ok())
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn test_strip_suffix() {
395 assert_eq!(strip_suffix("45deg", "deg"), Some("45"));
396 assert_eq!(strip_suffix("90DEG", "deg"), Some("90"));
397 assert_eq!(strip_suffix("0.25turn", "turn"), Some("0.25"));
398 assert_eq!(strip_suffix("1.0Turn", "turn"), Some("1.0"));
399
400 assert_eq!(strip_suffix("", "deg"), None);
401 assert_eq!(strip_suffix("90", "deg"), None);
402 }
403
404 #[test]
405 fn test_parse_percent_or_float() {
406 let test_data = [
407 ("0%", Some((0.0, true))),
408 ("100%", Some((1.0, true))),
409 ("50%", Some((0.5, true))),
410 ("0", Some((0.0, false))),
411 ("1", Some((1.0, false))),
412 ("0.5", Some((0.5, false))),
413 ("100.0", Some((100.0, false))),
414 ("-23.7", Some((-23.7, false))),
415 ("%", None),
416 ("1x", None),
417 ];
418 for (s, expected) in test_data {
419 assert_eq!(parse_percent_or_float(s), expected);
420 }
421 }
422
423 #[test]
424 fn test_parse_percent_or_255() {
425 let test_data = [
426 ("0%", Some((0.0, true))),
427 ("100%", Some((1.0, true))),
428 ("50%", Some((0.5, true))),
429 ("-100%", Some((-1.0, true))),
430 ("0", Some((0.0, false))),
431 ("255", Some((1.0, false))),
432 ("127.5", Some((0.5, false))),
433 ("%", None),
434 ("255x", None),
435 ];
436 for (s, expected) in test_data {
437 assert_eq!(parse_percent_or_255(s), expected);
438 }
439 }
440
441 #[test]
442 fn test_parse_angle() {
443 let test_data = [
444 ("360", Some(360.0)),
445 ("127.356", Some(127.356)),
446 ("+120deg", Some(120.0)),
447 ("90deg", Some(90.0)),
448 ("-127deg", Some(-127.0)),
449 ("100grad", Some(90.0)),
450 ("1.5707963267948966rad", Some(90.0)),
451 ("0.25turn", Some(90.0)),
452 ("-0.25turn", Some(-90.0)),
453 ("O", None),
454 ("Odeg", None),
455 ("rad", None),
456 ];
457 for (s, expected) in test_data {
458 assert_eq!(parse_angle(s), expected);
459 }
460 }
461
462 #[test]
463 fn test_parse_hex() {
464 macro_rules! cmp {
466 ($a:expr, $b:expr) => {
467 assert_eq!(
468 parse_hex($a).unwrap().to_rgba8(),
469 parse_hex($b).unwrap().to_rgba8()
470 );
471 };
472 }
473 cmp!("abc", "ABC");
474 cmp!("DeF", "dEf");
475 cmp!("f0eB", "F0Eb");
476 cmp!("abcdef", "ABCDEF");
477 cmp!("Ff03E0cB", "fF03e0Cb");
478 }
479}