Skip to main content

qrcode_render/
colors.rs

1//! Common color types and conversions for rendering.
2//!
3//! This module provides convenience types and conversion functions that work
4//! across multiple render backends.
5//!
6//! # Supported formats
7//!
8//! | Format    | Type        | Example                    |
9//! |-----------|-------------|----------------------------|
10//! | RGB       | `(u8,u8,u8)`| `(255, 0, 128)`           |
11//! | RGBA      | `(u8,u8,u8,u8)` | `(255, 0, 128, 200)` |
12//! | Hex       | `&str`      | `"#ff0080"`               |
13//! | Named CSS | `&str`      | `"red"`, `"transparent"`  |
14//!
15//! # Example
16//!
17//! ```
18//! use qrcode_render::colors::hex_to_rgb;
19//!
20//! // Works under any feature combination (no renderer backend required).
21//! assert_eq!(hex_to_rgb("#1a1a2e"), Some((0x1a, 0x1a, 0x2e)));
22//! assert_eq!(hex_to_rgb("invalid"), None);
23//! ```
24
25#[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
35/// Parses a `#rgb` or `#rrggbb` hex color string into RGB bytes.
36///
37/// Returns `None` if the format is invalid.
38///
39/// # Example
40///
41/// ```
42/// use qrcode_render::colors::hex_to_rgb;
43/// assert_eq!(hex_to_rgb("#ff0080"), Some((255, 0, 128)));
44/// assert_eq!(hex_to_rgb("#000"), Some((0, 0, 0)));
45/// assert_eq!(hex_to_rgb("invalid"), None);
46/// ```
47pub 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
66/// Parses a `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa` hex color string into RGBA bytes.
67///
68/// For `#rrggbb` format, alpha defaults to 255 (fully opaque).
69///
70/// # Example
71///
72/// ```
73/// use qrcode_render::colors::hex_to_rgba;
74/// assert_eq!(hex_to_rgba("#ff0080"), Some((255, 0, 128, 255)));
75/// assert_eq!(hex_to_rgba("#ff008080"), Some((255, 0, 128, 128)));
76/// assert_eq!(hex_to_rgba("invalid"), None);
77/// ```
78pub 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
114/// Converts RGB bytes to a CSS hex string (e.g., `"#ff0080"`).
115///
116/// # Example
117///
118/// ```
119/// use qrcode_render::colors::rgb_to_hex;
120/// assert_eq!(rgb_to_hex(255, 0, 128), "#ff0080");
121/// ```
122pub fn rgb_to_hex(r: u8, g: u8, b: u8) -> String {
123    format!("#{r:02x}{g:02x}{b:02x}")
124}
125
126/// Converts RGBA bytes to a CSS hex string (e.g., `"#ff0080c0"`).
127///
128/// # Example
129///
130/// ```
131/// use qrcode_render::colors::rgba_to_hex;
132/// assert_eq!(rgba_to_hex(255, 0, 128, 192), "#ff0080c0");
133/// ```
134pub 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
138/// Converts RGB bytes to a `rgb()` CSS function string.
139///
140/// # Example
141///
142/// ```
143/// use qrcode_render::colors::rgb_to_css;
144/// assert_eq!(rgb_to_css(255, 0, 128), "rgb(255,0,128)");
145/// ```
146pub fn rgb_to_css(r: u8, g: u8, b: u8) -> String {
147    format!("rgb({r},{g},{b})")
148}
149
150/// Converts RGBA bytes to a `rgba()` CSS function string.
151///
152/// RGB channels remain integer bytes; alpha is normalized to `0..=1`.
153///
154/// # Example
155///
156/// ```
157/// use qrcode_render::colors::rgba_to_css;
158/// assert_eq!(rgba_to_css(255, 0, 128, 128), "rgba(255,0,128,0.5019607843137255)");
159/// ```
160pub 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/// A unified sRGBA color type for use across all render backends.
166///
167/// `Srgba` provides a single type that can create colors for any renderer:
168/// SVG (CSS hex), ANSI (escape codes), EPS/PDF (0.0–1.0 arrays), image (`Rgba<u8>`).
169///
170/// # Example
171///
172/// ```
173/// use qrcode_render::colors::Srgba;
174///
175/// let c = Srgba::from_hex("#ff0080").unwrap();
176/// assert_eq!(c.to_hex(), "#ff0080");
177/// assert_eq!(c.to_css(), "rgba(255,0,128,1)");
178/// let arr = c.to_array();
179/// assert!((arr[0] - 1.0).abs() < 0.001);
180/// ```
181#[derive(Copy, Clone, Debug, PartialEq, Eq)]
182pub struct Srgba {
183    /// Red component.
184    pub r: u8,
185    /// Green component.
186    pub g: u8,
187    /// Blue component.
188    pub b: u8,
189    /// Alpha component (0 = transparent, 255 = opaque).
190    pub a: u8,
191}
192
193/// A color space that can convert to and from sRGB.
194///
195/// Render backends can accept a concrete color space and lower it to their
196/// native paint operators. RGB-oriented outputs keep using [`RgbColor`], while
197/// print-oriented outputs such as EPS/PDF can preserve [`CmykColor`].
198pub trait ColorSpace: Copy + Sized {
199    /// Component storage used by this color space.
200    type Component: Copy;
201
202    /// Converts this color to sRGB.
203    fn to_rgb(self) -> RgbColor;
204
205    /// Converts an sRGB color into this color space.
206    fn from_rgb(rgb: RgbColor) -> Self;
207}
208
209/// An opaque sRGB color.
210#[derive(Copy, Clone, Debug, PartialEq, Eq)]
211pub struct RgbColor {
212    /// Red component.
213    pub r: u8,
214    /// Green component.
215    pub g: u8,
216    /// Blue component.
217    pub b: u8,
218}
219
220impl RgbColor {
221    /// Creates an opaque sRGB color.
222    pub const fn new(r: u8, g: u8, b: u8) -> Self {
223        Self { r, g, b }
224    }
225
226    /// Converts this color to normalized RGB components.
227    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    /// Converts this color to an opaque [`Srgba`].
232    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/// A CMYK color with normalized `0.0..=1.0` components.
268#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
269pub struct CmykColor {
270    /// Cyan component.
271    pub c: f64,
272    /// Magenta component.
273    pub m: f64,
274    /// Yellow component.
275    pub y: f64,
276    /// Key/black component.
277    pub k: f64,
278}
279
280impl CmykColor {
281    /// Creates a CMYK color, clamping all components to `0.0..=1.0`.
282    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    /// Converts this color to normalized CMYK components.
287    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/// A CIE L*a*b* color using a D65 white point approximation.
328#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
329pub struct LabColor {
330    /// Lightness.
331    pub l: f64,
332    /// Green-red axis.
333    pub a: f64,
334    /// Blue-yellow axis.
335    pub b: f64,
336}
337
338impl LabColor {
339    /// Creates a Lab color.
340    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    /// Creates a new sRGBA color.
389    pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
390        Self { r, g, b, a }
391    }
392
393    /// Creates a fully opaque sRGBA color.
394    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
395        Self { r, g, b, a: 255 }
396    }
397
398    /// Parses a hex color string (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`).
399    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    /// Converts to a CSS hex string (e.g., `"#ff0080"` or `"#ff0080c0"` if alpha < 255).
405    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    /// Converts to a CSS `rgba()` function string with alpha normalized to `0..=1`.
410    pub fn to_css(self) -> String {
411        rgba_to_css(self.r, self.g, self.b, self.a)
412    }
413
414    /// Converts to an ANSI foreground escape sequence (`\x1b[38;2;R;G;Bm`).
415    pub fn to_ansi_fg(self) -> String {
416        format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
417    }
418
419    /// Converts to an ANSI background escape sequence (`\x1b[48;2;R;G;Bm`).
420    pub fn to_ansi_bg(self) -> String {
421        format!("\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
422    }
423
424    /// Converts to a `[f64; 3]` array with values in 0.0–1.0, suitable for EPS/PDF renderers.
425    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    /// Linearly interpolates between `self` and `other`.
430    ///
431    /// `t = 0.0` returns `self`, `t = 1.0` returns `other`.
432    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                // Check the CSS number against the original byte by reversing
607                // the scale, independently of the formatter's division.
608                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}