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

Provided by KDAB

Privacy Policy