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