Skip to main content

oxidize_pdf/graphics/
color.rs

1/// Represents a color in PDF documents.
2///
3/// Supports RGB, Grayscale, and CMYK color spaces.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum Color {
6    /// RGB color (red, green, blue) with values from 0.0 to 1.0
7    Rgb(f64, f64, f64),
8    /// Grayscale color with value from 0.0 (black) to 1.0 (white)
9    Gray(f64),
10    /// CMYK color (cyan, magenta, yellow, key/black) with values from 0.0 to 1.0
11    Cmyk(f64, f64, f64, f64),
12}
13
14impl Color {
15    /// Creates an RGB color with values clamped to 0.0-1.0.
16    pub fn rgb(r: f64, g: f64, b: f64) -> Self {
17        Color::Rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
18    }
19
20    /// Create a color from a hex string like "#RRGGBB"
21    pub fn hex(hex_str: &str) -> Self {
22        let hex = hex_str.trim_start_matches('#');
23        if hex.len() != 6 {
24            return Color::black(); // Default fallback
25        }
26
27        let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0;
28        let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0;
29        let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0;
30
31        Color::rgb(r, g, b)
32    }
33
34    /// Creates a grayscale color with value clamped to 0.0-1.0.
35    pub fn gray(value: f64) -> Self {
36        Color::Gray(value.clamp(0.0, 1.0))
37    }
38
39    /// Creates a CMYK color with values clamped to 0.0-1.0.
40    pub fn cmyk(c: f64, m: f64, y: f64, k: f64) -> Self {
41        Color::Cmyk(
42            c.clamp(0.0, 1.0),
43            m.clamp(0.0, 1.0),
44            y.clamp(0.0, 1.0),
45            k.clamp(0.0, 1.0),
46        )
47    }
48
49    /// Black color (gray 0.0).
50    pub fn black() -> Self {
51        Color::Gray(0.0)
52    }
53
54    /// White color (gray 1.0).
55    pub fn white() -> Self {
56        Color::Gray(1.0)
57    }
58
59    /// Red color (RGB 1,0,0).
60    pub fn red() -> Self {
61        Color::Rgb(1.0, 0.0, 0.0)
62    }
63
64    /// Green color (RGB 0,1,0).
65    pub fn green() -> Self {
66        Color::Rgb(0.0, 1.0, 0.0)
67    }
68
69    /// Blue color (RGB 0,0,1).
70    pub fn blue() -> Self {
71        Color::Rgb(0.0, 0.0, 1.0)
72    }
73
74    pub fn yellow() -> Self {
75        Color::Rgb(1.0, 1.0, 0.0)
76    }
77
78    pub fn cyan() -> Self {
79        Color::Rgb(0.0, 1.0, 1.0)
80    }
81
82    pub fn magenta() -> Self {
83        Color::Rgb(1.0, 0.0, 1.0)
84    }
85
86    /// Pure cyan color in CMYK space (100% cyan, 0% magenta, 0% yellow, 0% black)
87    pub fn cmyk_cyan() -> Self {
88        Color::Cmyk(1.0, 0.0, 0.0, 0.0)
89    }
90
91    /// Pure magenta color in CMYK space (0% cyan, 100% magenta, 0% yellow, 0% black)
92    pub fn cmyk_magenta() -> Self {
93        Color::Cmyk(0.0, 1.0, 0.0, 0.0)
94    }
95
96    /// Pure yellow color in CMYK space (0% cyan, 0% magenta, 100% yellow, 0% black)
97    pub fn cmyk_yellow() -> Self {
98        Color::Cmyk(0.0, 0.0, 1.0, 0.0)
99    }
100
101    /// Pure black color in CMYK space (0% cyan, 0% magenta, 0% yellow, 100% black)
102    pub fn cmyk_black() -> Self {
103        Color::Cmyk(0.0, 0.0, 0.0, 1.0)
104    }
105
106    /// Get red component (converts other color spaces to RGB approximation)
107    pub fn r(&self) -> f64 {
108        match self {
109            Color::Rgb(r, _, _) => *r,
110            Color::Gray(g) => *g,
111            Color::Cmyk(c, _, _, k) => (1.0 - c) * (1.0 - k),
112        }
113    }
114
115    /// Get green component (converts other color spaces to RGB approximation)
116    pub fn g(&self) -> f64 {
117        match self {
118            Color::Rgb(_, g, _) => *g,
119            Color::Gray(g) => *g,
120            Color::Cmyk(_, m, _, k) => (1.0 - m) * (1.0 - k),
121        }
122    }
123
124    /// Get blue component (converts other color spaces to RGB approximation)
125    pub fn b(&self) -> f64 {
126        match self {
127            Color::Rgb(_, _, b) => *b,
128            Color::Gray(g) => *g,
129            Color::Cmyk(_, _, y, k) => (1.0 - y) * (1.0 - k),
130        }
131    }
132
133    /// Get CMYK components (for CMYK colors, or conversion for others)
134    pub fn cmyk_components(&self) -> (f64, f64, f64, f64) {
135        match self {
136            Color::Cmyk(c, m, y, k) => (*c, *m, *y, *k),
137            Color::Rgb(r, g, b) => {
138                // Convert RGB to CMYK using standard formula
139                let k = 1.0 - r.max(*g).max(*b);
140                if k >= 1.0 {
141                    (0.0, 0.0, 0.0, 1.0)
142                } else {
143                    let c = (1.0 - r - k) / (1.0 - k);
144                    let m = (1.0 - g - k) / (1.0 - k);
145                    let y = (1.0 - b - k) / (1.0 - k);
146                    (c, m, y, k)
147                }
148            }
149            Color::Gray(g) => {
150                // Convert grayscale to CMYK (K channel only)
151                let k = 1.0 - g;
152                (0.0, 0.0, 0.0, k)
153            }
154        }
155    }
156
157    /// Convert to RGB color space
158    pub fn to_rgb(&self) -> Color {
159        match self {
160            Color::Rgb(_, _, _) => *self,
161            Color::Gray(g) => Color::Rgb(*g, *g, *g),
162            Color::Cmyk(c, m, y, k) => {
163                // Standard CMYK to RGB conversion
164                let r = (1.0 - c) * (1.0 - k);
165                let g = (1.0 - m) * (1.0 - k);
166                let b = (1.0 - y) * (1.0 - k);
167                Color::Rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
168            }
169        }
170    }
171
172    /// Convert to CMYK color space
173    pub fn to_cmyk(&self) -> Color {
174        match self {
175            Color::Cmyk(_, _, _, _) => *self,
176            _ => {
177                let (c, m, y, k) = self.cmyk_components();
178                Color::Cmyk(c, m, y, k)
179            }
180        }
181    }
182
183    /// Get the color space name for PDF
184    pub fn color_space_name(&self) -> &'static str {
185        match self {
186            Color::Gray(_) => "DeviceGray",
187            Color::Rgb(_, _, _) => "DeviceRGB",
188            Color::Cmyk(_, _, _, _) => "DeviceCMYK",
189        }
190    }
191
192    /// Check if this color is in CMYK color space
193    pub fn is_cmyk(&self) -> bool {
194        matches!(self, Color::Cmyk(_, _, _, _))
195    }
196
197    /// Check if this color is in RGB color space
198    pub fn is_rgb(&self) -> bool {
199        matches!(self, Color::Rgb(_, _, _))
200    }
201
202    /// Check if this color is in grayscale color space
203    pub fn is_gray(&self) -> bool {
204        matches!(self, Color::Gray(_))
205    }
206
207    /// Convert to PDF array representation
208    pub fn to_pdf_array(&self) -> crate::objects::Object {
209        use crate::objects::Object;
210        match self {
211            Color::Gray(g) => Object::Array(vec![Object::Real(*g)]),
212            Color::Rgb(r, g, b) => {
213                Object::Array(vec![Object::Real(*r), Object::Real(*g), Object::Real(*b)])
214            }
215            Color::Cmyk(c, m, y, k) => Object::Array(vec![
216                Object::Real(*c),
217                Object::Real(*m),
218                Object::Real(*y),
219                Object::Real(*k),
220            ]),
221        }
222    }
223}
224
225/// Substitute non-finite floats with `0.0` for safe content-stream emission
226/// (issue #220). Direct enum construction such as `Color::Rgb(f64::NAN, …)`
227/// bypasses the `Color::rgb`/`gray`/`cmyk` clamps; emitting the raw value with
228/// `{:.3}` would produce `NaN`/`inf`/`-inf` tokens, which ISO 32000-1 §7.3.3
229/// rejects as numeric values and conformant viewers reject the entire content
230/// stream.
231///
232/// This is a finite-only check — finite values pass through unchanged
233/// (including values outside `[0.0, 1.0]`, which the renderer is responsible
234/// for clamping). The aim is strictly to keep the wire format syntactically
235/// valid, not to enforce colour-value semantics here.
236#[inline]
237pub(crate) fn finite_or_zero(val: f64) -> f64 {
238    if val.is_finite() {
239        val
240    } else {
241        0.0
242    }
243}
244
245/// Single source of truth for non-stroking colour-operator emission (issue
246/// #221). Returns the operator as a `String` *without* trailing newline,
247/// with NaN/inf sanitisation (issue #220) and `.3`-precision formatting.
248///
249/// Operators emitted (ISO 32000-1 §8.6.8):
250/// - `Color::Rgb(r, g, b)`     → `"r g b rg"`
251/// - `Color::Gray(g)`          → `"g g"`
252/// - `Color::Cmyk(c, m, y, k)` → `"c m y k k"`
253///
254/// Use this when embedding the operator inside a larger PDF expression —
255/// e.g. an annotation `/DA` default-appearance string `/Helv 12 Tf 0 0 0 rg`.
256/// For content-stream emission with trailing newline, prefer
257/// [`write_fill_color`] (writes to `&mut String`) or
258/// [`write_fill_color_bytes`] (writes to `&mut Vec<u8>`).
259pub(crate) fn fill_color_op(color: Color) -> String {
260    use std::fmt::Write;
261    let mut s = String::new();
262    match color {
263        Color::Rgb(r, g, b) => write!(
264            &mut s,
265            "{:.3} {:.3} {:.3} rg",
266            finite_or_zero(r),
267            finite_or_zero(g),
268            finite_or_zero(b)
269        ),
270        Color::Gray(gray) => write!(&mut s, "{:.3} g", finite_or_zero(gray)),
271        Color::Cmyk(c, m, y, k) => write!(
272            &mut s,
273            "{:.3} {:.3} {:.3} {:.3} k",
274            finite_or_zero(c),
275            finite_or_zero(m),
276            finite_or_zero(y),
277            finite_or_zero(k)
278        ),
279    }
280    .expect("writing to a String never fails");
281    s
282}
283
284/// Single source of truth for stroking colour-operator emission. Companion
285/// to [`fill_color_op`] (uppercase `RG` / `G` / `K`). No trailing newline,
286/// NaN/inf sanitised.
287pub(crate) fn stroke_color_op(color: Color) -> String {
288    use std::fmt::Write;
289    let mut s = String::new();
290    match color {
291        Color::Rgb(r, g, b) => write!(
292            &mut s,
293            "{:.3} {:.3} {:.3} RG",
294            finite_or_zero(r),
295            finite_or_zero(g),
296            finite_or_zero(b)
297        ),
298        Color::Gray(gray) => write!(&mut s, "{:.3} G", finite_or_zero(gray)),
299        Color::Cmyk(c, m, y, k) => write!(
300            &mut s,
301            "{:.3} {:.3} {:.3} {:.3} K",
302            finite_or_zero(c),
303            finite_or_zero(m),
304            finite_or_zero(y),
305            finite_or_zero(k)
306        ),
307    }
308    .expect("writing to a String never fails");
309    s
310}
311
312/// Append the non-stroking colour operator for `color` to a `String`
313/// content-stream buffer, followed by a `\n`. Direct emission via
314/// `std::fmt::Write` — no intermediate `String` allocation.
315pub(crate) fn write_fill_color(ops: &mut String, color: Color) {
316    use std::fmt::Write;
317    match color {
318        Color::Rgb(r, g, b) => writeln!(
319            ops,
320            "{:.3} {:.3} {:.3} rg",
321            finite_or_zero(r),
322            finite_or_zero(g),
323            finite_or_zero(b)
324        ),
325        Color::Gray(gray) => writeln!(ops, "{:.3} g", finite_or_zero(gray)),
326        Color::Cmyk(c, m, y, k) => writeln!(
327            ops,
328            "{:.3} {:.3} {:.3} {:.3} k",
329            finite_or_zero(c),
330            finite_or_zero(m),
331            finite_or_zero(y),
332            finite_or_zero(k)
333        ),
334    }
335    .expect("writing to a String never fails");
336}
337
338/// Append the stroking colour operator for `color` to a `String`
339/// content-stream buffer, followed by a `\n`. Companion to
340/// [`write_fill_color`] (uppercase `RG` / `G` / `K`).
341pub(crate) fn write_stroke_color(ops: &mut String, color: Color) {
342    use std::fmt::Write;
343    match color {
344        Color::Rgb(r, g, b) => writeln!(
345            ops,
346            "{:.3} {:.3} {:.3} RG",
347            finite_or_zero(r),
348            finite_or_zero(g),
349            finite_or_zero(b)
350        ),
351        Color::Gray(gray) => writeln!(ops, "{:.3} G", finite_or_zero(gray)),
352        Color::Cmyk(c, m, y, k) => writeln!(
353            ops,
354            "{:.3} {:.3} {:.3} {:.3} K",
355            finite_or_zero(c),
356            finite_or_zero(m),
357            finite_or_zero(y),
358            finite_or_zero(k)
359        ),
360    }
361    .expect("writing to a String never fails");
362}
363
364/// Append the non-stroking colour operator for `color` to a `Vec<u8>`
365/// content-stream buffer, followed by `\n`. Direct emission via
366/// `std::io::Write` — no intermediate allocation. For form-widget streams
367/// in `forms/button_widget.rs`, `forms/appearance.rs`, etc., which use
368/// `Vec<u8>` as their accumulator.
369pub(crate) fn write_fill_color_bytes(ops: &mut Vec<u8>, color: Color) {
370    use std::io::Write;
371    match color {
372        Color::Rgb(r, g, b) => writeln!(
373            ops,
374            "{:.3} {:.3} {:.3} rg",
375            finite_or_zero(r),
376            finite_or_zero(g),
377            finite_or_zero(b)
378        ),
379        Color::Gray(gray) => writeln!(ops, "{:.3} g", finite_or_zero(gray)),
380        Color::Cmyk(c, m, y, k) => writeln!(
381            ops,
382            "{:.3} {:.3} {:.3} {:.3} k",
383            finite_or_zero(c),
384            finite_or_zero(m),
385            finite_or_zero(y),
386            finite_or_zero(k)
387        ),
388    }
389    .expect("writing to a Vec<u8> never fails");
390}
391
392/// Append the stroking colour operator for `color` to a `Vec<u8>`
393/// content-stream buffer, followed by `\n`. Companion to
394/// [`write_fill_color_bytes`].
395pub(crate) fn write_stroke_color_bytes(ops: &mut Vec<u8>, color: Color) {
396    use std::io::Write;
397    match color {
398        Color::Rgb(r, g, b) => writeln!(
399            ops,
400            "{:.3} {:.3} {:.3} RG",
401            finite_or_zero(r),
402            finite_or_zero(g),
403            finite_or_zero(b)
404        ),
405        Color::Gray(gray) => writeln!(ops, "{:.3} G", finite_or_zero(gray)),
406        Color::Cmyk(c, m, y, k) => writeln!(
407            ops,
408            "{:.3} {:.3} {:.3} {:.3} K",
409            finite_or_zero(c),
410            finite_or_zero(m),
411            finite_or_zero(y),
412            finite_or_zero(k)
413        ),
414    }
415    .expect("writing to a Vec<u8> never fails");
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    #[test]
423    fn test_rgb_color_creation() {
424        let color = Color::rgb(0.5, 0.7, 0.3);
425        assert_eq!(color, Color::Rgb(0.5, 0.7, 0.3));
426    }
427
428    #[test]
429    fn test_rgb_color_clamping() {
430        let color = Color::rgb(1.5, -0.3, 0.5);
431        assert_eq!(color, Color::Rgb(1.0, 0.0, 0.5));
432    }
433
434    #[test]
435    fn test_gray_color_creation() {
436        let color = Color::gray(0.5);
437        assert_eq!(color, Color::Gray(0.5));
438    }
439
440    #[test]
441    fn test_gray_color_clamping() {
442        let color1 = Color::gray(1.5);
443        assert_eq!(color1, Color::Gray(1.0));
444
445        let color2 = Color::gray(-0.5);
446        assert_eq!(color2, Color::Gray(0.0));
447    }
448
449    #[test]
450    fn test_cmyk_color_creation() {
451        let color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
452        assert_eq!(color, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
453    }
454
455    #[test]
456    fn test_cmyk_color_clamping() {
457        let color = Color::cmyk(1.5, -0.2, 0.5, 2.0);
458        assert_eq!(color, Color::Cmyk(1.0, 0.0, 0.5, 1.0));
459    }
460
461    #[test]
462    fn test_predefined_colors() {
463        assert_eq!(Color::black(), Color::Gray(0.0));
464        assert_eq!(Color::white(), Color::Gray(1.0));
465        assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
466        assert_eq!(Color::green(), Color::Rgb(0.0, 1.0, 0.0));
467        assert_eq!(Color::blue(), Color::Rgb(0.0, 0.0, 1.0));
468        assert_eq!(Color::yellow(), Color::Rgb(1.0, 1.0, 0.0));
469        assert_eq!(Color::cyan(), Color::Rgb(0.0, 1.0, 1.0));
470        assert_eq!(Color::magenta(), Color::Rgb(1.0, 0.0, 1.0));
471    }
472
473    #[test]
474    fn test_color_equality() {
475        let color1 = Color::rgb(0.5, 0.5, 0.5);
476        let color2 = Color::rgb(0.5, 0.5, 0.5);
477        let color3 = Color::rgb(0.5, 0.5, 0.6);
478
479        assert_eq!(color1, color2);
480        assert_ne!(color1, color3);
481
482        let gray1 = Color::gray(0.5);
483        let gray2 = Color::gray(0.5);
484        assert_eq!(gray1, gray2);
485
486        let cmyk1 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
487        let cmyk2 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
488        assert_eq!(cmyk1, cmyk2);
489    }
490
491    #[test]
492    fn test_color_different_types_inequality() {
493        let rgb = Color::rgb(0.5, 0.5, 0.5);
494        let gray = Color::gray(0.5);
495        let cmyk = Color::cmyk(0.5, 0.5, 0.5, 0.5);
496
497        assert_ne!(rgb, gray);
498        assert_ne!(rgb, cmyk);
499        assert_ne!(gray, cmyk);
500    }
501
502    #[test]
503    fn test_color_debug() {
504        let rgb = Color::rgb(0.1, 0.2, 0.3);
505        let debug_str = format!("{rgb:?}");
506        assert!(debug_str.contains("Rgb"));
507        assert!(debug_str.contains("0.1"));
508        assert!(debug_str.contains("0.2"));
509        assert!(debug_str.contains("0.3"));
510
511        let gray = Color::gray(0.5);
512        let gray_debug = format!("{gray:?}");
513        assert!(gray_debug.contains("Gray"));
514        assert!(gray_debug.contains("0.5"));
515
516        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
517        let cmyk_debug = format!("{cmyk:?}");
518        assert!(cmyk_debug.contains("Cmyk"));
519        assert!(cmyk_debug.contains("0.1"));
520        assert!(cmyk_debug.contains("0.2"));
521        assert!(cmyk_debug.contains("0.3"));
522        assert!(cmyk_debug.contains("0.4"));
523    }
524
525    #[test]
526    fn test_color_clone() {
527        let rgb = Color::rgb(0.5, 0.6, 0.7);
528        let rgb_clone = rgb;
529        assert_eq!(rgb, rgb_clone);
530
531        let gray = Color::gray(0.5);
532        let gray_clone = gray;
533        assert_eq!(gray, gray_clone);
534
535        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
536        let cmyk_clone = cmyk;
537        assert_eq!(cmyk, cmyk_clone);
538    }
539
540    #[test]
541    fn test_color_copy() {
542        let rgb = Color::rgb(0.5, 0.6, 0.7);
543        let rgb_copy = rgb; // Copy semantics
544        assert_eq!(rgb, rgb_copy);
545
546        // Both should still be usable
547        assert_eq!(rgb, Color::Rgb(0.5, 0.6, 0.7));
548        assert_eq!(rgb_copy, Color::Rgb(0.5, 0.6, 0.7));
549    }
550
551    #[test]
552    fn test_edge_case_values() {
553        // Test exact boundary values
554        let color = Color::rgb(0.0, 0.5, 1.0);
555        assert_eq!(color, Color::Rgb(0.0, 0.5, 1.0));
556
557        let gray = Color::gray(0.0);
558        assert_eq!(gray, Color::Gray(0.0));
559
560        let gray_max = Color::gray(1.0);
561        assert_eq!(gray_max, Color::Gray(1.0));
562
563        let cmyk = Color::cmyk(0.0, 0.0, 0.0, 0.0);
564        assert_eq!(cmyk, Color::Cmyk(0.0, 0.0, 0.0, 0.0));
565
566        let cmyk_max = Color::cmyk(1.0, 1.0, 1.0, 1.0);
567        assert_eq!(cmyk_max, Color::Cmyk(1.0, 1.0, 1.0, 1.0));
568    }
569
570    #[test]
571    fn test_floating_point_precision() {
572        let color = Color::rgb(0.333333333, 0.666666666, 0.999999999);
573        match color {
574            Color::Rgb(r, g, b) => {
575                assert!((r - 0.333333333).abs() < 1e-9);
576                assert!((g - 0.666666666).abs() < 1e-9);
577                assert!((b - 0.999999999).abs() < 1e-9);
578            }
579            _ => panic!("Expected RGB color"),
580        }
581    }
582
583    #[test]
584    fn test_rgb_clamping_infinity() {
585        // Test infinity handling
586        let inf_color = Color::rgb(f64::INFINITY, f64::NEG_INFINITY, 0.5);
587        assert_eq!(inf_color, Color::Rgb(1.0, 0.0, 0.5));
588
589        // Test large positive and negative values
590        let large_color = Color::rgb(1000.0, -1000.0, 0.5);
591        assert_eq!(large_color, Color::Rgb(1.0, 0.0, 0.5));
592    }
593
594    #[test]
595    fn test_cmyk_all_components() {
596        // Test that all CMYK components are properly stored
597        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
598        match cmyk {
599            Color::Cmyk(c, m, y, k) => {
600                assert_eq!(c, 0.1);
601                assert_eq!(m, 0.2);
602                assert_eq!(y, 0.3);
603                assert_eq!(k, 0.4);
604            }
605            _ => panic!("Expected CMYK color"),
606        }
607    }
608
609    #[test]
610    fn test_pattern_matching() {
611        let colors = vec![
612            Color::rgb(0.5, 0.5, 0.5),
613            Color::gray(0.5),
614            Color::cmyk(0.1, 0.2, 0.3, 0.4),
615        ];
616
617        let mut rgb_count = 0;
618        let mut gray_count = 0;
619        let mut cmyk_count = 0;
620
621        for color in colors {
622            match color {
623                Color::Rgb(_, _, _) => rgb_count += 1,
624                Color::Gray(_) => gray_count += 1,
625                Color::Cmyk(_, _, _, _) => cmyk_count += 1,
626            }
627        }
628
629        assert_eq!(rgb_count, 1);
630        assert_eq!(gray_count, 1);
631        assert_eq!(cmyk_count, 1);
632    }
633
634    #[test]
635    fn test_cmyk_pure_colors() {
636        // Test pure CMYK colors
637        assert_eq!(Color::cmyk_cyan(), Color::Cmyk(1.0, 0.0, 0.0, 0.0));
638        assert_eq!(Color::cmyk_magenta(), Color::Cmyk(0.0, 1.0, 0.0, 0.0));
639        assert_eq!(Color::cmyk_yellow(), Color::Cmyk(0.0, 0.0, 1.0, 0.0));
640        assert_eq!(Color::cmyk_black(), Color::Cmyk(0.0, 0.0, 0.0, 1.0));
641    }
642
643    #[test]
644    fn test_cmyk_to_rgb_conversion() {
645        // Test CMYK to RGB conversion
646        let pure_cyan = Color::cmyk_cyan().to_rgb();
647        match pure_cyan {
648            Color::Rgb(r, g, b) => {
649                assert_eq!(r, 0.0);
650                assert_eq!(g, 1.0);
651                assert_eq!(b, 1.0);
652            }
653            _ => panic!("Expected RGB color"),
654        }
655
656        let pure_magenta = Color::cmyk_magenta().to_rgb();
657        match pure_magenta {
658            Color::Rgb(r, g, b) => {
659                assert_eq!(r, 1.0);
660                assert_eq!(g, 0.0);
661                assert_eq!(b, 1.0);
662            }
663            _ => panic!("Expected RGB color"),
664        }
665
666        let pure_yellow = Color::cmyk_yellow().to_rgb();
667        match pure_yellow {
668            Color::Rgb(r, g, b) => {
669                assert_eq!(r, 1.0);
670                assert_eq!(g, 1.0);
671                assert_eq!(b, 0.0);
672            }
673            _ => panic!("Expected RGB color"),
674        }
675
676        let pure_black = Color::cmyk_black().to_rgb();
677        match pure_black {
678            Color::Rgb(r, g, b) => {
679                assert_eq!(r, 0.0);
680                assert_eq!(g, 0.0);
681                assert_eq!(b, 0.0);
682            }
683            _ => panic!("Expected RGB color"),
684        }
685    }
686
687    #[test]
688    fn test_rgb_to_cmyk_conversion() {
689        // Test RGB to CMYK conversion
690        let red = Color::red().to_cmyk();
691        let (c, m, y, k) = red.cmyk_components();
692        assert_eq!(c, 0.0);
693        assert_eq!(m, 1.0);
694        assert_eq!(y, 1.0);
695        assert_eq!(k, 0.0);
696
697        let green = Color::green().to_cmyk();
698        let (c, m, y, k) = green.cmyk_components();
699        assert_eq!(c, 1.0);
700        assert_eq!(m, 0.0);
701        assert_eq!(y, 1.0);
702        assert_eq!(k, 0.0);
703
704        let blue = Color::blue().to_cmyk();
705        let (c, m, y, k) = blue.cmyk_components();
706        assert_eq!(c, 1.0);
707        assert_eq!(m, 1.0);
708        assert_eq!(y, 0.0);
709        assert_eq!(k, 0.0);
710
711        let black = Color::black().to_cmyk();
712        let (c, m, y, k) = black.cmyk_components();
713        assert_eq!(c, 0.0);
714        assert_eq!(m, 0.0);
715        assert_eq!(y, 0.0);
716        assert_eq!(k, 1.0);
717    }
718
719    #[test]
720    fn test_color_space_detection() {
721        assert!(Color::rgb(0.5, 0.5, 0.5).is_rgb());
722        assert!(!Color::rgb(0.5, 0.5, 0.5).is_cmyk());
723        assert!(!Color::rgb(0.5, 0.5, 0.5).is_gray());
724
725        assert!(Color::gray(0.5).is_gray());
726        assert!(!Color::gray(0.5).is_rgb());
727        assert!(!Color::gray(0.5).is_cmyk());
728
729        assert!(Color::cmyk(0.1, 0.2, 0.3, 0.4).is_cmyk());
730        assert!(!Color::cmyk(0.1, 0.2, 0.3, 0.4).is_rgb());
731        assert!(!Color::cmyk(0.1, 0.2, 0.3, 0.4).is_gray());
732    }
733
734    #[test]
735    fn test_color_space_names() {
736        assert_eq!(Color::rgb(0.5, 0.5, 0.5).color_space_name(), "DeviceRGB");
737        assert_eq!(Color::gray(0.5).color_space_name(), "DeviceGray");
738        assert_eq!(
739            Color::cmyk(0.1, 0.2, 0.3, 0.4).color_space_name(),
740            "DeviceCMYK"
741        );
742    }
743
744    #[test]
745    fn test_cmyk_components_extraction() {
746        let cmyk_color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
747        let (c, m, y, k) = cmyk_color.cmyk_components();
748        assert_eq!(c, 0.1);
749        assert_eq!(m, 0.2);
750        assert_eq!(y, 0.3);
751        assert_eq!(k, 0.4);
752
753        // Test RGB to CMYK component conversion
754        let white = Color::white();
755        let (c, m, y, k) = white.cmyk_components();
756        assert_eq!(c, 0.0);
757        assert_eq!(m, 0.0);
758        assert_eq!(y, 0.0);
759        assert_eq!(k, 0.0);
760    }
761
762    #[test]
763    fn test_roundtrip_conversions() {
764        // Test that conversion cycles preserve color reasonably well
765        let original_rgb = Color::rgb(0.6, 0.3, 0.9);
766        let converted_cmyk = original_rgb.to_cmyk();
767        let back_to_rgb = converted_cmyk.to_rgb();
768
769        let orig_components = (original_rgb.r(), original_rgb.g(), original_rgb.b());
770        let final_components = (back_to_rgb.r(), back_to_rgb.g(), back_to_rgb.b());
771
772        // Allow small tolerance for floating point conversion errors
773        assert!((orig_components.0 - final_components.0).abs() < 0.001);
774        assert!((orig_components.1 - final_components.1).abs() < 0.001);
775        assert!((orig_components.2 - final_components.2).abs() < 0.001);
776    }
777
778    #[test]
779    fn test_grayscale_to_cmyk_conversion() {
780        let gray = Color::gray(0.7);
781        let (c, m, y, k) = gray.cmyk_components();
782
783        assert_eq!(c, 0.0);
784        assert_eq!(m, 0.0);
785        assert_eq!(y, 0.0);
786        assert!((k - 0.3).abs() < 1e-10); // k = 1.0 - gray_value (with tolerance for floating point precision)
787
788        let gray_as_cmyk = gray.to_cmyk();
789        let cmyk_components = gray_as_cmyk.cmyk_components();
790        assert_eq!(cmyk_components.0, 0.0);
791        assert_eq!(cmyk_components.1, 0.0);
792        assert_eq!(cmyk_components.2, 0.0);
793        assert!((cmyk_components.3 - 0.3).abs() < 1e-10);
794    }
795
796    // =============================================================================
797    // RIGOROUS TESTS FOR UNCOVERED LINES (Target: 100% coverage)
798    // =============================================================================
799
800    #[test]
801    fn test_hex_color_invalid_length() {
802        // Line 24: hex string with invalid length should return black
803        let color1 = Color::hex("#FFF");
804        assert_eq!(
805            color1,
806            Color::black(),
807            "Short hex string should return black"
808        );
809
810        let color2 = Color::hex("#FFFFFFF");
811        assert_eq!(
812            color2,
813            Color::black(),
814            "Long hex string should return black"
815        );
816
817        let color3 = Color::hex("");
818        assert_eq!(
819            color3,
820            Color::black(),
821            "Empty hex string should return black"
822        );
823    }
824
825    #[test]
826    fn test_hex_color_valid() {
827        let color = Color::hex("#FF0080");
828
829        // Verify RGB components: FF = 255/255 = 1.0, 00 = 0/255 = 0.0, 80 = 128/255 ≈ 0.502
830        assert!((color.r() - 1.0).abs() < 0.01, "Red should be 1.0");
831        assert!((color.g() - 0.0).abs() < 0.01, "Green should be 0.0");
832        assert!((color.b() - 0.502).abs() < 0.01, "Blue should be ~0.502");
833    }
834
835    #[test]
836    fn test_hex_color_without_hash() {
837        let color = Color::hex("00FF00");
838
839        // Green color: 00 = 0, FF = 255, 00 = 0
840        assert_eq!(color.r(), 0.0, "Red should be 0.0");
841        assert_eq!(color.g(), 1.0, "Green should be 1.0");
842        assert_eq!(color.b(), 0.0, "Blue should be 0.0");
843    }
844
845    #[test]
846    fn test_cmyk_r_component() {
847        // Lines 110-111: Test CMYK branch of r() method
848        let cmyk_color = Color::cmyk(0.5, 0.2, 0.3, 0.1);
849        let r = cmyk_color.r();
850
851        // Formula: (1.0 - c) * (1.0 - k) = (1.0 - 0.5) * (1.0 - 0.1) = 0.5 * 0.9 = 0.45
852        assert!((r - 0.45).abs() < 1e-10, "CMYK r() should be 0.45");
853    }
854
855    #[test]
856    fn test_cmyk_g_component() {
857        // Lines 119-120: Test CMYK branch of g() method
858        let cmyk_color = Color::cmyk(0.2, 0.6, 0.3, 0.2);
859        let g = cmyk_color.g();
860
861        // Formula: (1.0 - m) * (1.0 - k) = (1.0 - 0.6) * (1.0 - 0.2) = 0.4 * 0.8 = 0.32
862        assert!((g - 0.32).abs() < 1e-10, "CMYK g() should be 0.32");
863    }
864
865    #[test]
866    fn test_cmyk_b_component() {
867        // Lines 128-129: Test CMYK branch of b() method
868        let cmyk_color = Color::cmyk(0.3, 0.2, 0.7, 0.15);
869        let b = cmyk_color.b();
870
871        // Formula: (1.0 - y) * (1.0 - k) = (1.0 - 0.7) * (1.0 - 0.15) = 0.3 * 0.85 = 0.255
872        assert!((b - 0.255).abs() < 1e-10, "CMYK b() should be 0.255");
873    }
874
875    #[test]
876    fn test_to_rgb_when_already_rgb() {
877        // Line 141: to_rgb() when color is already RGB should return self
878        let rgb_color = Color::rgb(0.5, 0.6, 0.7);
879        let converted = rgb_color.to_rgb();
880
881        assert_eq!(converted, rgb_color, "to_rgb() on RGB should return self");
882        assert_eq!(converted.r(), 0.5);
883        assert_eq!(converted.g(), 0.6);
884        assert_eq!(converted.b(), 0.7);
885    }
886
887    #[test]
888    fn test_to_cmyk_when_already_cmyk() {
889        // Lines 160-161: to_cmyk() when color is already CMYK should return self
890        let cmyk_color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
891        let converted = cmyk_color.to_cmyk();
892
893        assert_eq!(
894            converted, cmyk_color,
895            "to_cmyk() on CMYK should return self"
896        );
897        let (c, m, y, k) = converted.cmyk_components();
898        assert_eq!(c, 0.1);
899        assert_eq!(m, 0.2);
900        assert_eq!(y, 0.3);
901        assert_eq!(k, 0.4);
902    }
903
904    #[test]
905    fn test_to_pdf_array_gray() {
906        // Line 175: to_pdf_array() for Gray color
907        use crate::objects::Object;
908
909        let gray = Color::gray(0.75);
910        let array = gray.to_pdf_array();
911
912        match array {
913            Object::Array(vec) => {
914                assert_eq!(vec.len(), 1, "Gray PDF array should have 1 element");
915                match &vec[0] {
916                    Object::Real(val) => assert_eq!(*val, 0.75, "Gray value should be 0.75"),
917                    _ => panic!("Expected Real object"),
918                }
919            }
920            _ => panic!("Expected Array object"),
921        }
922    }
923
924    #[test]
925    fn test_to_pdf_array_rgb() {
926        // Lines 211, 215-219: to_pdf_array() for RGB color
927        use crate::objects::Object;
928
929        let rgb = Color::rgb(0.2, 0.5, 0.9);
930        let array = rgb.to_pdf_array();
931
932        match array {
933            Object::Array(vec) => {
934                assert_eq!(vec.len(), 3, "RGB PDF array should have 3 elements");
935                match (&vec[0], &vec[1], &vec[2]) {
936                    (Object::Real(r), Object::Real(g), Object::Real(b)) => {
937                        assert_eq!(*r, 0.2, "Red should be 0.2");
938                        assert_eq!(*g, 0.5, "Green should be 0.5");
939                        assert_eq!(*b, 0.9, "Blue should be 0.9");
940                    }
941                    _ => panic!("Expected Real objects"),
942                }
943            }
944            _ => panic!("Expected Array object"),
945        }
946    }
947
948    #[test]
949    fn test_to_pdf_array_cmyk() {
950        // Lines 215-219: to_pdf_array() for CMYK color
951        use crate::objects::Object;
952
953        let cmyk = Color::cmyk(0.1, 0.3, 0.5, 0.7);
954        let array = cmyk.to_pdf_array();
955
956        match array {
957            Object::Array(vec) => {
958                assert_eq!(vec.len(), 4, "CMYK PDF array should have 4 elements");
959                match (&vec[0], &vec[1], &vec[2], &vec[3]) {
960                    (Object::Real(c), Object::Real(m), Object::Real(y), Object::Real(k)) => {
961                        assert_eq!(*c, 0.1, "Cyan should be 0.1");
962                        assert_eq!(*m, 0.3, "Magenta should be 0.3");
963                        assert_eq!(*y, 0.5, "Yellow should be 0.5");
964                        assert_eq!(*k, 0.7, "Black should be 0.7");
965                    }
966                    _ => panic!("Expected Real objects"),
967                }
968            }
969            _ => panic!("Expected Array object"),
970        }
971    }
972
973    #[test]
974    fn test_cmyk_components_all_branches() {
975        // Test all match branches in cmyk_components()
976
977        // CMYK color (direct return)
978        let cmyk = Color::cmyk(0.2, 0.4, 0.6, 0.8);
979        let (c, m, y, k) = cmyk.cmyk_components();
980        assert_eq!(c, 0.2);
981        assert_eq!(m, 0.4);
982        assert_eq!(y, 0.6);
983        assert_eq!(k, 0.8);
984
985        // RGB color (conversion)
986        let rgb = Color::rgb(0.5, 0.25, 0.75);
987        let (_c, _m, _y, k) = rgb.cmyk_components();
988        // k = 1.0 - max(0.5, 0.25, 0.75) = 1.0 - 0.75 = 0.25
989        assert!((k - 0.25).abs() < 1e-10, "K should be 0.25");
990
991        // Gray color (K channel only)
992        let gray = Color::gray(0.4);
993        let (c, m, y, k) = gray.cmyk_components();
994        assert_eq!(c, 0.0);
995        assert_eq!(m, 0.0);
996        assert_eq!(y, 0.0);
997        assert!((k - 0.6).abs() < 1e-10, "K should be 0.6 (1.0 - 0.4)");
998    }
999
1000    #[test]
1001    fn test_color_conversions_preserve_match_branches() {
1002        // Ensure all conversion branches are exercised
1003
1004        // RGB → CMYK → RGB
1005        let rgb = Color::rgb(0.8, 0.4, 0.6);
1006        let as_cmyk = rgb.to_cmyk();
1007        let back_to_rgb = as_cmyk.to_rgb();
1008
1009        // Verify conversion preserves color within tolerance
1010        assert!((rgb.r() - back_to_rgb.r()).abs() < 0.01);
1011        assert!((rgb.g() - back_to_rgb.g()).abs() < 0.01);
1012        assert!((rgb.b() - back_to_rgb.b()).abs() < 0.01);
1013
1014        // Gray → CMYK
1015        let gray = Color::gray(0.5);
1016        let gray_as_cmyk = gray.to_cmyk();
1017        let (c, m, y, k) = gray_as_cmyk.cmyk_components();
1018        assert_eq!(c, 0.0);
1019        assert_eq!(m, 0.0);
1020        assert_eq!(y, 0.0);
1021        assert!((k - 0.5).abs() < 1e-10);
1022    }
1023
1024    #[test]
1025    fn test_gray_r_g_b_components() {
1026        // Lines 110, 119, 128: Gray branch of r(), g(), b()
1027        let gray = Color::gray(0.6);
1028
1029        let r = gray.r();
1030        let g = gray.g();
1031        let b = gray.b();
1032
1033        // Gray returns same value for r, g, and b
1034        assert_eq!(r, 0.6, "Gray r() should return gray value");
1035        assert_eq!(g, 0.6, "Gray g() should return gray value");
1036        assert_eq!(b, 0.6, "Gray b() should return gray value");
1037    }
1038
1039    #[test]
1040    fn test_to_rgb_gray_conversion() {
1041        // Line 161: Gray → RGB conversion
1042        let gray = Color::gray(0.4);
1043        let rgb = gray.to_rgb();
1044
1045        match rgb {
1046            Color::Rgb(r, g, b) => {
1047                assert_eq!(r, 0.4, "Gray → RGB should set r = gray value");
1048                assert_eq!(g, 0.4, "Gray → RGB should set g = gray value");
1049                assert_eq!(b, 0.4, "Gray → RGB should set b = gray value");
1050            }
1051            _ => panic!("Expected RGB color from Gray.to_rgb()"),
1052        }
1053    }
1054
1055    #[test]
1056    fn test_rgb_black_to_cmyk() {
1057        // Line 141: RGB(0,0,0) should convert to CMYK (0,0,0,1) - pure black
1058        let black_rgb = Color::rgb(0.0, 0.0, 0.0);
1059        let (c, m, y, k) = black_rgb.cmyk_components();
1060
1061        // When all RGB are 0, k = 1.0 - max(0,0,0) = 1.0, so k >= 1.0 branch activates
1062        assert_eq!(c, 0.0, "Cyan should be 0 for pure black");
1063        assert_eq!(m, 0.0, "Magenta should be 0 for pure black");
1064        assert_eq!(y, 0.0, "Yellow should be 0 for pure black");
1065        assert_eq!(k, 1.0, "K should be 1.0 for pure black");
1066    }
1067
1068    // ---------- write_fill_color / write_stroke_color helpers (issues #220, #221) ----------
1069
1070    #[test]
1071    fn finite_or_zero_passes_finite_values_unchanged() {
1072        assert_eq!(finite_or_zero(0.0), 0.0);
1073        assert_eq!(finite_or_zero(0.5), 0.5);
1074        assert_eq!(finite_or_zero(1.0), 1.0);
1075        assert_eq!(finite_or_zero(-0.5), -0.5); // out-of-range but finite — caller's problem
1076        assert_eq!(finite_or_zero(2.0), 2.0);
1077    }
1078
1079    #[test]
1080    fn finite_or_zero_replaces_nan_with_zero() {
1081        assert_eq!(finite_or_zero(f64::NAN), 0.0);
1082    }
1083
1084    #[test]
1085    fn finite_or_zero_replaces_pos_inf_with_zero() {
1086        assert_eq!(finite_or_zero(f64::INFINITY), 0.0);
1087    }
1088
1089    #[test]
1090    fn finite_or_zero_replaces_neg_inf_with_zero() {
1091        assert_eq!(finite_or_zero(f64::NEG_INFINITY), 0.0);
1092    }
1093
1094    #[test]
1095    fn write_fill_color_rgb_emits_lowercase_rg_with_three_decimals() {
1096        let mut ops = String::new();
1097        write_fill_color(&mut ops, Color::Rgb(0.25, 0.5, 0.75));
1098        assert_eq!(ops, "0.250 0.500 0.750 rg\n");
1099    }
1100
1101    #[test]
1102    fn write_fill_color_gray_emits_lowercase_g() {
1103        let mut ops = String::new();
1104        write_fill_color(&mut ops, Color::Gray(0.5));
1105        assert_eq!(ops, "0.500 g\n");
1106    }
1107
1108    #[test]
1109    fn write_fill_color_cmyk_emits_lowercase_k() {
1110        let mut ops = String::new();
1111        write_fill_color(&mut ops, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
1112        assert_eq!(ops, "0.100 0.200 0.300 0.400 k\n");
1113    }
1114
1115    #[test]
1116    fn write_stroke_color_rgb_emits_uppercase_rg() {
1117        let mut ops = String::new();
1118        write_stroke_color(&mut ops, Color::Rgb(0.25, 0.5, 0.75));
1119        assert_eq!(ops, "0.250 0.500 0.750 RG\n");
1120    }
1121
1122    #[test]
1123    fn write_stroke_color_gray_emits_uppercase_g() {
1124        let mut ops = String::new();
1125        write_stroke_color(&mut ops, Color::Gray(0.5));
1126        assert_eq!(ops, "0.500 G\n");
1127    }
1128
1129    #[test]
1130    fn write_stroke_color_cmyk_emits_uppercase_k() {
1131        let mut ops = String::new();
1132        write_stroke_color(&mut ops, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
1133        assert_eq!(ops, "0.100 0.200 0.300 0.400 K\n");
1134    }
1135
1136    #[test]
1137    fn write_fill_color_sanitises_nan_red_only() {
1138        let mut ops = String::new();
1139        write_fill_color(&mut ops, Color::Rgb(f64::NAN, 0.5, 0.75));
1140        assert_eq!(ops, "0.000 0.500 0.750 rg\n");
1141    }
1142
1143    #[test]
1144    fn write_fill_color_sanitises_pos_inf() {
1145        let mut ops = String::new();
1146        write_fill_color(&mut ops, Color::Gray(f64::INFINITY));
1147        assert_eq!(ops, "0.000 g\n");
1148    }
1149
1150    #[test]
1151    fn write_fill_color_sanitises_neg_inf() {
1152        let mut ops = String::new();
1153        write_fill_color(&mut ops, Color::Cmyk(f64::NEG_INFINITY, 0.0, 0.0, 0.0));
1154        assert_eq!(ops, "0.000 0.000 0.000 0.000 k\n");
1155    }
1156
1157    #[test]
1158    fn write_stroke_color_sanitises_nan_in_cmyk() {
1159        let mut ops = String::new();
1160        write_stroke_color(&mut ops, Color::Cmyk(0.1, f64::NAN, 0.3, f64::INFINITY));
1161        assert_eq!(ops, "0.100 0.000 0.300 0.000 K\n");
1162    }
1163
1164    #[test]
1165    fn write_fill_color_appends_to_existing_ops_buffer() {
1166        let mut ops = String::from("BT\n");
1167        write_fill_color(&mut ops, Color::Rgb(0.0, 0.0, 0.0));
1168        assert_eq!(ops, "BT\n0.000 0.000 0.000 rg\n");
1169    }
1170
1171    #[test]
1172    fn fill_and_stroke_operators_differ_only_in_case() {
1173        // Lock the case-difference contract: the same colour goes through both
1174        // helpers and emits operators that match in everything except the
1175        // case of the operator-name suffix.
1176        let mut fill_ops = String::new();
1177        let mut stroke_ops = String::new();
1178        write_fill_color(&mut fill_ops, Color::Rgb(0.1, 0.2, 0.3));
1179        write_stroke_color(&mut stroke_ops, Color::Rgb(0.1, 0.2, 0.3));
1180        assert_eq!(fill_ops, "0.100 0.200 0.300 rg\n");
1181        assert_eq!(stroke_ops, "0.100 0.200 0.300 RG\n");
1182    }
1183}