1#[cfg(not(feature = "std"))]
26#[allow(unused_imports)]
27use alloc::{
28 borrow::ToOwned,
29 format,
30 string::{String, ToString},
31 vec,
32 vec::Vec,
33};
34
35pub fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> {
48 let hex = hex.strip_prefix('#').unwrap_or(hex).as_bytes();
49 match hex.len() {
50 3 => {
51 let r = hex_nibble(hex[0])? * 17;
52 let g = hex_nibble(hex[1])? * 17;
53 let b = hex_nibble(hex[2])? * 17;
54 Some((r, g, b))
55 }
56 6 => {
57 let r = hex_byte(hex[0], hex[1])?;
58 let g = hex_byte(hex[2], hex[3])?;
59 let b = hex_byte(hex[4], hex[5])?;
60 Some((r, g, b))
61 }
62 _ => None,
63 }
64}
65
66pub fn hex_to_rgba(hex: &str) -> Option<(u8, u8, u8, u8)> {
79 let hex = hex.strip_prefix('#').unwrap_or(hex);
80 let bytes = hex.as_bytes();
81 match bytes.len() {
82 3 | 6 => hex_to_rgb(hex).map(|(r, g, b)| (r, g, b, 255)),
83 4 => {
84 let r = hex_nibble(bytes[0])? * 17;
85 let g = hex_nibble(bytes[1])? * 17;
86 let b = hex_nibble(bytes[2])? * 17;
87 let a = hex_nibble(bytes[3])? * 17;
88 Some((r, g, b, a))
89 }
90 8 => {
91 let r = hex_byte(bytes[0], bytes[1])?;
92 let g = hex_byte(bytes[2], bytes[3])?;
93 let b = hex_byte(bytes[4], bytes[5])?;
94 let a = hex_byte(bytes[6], bytes[7])?;
95 Some((r, g, b, a))
96 }
97 _ => None,
98 }
99}
100
101fn hex_nibble(byte: u8) -> Option<u8> {
102 match byte {
103 b'0'..=b'9' => Some(byte - b'0'),
104 b'a'..=b'f' => Some(byte - b'a' + 10),
105 b'A'..=b'F' => Some(byte - b'A' + 10),
106 _ => None,
107 }
108}
109
110fn hex_byte(high: u8, low: u8) -> Option<u8> {
111 Some(hex_nibble(high)? * 16 + hex_nibble(low)?)
112}
113
114pub fn rgb_to_hex(r: u8, g: u8, b: u8) -> String {
123 format!("#{r:02x}{g:02x}{b:02x}")
124}
125
126pub fn rgba_to_hex(r: u8, g: u8, b: u8, a: u8) -> String {
135 format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
136}
137
138pub fn rgb_to_css(r: u8, g: u8, b: u8) -> String {
147 format!("rgb({r},{g},{b})")
148}
149
150pub fn rgba_to_css(r: u8, g: u8, b: u8, a: u8) -> String {
161 let alpha = f64::from(a) / 255.0;
162 format!("rgba({r},{g},{b},{alpha})")
163}
164
165#[derive(Copy, Clone, Debug, PartialEq, Eq)]
182pub struct Srgba {
183 pub r: u8,
185 pub g: u8,
187 pub b: u8,
189 pub a: u8,
191}
192
193pub trait ColorSpace: Copy + Sized {
199 type Component: Copy;
201
202 fn to_rgb(self) -> RgbColor;
204
205 fn from_rgb(rgb: RgbColor) -> Self;
207}
208
209#[derive(Copy, Clone, Debug, PartialEq, Eq)]
211pub struct RgbColor {
212 pub r: u8,
214 pub g: u8,
216 pub b: u8,
218}
219
220impl RgbColor {
221 pub const fn new(r: u8, g: u8, b: u8) -> Self {
223 Self { r, g, b }
224 }
225
226 pub fn to_array(self) -> [f64; 3] {
228 [self.r as f64 / 255.0, self.g as f64 / 255.0, self.b as f64 / 255.0]
229 }
230
231 pub const fn to_srgba(self) -> Srgba {
233 Srgba::rgb(self.r, self.g, self.b)
234 }
235}
236
237impl ColorSpace for RgbColor {
238 type Component = u8;
239
240 fn to_rgb(self) -> RgbColor {
241 self
242 }
243
244 fn from_rgb(rgb: RgbColor) -> Self {
245 rgb
246 }
247}
248
249impl From<(u8, u8, u8)> for RgbColor {
250 fn from((r, g, b): (u8, u8, u8)) -> Self {
251 Self::new(r, g, b)
252 }
253}
254
255impl From<Srgba> for RgbColor {
256 fn from(color: Srgba) -> Self {
257 Self::new(color.r, color.g, color.b)
258 }
259}
260
261impl From<RgbColor> for Srgba {
262 fn from(color: RgbColor) -> Self {
263 color.to_srgba()
264 }
265}
266
267#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
269pub struct CmykColor {
270 pub c: f64,
272 pub m: f64,
274 pub y: f64,
276 pub k: f64,
278}
279
280impl CmykColor {
281 pub fn new(c: f64, m: f64, y: f64, k: f64) -> Self {
283 Self { c: clamp_unit(c), m: clamp_unit(m), y: clamp_unit(y), k: clamp_unit(k) }
284 }
285
286 pub fn to_array(self) -> [f64; 4] {
288 [self.c, self.m, self.y, self.k]
289 }
290}
291
292impl ColorSpace for CmykColor {
293 type Component = f64;
294
295 fn to_rgb(self) -> RgbColor {
296 let r = (1.0 - self.c) * (1.0 - self.k);
297 let g = (1.0 - self.m) * (1.0 - self.k);
298 let b = (1.0 - self.y) * (1.0 - self.k);
299 RgbColor::new(unit_to_u8(r), unit_to_u8(g), unit_to_u8(b))
300 }
301
302 fn from_rgb(rgb: RgbColor) -> Self {
303 let r = rgb.r as f64 / 255.0;
304 let g = rgb.g as f64 / 255.0;
305 let b = rgb.b as f64 / 255.0;
306 let k = 1.0 - r.max(g).max(b);
307 if k >= 1.0 {
308 return Self::new(0.0, 0.0, 0.0, 1.0);
309 }
310 let denom = 1.0 - k;
311 Self::new((1.0 - r - k) / denom, (1.0 - g - k) / denom, (1.0 - b - k) / denom, k)
312 }
313}
314
315impl From<RgbColor> for CmykColor {
316 fn from(color: RgbColor) -> Self {
317 Self::from_rgb(color)
318 }
319}
320
321impl From<CmykColor> for RgbColor {
322 fn from(color: CmykColor) -> Self {
323 color.to_rgb()
324 }
325}
326
327#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
329pub struct LabColor {
330 pub l: f64,
332 pub a: f64,
334 pub b: f64,
336}
337
338impl LabColor {
339 pub const fn new(l: f64, a: f64, b: f64) -> Self {
341 Self { l, a, b }
342 }
343}
344
345impl ColorSpace for LabColor {
346 type Component = f64;
347
348 fn to_rgb(self) -> RgbColor {
349 let fy = (self.l + 16.0) / 116.0;
350 let fx = fy + self.a / 500.0;
351 let fz = fy - self.b / 200.0;
352 let x = lab_inv(fx) * 0.95047;
353 let y = lab_inv(fy);
354 let z = lab_inv(fz) * 1.08883;
355 let r = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z;
356 let g = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z;
357 let b = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z;
358 RgbColor::new(unit_to_u8(r), unit_to_u8(g), unit_to_u8(b))
359 }
360
361 fn from_rgb(rgb: RgbColor) -> Self {
362 let r = rgb.r as f64 / 255.0;
363 let g = rgb.g as f64 / 255.0;
364 let b = rgb.b as f64 / 255.0;
365 let x = (0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047;
366 let y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b;
367 let z = (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / 1.08883;
368 let fx = lab_f(x);
369 let fy = lab_f(y);
370 let fz = lab_f(z);
371 Self::new(116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz))
372 }
373}
374
375impl From<RgbColor> for LabColor {
376 fn from(color: RgbColor) -> Self {
377 Self::from_rgb(color)
378 }
379}
380
381impl From<LabColor> for RgbColor {
382 fn from(color: LabColor) -> Self {
383 color.to_rgb()
384 }
385}
386
387impl Srgba {
388 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
390 Self { r, g, b, a }
391 }
392
393 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
395 Self { r, g, b, a: 255 }
396 }
397
398 pub fn from_hex(hex: &str) -> Option<Self> {
400 let (r, g, b, a) = hex_to_rgba(hex)?;
401 Some(Self { r, g, b, a })
402 }
403
404 pub fn to_hex(self) -> String {
406 if self.a == 255 { rgb_to_hex(self.r, self.g, self.b) } else { rgba_to_hex(self.r, self.g, self.b, self.a) }
407 }
408
409 pub fn to_css(self) -> String {
411 rgba_to_css(self.r, self.g, self.b, self.a)
412 }
413
414 pub fn to_ansi_fg(self) -> String {
416 format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
417 }
418
419 pub fn to_ansi_bg(self) -> String {
421 format!("\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
422 }
423
424 pub fn to_array(self) -> [f64; 3] {
426 [self.r as f64 / 255.0, self.g as f64 / 255.0, self.b as f64 / 255.0]
427 }
428
429 pub fn lerp(self, other: Self, t: f32) -> Self {
433 let t = t.clamp(0.0, 1.0);
434 let inv = 1.0 - t;
435 Self {
436 r: (self.r as f32 * inv + other.r as f32 * t) as u8,
437 g: (self.g as f32 * inv + other.g as f32 * t) as u8,
438 b: (self.b as f32 * inv + other.b as f32 * t) as u8,
439 a: (self.a as f32 * inv + other.a as f32 * t) as u8,
440 }
441 }
442}
443
444fn clamp_unit(value: f64) -> f64 {
445 value.clamp(0.0, 1.0)
446}
447
448fn unit_to_u8(value: f64) -> u8 {
449 (clamp_unit(value) * 255.0 + 0.5) as u8
450}
451
452fn lab_f(value: f64) -> f64 {
453 if value > 0.008856 { cube_root(value) } else { 7.787 * value + 16.0 / 116.0 }
454}
455
456fn lab_inv(value: f64) -> f64 {
457 let cube = value * value * value;
458 if cube > 0.008856 { cube } else { (value - 16.0 / 116.0) / 7.787 }
459}
460
461fn cube_root(value: f64) -> f64 {
462 if value <= 0.0 {
463 return 0.0;
464 }
465 let mut x = value.max(1.0);
466 for _ in 0..12 {
467 x = (2.0 * x + value / (x * x)) / 3.0;
468 }
469 x
470}
471
472impl From<(u8, u8, u8)> for Srgba {
473 fn from((r, g, b): (u8, u8, u8)) -> Self {
474 Self::rgb(r, g, b)
475 }
476}
477
478impl From<(u8, u8, u8, u8)> for Srgba {
479 fn from((r, g, b, a): (u8, u8, u8, u8)) -> Self {
480 Self::new(r, g, b, a)
481 }
482}
483
484impl From<Srgba> for (u8, u8, u8, u8) {
485 fn from(c: Srgba) -> Self {
486 (c.r, c.g, c.b, c.a)
487 }
488}
489
490#[cfg(feature = "image")]
491impl From<Srgba> for image::Rgba<u8> {
492 fn from(c: Srgba) -> Self {
493 image::Rgba([c.r, c.g, c.b, c.a])
494 }
495}
496
497#[cfg(feature = "image")]
498impl From<image::Rgba<u8>> for Srgba {
499 fn from(p: image::Rgba<u8>) -> Self {
500 Self::new(p.0[0], p.0[1], p.0[2], p.0[3])
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn test_hex_to_rgb_6_digit() {
510 assert_eq!(hex_to_rgb("#ff0080"), Some((255, 0, 128)));
511 assert_eq!(hex_to_rgb("#000000"), Some((0, 0, 0)));
512 assert_eq!(hex_to_rgb("#ffffff"), Some((255, 255, 255)));
513 }
514
515 #[test]
516 fn test_hex_to_rgb_3_digit() {
517 assert_eq!(hex_to_rgb("#f08"), Some((255, 0, 136)));
518 assert_eq!(hex_to_rgb("#000"), Some((0, 0, 0)));
519 assert_eq!(hex_to_rgb("#fff"), Some((255, 255, 255)));
520 }
521
522 #[test]
523 fn test_hex_to_rgb_no_hash() {
524 assert_eq!(hex_to_rgb("ff0080"), Some((255, 0, 128)));
525 }
526
527 #[test]
528 fn test_hex_to_rgb_invalid() {
529 assert_eq!(hex_to_rgb("invalid"), None);
530 assert_eq!(hex_to_rgb("#gg0000"), None);
531 assert_eq!(hex_to_rgb("#12345"), None);
532 }
533
534 #[test]
535 fn hex_parsers_reject_non_ascii_without_panicking() {
536 for hex in ["#éa", "#aé", "#中0", "#é00", "#00é", "#0é000", "#0é00000", "#00000é0"] {
537 assert_eq!(hex_to_rgb(hex), None, "RGB accepted {hex:?}");
538 assert_eq!(hex_to_rgba(hex), None, "RGBA accepted {hex:?}");
539 assert_eq!(Srgba::from_hex(hex), None, "sRGBA accepted {hex:?}");
540 }
541 }
542
543 #[test]
544 fn hex_parsers_preserve_uppercase_and_optional_hash() {
545 for hex in ["#AbF", "AbF", "#AaBBff", "AaBBff"] {
546 assert_eq!(hex_to_rgb(hex), Some((170, 187, 255)));
547 assert_eq!(hex_to_rgba(hex), Some((170, 187, 255, 255)));
548 }
549 for hex in ["#AbF8", "AbF8", "#AaBBff88", "AaBBff88"] {
550 assert_eq!(hex_to_rgba(hex), Some((170, 187, 255, 136)));
551 }
552 }
553
554 #[test]
555 fn test_hex_to_rgba_6_digit() {
556 assert_eq!(hex_to_rgba("#ff0080"), Some((255, 0, 128, 255)));
557 }
558
559 #[test]
560 fn test_hex_to_rgba_8_digit() {
561 assert_eq!(hex_to_rgba("#ff008080"), Some((255, 0, 128, 128)));
562 }
563
564 #[test]
565 fn test_hex_to_rgba_4_digit() {
566 assert_eq!(hex_to_rgba("#f08c"), Some((255, 0, 136, 204)));
567 }
568
569 #[test]
570 fn test_rgb_to_hex() {
571 assert_eq!(rgb_to_hex(255, 0, 128), "#ff0080");
572 assert_eq!(rgb_to_hex(0, 0, 0), "#000000");
573 }
574
575 #[test]
576 fn test_rgba_to_hex() {
577 assert_eq!(rgba_to_hex(255, 0, 128, 192), "#ff0080c0");
578 }
579
580 #[test]
581 fn test_rgb_to_css() {
582 assert_eq!(rgb_to_css(255, 0, 128), "rgb(255,0,128)");
583 }
584
585 #[test]
586 fn test_rgba_to_css() {
587 assert_eq!(rgba_to_css(255, 0, 128, 0), "rgba(255,0,128,0)");
588 assert_eq!(rgba_to_css(255, 0, 128, 255), "rgba(255,0,128,1)");
589 assert_eq!(rgba_to_css(255, 0, 128, 128), "rgba(255,0,128,0.5019607843137255)");
590 }
591
592 #[test]
593 fn css_alpha_preserves_every_byte_value_and_rgb_channels() {
594 for alpha in 0..=u8::MAX {
595 for (r, g, b) in [(0, 0, 0), (255, 0, 128), (17, 128, 253)] {
596 let color = Srgba::new(r, g, b, alpha);
597 let css = rgba_to_css(r, g, b, alpha);
598 assert_eq!(color.to_css(), css);
599 let mut components = css.strip_prefix("rgba(").unwrap().strip_suffix(')').unwrap().split(',');
600 assert_eq!(components.next().unwrap().parse::<u8>().unwrap(), r);
601 assert_eq!(components.next().unwrap().parse::<u8>().unwrap(), g);
602 assert_eq!(components.next().unwrap().parse::<u8>().unwrap(), b);
603 let parsed_alpha = components.next().unwrap().parse::<f64>().unwrap();
604 assert!(components.next().is_none());
605 assert!((0.0..=1.0).contains(&parsed_alpha), "alpha {alpha}, CSS {css}");
606 let restored = parsed_alpha * 255.0;
609 assert!((restored - f64::from(alpha)).abs() <= f64::EPSILON * 255.0, "alpha {alpha}, CSS {css}");
610 assert_eq!((restored + 0.5) as u8, alpha);
611 }
612 }
613 }
614
615 #[test]
616 fn srgba_hex_preserves_alpha_bytes_and_opaque_short_form() {
617 for alpha in 0..=u8::MAX {
618 let color = Srgba::new(255, 0, 128, alpha);
619 let hex = color.to_hex();
620 assert_eq!(hex.len(), if alpha == 255 { 7 } else { 9 });
621 assert_eq!(hex_to_rgba(&hex), Some((255, 0, 128, alpha)));
622 assert_eq!(Srgba::from_hex(&hex), Some(color));
623 }
624 }
625
626 #[test]
627 fn test_roundtrip() {
628 let (r, g, b) = (42, 128, 255);
629 let hex = rgb_to_hex(r, g, b);
630 assert_eq!(hex_to_rgb(&hex), Some((r, g, b)));
631 }
632
633 #[test]
634 fn test_srgba_from_hex() {
635 let c = Srgba::from_hex("#ff0080").unwrap();
636 assert_eq!(c, Srgba::new(255, 0, 128, 255));
637 }
638
639 #[test]
640 fn test_srgba_from_hex_with_alpha() {
641 let c = Srgba::from_hex("#ff0080c0").unwrap();
642 assert_eq!(c, Srgba::new(255, 0, 128, 192));
643 }
644
645 #[test]
646 fn test_srgba_to_hex() {
647 assert_eq!(Srgba::rgb(255, 0, 128).to_hex(), "#ff0080");
648 assert_eq!(Srgba::new(255, 0, 128, 192).to_hex(), "#ff0080c0");
649 }
650
651 #[test]
652 fn test_srgba_to_css() {
653 assert_eq!(Srgba::new(255, 0, 128, 0).to_css(), "rgba(255,0,128,0)");
654 assert_eq!(Srgba::rgb(255, 0, 128).to_css(), "rgba(255,0,128,1)");
655 assert_eq!(Srgba::new(255, 0, 128, 128).to_css(), "rgba(255,0,128,0.5019607843137255)");
656 }
657
658 #[test]
659 fn test_srgba_to_ansi() {
660 let c = Srgba::rgb(0, 51, 102);
661 assert_eq!(c.to_ansi_fg(), "\x1b[38;2;0;51;102m");
662 assert_eq!(c.to_ansi_bg(), "\x1b[48;2;0;51;102m");
663 }
664
665 #[test]
666 fn test_srgba_to_array() {
667 let c = Srgba::rgb(255, 0, 128);
668 let arr = c.to_array();
669 assert!((arr[0] - 1.0).abs() < 0.001);
670 assert!((arr[1] - 0.0).abs() < 0.001);
671 assert!((arr[2] - 0.502).abs() < 0.01);
672 }
673
674 #[test]
675 fn test_srgba_lerp() {
676 let a = Srgba::rgb(0, 0, 0);
677 let b = Srgba::rgb(255, 255, 255);
678 assert_eq!(a.lerp(b, 0.0), a);
679 assert_eq!(a.lerp(b, 1.0), b);
680 let mid = a.lerp(b, 0.5);
681 assert_eq!(mid.r, 127);
682 }
683
684 #[test]
685 fn test_srgba_from_tuple() {
686 let c: Srgba = (255, 0, 128).into();
687 assert_eq!(c, Srgba::rgb(255, 0, 128));
688 }
689
690 #[test]
691 fn rgb_color_space_round_trips() {
692 let rgb = RgbColor::new(51, 102, 153);
693
694 assert_eq!(RgbColor::from_rgb(rgb), rgb);
695 assert_eq!(rgb.to_rgb(), rgb);
696 assert_eq!(rgb.to_srgba(), Srgba::rgb(51, 102, 153));
697 assert_eq!(rgb.to_array(), [0.2, 0.4, 0.6]);
698 }
699
700 #[test]
701 fn cmyk_color_space_converts_to_rgb() {
702 assert_eq!(CmykColor::new(0.0, 0.0, 0.0, 1.0).to_rgb(), RgbColor::new(0, 0, 0));
703 assert_eq!(CmykColor::new(0.0, 1.0, 1.0, 0.0).to_rgb(), RgbColor::new(255, 0, 0));
704
705 let cmyk = CmykColor::from_rgb(RgbColor::new(51, 102, 153));
706 assert_eq!(cmyk.to_rgb(), RgbColor::new(51, 102, 153));
707 }
708
709 #[test]
710 fn lab_color_space_handles_neutral_extremes() {
711 let white = LabColor::from_rgb(RgbColor::new(255, 255, 255));
712 assert!((white.l - 100.0).abs() < 0.01);
713 assert!(white.a.abs() < 0.01);
714 assert!(white.b.abs() < 0.01);
715
716 let black = LabColor::from_rgb(RgbColor::new(0, 0, 0));
717 assert!(black.l.abs() < 0.01);
718 assert_eq!(black.to_rgb(), RgbColor::new(0, 0, 0));
719 }
720}