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#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_rgb_color_creation() {
231        let color = Color::rgb(0.5, 0.7, 0.3);
232        assert_eq!(color, Color::Rgb(0.5, 0.7, 0.3));
233    }
234
235    #[test]
236    fn test_rgb_color_clamping() {
237        let color = Color::rgb(1.5, -0.3, 0.5);
238        assert_eq!(color, Color::Rgb(1.0, 0.0, 0.5));
239    }
240
241    #[test]
242    fn test_gray_color_creation() {
243        let color = Color::gray(0.5);
244        assert_eq!(color, Color::Gray(0.5));
245    }
246
247    #[test]
248    fn test_gray_color_clamping() {
249        let color1 = Color::gray(1.5);
250        assert_eq!(color1, Color::Gray(1.0));
251
252        let color2 = Color::gray(-0.5);
253        assert_eq!(color2, Color::Gray(0.0));
254    }
255
256    #[test]
257    fn test_cmyk_color_creation() {
258        let color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
259        assert_eq!(color, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
260    }
261
262    #[test]
263    fn test_cmyk_color_clamping() {
264        let color = Color::cmyk(1.5, -0.2, 0.5, 2.0);
265        assert_eq!(color, Color::Cmyk(1.0, 0.0, 0.5, 1.0));
266    }
267
268    #[test]
269    fn test_predefined_colors() {
270        assert_eq!(Color::black(), Color::Gray(0.0));
271        assert_eq!(Color::white(), Color::Gray(1.0));
272        assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
273        assert_eq!(Color::green(), Color::Rgb(0.0, 1.0, 0.0));
274        assert_eq!(Color::blue(), Color::Rgb(0.0, 0.0, 1.0));
275        assert_eq!(Color::yellow(), Color::Rgb(1.0, 1.0, 0.0));
276        assert_eq!(Color::cyan(), Color::Rgb(0.0, 1.0, 1.0));
277        assert_eq!(Color::magenta(), Color::Rgb(1.0, 0.0, 1.0));
278    }
279
280    #[test]
281    fn test_color_equality() {
282        let color1 = Color::rgb(0.5, 0.5, 0.5);
283        let color2 = Color::rgb(0.5, 0.5, 0.5);
284        let color3 = Color::rgb(0.5, 0.5, 0.6);
285
286        assert_eq!(color1, color2);
287        assert_ne!(color1, color3);
288
289        let gray1 = Color::gray(0.5);
290        let gray2 = Color::gray(0.5);
291        assert_eq!(gray1, gray2);
292
293        let cmyk1 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
294        let cmyk2 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
295        assert_eq!(cmyk1, cmyk2);
296    }
297
298    #[test]
299    fn test_color_different_types_inequality() {
300        let rgb = Color::rgb(0.5, 0.5, 0.5);
301        let gray = Color::gray(0.5);
302        let cmyk = Color::cmyk(0.5, 0.5, 0.5, 0.5);
303
304        assert_ne!(rgb, gray);
305        assert_ne!(rgb, cmyk);
306        assert_ne!(gray, cmyk);
307    }
308
309    #[test]
310    fn test_color_debug() {
311        let rgb = Color::rgb(0.1, 0.2, 0.3);
312        let debug_str = format!("{rgb:?}");
313        assert!(debug_str.contains("Rgb"));
314        assert!(debug_str.contains("0.1"));
315        assert!(debug_str.contains("0.2"));
316        assert!(debug_str.contains("0.3"));
317
318        let gray = Color::gray(0.5);
319        let gray_debug = format!("{gray:?}");
320        assert!(gray_debug.contains("Gray"));
321        assert!(gray_debug.contains("0.5"));
322
323        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
324        let cmyk_debug = format!("{cmyk:?}");
325        assert!(cmyk_debug.contains("Cmyk"));
326        assert!(cmyk_debug.contains("0.1"));
327        assert!(cmyk_debug.contains("0.2"));
328        assert!(cmyk_debug.contains("0.3"));
329        assert!(cmyk_debug.contains("0.4"));
330    }
331
332    #[test]
333    fn test_color_clone() {
334        let rgb = Color::rgb(0.5, 0.6, 0.7);
335        let rgb_clone = rgb;
336        assert_eq!(rgb, rgb_clone);
337
338        let gray = Color::gray(0.5);
339        let gray_clone = gray;
340        assert_eq!(gray, gray_clone);
341
342        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
343        let cmyk_clone = cmyk;
344        assert_eq!(cmyk, cmyk_clone);
345    }
346
347    #[test]
348    fn test_color_copy() {
349        let rgb = Color::rgb(0.5, 0.6, 0.7);
350        let rgb_copy = rgb; // Copy semantics
351        assert_eq!(rgb, rgb_copy);
352
353        // Both should still be usable
354        assert_eq!(rgb, Color::Rgb(0.5, 0.6, 0.7));
355        assert_eq!(rgb_copy, Color::Rgb(0.5, 0.6, 0.7));
356    }
357
358    #[test]
359    fn test_edge_case_values() {
360        // Test exact boundary values
361        let color = Color::rgb(0.0, 0.5, 1.0);
362        assert_eq!(color, Color::Rgb(0.0, 0.5, 1.0));
363
364        let gray = Color::gray(0.0);
365        assert_eq!(gray, Color::Gray(0.0));
366
367        let gray_max = Color::gray(1.0);
368        assert_eq!(gray_max, Color::Gray(1.0));
369
370        let cmyk = Color::cmyk(0.0, 0.0, 0.0, 0.0);
371        assert_eq!(cmyk, Color::Cmyk(0.0, 0.0, 0.0, 0.0));
372
373        let cmyk_max = Color::cmyk(1.0, 1.0, 1.0, 1.0);
374        assert_eq!(cmyk_max, Color::Cmyk(1.0, 1.0, 1.0, 1.0));
375    }
376
377    #[test]
378    fn test_floating_point_precision() {
379        let color = Color::rgb(0.333333333, 0.666666666, 0.999999999);
380        match color {
381            Color::Rgb(r, g, b) => {
382                assert!((r - 0.333333333).abs() < 1e-9);
383                assert!((g - 0.666666666).abs() < 1e-9);
384                assert!((b - 0.999999999).abs() < 1e-9);
385            }
386            _ => panic!("Expected RGB color"),
387        }
388    }
389
390    #[test]
391    fn test_rgb_clamping_infinity() {
392        // Test infinity handling
393        let inf_color = Color::rgb(f64::INFINITY, f64::NEG_INFINITY, 0.5);
394        assert_eq!(inf_color, Color::Rgb(1.0, 0.0, 0.5));
395
396        // Test large positive and negative values
397        let large_color = Color::rgb(1000.0, -1000.0, 0.5);
398        assert_eq!(large_color, Color::Rgb(1.0, 0.0, 0.5));
399    }
400
401    #[test]
402    fn test_cmyk_all_components() {
403        // Test that all CMYK components are properly stored
404        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
405        match cmyk {
406            Color::Cmyk(c, m, y, k) => {
407                assert_eq!(c, 0.1);
408                assert_eq!(m, 0.2);
409                assert_eq!(y, 0.3);
410                assert_eq!(k, 0.4);
411            }
412            _ => panic!("Expected CMYK color"),
413        }
414    }
415
416    #[test]
417    fn test_pattern_matching() {
418        let colors = vec![
419            Color::rgb(0.5, 0.5, 0.5),
420            Color::gray(0.5),
421            Color::cmyk(0.1, 0.2, 0.3, 0.4),
422        ];
423
424        let mut rgb_count = 0;
425        let mut gray_count = 0;
426        let mut cmyk_count = 0;
427
428        for color in colors {
429            match color {
430                Color::Rgb(_, _, _) => rgb_count += 1,
431                Color::Gray(_) => gray_count += 1,
432                Color::Cmyk(_, _, _, _) => cmyk_count += 1,
433            }
434        }
435
436        assert_eq!(rgb_count, 1);
437        assert_eq!(gray_count, 1);
438        assert_eq!(cmyk_count, 1);
439    }
440
441    #[test]
442    fn test_cmyk_pure_colors() {
443        // Test pure CMYK colors
444        assert_eq!(Color::cmyk_cyan(), Color::Cmyk(1.0, 0.0, 0.0, 0.0));
445        assert_eq!(Color::cmyk_magenta(), Color::Cmyk(0.0, 1.0, 0.0, 0.0));
446        assert_eq!(Color::cmyk_yellow(), Color::Cmyk(0.0, 0.0, 1.0, 0.0));
447        assert_eq!(Color::cmyk_black(), Color::Cmyk(0.0, 0.0, 0.0, 1.0));
448    }
449
450    #[test]
451    fn test_cmyk_to_rgb_conversion() {
452        // Test CMYK to RGB conversion
453        let pure_cyan = Color::cmyk_cyan().to_rgb();
454        match pure_cyan {
455            Color::Rgb(r, g, b) => {
456                assert_eq!(r, 0.0);
457                assert_eq!(g, 1.0);
458                assert_eq!(b, 1.0);
459            }
460            _ => panic!("Expected RGB color"),
461        }
462
463        let pure_magenta = Color::cmyk_magenta().to_rgb();
464        match pure_magenta {
465            Color::Rgb(r, g, b) => {
466                assert_eq!(r, 1.0);
467                assert_eq!(g, 0.0);
468                assert_eq!(b, 1.0);
469            }
470            _ => panic!("Expected RGB color"),
471        }
472
473        let pure_yellow = Color::cmyk_yellow().to_rgb();
474        match pure_yellow {
475            Color::Rgb(r, g, b) => {
476                assert_eq!(r, 1.0);
477                assert_eq!(g, 1.0);
478                assert_eq!(b, 0.0);
479            }
480            _ => panic!("Expected RGB color"),
481        }
482
483        let pure_black = Color::cmyk_black().to_rgb();
484        match pure_black {
485            Color::Rgb(r, g, b) => {
486                assert_eq!(r, 0.0);
487                assert_eq!(g, 0.0);
488                assert_eq!(b, 0.0);
489            }
490            _ => panic!("Expected RGB color"),
491        }
492    }
493
494    #[test]
495    fn test_rgb_to_cmyk_conversion() {
496        // Test RGB to CMYK conversion
497        let red = Color::red().to_cmyk();
498        let (c, m, y, k) = red.cmyk_components();
499        assert_eq!(c, 0.0);
500        assert_eq!(m, 1.0);
501        assert_eq!(y, 1.0);
502        assert_eq!(k, 0.0);
503
504        let green = Color::green().to_cmyk();
505        let (c, m, y, k) = green.cmyk_components();
506        assert_eq!(c, 1.0);
507        assert_eq!(m, 0.0);
508        assert_eq!(y, 1.0);
509        assert_eq!(k, 0.0);
510
511        let blue = Color::blue().to_cmyk();
512        let (c, m, y, k) = blue.cmyk_components();
513        assert_eq!(c, 1.0);
514        assert_eq!(m, 1.0);
515        assert_eq!(y, 0.0);
516        assert_eq!(k, 0.0);
517
518        let black = Color::black().to_cmyk();
519        let (c, m, y, k) = black.cmyk_components();
520        assert_eq!(c, 0.0);
521        assert_eq!(m, 0.0);
522        assert_eq!(y, 0.0);
523        assert_eq!(k, 1.0);
524    }
525
526    #[test]
527    fn test_color_space_detection() {
528        assert!(Color::rgb(0.5, 0.5, 0.5).is_rgb());
529        assert!(!Color::rgb(0.5, 0.5, 0.5).is_cmyk());
530        assert!(!Color::rgb(0.5, 0.5, 0.5).is_gray());
531
532        assert!(Color::gray(0.5).is_gray());
533        assert!(!Color::gray(0.5).is_rgb());
534        assert!(!Color::gray(0.5).is_cmyk());
535
536        assert!(Color::cmyk(0.1, 0.2, 0.3, 0.4).is_cmyk());
537        assert!(!Color::cmyk(0.1, 0.2, 0.3, 0.4).is_rgb());
538        assert!(!Color::cmyk(0.1, 0.2, 0.3, 0.4).is_gray());
539    }
540
541    #[test]
542    fn test_color_space_names() {
543        assert_eq!(Color::rgb(0.5, 0.5, 0.5).color_space_name(), "DeviceRGB");
544        assert_eq!(Color::gray(0.5).color_space_name(), "DeviceGray");
545        assert_eq!(
546            Color::cmyk(0.1, 0.2, 0.3, 0.4).color_space_name(),
547            "DeviceCMYK"
548        );
549    }
550
551    #[test]
552    fn test_cmyk_components_extraction() {
553        let cmyk_color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
554        let (c, m, y, k) = cmyk_color.cmyk_components();
555        assert_eq!(c, 0.1);
556        assert_eq!(m, 0.2);
557        assert_eq!(y, 0.3);
558        assert_eq!(k, 0.4);
559
560        // Test RGB to CMYK component conversion
561        let white = Color::white();
562        let (c, m, y, k) = white.cmyk_components();
563        assert_eq!(c, 0.0);
564        assert_eq!(m, 0.0);
565        assert_eq!(y, 0.0);
566        assert_eq!(k, 0.0);
567    }
568
569    #[test]
570    fn test_roundtrip_conversions() {
571        // Test that conversion cycles preserve color reasonably well
572        let original_rgb = Color::rgb(0.6, 0.3, 0.9);
573        let converted_cmyk = original_rgb.to_cmyk();
574        let back_to_rgb = converted_cmyk.to_rgb();
575
576        let orig_components = (original_rgb.r(), original_rgb.g(), original_rgb.b());
577        let final_components = (back_to_rgb.r(), back_to_rgb.g(), back_to_rgb.b());
578
579        // Allow small tolerance for floating point conversion errors
580        assert!((orig_components.0 - final_components.0).abs() < 0.001);
581        assert!((orig_components.1 - final_components.1).abs() < 0.001);
582        assert!((orig_components.2 - final_components.2).abs() < 0.001);
583    }
584
585    #[test]
586    fn test_grayscale_to_cmyk_conversion() {
587        let gray = Color::gray(0.7);
588        let (c, m, y, k) = gray.cmyk_components();
589
590        assert_eq!(c, 0.0);
591        assert_eq!(m, 0.0);
592        assert_eq!(y, 0.0);
593        assert!((k - 0.3).abs() < 1e-10); // k = 1.0 - gray_value (with tolerance for floating point precision)
594
595        let gray_as_cmyk = gray.to_cmyk();
596        let cmyk_components = gray_as_cmyk.cmyk_components();
597        assert_eq!(cmyk_components.0, 0.0);
598        assert_eq!(cmyk_components.1, 0.0);
599        assert_eq!(cmyk_components.2, 0.0);
600        assert!((cmyk_components.3 - 0.3).abs() < 1e-10);
601    }
602}