1use crate::error::{PdfError, Result};
10use crate::graphics::Color;
11use crate::objects::{Dictionary, Object};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum ShadingType {
17 FunctionBased = 1,
19 Axial = 2,
21 Radial = 3,
23 FreeFormGouraud = 4,
25 LatticeFormGouraud = 5,
27 CoonsPatch = 6,
29 TensorProductPatch = 7,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub struct ColorStop {
36 pub position: f64,
38 pub color: Color,
40}
41
42impl ColorStop {
43 pub fn new(position: f64, color: Color) -> Self {
45 Self {
46 position: position.clamp(0.0, 1.0),
47 color,
48 }
49 }
50}
51
52fn resolve_color_space(stops: &[ColorStop]) -> &'static str {
60 match stops.first() {
61 Some(first) => {
62 let name = first.color.color_space_name();
63 if stops.iter().all(|s| s.color.color_space_name() == name) {
64 name
65 } else {
66 "DeviceRGB"
67 }
68 }
69 None => "DeviceRGB",
70 }
71}
72
73fn color_components(color: &Color, space: &str) -> Vec<f64> {
75 match space {
76 "DeviceGray" => vec![match color {
77 Color::Gray(g) => *g,
78 other => {
82 unreachable!("color_components(DeviceGray) called with non-Gray color: {other:?}")
83 }
84 }],
85 "DeviceCMYK" => {
86 let (c, m, y, k) = color.cmyk_components();
87 vec![c, m, y, k]
88 }
89 _ => match color.to_rgb() {
91 Color::Rgb(r, g, b) => vec![r, g, b],
92 _ => unreachable!("to_rgb always yields Color::Rgb"),
93 },
94 }
95}
96
97fn type2_function(c0: &Color, c1: &Color, space: &str) -> Dictionary {
103 let mut dict = Dictionary::new();
104 dict.set("FunctionType", Object::Integer(2));
105 dict.set(
106 "Domain",
107 Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
108 );
109 dict.set(
110 "C0",
111 Object::Array(
112 color_components(c0, space)
113 .into_iter()
114 .map(Object::Real)
115 .collect(),
116 ),
117 );
118 dict.set(
119 "C1",
120 Object::Array(
121 color_components(c1, space)
122 .into_iter()
123 .map(Object::Real)
124 .collect(),
125 ),
126 );
127 dict.set("N", Object::Real(1.0));
128 dict
129}
130
131fn build_color_function(stops: &[ColorStop], space: &str) -> Result<Dictionary> {
139 match stops {
140 [] => Err(PdfError::InvalidStructure(
141 "Shading must have at least one color stop".to_string(),
142 )),
143 [only] => Ok(type2_function(&only.color, &only.color, space)),
144 [a, b] => Ok(type2_function(&a.color, &b.color, space)),
145 _ => {
146 let subfunctions: Vec<Object> = stops
147 .windows(2)
148 .map(|w| Object::Dictionary(type2_function(&w[0].color, &w[1].color, space)))
149 .collect();
150
151 let bounds: Vec<Object> = stops[1..stops.len() - 1]
153 .iter()
154 .map(|s| Object::Real(s.position))
155 .collect();
156
157 let encode: Vec<Object> = (0..subfunctions.len())
159 .flat_map(|_| [Object::Real(0.0), Object::Real(1.0)])
160 .collect();
161
162 let mut dict = Dictionary::new();
163 dict.set("FunctionType", Object::Integer(3));
164 dict.set(
165 "Domain",
166 Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
167 );
168 dict.set("Functions", Object::Array(subfunctions));
169 dict.set("Bounds", Object::Array(bounds));
170 dict.set("Encode", Object::Array(encode));
171 Ok(dict)
172 }
173 }
174}
175
176fn assemble_gradient_dict(
181 shading_type: ShadingType,
182 coords: Vec<Object>,
183 stops: &[ColorStop],
184 extend_start: bool,
185 extend_end: bool,
186) -> Result<Dictionary> {
187 let space = resolve_color_space(stops);
188 let function = build_color_function(stops, space)?;
189
190 let mut dict = Dictionary::new();
191 dict.set("ShadingType", Object::Integer(shading_type as i64));
192 dict.set("ColorSpace", Object::Name(space.to_string()));
193 dict.set("Coords", Object::Array(coords));
194 dict.set(
195 "Domain",
196 Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
197 );
198 dict.set("Function", Object::Dictionary(function));
199 dict.set(
200 "Extend",
201 Object::Array(vec![
202 Object::Boolean(extend_start),
203 Object::Boolean(extend_end),
204 ]),
205 );
206 Ok(dict)
207}
208
209#[derive(Debug, Clone, Copy, PartialEq)]
211pub struct Point {
212 pub x: f64,
213 pub y: f64,
214}
215
216impl Point {
217 pub fn new(x: f64, y: f64) -> Self {
219 Self { x, y }
220 }
221}
222
223#[derive(Debug, Clone)]
225pub struct AxialShading {
226 pub name: String,
228 pub start_point: Point,
230 pub end_point: Point,
232 pub color_stops: Vec<ColorStop>,
234 pub extend_start: bool,
236 pub extend_end: bool,
238}
239
240impl AxialShading {
241 pub fn new(
243 name: String,
244 start_point: Point,
245 end_point: Point,
246 color_stops: Vec<ColorStop>,
247 ) -> Self {
248 Self {
249 name,
250 start_point,
251 end_point,
252 color_stops,
253 extend_start: false,
254 extend_end: false,
255 }
256 }
257
258 pub fn with_extend(mut self, extend_start: bool, extend_end: bool) -> Self {
260 self.extend_start = extend_start;
261 self.extend_end = extend_end;
262 self
263 }
264
265 pub fn linear_gradient(
267 name: String,
268 start_point: Point,
269 end_point: Point,
270 start_color: Color,
271 end_color: Color,
272 ) -> Self {
273 let color_stops = vec![
274 ColorStop::new(0.0, start_color),
275 ColorStop::new(1.0, end_color),
276 ];
277
278 Self::new(name, start_point, end_point, color_stops)
279 }
280
281 pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
287 let coords = vec![
288 Object::Real(self.start_point.x),
289 Object::Real(self.start_point.y),
290 Object::Real(self.end_point.x),
291 Object::Real(self.end_point.y),
292 ];
293 assemble_gradient_dict(
294 ShadingType::Axial,
295 coords,
296 &self.color_stops,
297 self.extend_start,
298 self.extend_end,
299 )
300 }
301
302 pub fn validate(&self) -> Result<()> {
304 if self.color_stops.is_empty() {
305 return Err(PdfError::InvalidStructure(
306 "Axial shading must have at least one color stop".to_string(),
307 ));
308 }
309
310 for window in self.color_stops.windows(2) {
312 if window[0].position > window[1].position {
313 return Err(PdfError::InvalidStructure(
314 "Color stops must be in ascending order".to_string(),
315 ));
316 }
317 }
318
319 if (self.start_point.x - self.end_point.x).abs() < f64::EPSILON
321 && (self.start_point.y - self.end_point.y).abs() < f64::EPSILON
322 {
323 return Err(PdfError::InvalidStructure(
324 "Start and end points cannot be the same".to_string(),
325 ));
326 }
327
328 Ok(())
329 }
330}
331
332#[derive(Debug, Clone)]
334pub struct RadialShading {
335 pub name: String,
337 pub start_center: Point,
339 pub start_radius: f64,
341 pub end_center: Point,
343 pub end_radius: f64,
345 pub color_stops: Vec<ColorStop>,
347 pub extend_start: bool,
349 pub extend_end: bool,
351}
352
353impl RadialShading {
354 pub fn new(
356 name: String,
357 start_center: Point,
358 start_radius: f64,
359 end_center: Point,
360 end_radius: f64,
361 color_stops: Vec<ColorStop>,
362 ) -> Self {
363 Self {
364 name,
365 start_center,
366 start_radius: start_radius.max(0.0),
367 end_center,
368 end_radius: end_radius.max(0.0),
369 color_stops,
370 extend_start: false,
371 extend_end: false,
372 }
373 }
374
375 pub fn with_extend(mut self, extend_start: bool, extend_end: bool) -> Self {
377 self.extend_start = extend_start;
378 self.extend_end = extend_end;
379 self
380 }
381
382 pub fn radial_gradient(
384 name: String,
385 center: Point,
386 start_radius: f64,
387 end_radius: f64,
388 start_color: Color,
389 end_color: Color,
390 ) -> Self {
391 let color_stops = vec![
392 ColorStop::new(0.0, start_color),
393 ColorStop::new(1.0, end_color),
394 ];
395
396 Self::new(name, center, start_radius, center, end_radius, color_stops)
397 }
398
399 pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
405 let coords = vec![
406 Object::Real(self.start_center.x),
407 Object::Real(self.start_center.y),
408 Object::Real(self.start_radius),
409 Object::Real(self.end_center.x),
410 Object::Real(self.end_center.y),
411 Object::Real(self.end_radius),
412 ];
413 assemble_gradient_dict(
414 ShadingType::Radial,
415 coords,
416 &self.color_stops,
417 self.extend_start,
418 self.extend_end,
419 )
420 }
421
422 pub fn validate(&self) -> Result<()> {
424 if self.color_stops.is_empty() {
425 return Err(PdfError::InvalidStructure(
426 "Radial shading must have at least one color stop".to_string(),
427 ));
428 }
429
430 for window in self.color_stops.windows(2) {
432 if window[0].position > window[1].position {
433 return Err(PdfError::InvalidStructure(
434 "Color stops must be in ascending order".to_string(),
435 ));
436 }
437 }
438
439 if self.start_radius < 0.0 || self.end_radius < 0.0 {
441 return Err(PdfError::InvalidStructure(
442 "Radii cannot be negative".to_string(),
443 ));
444 }
445
446 Ok(())
447 }
448}
449
450#[derive(Debug, Clone)]
452pub struct FunctionBasedShading {
453 pub name: String,
455 pub domain: [f64; 4],
457 pub matrix: Option<[f64; 6]>,
459 pub function_id: u32,
461}
462
463impl FunctionBasedShading {
464 pub fn new(name: String, domain: [f64; 4], function_id: u32) -> Self {
466 Self {
467 name,
468 domain,
469 matrix: None,
470 function_id,
471 }
472 }
473
474 pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
476 self.matrix = Some(matrix);
477 self
478 }
479
480 pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
482 let mut shading_dict = Dictionary::new();
483
484 shading_dict.set(
486 "ShadingType",
487 Object::Integer(ShadingType::FunctionBased as i64),
488 );
489
490 let domain = vec![
492 Object::Real(self.domain[0]),
493 Object::Real(self.domain[1]),
494 Object::Real(self.domain[2]),
495 Object::Real(self.domain[3]),
496 ];
497 shading_dict.set("Domain", Object::Array(domain));
498
499 if let Some(matrix) = self.matrix {
501 let matrix_objects: Vec<Object> = matrix.iter().map(|&x| Object::Real(x)).collect();
502 shading_dict.set("Matrix", Object::Array(matrix_objects));
503 }
504
505 shading_dict.set("Function", Object::Integer(self.function_id as i64));
507
508 Ok(shading_dict)
509 }
510
511 pub fn validate(&self) -> Result<()> {
513 if self.domain[0] >= self.domain[1] || self.domain[2] >= self.domain[3] {
515 return Err(PdfError::InvalidStructure(
516 "Invalid domain: min values must be less than max values".to_string(),
517 ));
518 }
519
520 Ok(())
521 }
522}
523
524#[derive(Debug, Clone)]
526pub struct ShadingPattern {
527 pub name: String,
529 pub shading: ShadingDefinition,
531 pub matrix: Option<[f64; 6]>,
533}
534
535#[derive(Debug, Clone)]
537pub enum ShadingDefinition {
538 Axial(AxialShading),
540 Radial(RadialShading),
542 FunctionBased(FunctionBasedShading),
544}
545
546impl ShadingDefinition {
547 pub fn name(&self) -> &str {
549 match self {
550 ShadingDefinition::Axial(shading) => &shading.name,
551 ShadingDefinition::Radial(shading) => &shading.name,
552 ShadingDefinition::FunctionBased(shading) => &shading.name,
553 }
554 }
555
556 pub fn validate(&self) -> Result<()> {
558 match self {
559 ShadingDefinition::Axial(shading) => shading.validate(),
560 ShadingDefinition::Radial(shading) => shading.validate(),
561 ShadingDefinition::FunctionBased(shading) => shading.validate(),
562 }
563 }
564
565 pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
567 match self {
568 ShadingDefinition::Axial(shading) => shading.to_pdf_dictionary(),
569 ShadingDefinition::Radial(shading) => shading.to_pdf_dictionary(),
570 ShadingDefinition::FunctionBased(shading) => shading.to_pdf_dictionary(),
571 }
572 }
573}
574
575impl ShadingPattern {
576 pub fn new(name: String, shading: ShadingDefinition) -> Self {
578 Self {
579 name,
580 shading,
581 matrix: None,
582 }
583 }
584
585 pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
587 self.matrix = Some(matrix);
588 self
589 }
590
591 pub fn to_pdf_pattern_dictionary(&self) -> Result<Dictionary> {
603 let mut pattern_dict = Dictionary::new();
604
605 pattern_dict.set("Type", Object::Name("Pattern".to_string()));
607 pattern_dict.set("PatternType", Object::Integer(2)); pattern_dict.set(
614 "Shading",
615 Object::Dictionary(self.shading.to_pdf_dictionary()?),
616 );
617
618 if let Some(matrix) = self.matrix {
620 let matrix_objects: Vec<Object> = matrix.iter().map(|&x| Object::Real(x)).collect();
621 pattern_dict.set("Matrix", Object::Array(matrix_objects));
622 }
623
624 Ok(pattern_dict)
625 }
626
627 pub fn validate(&self) -> Result<()> {
629 self.shading.validate()
630 }
631}
632
633#[derive(Debug, Clone)]
635pub struct ShadingManager {
636 shadings: HashMap<String, ShadingDefinition>,
638 patterns: HashMap<String, ShadingPattern>,
640 next_id: usize,
642}
643
644impl Default for ShadingManager {
645 fn default() -> Self {
646 Self::new()
647 }
648}
649
650impl ShadingManager {
651 pub fn new() -> Self {
653 Self {
654 shadings: HashMap::new(),
655 patterns: HashMap::new(),
656 next_id: 1,
657 }
658 }
659
660 pub fn add_shading(&mut self, mut shading: ShadingDefinition) -> Result<String> {
662 shading.validate()?;
664
665 let name = shading.name().to_string();
666
667 let final_name = if name.is_empty() || self.shadings.contains_key(&name) {
669 let auto_name = format!("Sh{}", self.next_id);
670 self.next_id += 1;
671
672 match &mut shading {
674 ShadingDefinition::Axial(s) => s.name = auto_name.clone(),
675 ShadingDefinition::Radial(s) => s.name = auto_name.clone(),
676 ShadingDefinition::FunctionBased(s) => s.name = auto_name.clone(),
677 }
678
679 auto_name
680 } else {
681 name
682 };
683
684 self.shadings.insert(final_name.clone(), shading);
685 Ok(final_name)
686 }
687
688 pub fn add_shading_pattern(&mut self, mut pattern: ShadingPattern) -> Result<String> {
690 pattern.validate()?;
692
693 if pattern.name.is_empty() || self.patterns.contains_key(&pattern.name) {
695 pattern.name = format!("SP{}", self.next_id);
696 self.next_id += 1;
697 }
698
699 let name = pattern.name.clone();
700 self.patterns.insert(name.clone(), pattern);
701 Ok(name)
702 }
703
704 pub fn get_shading(&self, name: &str) -> Option<&ShadingDefinition> {
706 self.shadings.get(name)
707 }
708
709 pub fn get_pattern(&self, name: &str) -> Option<&ShadingPattern> {
711 self.patterns.get(name)
712 }
713
714 pub fn shadings(&self) -> &HashMap<String, ShadingDefinition> {
716 &self.shadings
717 }
718
719 pub fn patterns(&self) -> &HashMap<String, ShadingPattern> {
721 &self.patterns
722 }
723
724 pub fn clear(&mut self) {
726 self.shadings.clear();
727 self.patterns.clear();
728 self.next_id = 1;
729 }
730
731 pub fn shading_count(&self) -> usize {
733 self.shadings.len()
734 }
735
736 pub fn pattern_count(&self) -> usize {
738 self.patterns.len()
739 }
740
741 pub fn total_count(&self) -> usize {
743 self.shading_count() + self.pattern_count()
744 }
745
746 pub fn create_linear_gradient(
748 &mut self,
749 start_point: Point,
750 end_point: Point,
751 start_color: Color,
752 end_color: Color,
753 ) -> Result<String> {
754 let shading = ShadingDefinition::Axial(AxialShading::linear_gradient(
755 String::new(), start_point,
757 end_point,
758 start_color,
759 end_color,
760 ));
761
762 self.add_shading(shading)
763 }
764
765 pub fn create_radial_gradient(
767 &mut self,
768 center: Point,
769 start_radius: f64,
770 end_radius: f64,
771 start_color: Color,
772 end_color: Color,
773 ) -> Result<String> {
774 let shading = ShadingDefinition::Radial(RadialShading::radial_gradient(
775 String::new(), center,
777 start_radius,
778 end_radius,
779 start_color,
780 end_color,
781 ));
782
783 self.add_shading(shading)
784 }
785
786 pub fn to_resource_dictionary(&self) -> Result<String> {
788 if self.shadings.is_empty() && self.patterns.is_empty() {
789 return Ok(String::new());
790 }
791
792 let mut dict = String::new();
793
794 if !self.shadings.is_empty() {
796 dict.push_str("/Shading <<");
797 for name in self.shadings.keys() {
798 dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
799 }
800 dict.push_str(" >>");
801 }
802
803 if !self.patterns.is_empty() {
805 if !dict.is_empty() {
806 dict.push('\n');
807 }
808 dict.push_str("/Pattern <<");
809 for name in self.patterns.keys() {
810 dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
811 }
812 dict.push_str(" >>");
813 }
814
815 Ok(dict)
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822
823 #[test]
824 fn test_color_stop_creation() {
825 let stop = ColorStop::new(0.5, Color::red());
826 assert_eq!(stop.position, 0.5);
827 assert_eq!(stop.color, Color::red());
828
829 let stop_clamped = ColorStop::new(1.5, Color::blue());
831 assert_eq!(stop_clamped.position, 1.0);
832 }
833
834 #[test]
835 fn test_point_creation() {
836 let point = Point::new(10.0, 20.0);
837 assert_eq!(point.x, 10.0);
838 assert_eq!(point.y, 20.0);
839 }
840
841 #[test]
842 fn test_axial_shading_creation() {
843 let start = Point::new(0.0, 0.0);
844 let end = Point::new(100.0, 100.0);
845 let stops = vec![
846 ColorStop::new(0.0, Color::red()),
847 ColorStop::new(1.0, Color::blue()),
848 ];
849
850 let shading = AxialShading::new("TestGradient".to_string(), start, end, stops);
851 assert_eq!(shading.name, "TestGradient");
852 assert_eq!(shading.start_point, start);
853 assert_eq!(shading.end_point, end);
854 assert_eq!(shading.color_stops.len(), 2);
855 assert!(!shading.extend_start);
856 assert!(!shading.extend_end);
857 }
858
859 #[test]
860 fn test_axial_shading_linear_gradient() {
861 let start = Point::new(0.0, 0.0);
862 let end = Point::new(100.0, 0.0);
863 let shading = AxialShading::linear_gradient(
864 "LinearGrad".to_string(),
865 start,
866 end,
867 Color::red(),
868 Color::blue(),
869 );
870
871 assert_eq!(shading.color_stops.len(), 2);
872 assert_eq!(shading.color_stops[0].position, 0.0);
873 assert_eq!(shading.color_stops[1].position, 1.0);
874 }
875
876 #[test]
877 fn test_axial_shading_with_extend() {
878 let start = Point::new(0.0, 0.0);
879 let end = Point::new(100.0, 0.0);
880 let shading = AxialShading::linear_gradient(
881 "ExtendedGrad".to_string(),
882 start,
883 end,
884 Color::red(),
885 Color::blue(),
886 )
887 .with_extend(true, true);
888
889 assert!(shading.extend_start);
890 assert!(shading.extend_end);
891 }
892
893 #[test]
894 fn test_axial_shading_validation_valid() {
895 let start = Point::new(0.0, 0.0);
896 let end = Point::new(100.0, 0.0);
897 let shading = AxialShading::linear_gradient(
898 "ValidGrad".to_string(),
899 start,
900 end,
901 Color::red(),
902 Color::blue(),
903 );
904
905 assert!(shading.validate().is_ok());
906 }
907
908 #[test]
909 fn test_axial_shading_validation_no_stops() {
910 let start = Point::new(0.0, 0.0);
911 let end = Point::new(100.0, 0.0);
912 let shading = AxialShading::new("EmptyGrad".to_string(), start, end, Vec::new());
913
914 assert!(shading.validate().is_err());
915 }
916
917 #[test]
918 fn test_axial_shading_validation_same_points() {
919 let point = Point::new(50.0, 50.0);
920 let shading = AxialShading::linear_gradient(
921 "SamePointGrad".to_string(),
922 point,
923 point,
924 Color::red(),
925 Color::blue(),
926 );
927
928 assert!(shading.validate().is_err());
929 }
930
931 #[test]
932 fn test_radial_shading_creation() {
933 let center = Point::new(50.0, 50.0);
934 let stops = vec![
935 ColorStop::new(0.0, Color::red()),
936 ColorStop::new(1.0, Color::blue()),
937 ];
938
939 let shading =
940 RadialShading::new("RadialGrad".to_string(), center, 10.0, center, 50.0, stops);
941
942 assert_eq!(shading.name, "RadialGrad");
943 assert_eq!(shading.start_center, center);
944 assert_eq!(shading.start_radius, 10.0);
945 assert_eq!(shading.end_radius, 50.0);
946 }
947
948 #[test]
949 fn test_radial_shading_gradient() {
950 let center = Point::new(50.0, 50.0);
951 let shading = RadialShading::radial_gradient(
952 "SimpleRadial".to_string(),
953 center,
954 0.0,
955 25.0,
956 Color::white(),
957 Color::black(),
958 );
959
960 assert_eq!(shading.color_stops.len(), 2);
961 assert_eq!(shading.start_radius, 0.0);
962 assert_eq!(shading.end_radius, 25.0);
963 }
964
965 #[test]
966 fn test_radial_shading_radius_clamping() {
967 let center = Point::new(50.0, 50.0);
968 let stops = vec![ColorStop::new(0.0, Color::red())];
969
970 let shading = RadialShading::new(
971 "ClampedRadial".to_string(),
972 center,
973 -5.0, center,
975 10.0,
976 stops,
977 );
978
979 assert_eq!(shading.start_radius, 0.0);
980 }
981
982 #[test]
983 fn test_radial_shading_validation_valid() {
984 let center = Point::new(50.0, 50.0);
985 let shading = RadialShading::radial_gradient(
986 "ValidRadial".to_string(),
987 center,
988 0.0,
989 25.0,
990 Color::red(),
991 Color::blue(),
992 );
993
994 assert!(shading.validate().is_ok());
995 }
996
997 #[test]
998 fn test_function_based_shading_creation() {
999 let domain = [0.0, 1.0, 0.0, 1.0];
1000 let shading = FunctionBasedShading::new("FuncShading".to_string(), domain, 1);
1001
1002 assert_eq!(shading.name, "FuncShading");
1003 assert_eq!(shading.domain, domain);
1004 assert_eq!(shading.function_id, 1);
1005 assert!(shading.matrix.is_none());
1006 }
1007
1008 #[test]
1009 fn test_function_based_shading_with_matrix() {
1010 let domain = [0.0, 1.0, 0.0, 1.0];
1011 let matrix = [2.0, 0.0, 0.0, 2.0, 10.0, 20.0];
1012 let shading =
1013 FunctionBasedShading::new("FuncShading".to_string(), domain, 1).with_matrix(matrix);
1014
1015 assert_eq!(shading.matrix, Some(matrix));
1016 }
1017
1018 #[test]
1019 fn test_function_based_shading_validation_valid() {
1020 let domain = [0.0, 1.0, 0.0, 1.0];
1021 let shading = FunctionBasedShading::new("ValidFunc".to_string(), domain, 1);
1022
1023 assert!(shading.validate().is_ok());
1024 }
1025
1026 #[test]
1027 fn test_function_based_shading_validation_invalid_domain() {
1028 let domain = [1.0, 0.0, 0.0, 1.0]; let shading = FunctionBasedShading::new("InvalidFunc".to_string(), domain, 1);
1030
1031 assert!(shading.validate().is_err());
1032 }
1033
1034 #[test]
1035 fn test_shading_pattern_creation() {
1036 let start = Point::new(0.0, 0.0);
1037 let end = Point::new(100.0, 0.0);
1038 let axial = AxialShading::linear_gradient(
1039 "PatternGrad".to_string(),
1040 start,
1041 end,
1042 Color::red(),
1043 Color::blue(),
1044 );
1045 let shading = ShadingDefinition::Axial(axial);
1046 let pattern = ShadingPattern::new("Pattern1".to_string(), shading);
1047
1048 assert_eq!(pattern.name, "Pattern1");
1049 assert!(pattern.matrix.is_none());
1050 }
1051
1052 #[test]
1053 fn test_shading_pattern_with_matrix() {
1054 let start = Point::new(0.0, 0.0);
1055 let end = Point::new(100.0, 0.0);
1056 let axial = AxialShading::linear_gradient(
1057 "PatternGrad".to_string(),
1058 start,
1059 end,
1060 Color::red(),
1061 Color::blue(),
1062 );
1063 let shading = ShadingDefinition::Axial(axial);
1064 let matrix = [1.0, 0.0, 0.0, 1.0, 50.0, 50.0];
1065 let pattern = ShadingPattern::new("Pattern1".to_string(), shading).with_matrix(matrix);
1066
1067 assert_eq!(pattern.matrix, Some(matrix));
1068 }
1069
1070 #[test]
1071 fn test_shading_manager_creation() {
1072 let manager = ShadingManager::new();
1073 assert_eq!(manager.shading_count(), 0);
1074 assert_eq!(manager.pattern_count(), 0);
1075 assert_eq!(manager.total_count(), 0);
1076 }
1077
1078 #[test]
1079 fn test_shading_manager_add_shading() {
1080 let mut manager = ShadingManager::new();
1081 let start = Point::new(0.0, 0.0);
1082 let end = Point::new(100.0, 0.0);
1083 let axial = AxialShading::linear_gradient(
1084 "TestGrad".to_string(),
1085 start,
1086 end,
1087 Color::red(),
1088 Color::blue(),
1089 );
1090 let shading = ShadingDefinition::Axial(axial);
1091
1092 let name = manager.add_shading(shading).unwrap();
1093 assert_eq!(name, "TestGrad");
1094 assert_eq!(manager.shading_count(), 1);
1095
1096 let retrieved = manager.get_shading(&name).unwrap();
1097 assert_eq!(retrieved.name(), "TestGrad");
1098 }
1099
1100 #[test]
1101 fn test_shading_manager_auto_naming() {
1102 let mut manager = ShadingManager::new();
1103 let start = Point::new(0.0, 0.0);
1104 let end = Point::new(100.0, 0.0);
1105 let axial = AxialShading::linear_gradient(
1106 String::new(), start,
1108 end,
1109 Color::red(),
1110 Color::blue(),
1111 );
1112 let shading = ShadingDefinition::Axial(axial);
1113
1114 let name = manager.add_shading(shading).unwrap();
1115 assert_eq!(name, "Sh1");
1116
1117 let axial2 = AxialShading::linear_gradient(
1119 String::new(),
1120 start,
1121 end,
1122 Color::green(),
1123 Color::yellow(),
1124 );
1125 let shading2 = ShadingDefinition::Axial(axial2);
1126
1127 let name2 = manager.add_shading(shading2).unwrap();
1128 assert_eq!(name2, "Sh2");
1129 }
1130
1131 #[test]
1132 fn test_shading_manager_create_gradients() {
1133 let mut manager = ShadingManager::new();
1134
1135 let linear_name = manager
1136 .create_linear_gradient(
1137 Point::new(0.0, 0.0),
1138 Point::new(100.0, 0.0),
1139 Color::red(),
1140 Color::blue(),
1141 )
1142 .unwrap();
1143
1144 let radial_name = manager
1145 .create_radial_gradient(
1146 Point::new(50.0, 50.0),
1147 0.0,
1148 25.0,
1149 Color::white(),
1150 Color::black(),
1151 )
1152 .unwrap();
1153
1154 assert_eq!(manager.shading_count(), 2);
1155 assert!(manager.get_shading(&linear_name).is_some());
1156 assert!(manager.get_shading(&radial_name).is_some());
1157 }
1158
1159 #[test]
1160 fn test_shading_manager_clear() {
1161 let mut manager = ShadingManager::new();
1162
1163 manager
1164 .create_linear_gradient(
1165 Point::new(0.0, 0.0),
1166 Point::new(100.0, 0.0),
1167 Color::red(),
1168 Color::blue(),
1169 )
1170 .unwrap();
1171
1172 assert_eq!(manager.shading_count(), 1);
1173
1174 manager.clear();
1175 assert_eq!(manager.shading_count(), 0);
1176 assert_eq!(manager.total_count(), 0);
1177 }
1178
1179 #[test]
1180 fn test_axial_shading_pdf_dictionary() {
1181 let start = Point::new(0.0, 0.0);
1182 let end = Point::new(100.0, 50.0);
1183 let shading = AxialShading::linear_gradient(
1184 "TestPDF".to_string(),
1185 start,
1186 end,
1187 Color::red(),
1188 Color::blue(),
1189 )
1190 .with_extend(true, false);
1191
1192 let dict = shading.to_pdf_dictionary().unwrap();
1193
1194 if let Some(Object::Integer(shading_type)) = dict.get("ShadingType") {
1195 assert_eq!(*shading_type, 2); }
1197
1198 if let Some(Object::Array(coords)) = dict.get("Coords") {
1199 assert_eq!(coords.len(), 4);
1200 }
1201
1202 if let Some(Object::Array(extend)) = dict.get("Extend") {
1203 assert_eq!(extend.len(), 2);
1204 if let (Object::Boolean(start_extend), Object::Boolean(end_extend)) =
1205 (&extend[0], &extend[1])
1206 {
1207 assert!(*start_extend);
1208 assert!(!(*end_extend));
1209 }
1210 }
1211 }
1212
1213 fn type2_c0_c1(func: &Dictionary) -> (Vec<f64>, Vec<f64>) {
1217 let extract = |key: &str| -> Vec<f64> {
1218 match func.get(key) {
1219 Some(Object::Array(a)) => a
1220 .iter()
1221 .map(|o| match o {
1222 Object::Real(v) => *v,
1223 Object::Integer(v) => *v as f64,
1224 _ => panic!("{key} component is not numeric"),
1225 })
1226 .collect(),
1227 other => panic!("{key} is not an array: {other:?}"),
1228 }
1229 };
1230 (extract("C0"), extract("C1"))
1231 }
1232
1233 #[test]
1234 fn test_axial_two_stops_emits_real_type2_function() {
1235 let shading = AxialShading::linear_gradient(
1238 "G".to_string(),
1239 Point::new(0.0, 0.0),
1240 Point::new(100.0, 0.0),
1241 Color::red(),
1242 Color::blue(),
1243 );
1244 let dict = shading.to_pdf_dictionary().unwrap();
1245
1246 assert_eq!(
1248 dict.get("ColorSpace"),
1249 Some(&Object::Name("DeviceRGB".to_string())),
1250 "axial shading must declare /ColorSpace"
1251 );
1252
1253 let func = match dict.get("Function") {
1255 Some(Object::Dictionary(d)) => d,
1256 other => panic!("/Function must be a dictionary, got {other:?}"),
1257 };
1258 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(2)));
1259 let (c0, c1) = type2_c0_c1(func);
1260 assert_eq!(c0, vec![1.0, 0.0, 0.0], "C0 must be red");
1261 assert_eq!(c1, vec![0.0, 0.0, 1.0], "C1 must be blue");
1262 assert_eq!(func.get("N"), Some(&Object::Real(1.0)));
1263 assert_eq!(
1264 func.get("Domain"),
1265 Some(&Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]))
1266 );
1267 }
1268
1269 #[test]
1270 fn test_axial_three_stops_emits_type3_stitching() {
1271 let shading = AxialShading::new(
1274 "G".to_string(),
1275 Point::new(0.0, 0.0),
1276 Point::new(100.0, 0.0),
1277 vec![
1278 ColorStop::new(0.0, Color::red()),
1279 ColorStop::new(0.5, Color::green()),
1280 ColorStop::new(1.0, Color::blue()),
1281 ],
1282 );
1283 let dict = shading.to_pdf_dictionary().unwrap();
1284 let func = match dict.get("Function") {
1285 Some(Object::Dictionary(d)) => d,
1286 other => panic!("/Function must be a dictionary, got {other:?}"),
1287 };
1288 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(3)));
1289 assert_eq!(
1290 func.get("Bounds"),
1291 Some(&Object::Array(vec![Object::Real(0.5)])),
1292 "interior stop position is the only bound"
1293 );
1294 assert_eq!(
1295 func.get("Encode"),
1296 Some(&Object::Array(vec![
1297 Object::Real(0.0),
1298 Object::Real(1.0),
1299 Object::Real(0.0),
1300 Object::Real(1.0),
1301 ]))
1302 );
1303 let subfuncs = match func.get("Functions") {
1304 Some(Object::Array(a)) => a,
1305 other => panic!("/Functions must be an array, got {other:?}"),
1306 };
1307 assert_eq!(subfuncs.len(), 2, "two segments for three stops");
1308 let f0 = match &subfuncs[0] {
1310 Object::Dictionary(d) => d,
1311 other => panic!("subfunction 0 not a dict: {other:?}"),
1312 };
1313 let (c0, c1) = type2_c0_c1(f0);
1314 assert_eq!(c0, vec![1.0, 0.0, 0.0]);
1315 assert_eq!(c1, vec![0.0, 1.0, 0.0]);
1316 }
1317
1318 #[test]
1319 fn test_axial_gray_stops_emit_devicegray_function() {
1320 let shading = AxialShading::linear_gradient(
1322 "G".to_string(),
1323 Point::new(0.0, 0.0),
1324 Point::new(10.0, 0.0),
1325 Color::black(),
1326 Color::white(),
1327 );
1328 let dict = shading.to_pdf_dictionary().unwrap();
1329 assert_eq!(
1330 dict.get("ColorSpace"),
1331 Some(&Object::Name("DeviceGray".to_string()))
1332 );
1333 let func = match dict.get("Function") {
1334 Some(Object::Dictionary(d)) => d,
1335 other => panic!("/Function must be a dictionary, got {other:?}"),
1336 };
1337 let (c0, c1) = type2_c0_c1(func);
1338 assert_eq!(c0, vec![0.0], "black");
1339 assert_eq!(c1, vec![1.0], "white");
1340 }
1341
1342 #[test]
1343 fn test_axial_cmyk_stops_emit_devicecmyk_function() {
1344 let shading = AxialShading::linear_gradient(
1346 "G".to_string(),
1347 Point::new(0.0, 0.0),
1348 Point::new(10.0, 0.0),
1349 Color::Cmyk(1.0, 0.0, 0.0, 0.0),
1350 Color::Cmyk(0.0, 1.0, 0.0, 0.0),
1351 );
1352 let dict = shading.to_pdf_dictionary().unwrap();
1353 assert_eq!(
1354 dict.get("ColorSpace"),
1355 Some(&Object::Name("DeviceCMYK".to_string()))
1356 );
1357 let func = match dict.get("Function") {
1358 Some(Object::Dictionary(d)) => d,
1359 other => panic!("/Function must be a dictionary, got {other:?}"),
1360 };
1361 let (c0, c1) = type2_c0_c1(func);
1362 assert_eq!(c0, vec![1.0, 0.0, 0.0, 0.0], "C0 = cyan, 4 components");
1363 assert_eq!(c1, vec![0.0, 1.0, 0.0, 0.0], "C1 = magenta, 4 components");
1364 }
1365
1366 #[test]
1367 fn test_axial_four_stops_type3_has_three_subfunctions_two_bounds() {
1368 let shading = AxialShading::new(
1369 "G".to_string(),
1370 Point::new(0.0, 0.0),
1371 Point::new(100.0, 0.0),
1372 vec![
1373 ColorStop::new(0.0, Color::red()),
1374 ColorStop::new(0.3, Color::green()),
1375 ColorStop::new(0.7, Color::blue()),
1376 ColorStop::new(1.0, Color::white()),
1377 ],
1378 );
1379 let dict = shading.to_pdf_dictionary().unwrap();
1380 let func = match dict.get("Function") {
1381 Some(Object::Dictionary(d)) => d,
1382 other => panic!("/Function must be a dictionary, got {other:?}"),
1383 };
1384 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(3)));
1385 let subfuncs = match func.get("Functions") {
1386 Some(Object::Array(a)) => a,
1387 other => panic!("/Functions array expected, got {other:?}"),
1388 };
1389 assert_eq!(subfuncs.len(), 3, "4 stops → 3 segments");
1390 assert_eq!(
1391 func.get("Bounds"),
1392 Some(&Object::Array(vec![Object::Real(0.3), Object::Real(0.7)])),
1393 "two interior bounds at the middle stops"
1394 );
1395 assert_eq!(
1396 func.get("Encode"),
1397 Some(&Object::Array(vec![
1398 Object::Real(0.0),
1399 Object::Real(1.0),
1400 Object::Real(0.0),
1401 Object::Real(1.0),
1402 Object::Real(0.0),
1403 Object::Real(1.0),
1404 ]))
1405 );
1406 }
1407
1408 #[test]
1409 fn test_single_stop_emits_constant_type2() {
1410 let shading = AxialShading::new(
1412 "G".to_string(),
1413 Point::new(0.0, 0.0),
1414 Point::new(10.0, 0.0),
1415 vec![ColorStop::new(0.0, Color::Rgb(0.2, 0.4, 0.6))],
1416 );
1417 let func = match shading.to_pdf_dictionary().unwrap().get("Function") {
1418 Some(Object::Dictionary(d)) => d.clone(),
1419 other => panic!("/Function must be a dictionary, got {other:?}"),
1420 };
1421 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(2)));
1422 let (c0, c1) = type2_c0_c1(&func);
1423 assert_eq!(c0, c1, "constant colour: C0 == C1");
1424 assert_eq!(c0, vec![0.2, 0.4, 0.6]);
1425 }
1426
1427 #[test]
1428 fn test_mixed_color_spaces_promote_to_rgb() {
1429 let shading = AxialShading::new(
1431 "G".to_string(),
1432 Point::new(0.0, 0.0),
1433 Point::new(10.0, 0.0),
1434 vec![
1435 ColorStop::new(0.0, Color::Gray(0.5)),
1436 ColorStop::new(1.0, Color::Rgb(1.0, 0.0, 0.0)),
1437 ],
1438 );
1439 let dict = shading.to_pdf_dictionary().unwrap();
1440 assert_eq!(
1441 dict.get("ColorSpace"),
1442 Some(&Object::Name("DeviceRGB".to_string()))
1443 );
1444 let func = match dict.get("Function") {
1445 Some(Object::Dictionary(d)) => d,
1446 other => panic!("/Function must be a dictionary, got {other:?}"),
1447 };
1448 let (c0, c1) = type2_c0_c1(func);
1449 assert_eq!(c0, vec![0.5, 0.5, 0.5], "gray 0.5 promoted to RGB");
1450 assert_eq!(c1, vec![1.0, 0.0, 0.0]);
1451 }
1452
1453 #[test]
1454 fn test_radial_emits_real_function_and_colorspace() {
1455 let center = Point::new(50.0, 50.0);
1456 let shading = RadialShading::radial_gradient(
1457 "R".to_string(),
1458 center,
1459 0.0,
1460 25.0,
1461 Color::cyan(),
1462 Color::magenta(),
1463 );
1464 let dict = shading.to_pdf_dictionary().unwrap();
1465 assert_eq!(
1466 dict.get("ColorSpace"),
1467 Some(&Object::Name("DeviceRGB".to_string()))
1468 );
1469 let func = match dict.get("Function") {
1470 Some(Object::Dictionary(d)) => d,
1471 other => panic!("/Function must be a dictionary, got {other:?}"),
1472 };
1473 assert_eq!(func.get("FunctionType"), Some(&Object::Integer(2)));
1474 }
1475
1476 #[test]
1477 fn test_shading_pattern_inlines_real_shading_not_placeholder() {
1478 let axial = AxialShading::linear_gradient(
1480 "P".to_string(),
1481 Point::new(0.0, 0.0),
1482 Point::new(100.0, 0.0),
1483 Color::red(),
1484 Color::blue(),
1485 );
1486 let pattern = ShadingPattern::new("SP1".to_string(), ShadingDefinition::Axial(axial));
1487 let dict = pattern.to_pdf_pattern_dictionary().unwrap();
1488 assert_eq!(dict.get("PatternType"), Some(&Object::Integer(2)));
1489 let shading = match dict.get("Shading") {
1490 Some(Object::Dictionary(d)) => d,
1491 other => panic!("/Shading must be an inline dict, got {other:?}"),
1492 };
1493 assert_eq!(shading.get("ShadingType"), Some(&Object::Integer(2)));
1494 assert!(
1495 matches!(shading.get("Function"), Some(Object::Dictionary(_))),
1496 "inlined shading must carry a real /Function"
1497 );
1498 }
1499
1500 #[test]
1501 fn test_radial_shading_pdf_dictionary() {
1502 let center = Point::new(50.0, 50.0);
1503 let shading = RadialShading::radial_gradient(
1504 "TestRadialPDF".to_string(),
1505 center,
1506 10.0,
1507 30.0,
1508 Color::yellow(),
1509 Color::red(),
1510 );
1511
1512 let dict = shading.to_pdf_dictionary().unwrap();
1513
1514 if let Some(Object::Integer(shading_type)) = dict.get("ShadingType") {
1515 assert_eq!(*shading_type, 3); }
1517
1518 if let Some(Object::Array(coords)) = dict.get("Coords") {
1519 assert_eq!(coords.len(), 6); }
1521 }
1522}