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    /// Creates a grayscale color with value clamped to 0.0-1.0.
21    pub fn gray(value: f64) -> Self {
22        Color::Gray(value.clamp(0.0, 1.0))
23    }
24
25    /// Creates a CMYK color with values clamped to 0.0-1.0.
26    pub fn cmyk(c: f64, m: f64, y: f64, k: f64) -> Self {
27        Color::Cmyk(
28            c.clamp(0.0, 1.0),
29            m.clamp(0.0, 1.0),
30            y.clamp(0.0, 1.0),
31            k.clamp(0.0, 1.0),
32        )
33    }
34
35    /// Black color (gray 0.0).
36    pub fn black() -> Self {
37        Color::Gray(0.0)
38    }
39
40    /// White color (gray 1.0).
41    pub fn white() -> Self {
42        Color::Gray(1.0)
43    }
44
45    /// Red color (RGB 1,0,0).
46    pub fn red() -> Self {
47        Color::Rgb(1.0, 0.0, 0.0)
48    }
49
50    /// Green color (RGB 0,1,0).
51    pub fn green() -> Self {
52        Color::Rgb(0.0, 1.0, 0.0)
53    }
54
55    /// Blue color (RGB 0,0,1).
56    pub fn blue() -> Self {
57        Color::Rgb(0.0, 0.0, 1.0)
58    }
59
60    pub fn yellow() -> Self {
61        Color::Rgb(1.0, 1.0, 0.0)
62    }
63
64    pub fn cyan() -> Self {
65        Color::Rgb(0.0, 1.0, 1.0)
66    }
67
68    pub fn magenta() -> Self {
69        Color::Rgb(1.0, 0.0, 1.0)
70    }
71
72    /// Pure cyan color in CMYK space (100% cyan, 0% magenta, 0% yellow, 0% black)
73    pub fn cmyk_cyan() -> Self {
74        Color::Cmyk(1.0, 0.0, 0.0, 0.0)
75    }
76
77    /// Pure magenta color in CMYK space (0% cyan, 100% magenta, 0% yellow, 0% black)
78    pub fn cmyk_magenta() -> Self {
79        Color::Cmyk(0.0, 1.0, 0.0, 0.0)
80    }
81
82    /// Pure yellow color in CMYK space (0% cyan, 0% magenta, 100% yellow, 0% black)
83    pub fn cmyk_yellow() -> Self {
84        Color::Cmyk(0.0, 0.0, 1.0, 0.0)
85    }
86
87    /// Pure black color in CMYK space (0% cyan, 0% magenta, 0% yellow, 100% black)
88    pub fn cmyk_black() -> Self {
89        Color::Cmyk(0.0, 0.0, 0.0, 1.0)
90    }
91
92    /// Get red component (converts other color spaces to RGB approximation)
93    pub fn r(&self) -> f64 {
94        match self {
95            Color::Rgb(r, _, _) => *r,
96            Color::Gray(g) => *g,
97            Color::Cmyk(c, _, _, k) => (1.0 - c) * (1.0 - k),
98        }
99    }
100
101    /// Get green component (converts other color spaces to RGB approximation)
102    pub fn g(&self) -> f64 {
103        match self {
104            Color::Rgb(_, g, _) => *g,
105            Color::Gray(g) => *g,
106            Color::Cmyk(_, m, _, k) => (1.0 - m) * (1.0 - k),
107        }
108    }
109
110    /// Get blue component (converts other color spaces to RGB approximation)
111    pub fn b(&self) -> f64 {
112        match self {
113            Color::Rgb(_, _, b) => *b,
114            Color::Gray(g) => *g,
115            Color::Cmyk(_, _, y, k) => (1.0 - y) * (1.0 - k),
116        }
117    }
118
119    /// Get CMYK components (for CMYK colors, or conversion for others)
120    pub fn cmyk_components(&self) -> (f64, f64, f64, f64) {
121        match self {
122            Color::Cmyk(c, m, y, k) => (*c, *m, *y, *k),
123            Color::Rgb(r, g, b) => {
124                // Convert RGB to CMYK using standard formula
125                let k = 1.0 - r.max(*g).max(*b);
126                if k >= 1.0 {
127                    (0.0, 0.0, 0.0, 1.0)
128                } else {
129                    let c = (1.0 - r - k) / (1.0 - k);
130                    let m = (1.0 - g - k) / (1.0 - k);
131                    let y = (1.0 - b - k) / (1.0 - k);
132                    (c, m, y, k)
133                }
134            }
135            Color::Gray(g) => {
136                // Convert grayscale to CMYK (K channel only)
137                let k = 1.0 - g;
138                (0.0, 0.0, 0.0, k)
139            }
140        }
141    }
142
143    /// Convert to RGB color space
144    pub fn to_rgb(&self) -> Color {
145        match self {
146            Color::Rgb(_, _, _) => *self,
147            Color::Gray(g) => Color::Rgb(*g, *g, *g),
148            Color::Cmyk(c, m, y, k) => {
149                // Standard CMYK to RGB conversion
150                let r = (1.0 - c) * (1.0 - k);
151                let g = (1.0 - m) * (1.0 - k);
152                let b = (1.0 - y) * (1.0 - k);
153                Color::Rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
154            }
155        }
156    }
157
158    /// Convert to CMYK color space
159    pub fn to_cmyk(&self) -> Color {
160        match self {
161            Color::Cmyk(_, _, _, _) => *self,
162            _ => {
163                let (c, m, y, k) = self.cmyk_components();
164                Color::Cmyk(c, m, y, k)
165            }
166        }
167    }
168
169    /// Get the color space name for PDF
170    pub fn color_space_name(&self) -> &'static str {
171        match self {
172            Color::Gray(_) => "DeviceGray",
173            Color::Rgb(_, _, _) => "DeviceRGB",
174            Color::Cmyk(_, _, _, _) => "DeviceCMYK",
175        }
176    }
177
178    /// Check if this color is in CMYK color space
179    pub fn is_cmyk(&self) -> bool {
180        matches!(self, Color::Cmyk(_, _, _, _))
181    }
182
183    /// Check if this color is in RGB color space
184    pub fn is_rgb(&self) -> bool {
185        matches!(self, Color::Rgb(_, _, _))
186    }
187
188    /// Check if this color is in grayscale color space
189    pub fn is_gray(&self) -> bool {
190        matches!(self, Color::Gray(_))
191    }
192
193    /// Convert to PDF array representation
194    pub fn to_pdf_array(&self) -> crate::objects::Object {
195        use crate::objects::Object;
196        match self {
197            Color::Gray(g) => Object::Array(vec![Object::Real(*g)]),
198            Color::Rgb(r, g, b) => {
199                Object::Array(vec![Object::Real(*r), Object::Real(*g), Object::Real(*b)])
200            }
201            Color::Cmyk(c, m, y, k) => Object::Array(vec![
202                Object::Real(*c),
203                Object::Real(*m),
204                Object::Real(*y),
205                Object::Real(*k),
206            ]),
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_rgb_color_creation() {
217        let color = Color::rgb(0.5, 0.7, 0.3);
218        assert_eq!(color, Color::Rgb(0.5, 0.7, 0.3));
219    }
220
221    #[test]
222    fn test_rgb_color_clamping() {
223        let color = Color::rgb(1.5, -0.3, 0.5);
224        assert_eq!(color, Color::Rgb(1.0, 0.0, 0.5));
225    }
226
227    #[test]
228    fn test_gray_color_creation() {
229        let color = Color::gray(0.5);
230        assert_eq!(color, Color::Gray(0.5));
231    }
232
233    #[test]
234    fn test_gray_color_clamping() {
235        let color1 = Color::gray(1.5);
236        assert_eq!(color1, Color::Gray(1.0));
237
238        let color2 = Color::gray(-0.5);
239        assert_eq!(color2, Color::Gray(0.0));
240    }
241
242    #[test]
243    fn test_cmyk_color_creation() {
244        let color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
245        assert_eq!(color, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
246    }
247
248    #[test]
249    fn test_cmyk_color_clamping() {
250        let color = Color::cmyk(1.5, -0.2, 0.5, 2.0);
251        assert_eq!(color, Color::Cmyk(1.0, 0.0, 0.5, 1.0));
252    }
253
254    #[test]
255    fn test_predefined_colors() {
256        assert_eq!(Color::black(), Color::Gray(0.0));
257        assert_eq!(Color::white(), Color::Gray(1.0));
258        assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
259        assert_eq!(Color::green(), Color::Rgb(0.0, 1.0, 0.0));
260        assert_eq!(Color::blue(), Color::Rgb(0.0, 0.0, 1.0));
261        assert_eq!(Color::yellow(), Color::Rgb(1.0, 1.0, 0.0));
262        assert_eq!(Color::cyan(), Color::Rgb(0.0, 1.0, 1.0));
263        assert_eq!(Color::magenta(), Color::Rgb(1.0, 0.0, 1.0));
264    }
265
266    #[test]
267    fn test_color_equality() {
268        let color1 = Color::rgb(0.5, 0.5, 0.5);
269        let color2 = Color::rgb(0.5, 0.5, 0.5);
270        let color3 = Color::rgb(0.5, 0.5, 0.6);
271
272        assert_eq!(color1, color2);
273        assert_ne!(color1, color3);
274
275        let gray1 = Color::gray(0.5);
276        let gray2 = Color::gray(0.5);
277        assert_eq!(gray1, gray2);
278
279        let cmyk1 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
280        let cmyk2 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
281        assert_eq!(cmyk1, cmyk2);
282    }
283
284    #[test]
285    fn test_color_different_types_inequality() {
286        let rgb = Color::rgb(0.5, 0.5, 0.5);
287        let gray = Color::gray(0.5);
288        let cmyk = Color::cmyk(0.5, 0.5, 0.5, 0.5);
289
290        assert_ne!(rgb, gray);
291        assert_ne!(rgb, cmyk);
292        assert_ne!(gray, cmyk);
293    }
294
295    #[test]
296    fn test_color_debug() {
297        let rgb = Color::rgb(0.1, 0.2, 0.3);
298        let debug_str = format!("{rgb:?}");
299        assert!(debug_str.contains("Rgb"));
300        assert!(debug_str.contains("0.1"));
301        assert!(debug_str.contains("0.2"));
302        assert!(debug_str.contains("0.3"));
303
304        let gray = Color::gray(0.5);
305        let gray_debug = format!("{gray:?}");
306        assert!(gray_debug.contains("Gray"));
307        assert!(gray_debug.contains("0.5"));
308
309        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
310        let cmyk_debug = format!("{cmyk:?}");
311        assert!(cmyk_debug.contains("Cmyk"));
312        assert!(cmyk_debug.contains("0.1"));
313        assert!(cmyk_debug.contains("0.2"));
314        assert!(cmyk_debug.contains("0.3"));
315        assert!(cmyk_debug.contains("0.4"));
316    }
317
318    #[test]
319    fn test_color_clone() {
320        let rgb = Color::rgb(0.5, 0.6, 0.7);
321        let rgb_clone = rgb;
322        assert_eq!(rgb, rgb_clone);
323
324        let gray = Color::gray(0.5);
325        let gray_clone = gray;
326        assert_eq!(gray, gray_clone);
327
328        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
329        let cmyk_clone = cmyk;
330        assert_eq!(cmyk, cmyk_clone);
331    }
332
333    #[test]
334    fn test_color_copy() {
335        let rgb = Color::rgb(0.5, 0.6, 0.7);
336        let rgb_copy = rgb; // Copy semantics
337        assert_eq!(rgb, rgb_copy);
338
339        // Both should still be usable
340        assert_eq!(rgb, Color::Rgb(0.5, 0.6, 0.7));
341        assert_eq!(rgb_copy, Color::Rgb(0.5, 0.6, 0.7));
342    }
343
344    #[test]
345    fn test_edge_case_values() {
346        // Test exact boundary values
347        let color = Color::rgb(0.0, 0.5, 1.0);
348        assert_eq!(color, Color::Rgb(0.0, 0.5, 1.0));
349
350        let gray = Color::gray(0.0);
351        assert_eq!(gray, Color::Gray(0.0));
352
353        let gray_max = Color::gray(1.0);
354        assert_eq!(gray_max, Color::Gray(1.0));
355
356        let cmyk = Color::cmyk(0.0, 0.0, 0.0, 0.0);
357        assert_eq!(cmyk, Color::Cmyk(0.0, 0.0, 0.0, 0.0));
358
359        let cmyk_max = Color::cmyk(1.0, 1.0, 1.0, 1.0);
360        assert_eq!(cmyk_max, Color::Cmyk(1.0, 1.0, 1.0, 1.0));
361    }
362
363    #[test]
364    fn test_floating_point_precision() {
365        let color = Color::rgb(0.333333333, 0.666666666, 0.999999999);
366        match color {
367            Color::Rgb(r, g, b) => {
368                assert!((r - 0.333333333).abs() < 1e-9);
369                assert!((g - 0.666666666).abs() < 1e-9);
370                assert!((b - 0.999999999).abs() < 1e-9);
371            }
372            _ => panic!("Expected RGB color"),
373        }
374    }
375
376    #[test]
377    fn test_rgb_clamping_infinity() {
378        // Test infinity handling
379        let inf_color = Color::rgb(f64::INFINITY, f64::NEG_INFINITY, 0.5);
380        assert_eq!(inf_color, Color::Rgb(1.0, 0.0, 0.5));
381
382        // Test large positive and negative values
383        let large_color = Color::rgb(1000.0, -1000.0, 0.5);
384        assert_eq!(large_color, Color::Rgb(1.0, 0.0, 0.5));
385    }
386
387    #[test]
388    fn test_cmyk_all_components() {
389        // Test that all CMYK components are properly stored
390        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
391        match cmyk {
392            Color::Cmyk(c, m, y, k) => {
393                assert_eq!(c, 0.1);
394                assert_eq!(m, 0.2);
395                assert_eq!(y, 0.3);
396                assert_eq!(k, 0.4);
397            }
398            _ => panic!("Expected CMYK color"),
399        }
400    }
401
402    #[test]
403    fn test_pattern_matching() {
404        let colors = vec![
405            Color::rgb(0.5, 0.5, 0.5),
406            Color::gray(0.5),
407            Color::cmyk(0.1, 0.2, 0.3, 0.4),
408        ];
409
410        let mut rgb_count = 0;
411        let mut gray_count = 0;
412        let mut cmyk_count = 0;
413
414        for color in colors {
415            match color {
416                Color::Rgb(_, _, _) => rgb_count += 1,
417                Color::Gray(_) => gray_count += 1,
418                Color::Cmyk(_, _, _, _) => cmyk_count += 1,
419            }
420        }
421
422        assert_eq!(rgb_count, 1);
423        assert_eq!(gray_count, 1);
424        assert_eq!(cmyk_count, 1);
425    }
426
427    #[test]
428    fn test_cmyk_pure_colors() {
429        // Test pure CMYK colors
430        assert_eq!(Color::cmyk_cyan(), Color::Cmyk(1.0, 0.0, 0.0, 0.0));
431        assert_eq!(Color::cmyk_magenta(), Color::Cmyk(0.0, 1.0, 0.0, 0.0));
432        assert_eq!(Color::cmyk_yellow(), Color::Cmyk(0.0, 0.0, 1.0, 0.0));
433        assert_eq!(Color::cmyk_black(), Color::Cmyk(0.0, 0.0, 0.0, 1.0));
434    }
435
436    #[test]
437    fn test_cmyk_to_rgb_conversion() {
438        // Test CMYK to RGB conversion
439        let pure_cyan = Color::cmyk_cyan().to_rgb();
440        match pure_cyan {
441            Color::Rgb(r, g, b) => {
442                assert_eq!(r, 0.0);
443                assert_eq!(g, 1.0);
444                assert_eq!(b, 1.0);
445            }
446            _ => panic!("Expected RGB color"),
447        }
448
449        let pure_magenta = Color::cmyk_magenta().to_rgb();
450        match pure_magenta {
451            Color::Rgb(r, g, b) => {
452                assert_eq!(r, 1.0);
453                assert_eq!(g, 0.0);
454                assert_eq!(b, 1.0);
455            }
456            _ => panic!("Expected RGB color"),
457        }
458
459        let pure_yellow = Color::cmyk_yellow().to_rgb();
460        match pure_yellow {
461            Color::Rgb(r, g, b) => {
462                assert_eq!(r, 1.0);
463                assert_eq!(g, 1.0);
464                assert_eq!(b, 0.0);
465            }
466            _ => panic!("Expected RGB color"),
467        }
468
469        let pure_black = Color::cmyk_black().to_rgb();
470        match pure_black {
471            Color::Rgb(r, g, b) => {
472                assert_eq!(r, 0.0);
473                assert_eq!(g, 0.0);
474                assert_eq!(b, 0.0);
475            }
476            _ => panic!("Expected RGB color"),
477        }
478    }
479
480    #[test]
481    fn test_rgb_to_cmyk_conversion() {
482        // Test RGB to CMYK conversion
483        let red = Color::red().to_cmyk();
484        let (c, m, y, k) = red.cmyk_components();
485        assert_eq!(c, 0.0);
486        assert_eq!(m, 1.0);
487        assert_eq!(y, 1.0);
488        assert_eq!(k, 0.0);
489
490        let green = Color::green().to_cmyk();
491        let (c, m, y, k) = green.cmyk_components();
492        assert_eq!(c, 1.0);
493        assert_eq!(m, 0.0);
494        assert_eq!(y, 1.0);
495        assert_eq!(k, 0.0);
496
497        let blue = Color::blue().to_cmyk();
498        let (c, m, y, k) = blue.cmyk_components();
499        assert_eq!(c, 1.0);
500        assert_eq!(m, 1.0);
501        assert_eq!(y, 0.0);
502        assert_eq!(k, 0.0);
503
504        let black = Color::black().to_cmyk();
505        let (c, m, y, k) = black.cmyk_components();
506        assert_eq!(c, 0.0);
507        assert_eq!(m, 0.0);
508        assert_eq!(y, 0.0);
509        assert_eq!(k, 1.0);
510    }
511
512    #[test]
513    fn test_color_space_detection() {
514        assert!(Color::rgb(0.5, 0.5, 0.5).is_rgb());
515        assert!(!Color::rgb(0.5, 0.5, 0.5).is_cmyk());
516        assert!(!Color::rgb(0.5, 0.5, 0.5).is_gray());
517
518        assert!(Color::gray(0.5).is_gray());
519        assert!(!Color::gray(0.5).is_rgb());
520        assert!(!Color::gray(0.5).is_cmyk());
521
522        assert!(Color::cmyk(0.1, 0.2, 0.3, 0.4).is_cmyk());
523        assert!(!Color::cmyk(0.1, 0.2, 0.3, 0.4).is_rgb());
524        assert!(!Color::cmyk(0.1, 0.2, 0.3, 0.4).is_gray());
525    }
526
527    #[test]
528    fn test_color_space_names() {
529        assert_eq!(Color::rgb(0.5, 0.5, 0.5).color_space_name(), "DeviceRGB");
530        assert_eq!(Color::gray(0.5).color_space_name(), "DeviceGray");
531        assert_eq!(
532            Color::cmyk(0.1, 0.2, 0.3, 0.4).color_space_name(),
533            "DeviceCMYK"
534        );
535    }
536
537    #[test]
538    fn test_cmyk_components_extraction() {
539        let cmyk_color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
540        let (c, m, y, k) = cmyk_color.cmyk_components();
541        assert_eq!(c, 0.1);
542        assert_eq!(m, 0.2);
543        assert_eq!(y, 0.3);
544        assert_eq!(k, 0.4);
545
546        // Test RGB to CMYK component conversion
547        let white = Color::white();
548        let (c, m, y, k) = white.cmyk_components();
549        assert_eq!(c, 0.0);
550        assert_eq!(m, 0.0);
551        assert_eq!(y, 0.0);
552        assert_eq!(k, 0.0);
553    }
554
555    #[test]
556    fn test_roundtrip_conversions() {
557        // Test that conversion cycles preserve color reasonably well
558        let original_rgb = Color::rgb(0.6, 0.3, 0.9);
559        let converted_cmyk = original_rgb.to_cmyk();
560        let back_to_rgb = converted_cmyk.to_rgb();
561
562        let orig_components = (original_rgb.r(), original_rgb.g(), original_rgb.b());
563        let final_components = (back_to_rgb.r(), back_to_rgb.g(), back_to_rgb.b());
564
565        // Allow small tolerance for floating point conversion errors
566        assert!((orig_components.0 - final_components.0).abs() < 0.001);
567        assert!((orig_components.1 - final_components.1).abs() < 0.001);
568        assert!((orig_components.2 - final_components.2).abs() < 0.001);
569    }
570
571    #[test]
572    fn test_grayscale_to_cmyk_conversion() {
573        let gray = Color::gray(0.7);
574        let (c, m, y, k) = gray.cmyk_components();
575
576        assert_eq!(c, 0.0);
577        assert_eq!(m, 0.0);
578        assert_eq!(y, 0.0);
579        assert!((k - 0.3).abs() < 1e-10); // k = 1.0 - gray_value (with tolerance for floating point precision)
580
581        let gray_as_cmyk = gray.to_cmyk();
582        let cmyk_components = gray_as_cmyk.cmyk_components();
583        assert_eq!(cmyk_components.0, 0.0);
584        assert_eq!(cmyk_components.1, 0.0);
585        assert_eq!(cmyk_components.2, 0.0);
586        assert!((cmyk_components.3 - 0.3).abs() < 1e-10);
587    }
588}