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