1#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum Color {
6 Rgb(f64, f64, f64),
8 Gray(f64),
10 Cmyk(f64, f64, f64, f64),
12}
13
14impl Color {
15 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 pub fn hex(hex_str: &str) -> Self {
22 let hex = hex_str.trim_start_matches('#');
23 if hex.len() != 6 {
24 return Color::black(); }
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 pub fn gray(value: f64) -> Self {
36 Color::Gray(value.clamp(0.0, 1.0))
37 }
38
39 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 pub fn black() -> Self {
51 Color::Gray(0.0)
52 }
53
54 pub fn white() -> Self {
56 Color::Gray(1.0)
57 }
58
59 pub fn red() -> Self {
61 Color::Rgb(1.0, 0.0, 0.0)
62 }
63
64 pub fn green() -> Self {
66 Color::Rgb(0.0, 1.0, 0.0)
67 }
68
69 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 pub fn cmyk_cyan() -> Self {
88 Color::Cmyk(1.0, 0.0, 0.0, 0.0)
89 }
90
91 pub fn cmyk_magenta() -> Self {
93 Color::Cmyk(0.0, 1.0, 0.0, 0.0)
94 }
95
96 pub fn cmyk_yellow() -> Self {
98 Color::Cmyk(0.0, 0.0, 1.0, 0.0)
99 }
100
101 pub fn cmyk_black() -> Self {
103 Color::Cmyk(0.0, 0.0, 0.0, 1.0)
104 }
105
106 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 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 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 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 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 let k = 1.0 - g;
152 (0.0, 0.0, 0.0, k)
153 }
154 }
155 }
156
157 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 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 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 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 pub fn is_cmyk(&self) -> bool {
194 matches!(self, Color::Cmyk(_, _, _, _))
195 }
196
197 pub fn is_rgb(&self) -> bool {
199 matches!(self, Color::Rgb(_, _, _))
200 }
201
202 pub fn is_gray(&self) -> bool {
204 matches!(self, Color::Gray(_))
205 }
206
207 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; assert_eq!(rgb, rgb_copy);
352
353 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 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 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 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 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 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 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 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 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 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 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); 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}