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#[inline]
237pub(crate) fn finite_or_zero(val: f64) -> f64 {
238 if val.is_finite() {
239 val
240 } else {
241 0.0
242 }
243}
244
245pub(crate) fn fill_color_op(color: Color) -> String {
260 use std::fmt::Write;
261 let mut s = String::new();
262 match color {
263 Color::Rgb(r, g, b) => write!(
264 &mut s,
265 "{:.3} {:.3} {:.3} rg",
266 finite_or_zero(r),
267 finite_or_zero(g),
268 finite_or_zero(b)
269 ),
270 Color::Gray(gray) => write!(&mut s, "{:.3} g", finite_or_zero(gray)),
271 Color::Cmyk(c, m, y, k) => write!(
272 &mut s,
273 "{:.3} {:.3} {:.3} {:.3} k",
274 finite_or_zero(c),
275 finite_or_zero(m),
276 finite_or_zero(y),
277 finite_or_zero(k)
278 ),
279 }
280 .expect("writing to a String never fails");
281 s
282}
283
284pub(crate) fn stroke_color_op(color: Color) -> String {
288 use std::fmt::Write;
289 let mut s = String::new();
290 match color {
291 Color::Rgb(r, g, b) => write!(
292 &mut s,
293 "{:.3} {:.3} {:.3} RG",
294 finite_or_zero(r),
295 finite_or_zero(g),
296 finite_or_zero(b)
297 ),
298 Color::Gray(gray) => write!(&mut s, "{:.3} G", finite_or_zero(gray)),
299 Color::Cmyk(c, m, y, k) => write!(
300 &mut s,
301 "{:.3} {:.3} {:.3} {:.3} K",
302 finite_or_zero(c),
303 finite_or_zero(m),
304 finite_or_zero(y),
305 finite_or_zero(k)
306 ),
307 }
308 .expect("writing to a String never fails");
309 s
310}
311
312pub(crate) fn write_fill_color(ops: &mut String, color: Color) {
316 use std::fmt::Write;
317 match color {
318 Color::Rgb(r, g, b) => writeln!(
319 ops,
320 "{:.3} {:.3} {:.3} rg",
321 finite_or_zero(r),
322 finite_or_zero(g),
323 finite_or_zero(b)
324 ),
325 Color::Gray(gray) => writeln!(ops, "{:.3} g", finite_or_zero(gray)),
326 Color::Cmyk(c, m, y, k) => writeln!(
327 ops,
328 "{:.3} {:.3} {:.3} {:.3} k",
329 finite_or_zero(c),
330 finite_or_zero(m),
331 finite_or_zero(y),
332 finite_or_zero(k)
333 ),
334 }
335 .expect("writing to a String never fails");
336}
337
338pub(crate) fn write_stroke_color(ops: &mut String, color: Color) {
342 use std::fmt::Write;
343 match color {
344 Color::Rgb(r, g, b) => writeln!(
345 ops,
346 "{:.3} {:.3} {:.3} RG",
347 finite_or_zero(r),
348 finite_or_zero(g),
349 finite_or_zero(b)
350 ),
351 Color::Gray(gray) => writeln!(ops, "{:.3} G", finite_or_zero(gray)),
352 Color::Cmyk(c, m, y, k) => writeln!(
353 ops,
354 "{:.3} {:.3} {:.3} {:.3} K",
355 finite_or_zero(c),
356 finite_or_zero(m),
357 finite_or_zero(y),
358 finite_or_zero(k)
359 ),
360 }
361 .expect("writing to a String never fails");
362}
363
364pub(crate) fn write_fill_color_bytes(ops: &mut Vec<u8>, color: Color) {
370 use std::io::Write;
371 match color {
372 Color::Rgb(r, g, b) => writeln!(
373 ops,
374 "{:.3} {:.3} {:.3} rg",
375 finite_or_zero(r),
376 finite_or_zero(g),
377 finite_or_zero(b)
378 ),
379 Color::Gray(gray) => writeln!(ops, "{:.3} g", finite_or_zero(gray)),
380 Color::Cmyk(c, m, y, k) => writeln!(
381 ops,
382 "{:.3} {:.3} {:.3} {:.3} k",
383 finite_or_zero(c),
384 finite_or_zero(m),
385 finite_or_zero(y),
386 finite_or_zero(k)
387 ),
388 }
389 .expect("writing to a Vec<u8> never fails");
390}
391
392pub(crate) fn write_stroke_color_bytes(ops: &mut Vec<u8>, color: Color) {
396 use std::io::Write;
397 match color {
398 Color::Rgb(r, g, b) => writeln!(
399 ops,
400 "{:.3} {:.3} {:.3} RG",
401 finite_or_zero(r),
402 finite_or_zero(g),
403 finite_or_zero(b)
404 ),
405 Color::Gray(gray) => writeln!(ops, "{:.3} G", finite_or_zero(gray)),
406 Color::Cmyk(c, m, y, k) => writeln!(
407 ops,
408 "{:.3} {:.3} {:.3} {:.3} K",
409 finite_or_zero(c),
410 finite_or_zero(m),
411 finite_or_zero(y),
412 finite_or_zero(k)
413 ),
414 }
415 .expect("writing to a Vec<u8> never fails");
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_rgb_color_creation() {
424 let color = Color::rgb(0.5, 0.7, 0.3);
425 assert_eq!(color, Color::Rgb(0.5, 0.7, 0.3));
426 }
427
428 #[test]
429 fn test_rgb_color_clamping() {
430 let color = Color::rgb(1.5, -0.3, 0.5);
431 assert_eq!(color, Color::Rgb(1.0, 0.0, 0.5));
432 }
433
434 #[test]
435 fn test_gray_color_creation() {
436 let color = Color::gray(0.5);
437 assert_eq!(color, Color::Gray(0.5));
438 }
439
440 #[test]
441 fn test_gray_color_clamping() {
442 let color1 = Color::gray(1.5);
443 assert_eq!(color1, Color::Gray(1.0));
444
445 let color2 = Color::gray(-0.5);
446 assert_eq!(color2, Color::Gray(0.0));
447 }
448
449 #[test]
450 fn test_cmyk_color_creation() {
451 let color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
452 assert_eq!(color, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
453 }
454
455 #[test]
456 fn test_cmyk_color_clamping() {
457 let color = Color::cmyk(1.5, -0.2, 0.5, 2.0);
458 assert_eq!(color, Color::Cmyk(1.0, 0.0, 0.5, 1.0));
459 }
460
461 #[test]
462 fn test_predefined_colors() {
463 assert_eq!(Color::black(), Color::Gray(0.0));
464 assert_eq!(Color::white(), Color::Gray(1.0));
465 assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
466 assert_eq!(Color::green(), Color::Rgb(0.0, 1.0, 0.0));
467 assert_eq!(Color::blue(), Color::Rgb(0.0, 0.0, 1.0));
468 assert_eq!(Color::yellow(), Color::Rgb(1.0, 1.0, 0.0));
469 assert_eq!(Color::cyan(), Color::Rgb(0.0, 1.0, 1.0));
470 assert_eq!(Color::magenta(), Color::Rgb(1.0, 0.0, 1.0));
471 }
472
473 #[test]
474 fn test_color_equality() {
475 let color1 = Color::rgb(0.5, 0.5, 0.5);
476 let color2 = Color::rgb(0.5, 0.5, 0.5);
477 let color3 = Color::rgb(0.5, 0.5, 0.6);
478
479 assert_eq!(color1, color2);
480 assert_ne!(color1, color3);
481
482 let gray1 = Color::gray(0.5);
483 let gray2 = Color::gray(0.5);
484 assert_eq!(gray1, gray2);
485
486 let cmyk1 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
487 let cmyk2 = Color::cmyk(0.1, 0.2, 0.3, 0.4);
488 assert_eq!(cmyk1, cmyk2);
489 }
490
491 #[test]
492 fn test_color_different_types_inequality() {
493 let rgb = Color::rgb(0.5, 0.5, 0.5);
494 let gray = Color::gray(0.5);
495 let cmyk = Color::cmyk(0.5, 0.5, 0.5, 0.5);
496
497 assert_ne!(rgb, gray);
498 assert_ne!(rgb, cmyk);
499 assert_ne!(gray, cmyk);
500 }
501
502 #[test]
503 fn test_color_debug() {
504 let rgb = Color::rgb(0.1, 0.2, 0.3);
505 let debug_str = format!("{rgb:?}");
506 assert!(debug_str.contains("Rgb"));
507 assert!(debug_str.contains("0.1"));
508 assert!(debug_str.contains("0.2"));
509 assert!(debug_str.contains("0.3"));
510
511 let gray = Color::gray(0.5);
512 let gray_debug = format!("{gray:?}");
513 assert!(gray_debug.contains("Gray"));
514 assert!(gray_debug.contains("0.5"));
515
516 let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
517 let cmyk_debug = format!("{cmyk:?}");
518 assert!(cmyk_debug.contains("Cmyk"));
519 assert!(cmyk_debug.contains("0.1"));
520 assert!(cmyk_debug.contains("0.2"));
521 assert!(cmyk_debug.contains("0.3"));
522 assert!(cmyk_debug.contains("0.4"));
523 }
524
525 #[test]
526 fn test_color_clone() {
527 let rgb = Color::rgb(0.5, 0.6, 0.7);
528 let rgb_clone = rgb;
529 assert_eq!(rgb, rgb_clone);
530
531 let gray = Color::gray(0.5);
532 let gray_clone = gray;
533 assert_eq!(gray, gray_clone);
534
535 let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
536 let cmyk_clone = cmyk;
537 assert_eq!(cmyk, cmyk_clone);
538 }
539
540 #[test]
541 fn test_color_copy() {
542 let rgb = Color::rgb(0.5, 0.6, 0.7);
543 let rgb_copy = rgb; assert_eq!(rgb, rgb_copy);
545
546 assert_eq!(rgb, Color::Rgb(0.5, 0.6, 0.7));
548 assert_eq!(rgb_copy, Color::Rgb(0.5, 0.6, 0.7));
549 }
550
551 #[test]
552 fn test_edge_case_values() {
553 let color = Color::rgb(0.0, 0.5, 1.0);
555 assert_eq!(color, Color::Rgb(0.0, 0.5, 1.0));
556
557 let gray = Color::gray(0.0);
558 assert_eq!(gray, Color::Gray(0.0));
559
560 let gray_max = Color::gray(1.0);
561 assert_eq!(gray_max, Color::Gray(1.0));
562
563 let cmyk = Color::cmyk(0.0, 0.0, 0.0, 0.0);
564 assert_eq!(cmyk, Color::Cmyk(0.0, 0.0, 0.0, 0.0));
565
566 let cmyk_max = Color::cmyk(1.0, 1.0, 1.0, 1.0);
567 assert_eq!(cmyk_max, Color::Cmyk(1.0, 1.0, 1.0, 1.0));
568 }
569
570 #[test]
571 fn test_floating_point_precision() {
572 let color = Color::rgb(0.333333333, 0.666666666, 0.999999999);
573 match color {
574 Color::Rgb(r, g, b) => {
575 assert!((r - 0.333333333).abs() < 1e-9);
576 assert!((g - 0.666666666).abs() < 1e-9);
577 assert!((b - 0.999999999).abs() < 1e-9);
578 }
579 _ => panic!("Expected RGB color"),
580 }
581 }
582
583 #[test]
584 fn test_rgb_clamping_infinity() {
585 let inf_color = Color::rgb(f64::INFINITY, f64::NEG_INFINITY, 0.5);
587 assert_eq!(inf_color, Color::Rgb(1.0, 0.0, 0.5));
588
589 let large_color = Color::rgb(1000.0, -1000.0, 0.5);
591 assert_eq!(large_color, Color::Rgb(1.0, 0.0, 0.5));
592 }
593
594 #[test]
595 fn test_cmyk_all_components() {
596 let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
598 match cmyk {
599 Color::Cmyk(c, m, y, k) => {
600 assert_eq!(c, 0.1);
601 assert_eq!(m, 0.2);
602 assert_eq!(y, 0.3);
603 assert_eq!(k, 0.4);
604 }
605 _ => panic!("Expected CMYK color"),
606 }
607 }
608
609 #[test]
610 fn test_pattern_matching() {
611 let colors = vec![
612 Color::rgb(0.5, 0.5, 0.5),
613 Color::gray(0.5),
614 Color::cmyk(0.1, 0.2, 0.3, 0.4),
615 ];
616
617 let mut rgb_count = 0;
618 let mut gray_count = 0;
619 let mut cmyk_count = 0;
620
621 for color in colors {
622 match color {
623 Color::Rgb(_, _, _) => rgb_count += 1,
624 Color::Gray(_) => gray_count += 1,
625 Color::Cmyk(_, _, _, _) => cmyk_count += 1,
626 }
627 }
628
629 assert_eq!(rgb_count, 1);
630 assert_eq!(gray_count, 1);
631 assert_eq!(cmyk_count, 1);
632 }
633
634 #[test]
635 fn test_cmyk_pure_colors() {
636 assert_eq!(Color::cmyk_cyan(), Color::Cmyk(1.0, 0.0, 0.0, 0.0));
638 assert_eq!(Color::cmyk_magenta(), Color::Cmyk(0.0, 1.0, 0.0, 0.0));
639 assert_eq!(Color::cmyk_yellow(), Color::Cmyk(0.0, 0.0, 1.0, 0.0));
640 assert_eq!(Color::cmyk_black(), Color::Cmyk(0.0, 0.0, 0.0, 1.0));
641 }
642
643 #[test]
644 fn test_cmyk_to_rgb_conversion() {
645 let pure_cyan = Color::cmyk_cyan().to_rgb();
647 match pure_cyan {
648 Color::Rgb(r, g, b) => {
649 assert_eq!(r, 0.0);
650 assert_eq!(g, 1.0);
651 assert_eq!(b, 1.0);
652 }
653 _ => panic!("Expected RGB color"),
654 }
655
656 let pure_magenta = Color::cmyk_magenta().to_rgb();
657 match pure_magenta {
658 Color::Rgb(r, g, b) => {
659 assert_eq!(r, 1.0);
660 assert_eq!(g, 0.0);
661 assert_eq!(b, 1.0);
662 }
663 _ => panic!("Expected RGB color"),
664 }
665
666 let pure_yellow = Color::cmyk_yellow().to_rgb();
667 match pure_yellow {
668 Color::Rgb(r, g, b) => {
669 assert_eq!(r, 1.0);
670 assert_eq!(g, 1.0);
671 assert_eq!(b, 0.0);
672 }
673 _ => panic!("Expected RGB color"),
674 }
675
676 let pure_black = Color::cmyk_black().to_rgb();
677 match pure_black {
678 Color::Rgb(r, g, b) => {
679 assert_eq!(r, 0.0);
680 assert_eq!(g, 0.0);
681 assert_eq!(b, 0.0);
682 }
683 _ => panic!("Expected RGB color"),
684 }
685 }
686
687 #[test]
688 fn test_rgb_to_cmyk_conversion() {
689 let red = Color::red().to_cmyk();
691 let (c, m, y, k) = red.cmyk_components();
692 assert_eq!(c, 0.0);
693 assert_eq!(m, 1.0);
694 assert_eq!(y, 1.0);
695 assert_eq!(k, 0.0);
696
697 let green = Color::green().to_cmyk();
698 let (c, m, y, k) = green.cmyk_components();
699 assert_eq!(c, 1.0);
700 assert_eq!(m, 0.0);
701 assert_eq!(y, 1.0);
702 assert_eq!(k, 0.0);
703
704 let blue = Color::blue().to_cmyk();
705 let (c, m, y, k) = blue.cmyk_components();
706 assert_eq!(c, 1.0);
707 assert_eq!(m, 1.0);
708 assert_eq!(y, 0.0);
709 assert_eq!(k, 0.0);
710
711 let black = Color::black().to_cmyk();
712 let (c, m, y, k) = black.cmyk_components();
713 assert_eq!(c, 0.0);
714 assert_eq!(m, 0.0);
715 assert_eq!(y, 0.0);
716 assert_eq!(k, 1.0);
717 }
718
719 #[test]
720 fn test_color_space_detection() {
721 assert!(Color::rgb(0.5, 0.5, 0.5).is_rgb());
722 assert!(!Color::rgb(0.5, 0.5, 0.5).is_cmyk());
723 assert!(!Color::rgb(0.5, 0.5, 0.5).is_gray());
724
725 assert!(Color::gray(0.5).is_gray());
726 assert!(!Color::gray(0.5).is_rgb());
727 assert!(!Color::gray(0.5).is_cmyk());
728
729 assert!(Color::cmyk(0.1, 0.2, 0.3, 0.4).is_cmyk());
730 assert!(!Color::cmyk(0.1, 0.2, 0.3, 0.4).is_rgb());
731 assert!(!Color::cmyk(0.1, 0.2, 0.3, 0.4).is_gray());
732 }
733
734 #[test]
735 fn test_color_space_names() {
736 assert_eq!(Color::rgb(0.5, 0.5, 0.5).color_space_name(), "DeviceRGB");
737 assert_eq!(Color::gray(0.5).color_space_name(), "DeviceGray");
738 assert_eq!(
739 Color::cmyk(0.1, 0.2, 0.3, 0.4).color_space_name(),
740 "DeviceCMYK"
741 );
742 }
743
744 #[test]
745 fn test_cmyk_components_extraction() {
746 let cmyk_color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
747 let (c, m, y, k) = cmyk_color.cmyk_components();
748 assert_eq!(c, 0.1);
749 assert_eq!(m, 0.2);
750 assert_eq!(y, 0.3);
751 assert_eq!(k, 0.4);
752
753 let white = Color::white();
755 let (c, m, y, k) = white.cmyk_components();
756 assert_eq!(c, 0.0);
757 assert_eq!(m, 0.0);
758 assert_eq!(y, 0.0);
759 assert_eq!(k, 0.0);
760 }
761
762 #[test]
763 fn test_roundtrip_conversions() {
764 let original_rgb = Color::rgb(0.6, 0.3, 0.9);
766 let converted_cmyk = original_rgb.to_cmyk();
767 let back_to_rgb = converted_cmyk.to_rgb();
768
769 let orig_components = (original_rgb.r(), original_rgb.g(), original_rgb.b());
770 let final_components = (back_to_rgb.r(), back_to_rgb.g(), back_to_rgb.b());
771
772 assert!((orig_components.0 - final_components.0).abs() < 0.001);
774 assert!((orig_components.1 - final_components.1).abs() < 0.001);
775 assert!((orig_components.2 - final_components.2).abs() < 0.001);
776 }
777
778 #[test]
779 fn test_grayscale_to_cmyk_conversion() {
780 let gray = Color::gray(0.7);
781 let (c, m, y, k) = gray.cmyk_components();
782
783 assert_eq!(c, 0.0);
784 assert_eq!(m, 0.0);
785 assert_eq!(y, 0.0);
786 assert!((k - 0.3).abs() < 1e-10); let gray_as_cmyk = gray.to_cmyk();
789 let cmyk_components = gray_as_cmyk.cmyk_components();
790 assert_eq!(cmyk_components.0, 0.0);
791 assert_eq!(cmyk_components.1, 0.0);
792 assert_eq!(cmyk_components.2, 0.0);
793 assert!((cmyk_components.3 - 0.3).abs() < 1e-10);
794 }
795
796 #[test]
801 fn test_hex_color_invalid_length() {
802 let color1 = Color::hex("#FFF");
804 assert_eq!(
805 color1,
806 Color::black(),
807 "Short hex string should return black"
808 );
809
810 let color2 = Color::hex("#FFFFFFF");
811 assert_eq!(
812 color2,
813 Color::black(),
814 "Long hex string should return black"
815 );
816
817 let color3 = Color::hex("");
818 assert_eq!(
819 color3,
820 Color::black(),
821 "Empty hex string should return black"
822 );
823 }
824
825 #[test]
826 fn test_hex_color_valid() {
827 let color = Color::hex("#FF0080");
828
829 assert!((color.r() - 1.0).abs() < 0.01, "Red should be 1.0");
831 assert!((color.g() - 0.0).abs() < 0.01, "Green should be 0.0");
832 assert!((color.b() - 0.502).abs() < 0.01, "Blue should be ~0.502");
833 }
834
835 #[test]
836 fn test_hex_color_without_hash() {
837 let color = Color::hex("00FF00");
838
839 assert_eq!(color.r(), 0.0, "Red should be 0.0");
841 assert_eq!(color.g(), 1.0, "Green should be 1.0");
842 assert_eq!(color.b(), 0.0, "Blue should be 0.0");
843 }
844
845 #[test]
846 fn test_cmyk_r_component() {
847 let cmyk_color = Color::cmyk(0.5, 0.2, 0.3, 0.1);
849 let r = cmyk_color.r();
850
851 assert!((r - 0.45).abs() < 1e-10, "CMYK r() should be 0.45");
853 }
854
855 #[test]
856 fn test_cmyk_g_component() {
857 let cmyk_color = Color::cmyk(0.2, 0.6, 0.3, 0.2);
859 let g = cmyk_color.g();
860
861 assert!((g - 0.32).abs() < 1e-10, "CMYK g() should be 0.32");
863 }
864
865 #[test]
866 fn test_cmyk_b_component() {
867 let cmyk_color = Color::cmyk(0.3, 0.2, 0.7, 0.15);
869 let b = cmyk_color.b();
870
871 assert!((b - 0.255).abs() < 1e-10, "CMYK b() should be 0.255");
873 }
874
875 #[test]
876 fn test_to_rgb_when_already_rgb() {
877 let rgb_color = Color::rgb(0.5, 0.6, 0.7);
879 let converted = rgb_color.to_rgb();
880
881 assert_eq!(converted, rgb_color, "to_rgb() on RGB should return self");
882 assert_eq!(converted.r(), 0.5);
883 assert_eq!(converted.g(), 0.6);
884 assert_eq!(converted.b(), 0.7);
885 }
886
887 #[test]
888 fn test_to_cmyk_when_already_cmyk() {
889 let cmyk_color = Color::cmyk(0.1, 0.2, 0.3, 0.4);
891 let converted = cmyk_color.to_cmyk();
892
893 assert_eq!(
894 converted, cmyk_color,
895 "to_cmyk() on CMYK should return self"
896 );
897 let (c, m, y, k) = converted.cmyk_components();
898 assert_eq!(c, 0.1);
899 assert_eq!(m, 0.2);
900 assert_eq!(y, 0.3);
901 assert_eq!(k, 0.4);
902 }
903
904 #[test]
905 fn test_to_pdf_array_gray() {
906 use crate::objects::Object;
908
909 let gray = Color::gray(0.75);
910 let array = gray.to_pdf_array();
911
912 match array {
913 Object::Array(vec) => {
914 assert_eq!(vec.len(), 1, "Gray PDF array should have 1 element");
915 match &vec[0] {
916 Object::Real(val) => assert_eq!(*val, 0.75, "Gray value should be 0.75"),
917 _ => panic!("Expected Real object"),
918 }
919 }
920 _ => panic!("Expected Array object"),
921 }
922 }
923
924 #[test]
925 fn test_to_pdf_array_rgb() {
926 use crate::objects::Object;
928
929 let rgb = Color::rgb(0.2, 0.5, 0.9);
930 let array = rgb.to_pdf_array();
931
932 match array {
933 Object::Array(vec) => {
934 assert_eq!(vec.len(), 3, "RGB PDF array should have 3 elements");
935 match (&vec[0], &vec[1], &vec[2]) {
936 (Object::Real(r), Object::Real(g), Object::Real(b)) => {
937 assert_eq!(*r, 0.2, "Red should be 0.2");
938 assert_eq!(*g, 0.5, "Green should be 0.5");
939 assert_eq!(*b, 0.9, "Blue should be 0.9");
940 }
941 _ => panic!("Expected Real objects"),
942 }
943 }
944 _ => panic!("Expected Array object"),
945 }
946 }
947
948 #[test]
949 fn test_to_pdf_array_cmyk() {
950 use crate::objects::Object;
952
953 let cmyk = Color::cmyk(0.1, 0.3, 0.5, 0.7);
954 let array = cmyk.to_pdf_array();
955
956 match array {
957 Object::Array(vec) => {
958 assert_eq!(vec.len(), 4, "CMYK PDF array should have 4 elements");
959 match (&vec[0], &vec[1], &vec[2], &vec[3]) {
960 (Object::Real(c), Object::Real(m), Object::Real(y), Object::Real(k)) => {
961 assert_eq!(*c, 0.1, "Cyan should be 0.1");
962 assert_eq!(*m, 0.3, "Magenta should be 0.3");
963 assert_eq!(*y, 0.5, "Yellow should be 0.5");
964 assert_eq!(*k, 0.7, "Black should be 0.7");
965 }
966 _ => panic!("Expected Real objects"),
967 }
968 }
969 _ => panic!("Expected Array object"),
970 }
971 }
972
973 #[test]
974 fn test_cmyk_components_all_branches() {
975 let cmyk = Color::cmyk(0.2, 0.4, 0.6, 0.8);
979 let (c, m, y, k) = cmyk.cmyk_components();
980 assert_eq!(c, 0.2);
981 assert_eq!(m, 0.4);
982 assert_eq!(y, 0.6);
983 assert_eq!(k, 0.8);
984
985 let rgb = Color::rgb(0.5, 0.25, 0.75);
987 let (_c, _m, _y, k) = rgb.cmyk_components();
988 assert!((k - 0.25).abs() < 1e-10, "K should be 0.25");
990
991 let gray = Color::gray(0.4);
993 let (c, m, y, k) = gray.cmyk_components();
994 assert_eq!(c, 0.0);
995 assert_eq!(m, 0.0);
996 assert_eq!(y, 0.0);
997 assert!((k - 0.6).abs() < 1e-10, "K should be 0.6 (1.0 - 0.4)");
998 }
999
1000 #[test]
1001 fn test_color_conversions_preserve_match_branches() {
1002 let rgb = Color::rgb(0.8, 0.4, 0.6);
1006 let as_cmyk = rgb.to_cmyk();
1007 let back_to_rgb = as_cmyk.to_rgb();
1008
1009 assert!((rgb.r() - back_to_rgb.r()).abs() < 0.01);
1011 assert!((rgb.g() - back_to_rgb.g()).abs() < 0.01);
1012 assert!((rgb.b() - back_to_rgb.b()).abs() < 0.01);
1013
1014 let gray = Color::gray(0.5);
1016 let gray_as_cmyk = gray.to_cmyk();
1017 let (c, m, y, k) = gray_as_cmyk.cmyk_components();
1018 assert_eq!(c, 0.0);
1019 assert_eq!(m, 0.0);
1020 assert_eq!(y, 0.0);
1021 assert!((k - 0.5).abs() < 1e-10);
1022 }
1023
1024 #[test]
1025 fn test_gray_r_g_b_components() {
1026 let gray = Color::gray(0.6);
1028
1029 let r = gray.r();
1030 let g = gray.g();
1031 let b = gray.b();
1032
1033 assert_eq!(r, 0.6, "Gray r() should return gray value");
1035 assert_eq!(g, 0.6, "Gray g() should return gray value");
1036 assert_eq!(b, 0.6, "Gray b() should return gray value");
1037 }
1038
1039 #[test]
1040 fn test_to_rgb_gray_conversion() {
1041 let gray = Color::gray(0.4);
1043 let rgb = gray.to_rgb();
1044
1045 match rgb {
1046 Color::Rgb(r, g, b) => {
1047 assert_eq!(r, 0.4, "Gray → RGB should set r = gray value");
1048 assert_eq!(g, 0.4, "Gray → RGB should set g = gray value");
1049 assert_eq!(b, 0.4, "Gray → RGB should set b = gray value");
1050 }
1051 _ => panic!("Expected RGB color from Gray.to_rgb()"),
1052 }
1053 }
1054
1055 #[test]
1056 fn test_rgb_black_to_cmyk() {
1057 let black_rgb = Color::rgb(0.0, 0.0, 0.0);
1059 let (c, m, y, k) = black_rgb.cmyk_components();
1060
1061 assert_eq!(c, 0.0, "Cyan should be 0 for pure black");
1063 assert_eq!(m, 0.0, "Magenta should be 0 for pure black");
1064 assert_eq!(y, 0.0, "Yellow should be 0 for pure black");
1065 assert_eq!(k, 1.0, "K should be 1.0 for pure black");
1066 }
1067
1068 #[test]
1071 fn finite_or_zero_passes_finite_values_unchanged() {
1072 assert_eq!(finite_or_zero(0.0), 0.0);
1073 assert_eq!(finite_or_zero(0.5), 0.5);
1074 assert_eq!(finite_or_zero(1.0), 1.0);
1075 assert_eq!(finite_or_zero(-0.5), -0.5); assert_eq!(finite_or_zero(2.0), 2.0);
1077 }
1078
1079 #[test]
1080 fn finite_or_zero_replaces_nan_with_zero() {
1081 assert_eq!(finite_or_zero(f64::NAN), 0.0);
1082 }
1083
1084 #[test]
1085 fn finite_or_zero_replaces_pos_inf_with_zero() {
1086 assert_eq!(finite_or_zero(f64::INFINITY), 0.0);
1087 }
1088
1089 #[test]
1090 fn finite_or_zero_replaces_neg_inf_with_zero() {
1091 assert_eq!(finite_or_zero(f64::NEG_INFINITY), 0.0);
1092 }
1093
1094 #[test]
1095 fn write_fill_color_rgb_emits_lowercase_rg_with_three_decimals() {
1096 let mut ops = String::new();
1097 write_fill_color(&mut ops, Color::Rgb(0.25, 0.5, 0.75));
1098 assert_eq!(ops, "0.250 0.500 0.750 rg\n");
1099 }
1100
1101 #[test]
1102 fn write_fill_color_gray_emits_lowercase_g() {
1103 let mut ops = String::new();
1104 write_fill_color(&mut ops, Color::Gray(0.5));
1105 assert_eq!(ops, "0.500 g\n");
1106 }
1107
1108 #[test]
1109 fn write_fill_color_cmyk_emits_lowercase_k() {
1110 let mut ops = String::new();
1111 write_fill_color(&mut ops, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
1112 assert_eq!(ops, "0.100 0.200 0.300 0.400 k\n");
1113 }
1114
1115 #[test]
1116 fn write_stroke_color_rgb_emits_uppercase_rg() {
1117 let mut ops = String::new();
1118 write_stroke_color(&mut ops, Color::Rgb(0.25, 0.5, 0.75));
1119 assert_eq!(ops, "0.250 0.500 0.750 RG\n");
1120 }
1121
1122 #[test]
1123 fn write_stroke_color_gray_emits_uppercase_g() {
1124 let mut ops = String::new();
1125 write_stroke_color(&mut ops, Color::Gray(0.5));
1126 assert_eq!(ops, "0.500 G\n");
1127 }
1128
1129 #[test]
1130 fn write_stroke_color_cmyk_emits_uppercase_k() {
1131 let mut ops = String::new();
1132 write_stroke_color(&mut ops, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
1133 assert_eq!(ops, "0.100 0.200 0.300 0.400 K\n");
1134 }
1135
1136 #[test]
1137 fn write_fill_color_sanitises_nan_red_only() {
1138 let mut ops = String::new();
1139 write_fill_color(&mut ops, Color::Rgb(f64::NAN, 0.5, 0.75));
1140 assert_eq!(ops, "0.000 0.500 0.750 rg\n");
1141 }
1142
1143 #[test]
1144 fn write_fill_color_sanitises_pos_inf() {
1145 let mut ops = String::new();
1146 write_fill_color(&mut ops, Color::Gray(f64::INFINITY));
1147 assert_eq!(ops, "0.000 g\n");
1148 }
1149
1150 #[test]
1151 fn write_fill_color_sanitises_neg_inf() {
1152 let mut ops = String::new();
1153 write_fill_color(&mut ops, Color::Cmyk(f64::NEG_INFINITY, 0.0, 0.0, 0.0));
1154 assert_eq!(ops, "0.000 0.000 0.000 0.000 k\n");
1155 }
1156
1157 #[test]
1158 fn write_stroke_color_sanitises_nan_in_cmyk() {
1159 let mut ops = String::new();
1160 write_stroke_color(&mut ops, Color::Cmyk(0.1, f64::NAN, 0.3, f64::INFINITY));
1161 assert_eq!(ops, "0.100 0.000 0.300 0.000 K\n");
1162 }
1163
1164 #[test]
1165 fn write_fill_color_appends_to_existing_ops_buffer() {
1166 let mut ops = String::from("BT\n");
1167 write_fill_color(&mut ops, Color::Rgb(0.0, 0.0, 0.0));
1168 assert_eq!(ops, "BT\n0.000 0.000 0.000 rg\n");
1169 }
1170
1171 #[test]
1172 fn fill_and_stroke_operators_differ_only_in_case() {
1173 let mut fill_ops = String::new();
1177 let mut stroke_ops = String::new();
1178 write_fill_color(&mut fill_ops, Color::Rgb(0.1, 0.2, 0.3));
1179 write_stroke_color(&mut stroke_ops, Color::Rgb(0.1, 0.2, 0.3));
1180 assert_eq!(fill_ops, "0.100 0.200 0.300 rg\n");
1181 assert_eq!(stroke_ops, "0.100 0.200 0.300 RG\n");
1182 }
1183}