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
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_rgb_color_creation() {
79        let color = Color::rgb(0.5, 0.7, 0.3);
80        assert_eq!(color, Color::Rgb(0.5, 0.7, 0.3));
81    }
82
83    #[test]
84    fn test_rgb_color_clamping() {
85        let color = Color::rgb(1.5, -0.3, 0.5);
86        assert_eq!(color, Color::Rgb(1.0, 0.0, 0.5));
87    }
88
89    #[test]
90    fn test_gray_color_creation() {
91        let color = Color::gray(0.5);
92        assert_eq!(color, Color::Gray(0.5));
93    }
94
95    #[test]
96    fn test_gray_color_clamping() {
97        let color1 = Color::gray(1.5);
98        assert_eq!(color1, Color::Gray(1.0));
99
100        let color2 = Color::gray(-0.5);
101        assert_eq!(color2, Color::Gray(0.0));
102    }
103
104    #[test]
105    fn test_cmyk_color_creation() {
106        let color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
107        assert_eq!(color, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
108    }
109
110    #[test]
111    fn test_cmyk_color_clamping() {
112        let color = Color::cmyk(1.5, -0.2, 0.5, 2.0);
113        assert_eq!(color, Color::Cmyk(1.0, 0.0, 0.5, 1.0));
114    }
115
116    #[test]
117    fn test_predefined_colors() {
118        assert_eq!(Color::black(), Color::Gray(0.0));
119        assert_eq!(Color::white(), Color::Gray(1.0));
120        assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
121        assert_eq!(Color::green(), Color::Rgb(0.0, 1.0, 0.0));
122        assert_eq!(Color::blue(), Color::Rgb(0.0, 0.0, 1.0));
123        assert_eq!(Color::yellow(), Color::Rgb(1.0, 1.0, 0.0));
124        assert_eq!(Color::cyan(), Color::Rgb(0.0, 1.0, 1.0));
125        assert_eq!(Color::magenta(), Color::Rgb(1.0, 0.0, 1.0));
126    }
127
128    #[test]
129    fn test_color_equality() {
130        let color1 = Color::rgb(0.5, 0.5, 0.5);
131        let color2 = Color::rgb(0.5, 0.5, 0.5);
132        let color3 = Color::rgb(0.5, 0.5, 0.6);
133
134        assert_eq!(color1, color2);
135        assert_ne!(color1, color3);
136
137        let gray1 = Color::gray(0.5);
138        let gray2 = Color::gray(0.5);
139        assert_eq!(gray1, gray2);
140
141        let cmyk1 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
142        let cmyk2 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
143        assert_eq!(cmyk1, cmyk2);
144    }
145
146    #[test]
147    fn test_color_different_types_inequality() {
148        let rgb = Color::rgb(0.5, 0.5, 0.5);
149        let gray = Color::gray(0.5);
150        let cmyk = Color::cmyk(0.5, 0.5, 0.5, 0.5);
151
152        assert_ne!(rgb, gray);
153        assert_ne!(rgb, cmyk);
154        assert_ne!(gray, cmyk);
155    }
156
157    #[test]
158    fn test_color_debug() {
159        let rgb = Color::rgb(0.1, 0.2, 0.3);
160        let debug_str = format!("{:?}", rgb);
161        assert!(debug_str.contains("Rgb"));
162        assert!(debug_str.contains("0.1"));
163        assert!(debug_str.contains("0.2"));
164        assert!(debug_str.contains("0.3"));
165
166        let gray = Color::gray(0.5);
167        let gray_debug = format!("{:?}", gray);
168        assert!(gray_debug.contains("Gray"));
169        assert!(gray_debug.contains("0.5"));
170
171        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
172        let cmyk_debug = format!("{:?}", cmyk);
173        assert!(cmyk_debug.contains("Cmyk"));
174        assert!(cmyk_debug.contains("0.1"));
175        assert!(cmyk_debug.contains("0.2"));
176        assert!(cmyk_debug.contains("0.3"));
177        assert!(cmyk_debug.contains("0.4"));
178    }
179
180    #[test]
181    fn test_color_clone() {
182        let rgb = Color::rgb(0.5, 0.6, 0.7);
183        let rgb_clone = rgb;
184        assert_eq!(rgb, rgb_clone);
185
186        let gray = Color::gray(0.5);
187        let gray_clone = gray;
188        assert_eq!(gray, gray_clone);
189
190        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
191        let cmyk_clone = cmyk;
192        assert_eq!(cmyk, cmyk_clone);
193    }
194
195    #[test]
196    fn test_color_copy() {
197        let rgb = Color::rgb(0.5, 0.6, 0.7);
198        let rgb_copy = rgb; // Copy semantics
199        assert_eq!(rgb, rgb_copy);
200
201        // Both should still be usable
202        assert_eq!(rgb, Color::Rgb(0.5, 0.6, 0.7));
203        assert_eq!(rgb_copy, Color::Rgb(0.5, 0.6, 0.7));
204    }
205
206    #[test]
207    fn test_edge_case_values() {
208        // Test exact boundary values
209        let color = Color::rgb(0.0, 0.5, 1.0);
210        assert_eq!(color, Color::Rgb(0.0, 0.5, 1.0));
211
212        let gray = Color::gray(0.0);
213        assert_eq!(gray, Color::Gray(0.0));
214
215        let gray_max = Color::gray(1.0);
216        assert_eq!(gray_max, Color::Gray(1.0));
217
218        let cmyk = Color::cmyk(0.0, 0.0, 0.0, 0.0);
219        assert_eq!(cmyk, Color::Cmyk(0.0, 0.0, 0.0, 0.0));
220
221        let cmyk_max = Color::cmyk(1.0, 1.0, 1.0, 1.0);
222        assert_eq!(cmyk_max, Color::Cmyk(1.0, 1.0, 1.0, 1.0));
223    }
224
225    #[test]
226    fn test_floating_point_precision() {
227        let color = Color::rgb(0.333333333, 0.666666666, 0.999999999);
228        match color {
229            Color::Rgb(r, g, b) => {
230                assert!((r - 0.333333333).abs() < 1e-9);
231                assert!((g - 0.666666666).abs() < 1e-9);
232                assert!((b - 0.999999999).abs() < 1e-9);
233            }
234            _ => panic!("Expected RGB color"),
235        }
236    }
237
238    #[test]
239    fn test_rgb_clamping_infinity() {
240        // Test infinity handling
241        let inf_color = Color::rgb(f64::INFINITY, f64::NEG_INFINITY, 0.5);
242        assert_eq!(inf_color, Color::Rgb(1.0, 0.0, 0.5));
243
244        // Test large positive and negative values
245        let large_color = Color::rgb(1000.0, -1000.0, 0.5);
246        assert_eq!(large_color, Color::Rgb(1.0, 0.0, 0.5));
247    }
248
249    #[test]
250    fn test_cmyk_all_components() {
251        // Test that all CMYK components are properly stored
252        let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
253        match cmyk {
254            Color::Cmyk(c, m, y, k) => {
255                assert_eq!(c, 0.1);
256                assert_eq!(m, 0.2);
257                assert_eq!(y, 0.3);
258                assert_eq!(k, 0.4);
259            }
260            _ => panic!("Expected CMYK color"),
261        }
262    }
263
264    #[test]
265    fn test_pattern_matching() {
266        let colors = vec![
267            Color::rgb(0.5, 0.5, 0.5),
268            Color::gray(0.5),
269            Color::cmyk(0.1, 0.2, 0.3, 0.4),
270        ];
271
272        let mut rgb_count = 0;
273        let mut gray_count = 0;
274        let mut cmyk_count = 0;
275
276        for color in colors {
277            match color {
278                Color::Rgb(_, _, _) => rgb_count += 1,
279                Color::Gray(_) => gray_count += 1,
280                Color::Cmyk(_, _, _, _) => cmyk_count += 1,
281            }
282        }
283
284        assert_eq!(rgb_count, 1);
285        assert_eq!(gray_count, 1);
286        assert_eq!(cmyk_count, 1);
287    }
288}