oxidize_pdf/graphics/
patterns.rs

1//! Pattern support for PDF graphics according to ISO 32000-1 Section 8.7
2//!
3//! This module provides comprehensive support for PDF patterns including:
4//! - Tiling patterns (colored and uncolored)
5//! - Pattern dictionaries
6//! - Pattern coordinate systems
7//! - Pattern resources
8
9use crate::error::{PdfError, Result};
10use crate::graphics::GraphicsContext;
11use crate::objects::{Dictionary, Object};
12use std::collections::HashMap;
13
14/// Pattern type enumeration
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum PatternType {
17    /// Tiling pattern (Type 1)
18    Tiling = 1,
19    /// Shading pattern (Type 2) - for future implementation
20    Shading = 2,
21}
22
23/// Tiling type for tiling patterns
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum TilingType {
26    /// Constant spacing
27    ConstantSpacing = 1,
28    /// No distortion
29    NoDistortion = 2,
30    /// Constant spacing and faster tiling
31    ConstantSpacingFaster = 3,
32}
33
34/// Paint type for tiling patterns
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum PaintType {
37    /// Colored tiling pattern
38    Colored = 1,
39    /// Uncolored tiling pattern
40    Uncolored = 2,
41}
42
43/// Pattern coordinate system transformation matrix
44#[derive(Debug, Clone, PartialEq)]
45pub struct PatternMatrix {
46    /// 2x3 transformation matrix [a b c d e f]
47    pub matrix: [f64; 6],
48}
49
50impl PatternMatrix {
51    /// Create identity matrix
52    pub fn identity() -> Self {
53        Self {
54            matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
55        }
56    }
57
58    /// Create translation matrix
59    pub fn translation(tx: f64, ty: f64) -> Self {
60        Self {
61            matrix: [1.0, 0.0, 0.0, 1.0, tx, ty],
62        }
63    }
64
65    /// Create scaling matrix
66    pub fn scale(sx: f64, sy: f64) -> Self {
67        Self {
68            matrix: [sx, 0.0, 0.0, sy, 0.0, 0.0],
69        }
70    }
71
72    /// Create rotation matrix (angle in radians)
73    pub fn rotation(angle: f64) -> Self {
74        let cos_a = angle.cos();
75        let sin_a = angle.sin();
76        Self {
77            matrix: [cos_a, sin_a, -sin_a, cos_a, 0.0, 0.0],
78        }
79    }
80
81    /// Multiply with another matrix
82    pub fn multiply(&self, other: &PatternMatrix) -> Self {
83        let a1 = self.matrix[0];
84        let b1 = self.matrix[1];
85        let c1 = self.matrix[2];
86        let d1 = self.matrix[3];
87        let e1 = self.matrix[4];
88        let f1 = self.matrix[5];
89
90        let a2 = other.matrix[0];
91        let b2 = other.matrix[1];
92        let c2 = other.matrix[2];
93        let d2 = other.matrix[3];
94        let e2 = other.matrix[4];
95        let f2 = other.matrix[5];
96
97        Self {
98            matrix: [
99                a1 * a2 + b1 * c2,
100                a1 * b2 + b1 * d2,
101                c1 * a2 + d1 * c2,
102                c1 * b2 + d1 * d2,
103                e1 * a2 + f1 * c2 + e2,
104                e1 * b2 + f1 * d2 + f2,
105            ],
106        }
107    }
108
109    /// Convert to PDF array format
110    pub fn to_pdf_array(&self) -> Vec<Object> {
111        self.matrix.iter().map(|&x| Object::Real(x)).collect()
112    }
113}
114
115/// Tiling pattern definition according to ISO 32000-1
116#[derive(Debug, Clone)]
117pub struct TilingPattern {
118    /// Pattern name for referencing
119    pub name: String,
120    /// Paint type (colored or uncolored)
121    pub paint_type: PaintType,
122    /// Tiling type
123    pub tiling_type: TilingType,
124    /// Bounding box [xmin, ymin, xmax, ymax]
125    pub bbox: [f64; 4],
126    /// Horizontal spacing between pattern cells
127    pub x_step: f64,
128    /// Vertical spacing between pattern cells
129    pub y_step: f64,
130    /// Pattern transformation matrix
131    pub matrix: PatternMatrix,
132    /// Pattern content stream (drawing commands)
133    pub content_stream: Vec<u8>,
134    /// Resources dictionary for pattern content
135    pub resources: Option<Dictionary>,
136}
137
138impl TilingPattern {
139    /// Create a new tiling pattern
140    pub fn new(
141        name: String,
142        paint_type: PaintType,
143        tiling_type: TilingType,
144        bbox: [f64; 4],
145        x_step: f64,
146        y_step: f64,
147    ) -> Self {
148        Self {
149            name,
150            paint_type,
151            tiling_type,
152            bbox,
153            x_step,
154            y_step,
155            matrix: PatternMatrix::identity(),
156            content_stream: Vec::new(),
157            resources: None,
158        }
159    }
160
161    /// Set pattern transformation matrix
162    pub fn with_matrix(mut self, matrix: PatternMatrix) -> Self {
163        self.matrix = matrix;
164        self
165    }
166
167    /// Set pattern content stream
168    pub fn with_content_stream(mut self, content: Vec<u8>) -> Self {
169        self.content_stream = content;
170        self
171    }
172
173    /// Set pattern resources
174    pub fn with_resources(mut self, resources: Dictionary) -> Self {
175        self.resources = Some(resources);
176        self
177    }
178
179    /// Add drawing command to content stream
180    pub fn add_command(&mut self, command: &str) {
181        self.content_stream.extend_from_slice(command.as_bytes());
182        self.content_stream.push(b'\n');
183    }
184
185    /// Add rectangle to pattern
186    pub fn add_rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) {
187        self.add_command(&format!("{x} {y} {width} {height} re"));
188    }
189
190    /// Add line to pattern
191    pub fn add_line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
192        self.add_command(&format!("{x1} {y1} m"));
193        self.add_command(&format!("{x2} {y2} l"));
194    }
195
196    /// Add circle to pattern (using Bézier curves)
197    pub fn add_circle(&mut self, cx: f64, cy: f64, radius: f64) {
198        let k = 0.5522847498; // Approximation constant for circle with Bézier curves
199        let kr = k * radius;
200
201        // Start at rightmost point
202        self.add_command(&format!("{} {} m", cx + radius, cy));
203
204        // Four Bézier curves to approximate circle
205        self.add_command(&format!(
206            "{} {} {} {} {} {} c",
207            cx + radius,
208            cy + kr,
209            cx + kr,
210            cy + radius,
211            cx,
212            cy + radius
213        ));
214        self.add_command(&format!(
215            "{} {} {} {} {} {} c",
216            cx - kr,
217            cy + radius,
218            cx - radius,
219            cy + kr,
220            cx - radius,
221            cy
222        ));
223        self.add_command(&format!(
224            "{} {} {} {} {} {} c",
225            cx - radius,
226            cy - kr,
227            cx - kr,
228            cy - radius,
229            cx,
230            cy - radius
231        ));
232        self.add_command(&format!(
233            "{} {} {} {} {} {} c",
234            cx + kr,
235            cy - radius,
236            cx + radius,
237            cy - kr,
238            cx + radius,
239            cy
240        ));
241    }
242
243    /// Set stroke operation
244    pub fn stroke(&mut self) {
245        self.add_command("S");
246    }
247
248    /// Set fill operation
249    pub fn fill(&mut self) {
250        self.add_command("f");
251    }
252
253    /// Set fill and stroke operation
254    pub fn fill_and_stroke(&mut self) {
255        self.add_command("B");
256    }
257
258    /// Generate PDF pattern dictionary
259    pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
260        let mut pattern_dict = Dictionary::new();
261
262        // Basic pattern properties
263        pattern_dict.set("Type", Object::Name("Pattern".to_string()));
264        pattern_dict.set("PatternType", Object::Integer(PatternType::Tiling as i64));
265        pattern_dict.set("PaintType", Object::Integer(self.paint_type as i64));
266        pattern_dict.set("TilingType", Object::Integer(self.tiling_type as i64));
267
268        // Bounding box
269        let bbox_array = vec![
270            Object::Real(self.bbox[0]),
271            Object::Real(self.bbox[1]),
272            Object::Real(self.bbox[2]),
273            Object::Real(self.bbox[3]),
274        ];
275        pattern_dict.set("BBox", Object::Array(bbox_array));
276
277        // Step sizes
278        pattern_dict.set("XStep", Object::Real(self.x_step));
279        pattern_dict.set("YStep", Object::Real(self.y_step));
280
281        // Transformation matrix
282        pattern_dict.set("Matrix", Object::Array(self.matrix.to_pdf_array()));
283
284        // Resources (if any)
285        if let Some(ref resources) = self.resources {
286            pattern_dict.set("Resources", Object::Dictionary(resources.clone()));
287        }
288
289        // Length of content stream
290        pattern_dict.set("Length", Object::Integer(self.content_stream.len() as i64));
291
292        Ok(pattern_dict)
293    }
294
295    /// Validate pattern parameters
296    pub fn validate(&self) -> Result<()> {
297        // Check bounding box validity
298        if self.bbox[0] >= self.bbox[2] || self.bbox[1] >= self.bbox[3] {
299            return Err(PdfError::InvalidStructure(
300                "Pattern bounding box is invalid".to_string(),
301            ));
302        }
303
304        // Check step sizes
305        if self.x_step <= 0.0 || self.y_step <= 0.0 {
306            return Err(PdfError::InvalidStructure(
307                "Pattern step sizes must be positive".to_string(),
308            ));
309        }
310
311        // Check content stream
312        if self.content_stream.is_empty() {
313            return Err(PdfError::InvalidStructure(
314                "Pattern content stream cannot be empty".to_string(),
315            ));
316        }
317
318        Ok(())
319    }
320}
321
322/// Pattern manager for handling multiple patterns
323#[derive(Debug, Clone)]
324pub struct PatternManager {
325    /// Stored patterns
326    patterns: HashMap<String, TilingPattern>,
327    /// Next pattern ID
328    next_id: usize,
329}
330
331impl Default for PatternManager {
332    fn default() -> Self {
333        Self::new()
334    }
335}
336
337impl PatternManager {
338    /// Create a new pattern manager
339    pub fn new() -> Self {
340        Self {
341            patterns: HashMap::new(),
342            next_id: 1,
343        }
344    }
345
346    /// Add a pattern and return its name
347    pub fn add_pattern(&mut self, mut pattern: TilingPattern) -> Result<String> {
348        // Validate pattern before adding
349        pattern.validate()?;
350
351        // Generate unique name if not provided
352        if pattern.name.is_empty() {
353            pattern.name = format!("P{next_id}", next_id = self.next_id);
354            self.next_id += 1;
355        }
356
357        let name = pattern.name.clone();
358        self.patterns.insert(name.clone(), pattern);
359        Ok(name)
360    }
361
362    /// Get a pattern by name
363    pub fn get_pattern(&self, name: &str) -> Option<&TilingPattern> {
364        self.patterns.get(name)
365    }
366
367    /// Get all patterns
368    pub fn patterns(&self) -> &HashMap<String, TilingPattern> {
369        &self.patterns
370    }
371
372    /// Remove a pattern
373    pub fn remove_pattern(&mut self, name: &str) -> Option<TilingPattern> {
374        self.patterns.remove(name)
375    }
376
377    /// Clear all patterns
378    pub fn clear(&mut self) {
379        self.patterns.clear();
380        self.next_id = 1;
381    }
382
383    /// Count of registered patterns
384    pub fn count(&self) -> usize {
385        self.patterns.len()
386    }
387
388    /// Generate pattern resource dictionary
389    pub fn to_resource_dictionary(&self) -> Result<String> {
390        if self.patterns.is_empty() {
391            return Ok(String::new());
392        }
393
394        let mut dict = String::from("/Pattern <<");
395
396        for name in self.patterns.keys() {
397            // In a real implementation, this would reference the pattern object
398            dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
399        }
400
401        dict.push_str(" >>");
402        Ok(dict)
403    }
404
405    /// Create a simple checkerboard pattern
406    pub fn create_checkerboard_pattern(
407        &mut self,
408        cell_size: f64,
409        color1: [f64; 3], // RGB for first color
410        color2: [f64; 3], // RGB for second color
411    ) -> Result<String> {
412        let mut pattern = TilingPattern::new(
413            String::new(), // Will be auto-generated
414            PaintType::Colored,
415            TilingType::ConstantSpacing,
416            [0.0, 0.0, cell_size * 2.0, cell_size * 2.0],
417            cell_size * 2.0,
418            cell_size * 2.0,
419        );
420
421        // Add first color rectangle
422        pattern.add_command(&format!("{} {} {} rg", color1[0], color1[1], color1[2]));
423        pattern.add_rectangle(0.0, 0.0, cell_size, cell_size);
424        pattern.fill();
425
426        pattern.add_rectangle(cell_size, cell_size, cell_size, cell_size);
427        pattern.fill();
428
429        // Add second color rectangles
430        pattern.add_command(&format!("{} {} {} rg", color2[0], color2[1], color2[2]));
431        pattern.add_rectangle(cell_size, 0.0, cell_size, cell_size);
432        pattern.fill();
433
434        pattern.add_rectangle(0.0, cell_size, cell_size, cell_size);
435        pattern.fill();
436
437        self.add_pattern(pattern)
438    }
439
440    /// Create a simple stripe pattern
441    pub fn create_stripe_pattern(
442        &mut self,
443        stripe_width: f64,
444        angle: f64, // in degrees
445        color1: [f64; 3],
446        color2: [f64; 3],
447    ) -> Result<String> {
448        let pattern_size = stripe_width * 2.0;
449        let mut pattern = TilingPattern::new(
450            String::new(),
451            PaintType::Colored,
452            TilingType::ConstantSpacing,
453            [0.0, 0.0, pattern_size, pattern_size],
454            pattern_size,
455            pattern_size,
456        );
457
458        // Apply rotation if specified
459        if angle != 0.0 {
460            let rotation_matrix = PatternMatrix::rotation(angle.to_radians());
461            pattern = pattern.with_matrix(rotation_matrix);
462        }
463
464        // Add first color stripe
465        pattern.add_command(&format!("{} {} {} rg", color1[0], color1[1], color1[2]));
466        pattern.add_rectangle(0.0, 0.0, stripe_width, pattern_size);
467        pattern.fill();
468
469        // Add second color stripe
470        pattern.add_command(&format!("{} {} {} rg", color2[0], color2[1], color2[2]));
471        pattern.add_rectangle(stripe_width, 0.0, stripe_width, pattern_size);
472        pattern.fill();
473
474        self.add_pattern(pattern)
475    }
476
477    /// Create a dots pattern
478    pub fn create_dots_pattern(
479        &mut self,
480        dot_radius: f64,
481        spacing: f64,
482        dot_color: [f64; 3],
483        background_color: [f64; 3],
484    ) -> Result<String> {
485        let pattern_size = spacing;
486        let mut pattern = TilingPattern::new(
487            String::new(),
488            PaintType::Colored,
489            TilingType::ConstantSpacing,
490            [0.0, 0.0, pattern_size, pattern_size],
491            pattern_size,
492            pattern_size,
493        );
494
495        // Background
496        pattern.add_command(&format!(
497            "{} {} {} rg",
498            background_color[0], background_color[1], background_color[2]
499        ));
500        pattern.add_rectangle(0.0, 0.0, pattern_size, pattern_size);
501        pattern.fill();
502
503        // Dot
504        pattern.add_command(&format!(
505            "{} {} {} rg",
506            dot_color[0], dot_color[1], dot_color[2]
507        ));
508        pattern.add_circle(pattern_size / 2.0, pattern_size / 2.0, dot_radius);
509        pattern.fill();
510
511        self.add_pattern(pattern)
512    }
513}
514
515/// Extension trait for GraphicsContext to support patterns
516pub trait PatternGraphicsContext {
517    /// Set pattern as fill color
518    fn set_fill_pattern(&mut self, pattern_name: &str) -> Result<()>;
519
520    /// Set pattern as stroke color
521    fn set_stroke_pattern(&mut self, pattern_name: &str) -> Result<()>;
522}
523
524impl PatternGraphicsContext for GraphicsContext {
525    fn set_fill_pattern(&mut self, pattern_name: &str) -> Result<()> {
526        // In a real implementation, this would set the pattern in the graphics state
527        // For now, we'll store it as a command
528        self.add_command(&format!("/Pattern cs /{pattern_name} scn"));
529        Ok(())
530    }
531
532    fn set_stroke_pattern(&mut self, pattern_name: &str) -> Result<()> {
533        // Set pattern for stroking operations
534        self.add_command(&format!("/Pattern CS /{pattern_name} SCN"));
535        Ok(())
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542
543    #[test]
544    fn test_pattern_matrix_identity() {
545        let matrix = PatternMatrix::identity();
546        assert_eq!(matrix.matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
547    }
548
549    #[test]
550    fn test_pattern_matrix_translation() {
551        let matrix = PatternMatrix::translation(10.0, 20.0);
552        assert_eq!(matrix.matrix, [1.0, 0.0, 0.0, 1.0, 10.0, 20.0]);
553    }
554
555    #[test]
556    fn test_pattern_matrix_scale() {
557        let matrix = PatternMatrix::scale(2.0, 3.0);
558        assert_eq!(matrix.matrix, [2.0, 0.0, 0.0, 3.0, 0.0, 0.0]);
559    }
560
561    #[test]
562    fn test_pattern_matrix_multiply() {
563        let m1 = PatternMatrix::translation(10.0, 20.0);
564        let m2 = PatternMatrix::scale(2.0, 3.0);
565        let result = m1.multiply(&m2);
566        assert_eq!(result.matrix, [2.0, 0.0, 0.0, 3.0, 20.0, 60.0]);
567    }
568
569    #[test]
570    fn test_tiling_pattern_creation() {
571        let pattern = TilingPattern::new(
572            "TestPattern".to_string(),
573            PaintType::Colored,
574            TilingType::ConstantSpacing,
575            [0.0, 0.0, 100.0, 100.0],
576            50.0,
577            50.0,
578        );
579
580        assert_eq!(pattern.name, "TestPattern");
581        assert_eq!(pattern.paint_type, PaintType::Colored);
582        assert_eq!(pattern.tiling_type, TilingType::ConstantSpacing);
583        assert_eq!(pattern.bbox, [0.0, 0.0, 100.0, 100.0]);
584        assert_eq!(pattern.x_step, 50.0);
585        assert_eq!(pattern.y_step, 50.0);
586    }
587
588    #[test]
589    fn test_tiling_pattern_content_operations() {
590        let mut pattern = TilingPattern::new(
591            "TestPattern".to_string(),
592            PaintType::Colored,
593            TilingType::ConstantSpacing,
594            [0.0, 0.0, 100.0, 100.0],
595            100.0,
596            100.0,
597        );
598
599        pattern.add_rectangle(10.0, 10.0, 50.0, 50.0);
600        pattern.fill();
601
602        let content = String::from_utf8(pattern.content_stream).unwrap();
603        assert!(content.contains("10 10 50 50 re"));
604        assert!(content.contains("f"));
605    }
606
607    #[test]
608    fn test_tiling_pattern_circle() {
609        let mut pattern = TilingPattern::new(
610            "CirclePattern".to_string(),
611            PaintType::Colored,
612            TilingType::ConstantSpacing,
613            [0.0, 0.0, 100.0, 100.0],
614            100.0,
615            100.0,
616        );
617
618        pattern.add_circle(50.0, 50.0, 25.0);
619        pattern.stroke();
620
621        let content = String::from_utf8(pattern.content_stream).unwrap();
622        assert!(content.contains("75 50 m")); // Start point
623        assert!(content.contains("c")); // Curve commands
624        assert!(content.contains("S")); // Stroke command
625    }
626
627    #[test]
628    fn test_pattern_validation_valid() {
629        let pattern = TilingPattern::new(
630            "ValidPattern".to_string(),
631            PaintType::Colored,
632            TilingType::ConstantSpacing,
633            [0.0, 0.0, 100.0, 100.0],
634            50.0,
635            50.0,
636        );
637
638        // Add some content to make it valid
639        let mut pattern_with_content = pattern;
640        pattern_with_content.add_rectangle(0.0, 0.0, 50.0, 50.0);
641
642        assert!(pattern_with_content.validate().is_ok());
643    }
644
645    #[test]
646    fn test_pattern_validation_invalid_bbox() {
647        let pattern = TilingPattern::new(
648            "InvalidPattern".to_string(),
649            PaintType::Colored,
650            TilingType::ConstantSpacing,
651            [100.0, 100.0, 0.0, 0.0], // Invalid bbox
652            50.0,
653            50.0,
654        );
655
656        assert!(pattern.validate().is_err());
657    }
658
659    #[test]
660    fn test_pattern_validation_invalid_steps() {
661        let pattern = TilingPattern::new(
662            "InvalidPattern".to_string(),
663            PaintType::Colored,
664            TilingType::ConstantSpacing,
665            [0.0, 0.0, 100.0, 100.0],
666            0.0, // Invalid step
667            50.0,
668        );
669
670        assert!(pattern.validate().is_err());
671    }
672
673    #[test]
674    fn test_pattern_validation_empty_content() {
675        let pattern = TilingPattern::new(
676            "EmptyPattern".to_string(),
677            PaintType::Colored,
678            TilingType::ConstantSpacing,
679            [0.0, 0.0, 100.0, 100.0],
680            50.0,
681            50.0,
682        );
683
684        // No content added
685        assert!(pattern.validate().is_err());
686    }
687
688    #[test]
689    fn test_pattern_manager_creation() {
690        let manager = PatternManager::new();
691        assert_eq!(manager.count(), 0);
692        assert!(manager.patterns().is_empty());
693    }
694
695    #[test]
696    fn test_pattern_manager_add_pattern() {
697        let mut manager = PatternManager::new();
698        let mut pattern = TilingPattern::new(
699            "TestPattern".to_string(),
700            PaintType::Colored,
701            TilingType::ConstantSpacing,
702            [0.0, 0.0, 100.0, 100.0],
703            50.0,
704            50.0,
705        );
706
707        // Add content to make it valid
708        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
709
710        let name = manager.add_pattern(pattern).unwrap();
711        assert_eq!(name, "TestPattern");
712        assert_eq!(manager.count(), 1);
713
714        let retrieved = manager.get_pattern(&name).unwrap();
715        assert_eq!(retrieved.name, "TestPattern");
716    }
717
718    #[test]
719    fn test_pattern_manager_auto_naming() {
720        let mut manager = PatternManager::new();
721        let mut pattern = TilingPattern::new(
722            String::new(), // Empty name
723            PaintType::Colored,
724            TilingType::ConstantSpacing,
725            [0.0, 0.0, 100.0, 100.0],
726            50.0,
727            50.0,
728        );
729
730        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
731
732        let name = manager.add_pattern(pattern).unwrap();
733        assert_eq!(name, "P1");
734
735        let mut pattern2 = TilingPattern::new(
736            String::new(),
737            PaintType::Colored,
738            TilingType::ConstantSpacing,
739            [0.0, 0.0, 100.0, 100.0],
740            50.0,
741            50.0,
742        );
743
744        pattern2.add_rectangle(0.0, 0.0, 50.0, 50.0);
745
746        let name2 = manager.add_pattern(pattern2).unwrap();
747        assert_eq!(name2, "P2");
748    }
749
750    #[test]
751    fn test_pattern_manager_checkerboard() {
752        let mut manager = PatternManager::new();
753        let name = manager
754            .create_checkerboard_pattern(
755                25.0,
756                [1.0, 0.0, 0.0], // Red
757                [0.0, 0.0, 1.0], // Blue
758            )
759            .unwrap();
760
761        let pattern = manager.get_pattern(&name).unwrap();
762        assert_eq!(pattern.x_step, 50.0);
763        assert_eq!(pattern.y_step, 50.0);
764        assert!(!pattern.content_stream.is_empty());
765    }
766
767    #[test]
768    fn test_pattern_manager_stripes() {
769        let mut manager = PatternManager::new();
770        let name = manager
771            .create_stripe_pattern(
772                10.0,
773                45.0,            // 45 degrees
774                [0.0, 1.0, 0.0], // Green
775                [1.0, 1.0, 0.0], // Yellow
776            )
777            .unwrap();
778
779        let pattern = manager.get_pattern(&name).unwrap();
780        assert_eq!(pattern.x_step, 20.0);
781        assert_eq!(pattern.y_step, 20.0);
782        // Should have rotation matrix applied
783        assert_ne!(pattern.matrix.matrix, PatternMatrix::identity().matrix);
784    }
785
786    #[test]
787    fn test_pattern_manager_dots() {
788        let mut manager = PatternManager::new();
789        let name = manager
790            .create_dots_pattern(
791                5.0,             // radius
792                20.0,            // spacing
793                [1.0, 0.0, 1.0], // Magenta
794                [1.0, 1.0, 1.0], // White
795            )
796            .unwrap();
797
798        let pattern = manager.get_pattern(&name).unwrap();
799        assert_eq!(pattern.x_step, 20.0);
800        assert_eq!(pattern.y_step, 20.0);
801
802        let content = String::from_utf8(pattern.content_stream.clone()).unwrap();
803        assert!(content.contains("c")); // Should contain curve commands for circle
804    }
805
806    #[test]
807    fn test_pattern_pdf_dictionary_generation() {
808        let mut pattern = TilingPattern::new(
809            "TestPattern".to_string(),
810            PaintType::Colored,
811            TilingType::ConstantSpacing,
812            [0.0, 0.0, 100.0, 100.0],
813            50.0,
814            50.0,
815        );
816
817        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
818
819        let dict = pattern.to_pdf_dictionary().unwrap();
820
821        // Verify dictionary contents
822        if let Some(Object::Name(type_name)) = dict.get("Type") {
823            assert_eq!(type_name, "Pattern");
824        }
825        if let Some(Object::Integer(pattern_type)) = dict.get("PatternType") {
826            assert_eq!(*pattern_type, 1);
827        }
828        if let Some(Object::Integer(paint_type)) = dict.get("PaintType") {
829            assert_eq!(*paint_type, 1);
830        }
831        if let Some(Object::Array(bbox)) = dict.get("BBox") {
832            assert_eq!(bbox.len(), 4);
833        }
834    }
835
836    #[test]
837    fn test_pattern_manager_clear() {
838        let mut manager = PatternManager::new();
839        let mut pattern = TilingPattern::new(
840            "TestPattern".to_string(),
841            PaintType::Colored,
842            TilingType::ConstantSpacing,
843            [0.0, 0.0, 100.0, 100.0],
844            50.0,
845            50.0,
846        );
847
848        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
849        manager.add_pattern(pattern).unwrap();
850        assert_eq!(manager.count(), 1);
851
852        manager.clear();
853        assert_eq!(manager.count(), 0);
854        assert!(manager.patterns().is_empty());
855    }
856
857    #[test]
858    fn test_pattern_type_values() {
859        assert_eq!(PatternType::Tiling as i32, 1);
860        assert_eq!(PatternType::Shading as i32, 2);
861    }
862
863    #[test]
864    fn test_tiling_type_values() {
865        assert_eq!(TilingType::ConstantSpacing as i32, 1);
866        assert_eq!(TilingType::NoDistortion as i32, 2);
867        assert_eq!(TilingType::ConstantSpacingFaster as i32, 3);
868    }
869
870    #[test]
871    fn test_paint_type_values() {
872        assert_eq!(PaintType::Colored as i32, 1);
873        assert_eq!(PaintType::Uncolored as i32, 2);
874    }
875
876    #[test]
877    fn test_pattern_matrix_rotation() {
878        let angle = std::f64::consts::PI / 2.0; // 90 degrees
879        let matrix = PatternMatrix::rotation(angle);
880
881        // cos(90°) ≈ 0, sin(90°) ≈ 1
882        assert!((matrix.matrix[0]).abs() < 1e-10); // cos(90°) ≈ 0
883        assert!((matrix.matrix[1] - 1.0).abs() < 1e-10); // sin(90°) ≈ 1
884        assert!((matrix.matrix[2] + 1.0).abs() < 1e-10); // -sin(90°) ≈ -1
885        assert!((matrix.matrix[3]).abs() < 1e-10); // cos(90°) ≈ 0
886    }
887
888    #[test]
889    fn test_pattern_matrix_complex_multiply() {
890        let translate = PatternMatrix::translation(10.0, 20.0);
891        let scale = PatternMatrix::scale(2.0, 3.0);
892        let rotate = PatternMatrix::rotation(std::f64::consts::PI / 4.0); // 45 degrees
893
894        let result = translate.multiply(&scale).multiply(&rotate);
895
896        // Verify matrix multiplication was performed
897        assert_ne!(result.matrix, PatternMatrix::identity().matrix);
898    }
899
900    #[test]
901    fn test_tiling_pattern_with_matrix() {
902        let pattern = TilingPattern::new(
903            "TestPattern".to_string(),
904            PaintType::Colored,
905            TilingType::ConstantSpacing,
906            [0.0, 0.0, 100.0, 100.0],
907            50.0,
908            50.0,
909        );
910
911        let matrix = PatternMatrix::scale(2.0, 2.0);
912        let pattern_with_matrix = pattern.with_matrix(matrix);
913
914        assert_eq!(
915            pattern_with_matrix.matrix.matrix,
916            [2.0, 0.0, 0.0, 2.0, 0.0, 0.0]
917        );
918    }
919
920    #[test]
921    fn test_tiling_pattern_with_resources() {
922        let pattern = TilingPattern::new(
923            "TestPattern".to_string(),
924            PaintType::Colored,
925            TilingType::ConstantSpacing,
926            [0.0, 0.0, 100.0, 100.0],
927            50.0,
928            50.0,
929        );
930
931        let mut resources = Dictionary::new();
932        resources.set("Font", Object::Name("F1".to_string()));
933
934        let pattern_with_resources = pattern.with_resources(resources.clone());
935        assert_eq!(pattern_with_resources.resources, Some(resources));
936    }
937
938    #[test]
939    fn test_tiling_pattern_stroke() {
940        let mut pattern = TilingPattern::new(
941            "StrokePattern".to_string(),
942            PaintType::Colored,
943            TilingType::ConstantSpacing,
944            [0.0, 0.0, 100.0, 100.0],
945            100.0,
946            100.0,
947        );
948
949        pattern.add_rectangle(10.0, 10.0, 80.0, 80.0);
950        pattern.stroke();
951
952        let content = String::from_utf8(pattern.content_stream).unwrap();
953        assert!(content.contains("S"));
954        assert!(!content.contains("f")); // Should not contain fill
955    }
956
957    #[test]
958    fn test_tiling_pattern_add_command() {
959        let mut pattern = TilingPattern::new(
960            "CommandPattern".to_string(),
961            PaintType::Colored,
962            TilingType::ConstantSpacing,
963            [0.0, 0.0, 100.0, 100.0],
964            100.0,
965            100.0,
966        );
967
968        pattern.add_command("0.5 0.5 0.5 rg");
969        pattern.add_command("2 w");
970
971        let content = String::from_utf8(pattern.content_stream).unwrap();
972        assert!(content.contains("0.5 0.5 0.5 rg"));
973        assert!(content.contains("2 w"));
974    }
975
976    #[test]
977    fn test_pattern_manager_remove_pattern() {
978        let mut manager = PatternManager::new();
979        let mut pattern = TilingPattern::new(
980            "RemovablePattern".to_string(),
981            PaintType::Colored,
982            TilingType::ConstantSpacing,
983            [0.0, 0.0, 100.0, 100.0],
984            50.0,
985            50.0,
986        );
987
988        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
989        manager.add_pattern(pattern).unwrap();
990        assert_eq!(manager.count(), 1);
991
992        let removed = manager.remove_pattern("RemovablePattern");
993        assert!(removed.is_some());
994        assert_eq!(manager.count(), 0);
995
996        let removed_again = manager.remove_pattern("RemovablePattern");
997        assert!(removed_again.is_none());
998    }
999
1000    #[test]
1001    fn test_pattern_manager_to_resource_dictionary() {
1002        let mut manager = PatternManager::new();
1003
1004        // Empty manager
1005        assert_eq!(manager.to_resource_dictionary().unwrap(), "");
1006
1007        // Add patterns
1008        let mut pattern1 = TilingPattern::new(
1009            "P1".to_string(),
1010            PaintType::Colored,
1011            TilingType::ConstantSpacing,
1012            [0.0, 0.0, 10.0, 10.0],
1013            10.0,
1014            10.0,
1015        );
1016        pattern1.add_rectangle(0.0, 0.0, 10.0, 10.0);
1017        manager.add_pattern(pattern1).unwrap();
1018
1019        let dict = manager.to_resource_dictionary().unwrap();
1020        assert!(dict.starts_with("/Pattern <<"));
1021        assert!(dict.contains("/P1"));
1022        assert!(dict.ends_with(">>"));
1023    }
1024
1025    #[test]
1026    fn test_pattern_manager_default() {
1027        let manager = PatternManager::default();
1028        assert_eq!(manager.count(), 0);
1029        assert!(manager.patterns().is_empty());
1030    }
1031
1032    #[test]
1033    fn test_pattern_graphics_context_extension() {
1034        let mut context = GraphicsContext::new();
1035
1036        // Test fill pattern
1037        context.set_fill_pattern("TestPattern").unwrap();
1038        let commands = context.operations();
1039        assert!(commands.contains("/Pattern cs /TestPattern scn"));
1040
1041        // Test stroke pattern
1042        context.set_stroke_pattern("StrokePattern").unwrap();
1043        let commands = context.operations();
1044        assert!(commands.contains("/Pattern CS /StrokePattern SCN"));
1045    }
1046
1047    #[test]
1048    fn test_tiling_pattern_uncolored() {
1049        let pattern = TilingPattern::new(
1050            "UncoloredPattern".to_string(),
1051            PaintType::Uncolored,
1052            TilingType::NoDistortion,
1053            [0.0, 0.0, 50.0, 50.0],
1054            50.0,
1055            50.0,
1056        );
1057
1058        assert_eq!(pattern.paint_type, PaintType::Uncolored);
1059        assert_eq!(pattern.tiling_type, TilingType::NoDistortion);
1060    }
1061
1062    #[test]
1063    fn test_checkerboard_pattern_content() {
1064        let mut manager = PatternManager::new();
1065        let name = manager
1066            .create_checkerboard_pattern(
1067                10.0,
1068                [1.0, 1.0, 1.0], // White
1069                [0.0, 0.0, 0.0], // Black
1070            )
1071            .unwrap();
1072
1073        let pattern = manager.get_pattern(&name).unwrap();
1074        let content = String::from_utf8(pattern.content_stream.clone()).unwrap();
1075
1076        // Should contain color commands
1077        assert!(content.contains("1 1 1 rg")); // White
1078        assert!(content.contains("0 0 0 rg")); // Black
1079                                               // Should contain rectangles
1080        assert!(content.contains("re"));
1081        assert!(content.contains("f"));
1082    }
1083
1084    #[test]
1085    fn test_stripe_pattern_zero_angle() {
1086        let mut manager = PatternManager::new();
1087        let name = manager
1088            .create_stripe_pattern(
1089                5.0,
1090                0.0, // No rotation
1091                [1.0, 0.0, 0.0],
1092                [0.0, 1.0, 0.0],
1093            )
1094            .unwrap();
1095
1096        let pattern = manager.get_pattern(&name).unwrap();
1097        // With 0 angle, matrix should remain identity
1098        assert_eq!(pattern.matrix.matrix, PatternMatrix::identity().matrix);
1099    }
1100
1101    #[test]
1102    fn test_dots_pattern_content() {
1103        let mut manager = PatternManager::new();
1104        let name = manager
1105            .create_dots_pattern(
1106                3.0,             // Small radius
1107                10.0,            // Spacing
1108                [0.0, 0.0, 0.0], // Black dots
1109                [1.0, 1.0, 1.0], // White background
1110            )
1111            .unwrap();
1112
1113        let pattern = manager.get_pattern(&name).unwrap();
1114        let content = String::from_utf8(pattern.content_stream.clone()).unwrap();
1115
1116        // Should draw background rectangle
1117        assert!(content.contains("1 1 1 rg")); // White background
1118        assert!(content.contains("0 0 10 10 re")); // Background rectangle
1119
1120        // Should draw circle
1121        assert!(content.contains("0 0 0 rg")); // Black dot
1122        assert!(content.contains("m")); // Move to
1123        assert!(content.contains("c")); // Curve (for circle)
1124    }
1125
1126    #[test]
1127    fn test_pattern_validation_negative_step() {
1128        let pattern = TilingPattern::new(
1129            "NegativeStep".to_string(),
1130            PaintType::Colored,
1131            TilingType::ConstantSpacing,
1132            [0.0, 0.0, 100.0, 100.0],
1133            50.0,
1134            -50.0, // Negative y_step
1135        );
1136
1137        assert!(pattern.validate().is_err());
1138    }
1139
1140    #[test]
1141    fn test_circle_approximation() {
1142        let mut pattern = TilingPattern::new(
1143            "CircleTest".to_string(),
1144            PaintType::Colored,
1145            TilingType::ConstantSpacing,
1146            [0.0, 0.0, 100.0, 100.0],
1147            100.0,
1148            100.0,
1149        );
1150
1151        pattern.add_circle(50.0, 50.0, 0.0); // Zero radius
1152        let content = String::from_utf8(pattern.content_stream.clone()).unwrap();
1153
1154        // Should still generate move command but minimal curves
1155        assert!(content.contains("50 50 m"));
1156    }
1157
1158    #[test]
1159    fn test_pattern_manager_get_nonexistent() {
1160        let manager = PatternManager::new();
1161        assert!(manager.get_pattern("NonExistent").is_none());
1162    }
1163
1164    #[test]
1165    fn test_pattern_type_debug_clone_eq() {
1166        let pattern_type = PatternType::Tiling;
1167
1168        // Test Debug
1169        let debug_str = format!("{pattern_type:?}");
1170        assert!(debug_str.contains("Tiling"));
1171
1172        // Test Clone
1173        let cloned = pattern_type;
1174        assert_eq!(cloned, PatternType::Tiling);
1175
1176        // Test PartialEq
1177        assert_eq!(PatternType::Tiling, PatternType::Tiling);
1178        assert_ne!(PatternType::Tiling, PatternType::Shading);
1179    }
1180
1181    #[test]
1182    fn test_tiling_pattern_debug_clone() {
1183        let pattern = TilingPattern::new(
1184            "TestPattern".to_string(),
1185            PaintType::Colored,
1186            TilingType::ConstantSpacing,
1187            [0.0, 0.0, 100.0, 100.0],
1188            50.0,
1189            50.0,
1190        );
1191
1192        // Test Debug
1193        let debug_str = format!("{pattern:?}");
1194        assert!(debug_str.contains("TilingPattern"));
1195        assert!(debug_str.contains("TestPattern"));
1196
1197        // Test Clone
1198        let cloned = pattern.clone();
1199        assert_eq!(cloned.name, pattern.name);
1200        assert_eq!(cloned.paint_type, pattern.paint_type);
1201    }
1202
1203    #[test]
1204    fn test_pattern_matrix_debug_clone_eq() {
1205        let matrix = PatternMatrix::translation(5.0, 10.0);
1206
1207        // Test Debug
1208        let debug_str = format!("{matrix:?}");
1209        assert!(debug_str.contains("PatternMatrix"));
1210
1211        // Test Clone
1212        let cloned = matrix.clone();
1213        assert_eq!(cloned.matrix, matrix.matrix);
1214
1215        // Test PartialEq
1216        assert_eq!(matrix, cloned);
1217        assert_ne!(matrix, PatternMatrix::identity());
1218    }
1219
1220    #[test]
1221    fn test_pattern_type() {
1222        assert_eq!(PatternType::Tiling as i32, 1);
1223        assert_eq!(PatternType::Shading as i32, 2);
1224    }
1225
1226    #[test]
1227    fn test_tiling_type() {
1228        assert_eq!(TilingType::ConstantSpacing as i32, 1);
1229        assert_eq!(TilingType::NoDistortion as i32, 2);
1230        assert_eq!(TilingType::ConstantSpacingFaster as i32, 3);
1231    }
1232
1233    #[test]
1234    fn test_paint_type() {
1235        assert_eq!(PaintType::Colored as i32, 1);
1236        assert_eq!(PaintType::Uncolored as i32, 2);
1237    }
1238
1239    #[test]
1240    fn test_pattern_manager() {
1241        let mut manager = PatternManager::new();
1242
1243        let mut pattern = TilingPattern::new(
1244            "P1".to_string(),
1245            PaintType::Colored,
1246            TilingType::ConstantSpacing,
1247            [0.0, 0.0, 10.0, 10.0],
1248            10.0,
1249            10.0,
1250        );
1251        // Add some content to the pattern
1252        pattern.content_stream = vec![b'q', b' ', b'Q']; // Minimal valid PDF content
1253
1254        let name = manager.add_pattern(pattern).unwrap();
1255        assert_eq!(name, "P1");
1256
1257        // PatternManager stores patterns internally
1258        // We can verify it was added by checking the pattern count increases
1259        let mut pattern2 = TilingPattern::new(
1260            "P2".to_string(),
1261            PaintType::Uncolored,
1262            TilingType::NoDistortion,
1263            [0.0, 0.0, 20.0, 20.0],
1264            20.0,
1265            20.0,
1266        );
1267        pattern2.content_stream = vec![b'q', b' ', b'Q']; // Minimal valid PDF content
1268
1269        let name2 = manager.add_pattern(pattern2).unwrap();
1270        assert_eq!(name2, "P2");
1271    }
1272}