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);
49 match hex.len() {
50 3 => {
51 let r = u8::from_str_radix(&hex[0..1], 16).ok()? * 17;
52 let g = u8::from_str_radix(&hex[1..2], 16).ok()? * 17;
53 let b = u8::from_str_radix(&hex[2..3], 16).ok()? * 17;
54 Some((r, g, b))
55 }
56 6 => {
57 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
58 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
59 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
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 match hex.len() {
81 3 | 6 => hex_to_rgb(hex).map(|(r, g, b)| (r, g, b, 255)),
82 4 => {
83 let r = u8::from_str_radix(&hex[0..1], 16).ok()? * 17;
84 let g = u8::from_str_radix(&hex[1..2], 16).ok()? * 17;
85 let b = u8::from_str_radix(&hex[2..3], 16).ok()? * 17;
86 let a = u8::from_str_radix(&hex[3..4], 16).ok()? * 17;
87 Some((r, g, b, a))
88 }
89 8 => {
90 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
91 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
92 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
93 let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
94 Some((r, g, b, a))
95 }
96 _ => None,
97 }
98}
99
100pub fn rgb_to_hex(r: u8, g: u8, b: u8) -> String {
109 format!("#{r:02x}{g:02x}{b:02x}")
110}
111
112pub fn rgba_to_hex(r: u8, g: u8, b: u8, a: u8) -> String {
121 format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
122}
123
124pub fn rgb_to_css(r: u8, g: u8, b: u8) -> String {
133 format!("rgb({r},{g},{b})")
134}
135
136pub fn rgba_to_css(r: u8, g: u8, b: u8, a: u8) -> String {
145 format!("rgba({r},{g},{b},{a})")
146}
147
148#[derive(Copy, Clone, Debug, PartialEq, Eq)]
165pub struct Srgba {
166 pub r: u8,
168 pub g: u8,
170 pub b: u8,
172 pub a: u8,
174}
175
176pub trait ColorSpace: Copy + Sized {
182 type Component: Copy;
184
185 fn to_rgb(self) -> RgbColor;
187
188 fn from_rgb(rgb: RgbColor) -> Self;
190}
191
192#[derive(Copy, Clone, Debug, PartialEq, Eq)]
194pub struct RgbColor {
195 pub r: u8,
197 pub g: u8,
199 pub b: u8,
201}
202
203impl RgbColor {
204 pub const fn new(r: u8, g: u8, b: u8) -> Self {
206 Self { r, g, b }
207 }
208
209 pub fn to_array(self) -> [f64; 3] {
211 [self.r as f64 / 255.0, self.g as f64 / 255.0, self.b as f64 / 255.0]
212 }
213
214 pub const fn to_srgba(self) -> Srgba {
216 Srgba::rgb(self.r, self.g, self.b)
217 }
218}
219
220impl ColorSpace for RgbColor {
221 type Component = u8;
222
223 fn to_rgb(self) -> RgbColor {
224 self
225 }
226
227 fn from_rgb(rgb: RgbColor) -> Self {
228 rgb
229 }
230}
231
232impl From<(u8, u8, u8)> for RgbColor {
233 fn from((r, g, b): (u8, u8, u8)) -> Self {
234 Self::new(r, g, b)
235 }
236}
237
238impl From<Srgba> for RgbColor {
239 fn from(color: Srgba) -> Self {
240 Self::new(color.r, color.g, color.b)
241 }
242}
243
244impl From<RgbColor> for Srgba {
245 fn from(color: RgbColor) -> Self {
246 color.to_srgba()
247 }
248}
249
250#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
252pub struct CmykColor {
253 pub c: f64,
255 pub m: f64,
257 pub y: f64,
259 pub k: f64,
261}
262
263impl CmykColor {
264 pub fn new(c: f64, m: f64, y: f64, k: f64) -> Self {
266 Self { c: clamp_unit(c), m: clamp_unit(m), y: clamp_unit(y), k: clamp_unit(k) }
267 }
268
269 pub fn to_array(self) -> [f64; 4] {
271 [self.c, self.m, self.y, self.k]
272 }
273}
274
275impl ColorSpace for CmykColor {
276 type Component = f64;
277
278 fn to_rgb(self) -> RgbColor {
279 let r = (1.0 - self.c) * (1.0 - self.k);
280 let g = (1.0 - self.m) * (1.0 - self.k);
281 let b = (1.0 - self.y) * (1.0 - self.k);
282 RgbColor::new(unit_to_u8(r), unit_to_u8(g), unit_to_u8(b))
283 }
284
285 fn from_rgb(rgb: RgbColor) -> Self {
286 let r = rgb.r as f64 / 255.0;
287 let g = rgb.g as f64 / 255.0;
288 let b = rgb.b as f64 / 255.0;
289 let k = 1.0 - r.max(g).max(b);
290 if k >= 1.0 {
291 return Self::new(0.0, 0.0, 0.0, 1.0);
292 }
293 let denom = 1.0 - k;
294 Self::new((1.0 - r - k) / denom, (1.0 - g - k) / denom, (1.0 - b - k) / denom, k)
295 }
296}
297
298impl From<RgbColor> for CmykColor {
299 fn from(color: RgbColor) -> Self {
300 Self::from_rgb(color)
301 }
302}
303
304impl From<CmykColor> for RgbColor {
305 fn from(color: CmykColor) -> Self {
306 color.to_rgb()
307 }
308}
309
310#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
312pub struct LabColor {
313 pub l: f64,
315 pub a: f64,
317 pub b: f64,
319}
320
321impl LabColor {
322 pub const fn new(l: f64, a: f64, b: f64) -> Self {
324 Self { l, a, b }
325 }
326}
327
328impl ColorSpace for LabColor {
329 type Component = f64;
330
331 fn to_rgb(self) -> RgbColor {
332 let fy = (self.l + 16.0) / 116.0;
333 let fx = fy + self.a / 500.0;
334 let fz = fy - self.b / 200.0;
335 let x = lab_inv(fx) * 0.95047;
336 let y = lab_inv(fy);
337 let z = lab_inv(fz) * 1.08883;
338 let r = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z;
339 let g = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z;
340 let b = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z;
341 RgbColor::new(unit_to_u8(r), unit_to_u8(g), unit_to_u8(b))
342 }
343
344 fn from_rgb(rgb: RgbColor) -> Self {
345 let r = rgb.r as f64 / 255.0;
346 let g = rgb.g as f64 / 255.0;
347 let b = rgb.b as f64 / 255.0;
348 let x = (0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047;
349 let y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b;
350 let z = (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / 1.08883;
351 let fx = lab_f(x);
352 let fy = lab_f(y);
353 let fz = lab_f(z);
354 Self::new(116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz))
355 }
356}
357
358impl From<RgbColor> for LabColor {
359 fn from(color: RgbColor) -> Self {
360 Self::from_rgb(color)
361 }
362}
363
364impl From<LabColor> for RgbColor {
365 fn from(color: LabColor) -> Self {
366 color.to_rgb()
367 }
368}
369
370impl Srgba {
371 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
373 Self { r, g, b, a }
374 }
375
376 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
378 Self { r, g, b, a: 255 }
379 }
380
381 pub fn from_hex(hex: &str) -> Option<Self> {
383 let (r, g, b, a) = hex_to_rgba(hex)?;
384 Some(Self { r, g, b, a })
385 }
386
387 pub fn to_hex(self) -> String {
389 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) }
390 }
391
392 pub fn to_css(self) -> String {
394 rgba_to_css(self.r, self.g, self.b, self.a)
395 }
396
397 pub fn to_ansi_fg(self) -> String {
399 format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
400 }
401
402 pub fn to_ansi_bg(self) -> String {
404 format!("\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
405 }
406
407 pub fn to_array(self) -> [f64; 3] {
409 [self.r as f64 / 255.0, self.g as f64 / 255.0, self.b as f64 / 255.0]
410 }
411
412 pub fn lerp(self, other: Self, t: f32) -> Self {
416 let t = t.clamp(0.0, 1.0);
417 let inv = 1.0 - t;
418 Self {
419 r: (self.r as f32 * inv + other.r as f32 * t) as u8,
420 g: (self.g as f32 * inv + other.g as f32 * t) as u8,
421 b: (self.b as f32 * inv + other.b as f32 * t) as u8,
422 a: (self.a as f32 * inv + other.a as f32 * t) as u8,
423 }
424 }
425}
426
427fn clamp_unit(value: f64) -> f64 {
428 value.clamp(0.0, 1.0)
429}
430
431fn unit_to_u8(value: f64) -> u8 {
432 (clamp_unit(value) * 255.0 + 0.5) as u8
433}
434
435fn lab_f(value: f64) -> f64 {
436 if value > 0.008856 { cube_root(value) } else { 7.787 * value + 16.0 / 116.0 }
437}
438
439fn lab_inv(value: f64) -> f64 {
440 let cube = value * value * value;
441 if cube > 0.008856 { cube } else { (value - 16.0 / 116.0) / 7.787 }
442}
443
444fn cube_root(value: f64) -> f64 {
445 if value <= 0.0 {
446 return 0.0;
447 }
448 let mut x = value.max(1.0);
449 for _ in 0..12 {
450 x = (2.0 * x + value / (x * x)) / 3.0;
451 }
452 x
453}
454
455impl From<(u8, u8, u8)> for Srgba {
456 fn from((r, g, b): (u8, u8, u8)) -> Self {
457 Self::rgb(r, g, b)
458 }
459}
460
461impl From<(u8, u8, u8, u8)> for Srgba {
462 fn from((r, g, b, a): (u8, u8, u8, u8)) -> Self {
463 Self::new(r, g, b, a)
464 }
465}
466
467impl From<Srgba> for (u8, u8, u8, u8) {
468 fn from(c: Srgba) -> Self {
469 (c.r, c.g, c.b, c.a)
470 }
471}
472
473#[cfg(feature = "image")]
474impl From<Srgba> for image::Rgba<u8> {
475 fn from(c: Srgba) -> Self {
476 image::Rgba([c.r, c.g, c.b, c.a])
477 }
478}
479
480#[cfg(feature = "image")]
481impl From<image::Rgba<u8>> for Srgba {
482 fn from(p: image::Rgba<u8>) -> Self {
483 Self::new(p.0[0], p.0[1], p.0[2], p.0[3])
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn test_hex_to_rgb_6_digit() {
493 assert_eq!(hex_to_rgb("#ff0080"), Some((255, 0, 128)));
494 assert_eq!(hex_to_rgb("#000000"), Some((0, 0, 0)));
495 assert_eq!(hex_to_rgb("#ffffff"), Some((255, 255, 255)));
496 }
497
498 #[test]
499 fn test_hex_to_rgb_3_digit() {
500 assert_eq!(hex_to_rgb("#f08"), Some((255, 0, 136)));
501 assert_eq!(hex_to_rgb("#000"), Some((0, 0, 0)));
502 assert_eq!(hex_to_rgb("#fff"), Some((255, 255, 255)));
503 }
504
505 #[test]
506 fn test_hex_to_rgb_no_hash() {
507 assert_eq!(hex_to_rgb("ff0080"), Some((255, 0, 128)));
508 }
509
510 #[test]
511 fn test_hex_to_rgb_invalid() {
512 assert_eq!(hex_to_rgb("invalid"), None);
513 assert_eq!(hex_to_rgb("#gg0000"), None);
514 assert_eq!(hex_to_rgb("#12345"), None);
515 }
516
517 #[test]
518 fn test_hex_to_rgba_6_digit() {
519 assert_eq!(hex_to_rgba("#ff0080"), Some((255, 0, 128, 255)));
520 }
521
522 #[test]
523 fn test_hex_to_rgba_8_digit() {
524 assert_eq!(hex_to_rgba("#ff008080"), Some((255, 0, 128, 128)));
525 }
526
527 #[test]
528 fn test_hex_to_rgba_4_digit() {
529 assert_eq!(hex_to_rgba("#f08c"), Some((255, 0, 136, 204)));
530 }
531
532 #[test]
533 fn test_rgb_to_hex() {
534 assert_eq!(rgb_to_hex(255, 0, 128), "#ff0080");
535 assert_eq!(rgb_to_hex(0, 0, 0), "#000000");
536 }
537
538 #[test]
539 fn test_rgba_to_hex() {
540 assert_eq!(rgba_to_hex(255, 0, 128, 192), "#ff0080c0");
541 }
542
543 #[test]
544 fn test_rgb_to_css() {
545 assert_eq!(rgb_to_css(255, 0, 128), "rgb(255,0,128)");
546 }
547
548 #[test]
549 fn test_rgba_to_css() {
550 assert_eq!(rgba_to_css(255, 0, 128, 128), "rgba(255,0,128,128)");
551 }
552
553 #[test]
554 fn test_roundtrip() {
555 let (r, g, b) = (42, 128, 255);
556 let hex = rgb_to_hex(r, g, b);
557 assert_eq!(hex_to_rgb(&hex), Some((r, g, b)));
558 }
559
560 #[test]
561 fn test_srgba_from_hex() {
562 let c = Srgba::from_hex("#ff0080").unwrap();
563 assert_eq!(c, Srgba::new(255, 0, 128, 255));
564 }
565
566 #[test]
567 fn test_srgba_from_hex_with_alpha() {
568 let c = Srgba::from_hex("#ff0080c0").unwrap();
569 assert_eq!(c, Srgba::new(255, 0, 128, 192));
570 }
571
572 #[test]
573 fn test_srgba_to_hex() {
574 assert_eq!(Srgba::rgb(255, 0, 128).to_hex(), "#ff0080");
575 assert_eq!(Srgba::new(255, 0, 128, 192).to_hex(), "#ff0080c0");
576 }
577
578 #[test]
579 fn test_srgba_to_css() {
580 assert_eq!(Srgba::rgb(255, 0, 128).to_css(), "rgba(255,0,128,255)");
581 }
582
583 #[test]
584 fn test_srgba_to_ansi() {
585 let c = Srgba::rgb(0, 51, 102);
586 assert_eq!(c.to_ansi_fg(), "\x1b[38;2;0;51;102m");
587 assert_eq!(c.to_ansi_bg(), "\x1b[48;2;0;51;102m");
588 }
589
590 #[test]
591 fn test_srgba_to_array() {
592 let c = Srgba::rgb(255, 0, 128);
593 let arr = c.to_array();
594 assert!((arr[0] - 1.0).abs() < 0.001);
595 assert!((arr[1] - 0.0).abs() < 0.001);
596 assert!((arr[2] - 0.502).abs() < 0.01);
597 }
598
599 #[test]
600 fn test_srgba_lerp() {
601 let a = Srgba::rgb(0, 0, 0);
602 let b = Srgba::rgb(255, 255, 255);
603 assert_eq!(a.lerp(b, 0.0), a);
604 assert_eq!(a.lerp(b, 1.0), b);
605 let mid = a.lerp(b, 0.5);
606 assert_eq!(mid.r, 127);
607 }
608
609 #[test]
610 fn test_srgba_from_tuple() {
611 let c: Srgba = (255, 0, 128).into();
612 assert_eq!(c, Srgba::rgb(255, 0, 128));
613 }
614
615 #[test]
616 fn rgb_color_space_round_trips() {
617 let rgb = RgbColor::new(51, 102, 153);
618
619 assert_eq!(RgbColor::from_rgb(rgb), rgb);
620 assert_eq!(rgb.to_rgb(), rgb);
621 assert_eq!(rgb.to_srgba(), Srgba::rgb(51, 102, 153));
622 assert_eq!(rgb.to_array(), [0.2, 0.4, 0.6]);
623 }
624
625 #[test]
626 fn cmyk_color_space_converts_to_rgb() {
627 assert_eq!(CmykColor::new(0.0, 0.0, 0.0, 1.0).to_rgb(), RgbColor::new(0, 0, 0));
628 assert_eq!(CmykColor::new(0.0, 1.0, 1.0, 0.0).to_rgb(), RgbColor::new(255, 0, 0));
629
630 let cmyk = CmykColor::from_rgb(RgbColor::new(51, 102, 153));
631 assert_eq!(cmyk.to_rgb(), RgbColor::new(51, 102, 153));
632 }
633
634 #[test]
635 fn lab_color_space_handles_neutral_extremes() {
636 let white = LabColor::from_rgb(RgbColor::new(255, 255, 255));
637 assert!((white.l - 100.0).abs() < 0.01);
638 assert!(white.a.abs() < 0.01);
639 assert!(white.b.abs() < 0.01);
640
641 let black = LabColor::from_rgb(RgbColor::new(0, 0, 0));
642 assert!(black.l.abs() < 0.01);
643 assert_eq!(black.to_rgb(), RgbColor::new(0, 0, 0));
644 }
645}