1use std::fmt;
9use std::str::FromStr;
10
11use crate::media::{HslColor, HsvColor, KnownColor, KnownColors};
12use crate::utilities::span_helpers::{
13 try_parse_byte, try_parse_double, try_parse_uint, FixedF2, NumberStyles,
14};
15use crate::utilities::{CultureInfo, FormatError};
16
17const BYTE_TO_DOUBLE: f64 = 1.0 / 255.0;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
21pub struct Color {
22 pub a: u8,
24 pub r: u8,
26 pub g: u8,
28 pub b: u8,
30}
31
32#[inline]
34fn round_to_byte(value: f64) -> u8 {
35 value.round_ties_even() as u8
36}
37
38#[inline]
40fn round_to_int(value: f64) -> i32 {
41 value.round_ties_even() as i32
42}
43
44#[inline]
47pub(crate) fn utf16_len(s: &str) -> usize {
48 if s.is_ascii() {
49 s.len()
50 } else {
51 s.encode_utf16().count()
52 }
53}
54
55#[inline]
57pub(crate) fn starts_with_ignore_ascii_case(s: &str, prefix: &str) -> bool {
58 s.len() >= prefix.len() && s.as_bytes()[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
59}
60
61#[inline]
64pub(crate) fn split_components(s: &str) -> ([&str; 4], usize) {
65 let mut components = [""; 4];
66 let mut count = 0;
67 for part in s.split(',') {
68 if count < 4 {
69 components[count] = part;
70 }
71 count += 1;
72 }
73 (components, count)
74}
75
76pub(crate) fn strip_css_function<'a>(
80 s: &'a str,
81 with_alpha: &str,
82 without_alpha: &str,
83) -> Option<&'a str> {
84 let working = s.trim();
85
86 if working.is_empty() || !working.contains(',') {
87 return None;
88 }
89
90 let length = utf16_len(working);
91
92 if length >= 11 && starts_with_ignore_ascii_case(working, with_alpha) && working.ends_with(')')
93 {
94 return Some(&working[with_alpha.len()..working.len() - 1]);
95 }
96
97 if length >= 10
98 && starts_with_ignore_ascii_case(working, without_alpha)
99 && working.ends_with(')')
100 {
101 return Some(&working[without_alpha.len()..working.len() - 1]);
102 }
103
104 None
105}
106
107pub(crate) fn try_parse_double_or_percent(s: &str) -> Option<f64> {
110 match s.find('%') {
111 Some(percent_index) => {
112 try_parse_double(&s[..percent_index], NumberStyles::NUMBER).map(|p| p / 100.0)
113 }
114 None => try_parse_double(s, NumberStyles::NUMBER),
115 }
116}
117
118impl Color {
119 #[inline]
121 pub const fn new(a: u8, r: u8, g: u8, b: u8) -> Self {
122 Self { a, r, g, b }
123 }
124
125 #[inline]
127 pub const fn from_argb(a: u8, r: u8, g: u8, b: u8) -> Color {
128 Color::new(a, r, g, b)
129 }
130
131 #[inline]
133 pub const fn from_rgb(r: u8, g: u8, b: u8) -> Color {
134 Color::new(0xff, r, g, b)
135 }
136
137 #[inline]
139 pub const fn from_uint32(value: u32) -> Color {
140 Color::new(
141 ((value >> 24) & 0xff) as u8,
142 ((value >> 16) & 0xff) as u8,
143 ((value >> 8) & 0xff) as u8,
144 (value & 0xff) as u8,
145 )
146 }
147
148 pub fn parse(s: &str) -> Result<Color, FormatError> {
151 match Color::try_parse(s) {
152 Some(color) => Ok(color),
153 None => Err(FormatError::from_string(format!(
154 "Invalid color string: '{s}'."
155 ))),
156 }
157 }
158
159 pub fn try_parse(s: &str) -> Option<Color> {
161 if s.is_empty() {
162 return None;
163 }
164
165 let bytes = s.as_bytes();
166
167 if bytes[0] == b'#' {
168 if let Some(color) = Color::try_parse_hex_format(s) {
169 return Some(color);
170 }
171 }
172
173 if bytes.len() >= 3 && utf16_len(s) >= 10 {
176 let prefix = [
177 bytes[0].to_ascii_lowercase(),
178 bytes[1].to_ascii_lowercase(),
179 bytes[2].to_ascii_lowercase(),
180 ];
181
182 if &prefix == b"rgb" {
183 if let Some(color) = Color::try_parse_css_format(s) {
184 return Some(color);
185 }
186 }
187
188 if &prefix == b"hsl" {
189 if let Some(hsl_color) = HslColor::try_parse(s) {
190 return Some(hsl_color.to_rgb());
191 }
192 }
193
194 if &prefix == b"hsv" {
195 if let Some(hsv_color) = HsvColor::try_parse(s) {
196 return Some(hsv_color.to_rgb());
197 }
198 }
199 }
200
201 let known_color = KnownColors::get_known_color(s);
202
203 if known_color != KnownColor::NONE {
204 return Some(known_color.to_color());
205 }
206
207 None
208 }
209
210 fn try_parse_hex_format(s: &str) -> Option<Color> {
212 fn try_parse_core(input: &[u8]) -> Option<Color> {
213 let mut alpha_component = 0u32;
214
215 if input.len() == 6 {
216 alpha_component = 0xff000000;
217 } else if input.len() != 8 {
218 return None;
219 }
220
221 let text = std::str::from_utf8(input).ok()?;
222 let parsed = try_parse_uint(text, NumberStyles::HEX_NUMBER)?;
223
224 Some(Color::from_uint32(parsed | alpha_component))
225 }
226
227 let input = &s.as_bytes()[1..];
228
229 if !input.is_ascii() {
231 return None;
232 }
233
234 if input.len() == 3 || input.len() == 4 {
236 let extended_length = 2 * input.len();
237 let mut extended = [0u8; 8];
238
239 for (i, &c) in input.iter().enumerate() {
240 extended[2 * i] = c;
241 extended[2 * i + 1] = c;
242 }
243
244 return try_parse_core(&extended[..extended_length]);
245 }
246
247 try_parse_core(input)
248 }
249
250 fn try_parse_css_format(s: &str) -> Option<Color> {
252 fn internal_try_parse_byte(s: &str) -> Option<u8> {
255 match s.find('%') {
256 Some(percent_index) => try_parse_double(&s[..percent_index], NumberStyles::NUMBER)
257 .map(|percentage| round_to_byte((percentage / 100.0) * 255.0)),
258 None => try_parse_byte(s, NumberStyles::NUMBER),
259 }
260 }
261
262 let working = strip_css_function(s, "rgba(", "rgb(")?;
263 let (components, count) = split_components(working);
264
265 if count == 3 {
266 let red = internal_try_parse_byte(components[0])?;
268 let green = internal_try_parse_byte(components[1])?;
269 let blue = internal_try_parse_byte(components[2])?;
270
271 Some(Color::new(0xFF, red, green, blue))
272 } else if count == 4 {
273 let red = internal_try_parse_byte(components[0])?;
275 let green = internal_try_parse_byte(components[1])?;
276 let blue = internal_try_parse_byte(components[2])?;
277 let alpha = try_parse_double_or_percent(components[3])?;
278
279 Some(Color::new(round_to_byte(alpha * 255.0), red, green, blue))
280 } else {
281 None
282 }
283 }
284
285 pub fn to_string_format(
299 &self,
300 format: Option<&str>,
301 format_provider: Option<&CultureInfo>,
302 ) -> Result<String, FormatError> {
303 let format = match format {
304 Some(format) if !format.is_empty() => format,
305 _ => return Ok(self.to_string()),
306 };
307
308 let (a, r, g, b) = (self.a, self.r, self.g, self.b);
309
310 Ok(match format {
311 "X" => format!("#{a:02X}{r:02X}{g:02X}{b:02X}"),
312 "x" => format!("#{r:02X}{g:02X}{b:02X}"),
313 "H" => format!("#{r:02X}{g:02X}{b:02X}{a:02X}"),
314 "R" => self.format_rgb_css(true),
315 "r" => self.format_rgb_css(false),
316 "R%" => self.format_rgb_percent_css(true),
317 "r%" => self.format_rgb_percent_css(false),
318 "L" | "l" | "L%" | "l%" => return self.to_hsl().to_string_format(Some(format), format_provider),
319 "V" | "v" | "V%" | "v%" => return self.to_hsv().to_string_format(Some(format), format_provider),
320 _ => {
321 return Err(FormatError::from_string(format!(
322 "Format string '{format}' is not supported."
323 )))
324 }
325 })
326 }
327
328 fn format_rgb_css(&self, include_alpha: bool) -> String {
329 if include_alpha {
330 return format!(
331 "rgba({}, {}, {}, {})",
332 self.r,
333 self.g,
334 self.b,
335 FixedF2(self.a as f64 * BYTE_TO_DOUBLE)
336 );
337 }
338
339 format!("rgb({}, {}, {})", self.r, self.g, self.b)
340 }
341
342 fn format_rgb_percent_css(&self, include_alpha: bool) -> String {
343 let r_pct = round_to_int(self.r as f64 * BYTE_TO_DOUBLE * 100.0);
344 let g_pct = round_to_int(self.g as f64 * BYTE_TO_DOUBLE * 100.0);
345 let b_pct = round_to_int(self.b as f64 * BYTE_TO_DOUBLE * 100.0);
346
347 if include_alpha {
348 let a_pct = round_to_int(self.a as f64 * BYTE_TO_DOUBLE * 100.0);
349 return format!("rgba({r_pct}%, {g_pct}%, {b_pct}%, {a_pct}%)");
350 }
351
352 format!("rgb({r_pct}%, {g_pct}%, {b_pct}%)")
353 }
354
355 #[inline]
357 pub const fn to_uint32(&self) -> u32 {
358 ((self.a as u32) << 24) | ((self.r as u32) << 16) | ((self.g as u32) << 8) | (self.b as u32)
359 }
360
361 #[inline]
363 pub fn to_hsl(&self) -> HslColor {
364 Color::rgb_to_hsl(self.r, self.g, self.b, self.a)
365 }
366
367 #[inline]
369 pub fn to_hsv(&self) -> HsvColor {
370 Color::rgb_to_hsv(self.r, self.g, self.b, self.a)
371 }
372
373 #[inline]
375 pub const fn equals(&self, other: Color) -> bool {
376 self.a == other.a && self.r == other.r && self.g == other.g && self.b == other.b
377 }
378
379 #[inline]
381 pub fn rgb_to_hsl(red: u8, green: u8, blue: u8, alpha: u8) -> HslColor {
382 Color::normalized_rgb_to_hsl(
384 BYTE_TO_DOUBLE * red as f64,
385 BYTE_TO_DOUBLE * green as f64,
386 BYTE_TO_DOUBLE * blue as f64,
387 BYTE_TO_DOUBLE * alpha as f64,
388 )
389 }
390
391 pub fn normalized_rgb_to_hsl(r: f64, g: f64, b: f64, a: f64) -> HslColor {
395 let max = if r >= g {
396 if r >= b {
397 r
398 } else {
399 b
400 }
401 } else if g >= b {
402 g
403 } else {
404 b
405 };
406 let min = if r <= g {
407 if r <= b {
408 r
409 } else {
410 b
411 }
412 } else if g <= b {
413 g
414 } else {
415 b
416 };
417 let chroma = max - min;
418 let h1;
419
420 if chroma == 0.0 {
421 h1 = 0.0;
422 } else if max == r {
423 h1 = (((g - b) / chroma) + 6.0) % 6.0;
426 } else if max == g {
427 h1 = 2.0 + ((b - r) / chroma);
428 } else {
429 h1 = 4.0 + ((r - g) / chroma);
430 }
431
432 let lightness = 0.5 * (max + min);
433 let saturation = if chroma == 0.0 {
434 0.0
435 } else {
436 chroma / (1.0 - ((2.0 * lightness) - 1.0).abs())
437 };
438
439 HslColor::new_with_clamp(a, 60.0 * h1, saturation, lightness, false)
440 }
441
442 #[inline]
444 pub fn rgb_to_hsv(red: u8, green: u8, blue: u8, alpha: u8) -> HsvColor {
445 Color::normalized_rgb_to_hsv(
447 BYTE_TO_DOUBLE * red as f64,
448 BYTE_TO_DOUBLE * green as f64,
449 BYTE_TO_DOUBLE * blue as f64,
450 BYTE_TO_DOUBLE * alpha as f64,
451 )
452 }
453
454 pub fn normalized_rgb_to_hsv(r: f64, g: f64, b: f64, a: f64) -> HsvColor {
458 let mut hue;
459 let saturation;
460
461 let max = if r >= g {
462 if r >= b {
463 r
464 } else {
465 b
466 }
467 } else if g >= b {
468 g
469 } else {
470 b
471 };
472 let min = if r <= g {
473 if r <= b {
474 r
475 } else {
476 b
477 }
478 } else if g <= b {
479 g
480 } else {
481 b
482 };
483
484 let value = max;
487
488 let chroma = max - min;
491
492 if chroma == 0.0 {
496 hue = 0.0;
497 saturation = 0.0;
498 } else {
499 if r == max {
502 hue = 60.0 * (g - b) / chroma;
504 } else if g == max {
505 hue = 120.0 + (60.0 * (b - r) / chroma);
507 } else {
508 hue = 240.0 + (60.0 * (r - g) / chroma);
510 }
511
512 if hue < 0.0 {
515 hue += 360.0;
516 }
517
518 saturation = chroma / value;
520 }
521
522 HsvColor::new_with_clamp(a, hue, saturation, value, false)
523 }
524}
525
526impl FromStr for Color {
527 type Err = FormatError;
528 #[inline]
529 fn from_str(s: &str) -> Result<Self, FormatError> {
530 Color::parse(s)
531 }
532}
533
534impl fmt::Display for Color {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 let rgb = self.to_uint32();
538 match KnownColors::get_known_color_name(rgb) {
539 Some(name) => f.write_str(name),
540 None => write!(f, "#{rgb:08x}"),
541 }
542 }
543}
544
545#[cfg(test)]
548mod tests {
549 use super::*;
550
551 #[test]
552 fn css_parsing_edge_cases() {
553 assert_eq!(Color::try_parse("rgb(0,0,)"), None);
555 assert_eq!(Color::try_parse("rgb(0,0,0,0,0)"), None);
556 assert_eq!(Color::try_parse("rgb(256,0,0)"), None);
557 assert_eq!(Color::try_parse("rgb(1e1,0,0)"), None);
558 assert_eq!(Color::try_parse("rgb(-1,0,0)"), None);
559 assert_eq!(
560 Color::try_parse(" RGB( 1 , 2.0 , 3 ) "),
561 None,
562 "the prefix is checked before trimming"
563 );
564 assert_eq!(
565 Color::try_parse("RGB( 1 , 2.0 , 3 ) "),
566 Some(Color::new(255, 1, 2, 3))
567 );
568 assert_eq!(
569 Color::try_parse("rgba(1,2,3,200%)"),
570 Some(Color::new(255, 1, 2, 3))
571 );
572 assert_eq!(
573 Color::try_parse("rgb(150%,0%,-10%)"),
574 Some(Color::new(255, 255, 0, 0))
575 );
576 assert_eq!(Color::try_parse("#ggg"), None);
577 assert_eq!(Color::try_parse("#"), None);
578 assert_eq!(Color::try_parse("#\u{e9}\u{e9}\u{e9}"), None);
579 assert_eq!(Color::try_parse("notacolor"), None);
580 assert_eq!(
581 Color::try_parse("transparent"),
582 Some(Color::new(0, 255, 255, 255))
583 );
584 }
585
586 #[test]
587 fn uint32_round_trip_and_model_conversions() {
588 let c = Color::from_uint32(0x40ff8844);
589 assert_eq!(c, Color::new(0x40, 0xff, 0x88, 0x44));
590 assert_eq!(c.to_uint32(), 0x40ff8844);
591 assert_eq!(Color::from_rgb(1, 2, 3), Color::from_argb(255, 1, 2, 3));
592 for value in [
593 0xff000000u32,
594 0xffffffff,
595 0xff336699,
596 0x80ff0000,
597 0xff00ff00,
598 0xff0000ff,
599 0xff7f7f7f,
600 ] {
601 let color = Color::from_uint32(value);
602 assert_eq!(color.to_hsl().to_rgb(), color, "{value:08x} via hsl");
603 assert_eq!(color.to_hsv().to_rgb(), color, "{value:08x} via hsv");
604 }
605 assert_eq!(
606 "#ff8844".parse::<Color>().unwrap(),
607 Color::new(255, 255, 0x88, 0x44)
608 );
609 }
610}