oxidize_pdf/graphics/
shadings.rs

1//! Shading support for PDF graphics according to ISO 32000-1 Section 8.7.4
2//!
3//! This module provides basic support for PDF shadings including:
4//! - Axial shadings (linear gradients)
5//! - Radial shadings (radial gradients)
6//! - Function-based shadings
7//! - Shading dictionaries and patterns
8
9use crate::error::{PdfError, Result};
10use crate::graphics::Color;
11use crate::objects::{Dictionary, Object};
12use std::collections::HashMap;
13
14/// Shading type enumeration according to ISO 32000-1
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum ShadingType {
17    /// Function-based shading (Type 1)
18    FunctionBased = 1,
19    /// Axial shading (Type 2) - linear gradient
20    Axial = 2,
21    /// Radial shading (Type 3) - radial gradient
22    Radial = 3,
23    /// Free-form Gouraud-shaded triangle mesh (Type 4)
24    FreeFormGouraud = 4,
25    /// Lattice-form Gouraud-shaded triangle mesh (Type 5)
26    LatticeFormGouraud = 5,
27    /// Coons patch mesh (Type 6)
28    CoonsPatch = 6,
29    /// Tensor-product patch mesh (Type 7)
30    TensorProductPatch = 7,
31}
32
33/// Color stop for gradient definitions
34#[derive(Debug, Clone, PartialEq)]
35pub struct ColorStop {
36    /// Position along gradient (0.0 to 1.0)
37    pub position: f64,
38    /// Color at this position
39    pub color: Color,
40}
41
42impl ColorStop {
43    /// Create a new color stop
44    pub fn new(position: f64, color: Color) -> Self {
45        Self {
46            position: position.clamp(0.0, 1.0),
47            color,
48        }
49    }
50}
51
52/// Coordinate point for shading definitions
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct Point {
55    pub x: f64,
56    pub y: f64,
57}
58
59impl Point {
60    /// Create a new point
61    pub fn new(x: f64, y: f64) -> Self {
62        Self { x, y }
63    }
64}
65
66/// Axial (linear) shading definition
67#[derive(Debug, Clone)]
68pub struct AxialShading {
69    /// Shading name for referencing
70    pub name: String,
71    /// Start point of the gradient
72    pub start_point: Point,
73    /// End point of the gradient
74    pub end_point: Point,
75    /// Color stops along the gradient
76    pub color_stops: Vec<ColorStop>,
77    /// Whether to extend beyond the start point
78    pub extend_start: bool,
79    /// Whether to extend beyond the end point
80    pub extend_end: bool,
81}
82
83impl AxialShading {
84    /// Create a new axial shading
85    pub fn new(
86        name: String,
87        start_point: Point,
88        end_point: Point,
89        color_stops: Vec<ColorStop>,
90    ) -> Self {
91        Self {
92            name,
93            start_point,
94            end_point,
95            color_stops,
96            extend_start: false,
97            extend_end: false,
98        }
99    }
100
101    /// Set extension options
102    pub fn with_extend(mut self, extend_start: bool, extend_end: bool) -> Self {
103        self.extend_start = extend_start;
104        self.extend_end = extend_end;
105        self
106    }
107
108    /// Create a simple two-color linear gradient
109    pub fn linear_gradient(
110        name: String,
111        start_point: Point,
112        end_point: Point,
113        start_color: Color,
114        end_color: Color,
115    ) -> Self {
116        let color_stops = vec![
117            ColorStop::new(0.0, start_color),
118            ColorStop::new(1.0, end_color),
119        ];
120
121        Self::new(name, start_point, end_point, color_stops)
122    }
123
124    /// Generate PDF shading dictionary
125    pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
126        let mut shading_dict = Dictionary::new();
127
128        // Basic shading properties
129        shading_dict.set("ShadingType", Object::Integer(ShadingType::Axial as i64));
130
131        // Coordinate array [x0 y0 x1 y1]
132        let coords = vec![
133            Object::Real(self.start_point.x),
134            Object::Real(self.start_point.y),
135            Object::Real(self.end_point.x),
136            Object::Real(self.end_point.y),
137        ];
138        shading_dict.set("Coords", Object::Array(coords));
139
140        // Function (simplified - would reference actual function object)
141        // In a real implementation, this would create a proper function dictionary
142        shading_dict.set("Function", Object::Integer(1)); // Placeholder
143
144        // Extend array
145        let extend = vec![
146            Object::Boolean(self.extend_start),
147            Object::Boolean(self.extend_end),
148        ];
149        shading_dict.set("Extend", Object::Array(extend));
150
151        Ok(shading_dict)
152    }
153
154    /// Validate axial shading parameters
155    pub fn validate(&self) -> Result<()> {
156        if self.color_stops.is_empty() {
157            return Err(PdfError::InvalidStructure(
158                "Axial shading must have at least one color stop".to_string(),
159            ));
160        }
161
162        // Check that color stops are in order
163        for window in self.color_stops.windows(2) {
164            if window[0].position > window[1].position {
165                return Err(PdfError::InvalidStructure(
166                    "Color stops must be in ascending order".to_string(),
167                ));
168            }
169        }
170
171        // Check start and end points are different
172        if (self.start_point.x - self.end_point.x).abs() < f64::EPSILON
173            && (self.start_point.y - self.end_point.y).abs() < f64::EPSILON
174        {
175            return Err(PdfError::InvalidStructure(
176                "Start and end points cannot be the same".to_string(),
177            ));
178        }
179
180        Ok(())
181    }
182}
183
184/// Radial shading definition
185#[derive(Debug, Clone)]
186pub struct RadialShading {
187    /// Shading name for referencing
188    pub name: String,
189    /// Center point of the start circle
190    pub start_center: Point,
191    /// Radius of the start circle
192    pub start_radius: f64,
193    /// Center point of the end circle
194    pub end_center: Point,
195    /// Radius of the end circle
196    pub end_radius: f64,
197    /// Color stops along the gradient
198    pub color_stops: Vec<ColorStop>,
199    /// Whether to extend beyond the start circle
200    pub extend_start: bool,
201    /// Whether to extend beyond the end circle
202    pub extend_end: bool,
203}
204
205impl RadialShading {
206    /// Create a new radial shading
207    pub fn new(
208        name: String,
209        start_center: Point,
210        start_radius: f64,
211        end_center: Point,
212        end_radius: f64,
213        color_stops: Vec<ColorStop>,
214    ) -> Self {
215        Self {
216            name,
217            start_center,
218            start_radius: start_radius.max(0.0),
219            end_center,
220            end_radius: end_radius.max(0.0),
221            color_stops,
222            extend_start: false,
223            extend_end: false,
224        }
225    }
226
227    /// Set extension options
228    pub fn with_extend(mut self, extend_start: bool, extend_end: bool) -> Self {
229        self.extend_start = extend_start;
230        self.extend_end = extend_end;
231        self
232    }
233
234    /// Create a simple two-color radial gradient
235    pub fn radial_gradient(
236        name: String,
237        center: Point,
238        start_radius: f64,
239        end_radius: f64,
240        start_color: Color,
241        end_color: Color,
242    ) -> Self {
243        let color_stops = vec![
244            ColorStop::new(0.0, start_color),
245            ColorStop::new(1.0, end_color),
246        ];
247
248        Self::new(name, center, start_radius, center, end_radius, color_stops)
249    }
250
251    /// Generate PDF shading dictionary
252    pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
253        let mut shading_dict = Dictionary::new();
254
255        // Basic shading properties
256        shading_dict.set("ShadingType", Object::Integer(ShadingType::Radial as i64));
257
258        // Coordinate array [x0 y0 r0 x1 y1 r1]
259        let coords = vec![
260            Object::Real(self.start_center.x),
261            Object::Real(self.start_center.y),
262            Object::Real(self.start_radius),
263            Object::Real(self.end_center.x),
264            Object::Real(self.end_center.y),
265            Object::Real(self.end_radius),
266        ];
267        shading_dict.set("Coords", Object::Array(coords));
268
269        // Function (simplified - would reference actual function object)
270        shading_dict.set("Function", Object::Integer(1)); // Placeholder
271
272        // Extend array
273        let extend = vec![
274            Object::Boolean(self.extend_start),
275            Object::Boolean(self.extend_end),
276        ];
277        shading_dict.set("Extend", Object::Array(extend));
278
279        Ok(shading_dict)
280    }
281
282    /// Validate radial shading parameters
283    pub fn validate(&self) -> Result<()> {
284        if self.color_stops.is_empty() {
285            return Err(PdfError::InvalidStructure(
286                "Radial shading must have at least one color stop".to_string(),
287            ));
288        }
289
290        // Check that color stops are in order
291        for window in self.color_stops.windows(2) {
292            if window[0].position > window[1].position {
293                return Err(PdfError::InvalidStructure(
294                    "Color stops must be in ascending order".to_string(),
295                ));
296            }
297        }
298
299        // Check for valid radii
300        if self.start_radius < 0.0 || self.end_radius < 0.0 {
301            return Err(PdfError::InvalidStructure(
302                "Radii cannot be negative".to_string(),
303            ));
304        }
305
306        Ok(())
307    }
308}
309
310/// Function-based shading definition (simplified)
311#[derive(Debug, Clone)]
312pub struct FunctionBasedShading {
313    /// Shading name for referencing
314    pub name: String,
315    /// Domain of the function [xmin, xmax, ymin, ymax]
316    pub domain: [f64; 4],
317    /// Transformation matrix
318    pub matrix: Option<[f64; 6]>,
319    /// Function reference (placeholder)
320    pub function_id: u32,
321}
322
323impl FunctionBasedShading {
324    /// Create a new function-based shading
325    pub fn new(name: String, domain: [f64; 4], function_id: u32) -> Self {
326        Self {
327            name,
328            domain,
329            matrix: None,
330            function_id,
331        }
332    }
333
334    /// Set transformation matrix
335    pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
336        self.matrix = Some(matrix);
337        self
338    }
339
340    /// Generate PDF shading dictionary
341    pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
342        let mut shading_dict = Dictionary::new();
343
344        // Basic shading properties
345        shading_dict.set(
346            "ShadingType",
347            Object::Integer(ShadingType::FunctionBased as i64),
348        );
349
350        // Domain array
351        let domain = vec![
352            Object::Real(self.domain[0]),
353            Object::Real(self.domain[1]),
354            Object::Real(self.domain[2]),
355            Object::Real(self.domain[3]),
356        ];
357        shading_dict.set("Domain", Object::Array(domain));
358
359        // Matrix (if specified)
360        if let Some(matrix) = self.matrix {
361            let matrix_objects: Vec<Object> = matrix.iter().map(|&x| Object::Real(x)).collect();
362            shading_dict.set("Matrix", Object::Array(matrix_objects));
363        }
364
365        // Function reference
366        shading_dict.set("Function", Object::Integer(self.function_id as i64));
367
368        Ok(shading_dict)
369    }
370
371    /// Validate function-based shading parameters
372    pub fn validate(&self) -> Result<()> {
373        // Check domain validity
374        if self.domain[0] >= self.domain[1] || self.domain[2] >= self.domain[3] {
375            return Err(PdfError::InvalidStructure(
376                "Invalid domain: min values must be less than max values".to_string(),
377            ));
378        }
379
380        Ok(())
381    }
382}
383
384/// Shading pattern that combines a shading with pattern properties
385#[derive(Debug, Clone)]
386pub struct ShadingPattern {
387    /// Pattern name for referencing
388    pub name: String,
389    /// The underlying shading
390    pub shading: ShadingDefinition,
391    /// Pattern transformation matrix
392    pub matrix: Option<[f64; 6]>,
393}
394
395/// Enumeration of different shading types
396#[derive(Debug, Clone)]
397pub enum ShadingDefinition {
398    /// Axial (linear) shading
399    Axial(AxialShading),
400    /// Radial shading
401    Radial(RadialShading),
402    /// Function-based shading
403    FunctionBased(FunctionBasedShading),
404}
405
406impl ShadingDefinition {
407    /// Get the name of the shading
408    pub fn name(&self) -> &str {
409        match self {
410            ShadingDefinition::Axial(shading) => &shading.name,
411            ShadingDefinition::Radial(shading) => &shading.name,
412            ShadingDefinition::FunctionBased(shading) => &shading.name,
413        }
414    }
415
416    /// Validate the shading
417    pub fn validate(&self) -> Result<()> {
418        match self {
419            ShadingDefinition::Axial(shading) => shading.validate(),
420            ShadingDefinition::Radial(shading) => shading.validate(),
421            ShadingDefinition::FunctionBased(shading) => shading.validate(),
422        }
423    }
424
425    /// Generate PDF shading dictionary
426    pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
427        match self {
428            ShadingDefinition::Axial(shading) => shading.to_pdf_dictionary(),
429            ShadingDefinition::Radial(shading) => shading.to_pdf_dictionary(),
430            ShadingDefinition::FunctionBased(shading) => shading.to_pdf_dictionary(),
431        }
432    }
433}
434
435impl ShadingPattern {
436    /// Create a new shading pattern
437    pub fn new(name: String, shading: ShadingDefinition) -> Self {
438        Self {
439            name,
440            shading,
441            matrix: None,
442        }
443    }
444
445    /// Set pattern transformation matrix
446    pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
447        self.matrix = Some(matrix);
448        self
449    }
450
451    /// Generate PDF pattern dictionary for shading pattern
452    pub fn to_pdf_pattern_dictionary(&self) -> Result<Dictionary> {
453        let mut pattern_dict = Dictionary::new();
454
455        // Pattern properties
456        pattern_dict.set("Type", Object::Name("Pattern".to_string()));
457        pattern_dict.set("PatternType", Object::Integer(2)); // Shading pattern
458
459        // Shading reference (would be resolved during PDF generation)
460        pattern_dict.set("Shading", Object::Integer(1)); // Placeholder
461
462        // Matrix (if specified)
463        if let Some(matrix) = self.matrix {
464            let matrix_objects: Vec<Object> = matrix.iter().map(|&x| Object::Real(x)).collect();
465            pattern_dict.set("Matrix", Object::Array(matrix_objects));
466        }
467
468        Ok(pattern_dict)
469    }
470
471    /// Validate shading pattern
472    pub fn validate(&self) -> Result<()> {
473        self.shading.validate()
474    }
475}
476
477/// Shading manager for handling multiple shadings
478#[derive(Debug, Clone)]
479pub struct ShadingManager {
480    /// Stored shadings
481    shadings: HashMap<String, ShadingDefinition>,
482    /// Stored shading patterns
483    patterns: HashMap<String, ShadingPattern>,
484    /// Next shading ID
485    next_id: usize,
486}
487
488impl Default for ShadingManager {
489    fn default() -> Self {
490        Self::new()
491    }
492}
493
494impl ShadingManager {
495    /// Create a new shading manager
496    pub fn new() -> Self {
497        Self {
498            shadings: HashMap::new(),
499            patterns: HashMap::new(),
500            next_id: 1,
501        }
502    }
503
504    /// Add a shading
505    pub fn add_shading(&mut self, mut shading: ShadingDefinition) -> Result<String> {
506        // Validate shading before adding
507        shading.validate()?;
508
509        let name = shading.name().to_string();
510
511        // Generate unique name if empty or already exists
512        let final_name = if name.is_empty() || self.shadings.contains_key(&name) {
513            let auto_name = format!("Sh{}", self.next_id);
514            self.next_id += 1;
515
516            // Update the shading name
517            match &mut shading {
518                ShadingDefinition::Axial(s) => s.name = auto_name.clone(),
519                ShadingDefinition::Radial(s) => s.name = auto_name.clone(),
520                ShadingDefinition::FunctionBased(s) => s.name = auto_name.clone(),
521            }
522
523            auto_name
524        } else {
525            name
526        };
527
528        self.shadings.insert(final_name.clone(), shading);
529        Ok(final_name)
530    }
531
532    /// Add a shading pattern
533    pub fn add_shading_pattern(&mut self, mut pattern: ShadingPattern) -> Result<String> {
534        // Validate pattern before adding
535        pattern.validate()?;
536
537        // Generate unique name if empty or already exists
538        if pattern.name.is_empty() || self.patterns.contains_key(&pattern.name) {
539            pattern.name = format!("SP{}", self.next_id);
540            self.next_id += 1;
541        }
542
543        let name = pattern.name.clone();
544        self.patterns.insert(name.clone(), pattern);
545        Ok(name)
546    }
547
548    /// Get a shading by name
549    pub fn get_shading(&self, name: &str) -> Option<&ShadingDefinition> {
550        self.shadings.get(name)
551    }
552
553    /// Get a shading pattern by name
554    pub fn get_pattern(&self, name: &str) -> Option<&ShadingPattern> {
555        self.patterns.get(name)
556    }
557
558    /// Get all shadings
559    pub fn shadings(&self) -> &HashMap<String, ShadingDefinition> {
560        &self.shadings
561    }
562
563    /// Get all patterns
564    pub fn patterns(&self) -> &HashMap<String, ShadingPattern> {
565        &self.patterns
566    }
567
568    /// Clear all shadings and patterns
569    pub fn clear(&mut self) {
570        self.shadings.clear();
571        self.patterns.clear();
572        self.next_id = 1;
573    }
574
575    /// Count of registered shadings
576    pub fn shading_count(&self) -> usize {
577        self.shadings.len()
578    }
579
580    /// Count of registered patterns
581    pub fn pattern_count(&self) -> usize {
582        self.patterns.len()
583    }
584
585    /// Total count of all items
586    pub fn total_count(&self) -> usize {
587        self.shading_count() + self.pattern_count()
588    }
589
590    /// Create a simple linear gradient
591    pub fn create_linear_gradient(
592        &mut self,
593        start_point: Point,
594        end_point: Point,
595        start_color: Color,
596        end_color: Color,
597    ) -> Result<String> {
598        let shading = ShadingDefinition::Axial(AxialShading::linear_gradient(
599            String::new(), // Auto-generated name
600            start_point,
601            end_point,
602            start_color,
603            end_color,
604        ));
605
606        self.add_shading(shading)
607    }
608
609    /// Create a simple radial gradient
610    pub fn create_radial_gradient(
611        &mut self,
612        center: Point,
613        start_radius: f64,
614        end_radius: f64,
615        start_color: Color,
616        end_color: Color,
617    ) -> Result<String> {
618        let shading = ShadingDefinition::Radial(RadialShading::radial_gradient(
619            String::new(), // Auto-generated name
620            center,
621            start_radius,
622            end_radius,
623            start_color,
624            end_color,
625        ));
626
627        self.add_shading(shading)
628    }
629
630    /// Generate shading resource dictionary for PDF
631    pub fn to_resource_dictionary(&self) -> Result<String> {
632        if self.shadings.is_empty() && self.patterns.is_empty() {
633            return Ok(String::new());
634        }
635
636        let mut dict = String::new();
637
638        // Shadings
639        if !self.shadings.is_empty() {
640            dict.push_str("/Shading <<");
641            for name in self.shadings.keys() {
642                dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
643            }
644            dict.push_str(" >>");
645        }
646
647        // Patterns
648        if !self.patterns.is_empty() {
649            if !dict.is_empty() {
650                dict.push('\n');
651            }
652            dict.push_str("/Pattern <<");
653            for name in self.patterns.keys() {
654                dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
655            }
656            dict.push_str(" >>");
657        }
658
659        Ok(dict)
660    }
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666
667    #[test]
668    fn test_color_stop_creation() {
669        let stop = ColorStop::new(0.5, Color::red());
670        assert_eq!(stop.position, 0.5);
671        assert_eq!(stop.color, Color::red());
672
673        // Test clamping
674        let stop_clamped = ColorStop::new(1.5, Color::blue());
675        assert_eq!(stop_clamped.position, 1.0);
676    }
677
678    #[test]
679    fn test_point_creation() {
680        let point = Point::new(10.0, 20.0);
681        assert_eq!(point.x, 10.0);
682        assert_eq!(point.y, 20.0);
683    }
684
685    #[test]
686    fn test_axial_shading_creation() {
687        let start = Point::new(0.0, 0.0);
688        let end = Point::new(100.0, 100.0);
689        let stops = vec![
690            ColorStop::new(0.0, Color::red()),
691            ColorStop::new(1.0, Color::blue()),
692        ];
693
694        let shading = AxialShading::new("TestGradient".to_string(), start, end, stops);
695        assert_eq!(shading.name, "TestGradient");
696        assert_eq!(shading.start_point, start);
697        assert_eq!(shading.end_point, end);
698        assert_eq!(shading.color_stops.len(), 2);
699        assert!(!shading.extend_start);
700        assert!(!shading.extend_end);
701    }
702
703    #[test]
704    fn test_axial_shading_linear_gradient() {
705        let start = Point::new(0.0, 0.0);
706        let end = Point::new(100.0, 0.0);
707        let shading = AxialShading::linear_gradient(
708            "LinearGrad".to_string(),
709            start,
710            end,
711            Color::red(),
712            Color::blue(),
713        );
714
715        assert_eq!(shading.color_stops.len(), 2);
716        assert_eq!(shading.color_stops[0].position, 0.0);
717        assert_eq!(shading.color_stops[1].position, 1.0);
718    }
719
720    #[test]
721    fn test_axial_shading_with_extend() {
722        let start = Point::new(0.0, 0.0);
723        let end = Point::new(100.0, 0.0);
724        let shading = AxialShading::linear_gradient(
725            "ExtendedGrad".to_string(),
726            start,
727            end,
728            Color::red(),
729            Color::blue(),
730        )
731        .with_extend(true, true);
732
733        assert!(shading.extend_start);
734        assert!(shading.extend_end);
735    }
736
737    #[test]
738    fn test_axial_shading_validation_valid() {
739        let start = Point::new(0.0, 0.0);
740        let end = Point::new(100.0, 0.0);
741        let shading = AxialShading::linear_gradient(
742            "ValidGrad".to_string(),
743            start,
744            end,
745            Color::red(),
746            Color::blue(),
747        );
748
749        assert!(shading.validate().is_ok());
750    }
751
752    #[test]
753    fn test_axial_shading_validation_no_stops() {
754        let start = Point::new(0.0, 0.0);
755        let end = Point::new(100.0, 0.0);
756        let shading = AxialShading::new("EmptyGrad".to_string(), start, end, Vec::new());
757
758        assert!(shading.validate().is_err());
759    }
760
761    #[test]
762    fn test_axial_shading_validation_same_points() {
763        let point = Point::new(50.0, 50.0);
764        let shading = AxialShading::linear_gradient(
765            "SamePointGrad".to_string(),
766            point,
767            point,
768            Color::red(),
769            Color::blue(),
770        );
771
772        assert!(shading.validate().is_err());
773    }
774
775    #[test]
776    fn test_radial_shading_creation() {
777        let center = Point::new(50.0, 50.0);
778        let stops = vec![
779            ColorStop::new(0.0, Color::red()),
780            ColorStop::new(1.0, Color::blue()),
781        ];
782
783        let shading =
784            RadialShading::new("RadialGrad".to_string(), center, 10.0, center, 50.0, stops);
785
786        assert_eq!(shading.name, "RadialGrad");
787        assert_eq!(shading.start_center, center);
788        assert_eq!(shading.start_radius, 10.0);
789        assert_eq!(shading.end_radius, 50.0);
790    }
791
792    #[test]
793    fn test_radial_shading_gradient() {
794        let center = Point::new(50.0, 50.0);
795        let shading = RadialShading::radial_gradient(
796            "SimpleRadial".to_string(),
797            center,
798            0.0,
799            25.0,
800            Color::white(),
801            Color::black(),
802        );
803
804        assert_eq!(shading.color_stops.len(), 2);
805        assert_eq!(shading.start_radius, 0.0);
806        assert_eq!(shading.end_radius, 25.0);
807    }
808
809    #[test]
810    fn test_radial_shading_radius_clamping() {
811        let center = Point::new(50.0, 50.0);
812        let stops = vec![ColorStop::new(0.0, Color::red())];
813
814        let shading = RadialShading::new(
815            "ClampedRadial".to_string(),
816            center,
817            -5.0, // Negative radius should be clamped to 0
818            center,
819            10.0,
820            stops,
821        );
822
823        assert_eq!(shading.start_radius, 0.0);
824    }
825
826    #[test]
827    fn test_radial_shading_validation_valid() {
828        let center = Point::new(50.0, 50.0);
829        let shading = RadialShading::radial_gradient(
830            "ValidRadial".to_string(),
831            center,
832            0.0,
833            25.0,
834            Color::red(),
835            Color::blue(),
836        );
837
838        assert!(shading.validate().is_ok());
839    }
840
841    #[test]
842    fn test_function_based_shading_creation() {
843        let domain = [0.0, 1.0, 0.0, 1.0];
844        let shading = FunctionBasedShading::new("FuncShading".to_string(), domain, 1);
845
846        assert_eq!(shading.name, "FuncShading");
847        assert_eq!(shading.domain, domain);
848        assert_eq!(shading.function_id, 1);
849        assert!(shading.matrix.is_none());
850    }
851
852    #[test]
853    fn test_function_based_shading_with_matrix() {
854        let domain = [0.0, 1.0, 0.0, 1.0];
855        let matrix = [2.0, 0.0, 0.0, 2.0, 10.0, 20.0];
856        let shading =
857            FunctionBasedShading::new("FuncShading".to_string(), domain, 1).with_matrix(matrix);
858
859        assert_eq!(shading.matrix, Some(matrix));
860    }
861
862    #[test]
863    fn test_function_based_shading_validation_valid() {
864        let domain = [0.0, 1.0, 0.0, 1.0];
865        let shading = FunctionBasedShading::new("ValidFunc".to_string(), domain, 1);
866
867        assert!(shading.validate().is_ok());
868    }
869
870    #[test]
871    fn test_function_based_shading_validation_invalid_domain() {
872        let domain = [1.0, 0.0, 0.0, 1.0]; // min > max
873        let shading = FunctionBasedShading::new("InvalidFunc".to_string(), domain, 1);
874
875        assert!(shading.validate().is_err());
876    }
877
878    #[test]
879    fn test_shading_pattern_creation() {
880        let start = Point::new(0.0, 0.0);
881        let end = Point::new(100.0, 0.0);
882        let axial = AxialShading::linear_gradient(
883            "PatternGrad".to_string(),
884            start,
885            end,
886            Color::red(),
887            Color::blue(),
888        );
889        let shading = ShadingDefinition::Axial(axial);
890        let pattern = ShadingPattern::new("Pattern1".to_string(), shading);
891
892        assert_eq!(pattern.name, "Pattern1");
893        assert!(pattern.matrix.is_none());
894    }
895
896    #[test]
897    fn test_shading_pattern_with_matrix() {
898        let start = Point::new(0.0, 0.0);
899        let end = Point::new(100.0, 0.0);
900        let axial = AxialShading::linear_gradient(
901            "PatternGrad".to_string(),
902            start,
903            end,
904            Color::red(),
905            Color::blue(),
906        );
907        let shading = ShadingDefinition::Axial(axial);
908        let matrix = [1.0, 0.0, 0.0, 1.0, 50.0, 50.0];
909        let pattern = ShadingPattern::new("Pattern1".to_string(), shading).with_matrix(matrix);
910
911        assert_eq!(pattern.matrix, Some(matrix));
912    }
913
914    #[test]
915    fn test_shading_manager_creation() {
916        let manager = ShadingManager::new();
917        assert_eq!(manager.shading_count(), 0);
918        assert_eq!(manager.pattern_count(), 0);
919        assert_eq!(manager.total_count(), 0);
920    }
921
922    #[test]
923    fn test_shading_manager_add_shading() {
924        let mut manager = ShadingManager::new();
925        let start = Point::new(0.0, 0.0);
926        let end = Point::new(100.0, 0.0);
927        let axial = AxialShading::linear_gradient(
928            "TestGrad".to_string(),
929            start,
930            end,
931            Color::red(),
932            Color::blue(),
933        );
934        let shading = ShadingDefinition::Axial(axial);
935
936        let name = manager.add_shading(shading).unwrap();
937        assert_eq!(name, "TestGrad");
938        assert_eq!(manager.shading_count(), 1);
939
940        let retrieved = manager.get_shading(&name).unwrap();
941        assert_eq!(retrieved.name(), "TestGrad");
942    }
943
944    #[test]
945    fn test_shading_manager_auto_naming() {
946        let mut manager = ShadingManager::new();
947        let start = Point::new(0.0, 0.0);
948        let end = Point::new(100.0, 0.0);
949        let axial = AxialShading::linear_gradient(
950            String::new(), // Empty name
951            start,
952            end,
953            Color::red(),
954            Color::blue(),
955        );
956        let shading = ShadingDefinition::Axial(axial);
957
958        let name = manager.add_shading(shading).unwrap();
959        assert_eq!(name, "Sh1");
960
961        // Add another with empty name
962        let axial2 = AxialShading::linear_gradient(
963            String::new(),
964            start,
965            end,
966            Color::green(),
967            Color::yellow(),
968        );
969        let shading2 = ShadingDefinition::Axial(axial2);
970
971        let name2 = manager.add_shading(shading2).unwrap();
972        assert_eq!(name2, "Sh2");
973    }
974
975    #[test]
976    fn test_shading_manager_create_gradients() {
977        let mut manager = ShadingManager::new();
978
979        let linear_name = manager
980            .create_linear_gradient(
981                Point::new(0.0, 0.0),
982                Point::new(100.0, 0.0),
983                Color::red(),
984                Color::blue(),
985            )
986            .unwrap();
987
988        let radial_name = manager
989            .create_radial_gradient(
990                Point::new(50.0, 50.0),
991                0.0,
992                25.0,
993                Color::white(),
994                Color::black(),
995            )
996            .unwrap();
997
998        assert_eq!(manager.shading_count(), 2);
999        assert!(manager.get_shading(&linear_name).is_some());
1000        assert!(manager.get_shading(&radial_name).is_some());
1001    }
1002
1003    #[test]
1004    fn test_shading_manager_clear() {
1005        let mut manager = ShadingManager::new();
1006
1007        manager
1008            .create_linear_gradient(
1009                Point::new(0.0, 0.0),
1010                Point::new(100.0, 0.0),
1011                Color::red(),
1012                Color::blue(),
1013            )
1014            .unwrap();
1015
1016        assert_eq!(manager.shading_count(), 1);
1017
1018        manager.clear();
1019        assert_eq!(manager.shading_count(), 0);
1020        assert_eq!(manager.total_count(), 0);
1021    }
1022
1023    #[test]
1024    fn test_axial_shading_pdf_dictionary() {
1025        let start = Point::new(0.0, 0.0);
1026        let end = Point::new(100.0, 50.0);
1027        let shading = AxialShading::linear_gradient(
1028            "TestPDF".to_string(),
1029            start,
1030            end,
1031            Color::red(),
1032            Color::blue(),
1033        )
1034        .with_extend(true, false);
1035
1036        let dict = shading.to_pdf_dictionary().unwrap();
1037
1038        if let Some(Object::Integer(shading_type)) = dict.get("ShadingType") {
1039            assert_eq!(*shading_type, 2); // Axial type
1040        }
1041
1042        if let Some(Object::Array(coords)) = dict.get("Coords") {
1043            assert_eq!(coords.len(), 4);
1044        }
1045
1046        if let Some(Object::Array(extend)) = dict.get("Extend") {
1047            assert_eq!(extend.len(), 2);
1048            if let (Object::Boolean(start_extend), Object::Boolean(end_extend)) =
1049                (&extend[0], &extend[1])
1050            {
1051                assert!(*start_extend);
1052                assert!(!(*end_extend));
1053            }
1054        }
1055    }
1056
1057    #[test]
1058    fn test_radial_shading_pdf_dictionary() {
1059        let center = Point::new(50.0, 50.0);
1060        let shading = RadialShading::radial_gradient(
1061            "TestRadialPDF".to_string(),
1062            center,
1063            10.0,
1064            30.0,
1065            Color::yellow(),
1066            Color::red(),
1067        );
1068
1069        let dict = shading.to_pdf_dictionary().unwrap();
1070
1071        if let Some(Object::Integer(shading_type)) = dict.get("ShadingType") {
1072            assert_eq!(*shading_type, 3); // Radial type
1073        }
1074
1075        if let Some(Object::Array(coords)) = dict.get("Coords") {
1076            assert_eq!(coords.len(), 6); // [x0 y0 r0 x1 y1 r1]
1077        }
1078    }
1079}