1use crate::{ByteExt, Error, Stream, colors};
5
6#[cfg(not(feature = "std"))]
7use kurbo::common::FloatFuncs;
8
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
13#[allow(missing_docs)]
14pub struct Color {
15 pub red: u8,
16 pub green: u8,
17 pub blue: u8,
18 pub alpha: u8,
19}
20
21impl Color {
22 #[inline]
24 pub fn new_rgb(red: u8, green: u8, blue: u8) -> Self {
25 Self {
26 red,
27 green,
28 blue,
29 alpha: 255,
30 }
31 }
32
33 #[inline]
35 pub fn new_rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
36 Self {
37 red,
38 green,
39 blue,
40 alpha,
41 }
42 }
43
44 #[inline]
46 pub fn black() -> Self {
47 Self::new_rgb(0, 0, 0)
48 }
49
50 #[inline]
52 pub fn white() -> Self {
53 Self::new_rgb(255, 255, 255)
54 }
55
56 #[inline]
58 pub fn gray() -> Self {
59 Self::new_rgb(128, 128, 128)
60 }
61
62 #[inline]
64 pub fn red() -> Self {
65 Self::new_rgb(255, 0, 0)
66 }
67
68 #[inline]
70 pub fn green() -> Self {
71 Self::new_rgb(0, 128, 0)
72 }
73
74 #[inline]
76 pub fn blue() -> Self {
77 Self::new_rgb(0, 0, 255)
78 }
79}
80
81impl core::str::FromStr for Color {
82 type Err = Error;
83
84 fn from_str(text: &str) -> Result<Self, Error> {
103 let mut s = Stream::from(text);
104 let color = s.parse_color()?;
105
106 s.skip_spaces();
109 if !s.at_end() {
110 return Err(Error::UnexpectedData(s.calc_char_pos()));
111 }
112
113 Ok(color)
114 }
115}
116
117impl Stream<'_> {
118 pub fn try_parse_color(&mut self) -> Option<Color> {
120 let mut s = *self;
121 if let Ok(color) = s.parse_color() {
122 *self = s;
123 Some(color)
124 } else {
125 None
126 }
127 }
128
129 pub fn parse_color(&mut self) -> Result<Color, Error> {
131 self.skip_spaces();
132
133 let mut color = Color::black();
134
135 if self.curr_byte()? == b'#' {
136 self.advance(1);
138 let color_str = self.consume_bytes(|_, c| c.is_hex_digit()).as_bytes();
139 match color_str.len() {
141 6 => {
142 color.red = hex_pair(color_str[0], color_str[1]);
144 color.green = hex_pair(color_str[2], color_str[3]);
145 color.blue = hex_pair(color_str[4], color_str[5]);
146 }
147 8 => {
148 color.red = hex_pair(color_str[0], color_str[1]);
150 color.green = hex_pair(color_str[2], color_str[3]);
151 color.blue = hex_pair(color_str[4], color_str[5]);
152 color.alpha = hex_pair(color_str[6], color_str[7]);
153 }
154 3 => {
155 color.red = short_hex(color_str[0]);
157 color.green = short_hex(color_str[1]);
158 color.blue = short_hex(color_str[2]);
159 }
160 4 => {
161 color.red = short_hex(color_str[0]);
163 color.green = short_hex(color_str[1]);
164 color.blue = short_hex(color_str[2]);
165 color.alpha = short_hex(color_str[3]);
166 }
167 _ => {
168 return Err(Error::InvalidValue);
169 }
170 }
171 } else {
172 let name = self.consume_ascii_ident().to_ascii_lowercase();
174 if name == "rgb" || name == "rgba" {
175 self.consume_byte(b'(')?;
176
177 let mut is_percent = false;
178 let value = self.parse_number()?;
179 if self.starts_with(b"%") {
180 self.advance(1);
181 is_percent = true;
182 }
183 self.skip_spaces();
184 self.parse_list_separator();
185
186 if is_percent {
187 color.red = ((value / 100.0) * 255.0).round() as u8;
190 color.green = (self.parse_list_number_or_percent()? * 255.0).round() as u8;
191 color.blue = (self.parse_list_number_or_percent()? * 255.0).round() as u8;
192 } else {
193 color.red = value.round() as u8;
194 color.green = self.parse_list_number()?.round() as u8;
195 color.blue = self.parse_list_number()?.round() as u8;
196 }
197
198 self.skip_spaces();
199 if !self.starts_with(b")") {
200 color.alpha = (self.parse_list_number()? * 255.0).round() as u8;
201 }
202
203 self.skip_spaces();
204 self.consume_byte(b')')?;
205 } else if name == "hsl" || name == "hsla" {
206 self.consume_byte(b'(')?;
207
208 let mut hue = self.parse_list_number()?;
209 hue = ((hue % 360.0) + 360.0) % 360.0;
210
211 let saturation = f64_bound(0.0, self.parse_list_number_or_percent()?, 1.0);
212 let lightness = f64_bound(0.0, self.parse_list_number_or_percent()?, 1.0);
213
214 color = hsl_to_rgb(hue as f32 / 60.0, saturation as f32, lightness as f32);
215
216 self.skip_spaces();
217 if !self.starts_with(b")") {
218 color.alpha = (self.parse_list_number()? * 255.0).round() as u8;
219 }
220
221 self.skip_spaces();
222 self.consume_byte(b')')?;
223 } else {
224 match colors::from_str(&name) {
225 Some(c) => {
226 color = c;
227 }
228 None => {
229 return Err(Error::InvalidValue);
230 }
231 }
232 }
233 }
234
235 Ok(color)
236 }
237}
238
239#[inline]
240fn from_hex(c: u8) -> u8 {
241 match c {
242 b'0'..=b'9' => c - b'0',
243 b'a'..=b'f' => c - b'a' + 10,
244 b'A'..=b'F' => c - b'A' + 10,
245 _ => b'0',
246 }
247}
248
249#[inline]
250fn short_hex(c: u8) -> u8 {
251 let h = from_hex(c);
252 (h << 4) | h
253}
254
255#[inline]
256fn hex_pair(c1: u8, c2: u8) -> u8 {
257 let h1 = from_hex(c1);
258 let h2 = from_hex(c2);
259 (h1 << 4) | h2
260}
261
262fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> Color {
265 let t2 = if lightness <= 0.5 {
266 lightness * (saturation + 1.0)
267 } else {
268 lightness + saturation - (lightness * saturation)
269 };
270
271 let t1 = lightness * 2.0 - t2;
272 let red = hue_to_rgb(t1, t2, hue + 2.0);
273 let green = hue_to_rgb(t1, t2, hue);
274 let blue = hue_to_rgb(t1, t2, hue - 2.0);
275 Color::new_rgb(
276 (red * 255.0).round() as u8,
277 (green * 255.0).round() as u8,
278 (blue * 255.0).round() as u8,
279 )
280}
281
282fn hue_to_rgb(t1: f32, t2: f32, mut hue: f32) -> f32 {
283 if hue < 0.0 {
284 hue += 6.0;
285 }
286 if hue >= 6.0 {
287 hue -= 6.0;
288 }
289
290 if hue < 1.0 {
291 (t2 - t1) * hue + t1
292 } else if hue < 3.0 {
293 t2
294 } else if hue < 4.0 {
295 (t2 - t1) * (4.0 - hue) + t1
296 } else {
297 t1
298 }
299}
300
301#[inline]
302fn f64_bound(min: f64, val: f64, max: f64) -> f64 {
303 debug_assert!(val.is_finite());
304 val.clamp(min, max)
305}
306
307#[rustfmt::skip]
308#[cfg(test)]
309mod tests {
310 use alloc::string::ToString;
311 use core::str::FromStr;
312 use crate::Color;
313
314 macro_rules! test {
315 ($name:ident, $text:expr, $color:expr) => {
316 #[test]
317 fn $name() {
318 assert_eq!(Color::from_str($text).unwrap(), $color);
319 }
320 };
321 }
322
323 test!(
324 rrggbb,
325 "#ff0000",
326 Color::new_rgb(255, 0, 0)
327 );
328
329 test!(
330 rrggbb_upper,
331 "#FF0000",
332 Color::new_rgb(255, 0, 0)
333 );
334
335 test!(
336 rgb_hex,
337 "#f00",
338 Color::new_rgb(255, 0, 0)
339 );
340
341 test!(
342 rrggbbaa,
343 "#ff0000ff",
344 Color::new_rgba(255, 0, 0, 255)
345 );
346
347 test!(
348 rrggbbaa_upper,
349 "#FF0000FF",
350 Color::new_rgba(255, 0, 0, 255)
351 );
352
353 test!(
354 rgba_hex,
355 "#f00f",
356 Color::new_rgba(255, 0, 0, 255)
357 );
358
359 test!(
360 rrggbb_spaced,
361 " #ff0000 ",
362 Color::new_rgb(255, 0, 0)
363 );
364
365 test!(
366 rgb_numeric,
367 "rgb(254, 203, 231)",
368 Color::new_rgb(254, 203, 231)
369 );
370
371 test!(
372 rgb_numeric_spaced,
373 " rgb( 77 , 77 , 77 ) ",
374 Color::new_rgb(77, 77, 77)
375 );
376
377 test!(
378 rgb_percentage,
379 "rgb(50%, 50%, 50%)",
380 Color::new_rgb(128, 128, 128)
381 );
382
383 test!(
384 rgb_percentage_overflow,
385 "rgb(140%, -10%, 130%)",
386 Color::new_rgb(255, 0, 255)
387 );
388
389 test!(
390 rgb_percentage_float,
391 "rgb(33.333%,46.666%,93.333%)",
392 Color::new_rgb(85, 119, 238)
393 );
394
395 test!(
396 rgb_numeric_upper_case,
397 "RGB(254, 203, 231)",
398 Color::new_rgb(254, 203, 231)
399 );
400
401 test!(
402 rgb_numeric_mixed_case,
403 "RgB(254, 203, 231)",
404 Color::new_rgb(254, 203, 231)
405 );
406
407 test!(
408 rgb_numeric_red_float,
409 "rgb(3.141592653, 110, 201)",
410 Color::new_rgb(3, 110, 201)
411 );
412
413 test!(
414 rgb_numeric_green_float,
415 "rgb(254, 150.829521289232389, 210)",
416 Color::new_rgb(254, 151, 210)
417 );
418
419 test!(
420 rgb_numeric_blue_float,
421 "rgb(96, 255, 0.2)",
422 Color::new_rgb(96, 255, 0)
423 );
424
425 test!(
426 rgb_numeric_all_float,
427 "rgb(0.0, 129.82, 231.092)",
428 Color::new_rgb(0, 130, 231)
429 );
430
431 test!(
432 rgb_numeric_all_float_with_alpha,
433 "rgb(0.0, 129.82, 231.092, 0.5)",
434 Color::new_rgba(0, 130, 231, 128)
435 );
436
437 test!(
438 rgb_numeric_all_float_overflow,
439 "rgb(290.2, 255.9, 300.0)",
440 Color::new_rgb(255, 255, 255)
441 );
442
443 test!(
444 name_red,
445 "red",
446 Color::new_rgb(255, 0, 0)
447 );
448
449 test!(
450 name_red_spaced,
451 " red ",
452 Color::new_rgb(255, 0, 0)
453 );
454
455 test!(
456 name_red_upper_case,
457 "RED",
458 Color::new_rgb(255, 0, 0)
459 );
460
461 test!(
462 name_red_mixed_case,
463 "ReD",
464 Color::new_rgb(255, 0, 0)
465 );
466
467 test!(
468 name_cornflowerblue,
469 "cornflowerblue",
470 Color::new_rgb(100, 149, 237)
471 );
472
473 test!(
474 transparent,
475 "transparent",
476 Color::new_rgba(0, 0, 0, 0)
477 );
478
479 test!(
480 rgba_half,
481 "rgba(10, 20, 30, 0.5)",
482 Color::new_rgba(10, 20, 30, 128)
483 );
484
485 test!(
486 rgba_numeric_red_float,
487 "rgba(3.141592653, 110, 201, 1.0)",
488 Color::new_rgba(3, 110, 201, 255)
489 );
490
491 test!(
492 rgba_numeric_all_float,
493 "rgba(0.0, 129.82, 231.092, 1.5)",
494 Color::new_rgba(0, 130, 231, 255)
495 );
496
497 test!(
498 rgba_negative,
499 "rgba(10, 20, 30, -2)",
500 Color::new_rgba(10, 20, 30, 0)
501 );
502
503 test!(
504 rgba_large_alpha,
505 "rgba(10, 20, 30, 2)",
506 Color::new_rgba(10, 20, 30, 255)
507 );
508
509 test!(
510 rgb_with_alpha,
511 "rgb(10, 20, 30, 0.5)",
512 Color::new_rgba(10, 20, 30, 128)
513 );
514
515 test!(
516 hsl_green,
517 "hsl(120, 100%, 75%)",
518 Color::new_rgba(128, 255, 128, 255)
519 );
520
521 test!(
522 hsl_yellow,
523 "hsl(60, 100%, 50%)",
524 Color::new_rgba(255, 255, 0, 255)
525 );
526
527 test!(
528 hsl_hue_360,
529 "hsl(360, 100%, 100%)",
530 Color::new_rgba(255, 255, 255, 255)
531 );
532
533 test!(
534 hsl_out_of_bounds,
535 "hsl(800, 150%, -50%)",
536 Color::new_rgba(0, 0, 0, 255)
537 );
538
539 test!(
540 hsla_green,
541 "hsla(120, 100%, 75%, 0.5)",
542 Color::new_rgba(128, 255, 128, 128)
543 );
544
545 test!(
546 hsl_with_alpha,
547 "hsl(120, 100%, 75%, 0.5)",
548 Color::new_rgba(128, 255, 128, 128)
549 );
550
551 test!(
552 hsl_to_rgb_red_round_up,
553 "hsl(230, 57%, 54%)",
554 Color::new_rgba(71, 93, 205, 255)
555 );
556
557 test!(
558 hsl_with_hue_float,
559 "hsl(120.152, 100%, 75%)",
560 Color::new_rgba(128, 255, 128, 255)
561 );
562
563 test!(
564 hsla_with_hue_float,
565 "hsla(120.152, 100%, 75%, 0.5)",
566 Color::new_rgba(128, 255, 128, 128)
567 );
568
569 macro_rules! test_err {
570 ($name:ident, $text:expr, $err:expr) => {
571 #[test]
572 fn $name() {
573 assert_eq!(Color::from_str($text).unwrap_err().to_string(), $err);
574 }
575 };
576 }
577
578 test_err!(
579 not_a_color_1,
580 "text",
581 "invalid value"
582 );
583
584 test_err!(
585 icc_color_not_supported_1,
586 "#CD853F icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)",
587 "unexpected data at position 9"
588 );
589
590 test_err!(
591 icc_color_not_supported_2,
592 "red icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)",
593 "unexpected data at position 5"
594 );
595
596 test_err!(
597 invalid_input_1,
598 "rgb(-0\x0d",
599 "unexpected end of stream"
600 );
601
602 test_err!(
603 invalid_input_2,
604 "#9ߞpx! ;",
605 "invalid value"
606 );
607
608 test_err!(
609 rgba_with_percent_alpha,
610 "rgba(10, 20, 30, 5%)",
611 "expected ')' not '%' at position 19"
612 );
613
614 test_err!(
615 rgb_mixed_units,
616 "rgb(140%, -10mm, 130pt)",
617 "invalid number at position 14"
618 );
619}