Skip to main content

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::{Color, 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. The public APIs accept raw `[f64; 3]`,
422        // so we wrap them in `Color::Rgb` and route through the shared
423        // sanitising helper (issues #220 + #221) — single emission site for
424        // every colour operator in the codebase.
425        let c1 = Color::Rgb(color1[0], color1[1], color1[2]);
426        let c2 = Color::Rgb(color2[0], color2[1], color2[2]);
427        pattern.add_command(crate::graphics::color::fill_color_op(c1).as_str());
428        pattern.add_rectangle(0.0, 0.0, cell_size, cell_size);
429        pattern.fill();
430
431        pattern.add_rectangle(cell_size, cell_size, cell_size, cell_size);
432        pattern.fill();
433
434        // Add second color rectangles
435        pattern.add_command(crate::graphics::color::fill_color_op(c2).as_str());
436        pattern.add_rectangle(cell_size, 0.0, cell_size, cell_size);
437        pattern.fill();
438
439        pattern.add_rectangle(0.0, cell_size, cell_size, cell_size);
440        pattern.fill();
441
442        self.add_pattern(pattern)
443    }
444
445    /// Create a simple stripe pattern
446    pub fn create_stripe_pattern(
447        &mut self,
448        stripe_width: f64,
449        angle: f64, // in degrees
450        color1: [f64; 3],
451        color2: [f64; 3],
452    ) -> Result<String> {
453        let pattern_size = stripe_width * 2.0;
454        let mut pattern = TilingPattern::new(
455            String::new(),
456            PaintType::Colored,
457            TilingType::ConstantSpacing,
458            [0.0, 0.0, pattern_size, pattern_size],
459            pattern_size,
460            pattern_size,
461        );
462
463        // Apply rotation if specified
464        if angle != 0.0 {
465            let rotation_matrix = PatternMatrix::rotation(angle.to_radians());
466            pattern = pattern.with_matrix(rotation_matrix);
467        }
468
469        // Add first color stripe (sanitised via shared helper).
470        let c1 = Color::Rgb(color1[0], color1[1], color1[2]);
471        let c2 = Color::Rgb(color2[0], color2[1], color2[2]);
472        pattern.add_command(crate::graphics::color::fill_color_op(c1).as_str());
473        pattern.add_rectangle(0.0, 0.0, stripe_width, pattern_size);
474        pattern.fill();
475
476        // Add second color stripe
477        pattern.add_command(crate::graphics::color::fill_color_op(c2).as_str());
478        pattern.add_rectangle(stripe_width, 0.0, stripe_width, pattern_size);
479        pattern.fill();
480
481        self.add_pattern(pattern)
482    }
483
484    /// Create a dots pattern
485    pub fn create_dots_pattern(
486        &mut self,
487        dot_radius: f64,
488        spacing: f64,
489        dot_color: [f64; 3],
490        background_color: [f64; 3],
491    ) -> Result<String> {
492        let pattern_size = spacing;
493        let mut pattern = TilingPattern::new(
494            String::new(),
495            PaintType::Colored,
496            TilingType::ConstantSpacing,
497            [0.0, 0.0, pattern_size, pattern_size],
498            pattern_size,
499            pattern_size,
500        );
501
502        // Background (sanitised via shared helper).
503        let bg = Color::Rgb(
504            background_color[0],
505            background_color[1],
506            background_color[2],
507        );
508        let dot = Color::Rgb(dot_color[0], dot_color[1], dot_color[2]);
509        pattern.add_command(crate::graphics::color::fill_color_op(bg).as_str());
510        pattern.add_rectangle(0.0, 0.0, pattern_size, pattern_size);
511        pattern.fill();
512
513        // Dot
514        pattern.add_command(crate::graphics::color::fill_color_op(dot).as_str());
515        pattern.add_circle(pattern_size / 2.0, pattern_size / 2.0, dot_radius);
516        pattern.fill();
517
518        self.add_pattern(pattern)
519    }
520}
521
522/// Extension trait for GraphicsContext to support patterns
523pub trait PatternGraphicsContext {
524    /// Set pattern as fill color
525    fn set_fill_pattern(&mut self, pattern_name: &str) -> Result<()>;
526
527    /// Set pattern as stroke color
528    fn set_stroke_pattern(&mut self, pattern_name: &str) -> Result<()>;
529}
530
531impl PatternGraphicsContext for GraphicsContext {
532    fn set_fill_pattern(&mut self, pattern_name: &str) -> Result<()> {
533        // In a real implementation, this would set the pattern in the graphics state
534        // For now, we'll store it as a command
535        self.add_command(&format!("/Pattern cs /{pattern_name} scn"));
536        Ok(())
537    }
538
539    fn set_stroke_pattern(&mut self, pattern_name: &str) -> Result<()> {
540        // Set pattern for stroking operations
541        self.add_command(&format!("/Pattern CS /{pattern_name} SCN"));
542        Ok(())
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    #[test]
551    fn test_pattern_matrix_identity() {
552        let matrix = PatternMatrix::identity();
553        assert_eq!(matrix.matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
554    }
555
556    #[test]
557    fn test_pattern_matrix_translation() {
558        let matrix = PatternMatrix::translation(10.0, 20.0);
559        assert_eq!(matrix.matrix, [1.0, 0.0, 0.0, 1.0, 10.0, 20.0]);
560    }
561
562    #[test]
563    fn test_pattern_matrix_scale() {
564        let matrix = PatternMatrix::scale(2.0, 3.0);
565        assert_eq!(matrix.matrix, [2.0, 0.0, 0.0, 3.0, 0.0, 0.0]);
566    }
567
568    #[test]
569    fn test_pattern_matrix_multiply() {
570        let m1 = PatternMatrix::translation(10.0, 20.0);
571        let m2 = PatternMatrix::scale(2.0, 3.0);
572        let result = m1.multiply(&m2);
573        assert_eq!(result.matrix, [2.0, 0.0, 0.0, 3.0, 20.0, 60.0]);
574    }
575
576    #[test]
577    fn test_tiling_pattern_creation() {
578        let pattern = TilingPattern::new(
579            "TestPattern".to_string(),
580            PaintType::Colored,
581            TilingType::ConstantSpacing,
582            [0.0, 0.0, 100.0, 100.0],
583            50.0,
584            50.0,
585        );
586
587        assert_eq!(pattern.name, "TestPattern");
588        assert_eq!(pattern.paint_type, PaintType::Colored);
589        assert_eq!(pattern.tiling_type, TilingType::ConstantSpacing);
590        assert_eq!(pattern.bbox, [0.0, 0.0, 100.0, 100.0]);
591        assert_eq!(pattern.x_step, 50.0);
592        assert_eq!(pattern.y_step, 50.0);
593    }
594
595    #[test]
596    fn test_tiling_pattern_content_operations() {
597        let mut pattern = TilingPattern::new(
598            "TestPattern".to_string(),
599            PaintType::Colored,
600            TilingType::ConstantSpacing,
601            [0.0, 0.0, 100.0, 100.0],
602            100.0,
603            100.0,
604        );
605
606        pattern.add_rectangle(10.0, 10.0, 50.0, 50.0);
607        pattern.fill();
608
609        let content = String::from_utf8(pattern.content_stream).unwrap();
610        assert!(content.contains("10 10 50 50 re"));
611        assert!(content.contains("f"));
612    }
613
614    #[test]
615    fn test_tiling_pattern_circle() {
616        let mut pattern = TilingPattern::new(
617            "CirclePattern".to_string(),
618            PaintType::Colored,
619            TilingType::ConstantSpacing,
620            [0.0, 0.0, 100.0, 100.0],
621            100.0,
622            100.0,
623        );
624
625        pattern.add_circle(50.0, 50.0, 25.0);
626        pattern.stroke();
627
628        let content = String::from_utf8(pattern.content_stream).unwrap();
629        assert!(content.contains("75 50 m")); // Start point
630        assert!(content.contains("c")); // Curve commands
631        assert!(content.contains("S")); // Stroke command
632    }
633
634    #[test]
635    fn test_pattern_validation_valid() {
636        let pattern = TilingPattern::new(
637            "ValidPattern".to_string(),
638            PaintType::Colored,
639            TilingType::ConstantSpacing,
640            [0.0, 0.0, 100.0, 100.0],
641            50.0,
642            50.0,
643        );
644
645        // Add some content to make it valid
646        let mut pattern_with_content = pattern;
647        pattern_with_content.add_rectangle(0.0, 0.0, 50.0, 50.0);
648
649        assert!(pattern_with_content.validate().is_ok());
650    }
651
652    #[test]
653    fn test_pattern_validation_invalid_bbox() {
654        let pattern = TilingPattern::new(
655            "InvalidPattern".to_string(),
656            PaintType::Colored,
657            TilingType::ConstantSpacing,
658            [100.0, 100.0, 0.0, 0.0], // Invalid bbox
659            50.0,
660            50.0,
661        );
662
663        assert!(pattern.validate().is_err());
664    }
665
666    #[test]
667    fn test_pattern_validation_invalid_steps() {
668        let pattern = TilingPattern::new(
669            "InvalidPattern".to_string(),
670            PaintType::Colored,
671            TilingType::ConstantSpacing,
672            [0.0, 0.0, 100.0, 100.0],
673            0.0, // Invalid step
674            50.0,
675        );
676
677        assert!(pattern.validate().is_err());
678    }
679
680    #[test]
681    fn test_pattern_validation_empty_content() {
682        let pattern = TilingPattern::new(
683            "EmptyPattern".to_string(),
684            PaintType::Colored,
685            TilingType::ConstantSpacing,
686            [0.0, 0.0, 100.0, 100.0],
687            50.0,
688            50.0,
689        );
690
691        // No content added
692        assert!(pattern.validate().is_err());
693    }
694
695    #[test]
696    fn test_pattern_manager_creation() {
697        let manager = PatternManager::new();
698        assert_eq!(manager.count(), 0);
699        assert!(manager.patterns().is_empty());
700    }
701
702    #[test]
703    fn test_pattern_manager_add_pattern() {
704        let mut manager = PatternManager::new();
705        let mut pattern = TilingPattern::new(
706            "TestPattern".to_string(),
707            PaintType::Colored,
708            TilingType::ConstantSpacing,
709            [0.0, 0.0, 100.0, 100.0],
710            50.0,
711            50.0,
712        );
713
714        // Add content to make it valid
715        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
716
717        let name = manager.add_pattern(pattern).unwrap();
718        assert_eq!(name, "TestPattern");
719        assert_eq!(manager.count(), 1);
720
721        let retrieved = manager.get_pattern(&name).unwrap();
722        assert_eq!(retrieved.name, "TestPattern");
723    }
724
725    #[test]
726    fn test_pattern_manager_auto_naming() {
727        let mut manager = PatternManager::new();
728        let mut pattern = TilingPattern::new(
729            String::new(), // Empty name
730            PaintType::Colored,
731            TilingType::ConstantSpacing,
732            [0.0, 0.0, 100.0, 100.0],
733            50.0,
734            50.0,
735        );
736
737        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
738
739        let name = manager.add_pattern(pattern).unwrap();
740        assert_eq!(name, "P1");
741
742        let mut pattern2 = TilingPattern::new(
743            String::new(),
744            PaintType::Colored,
745            TilingType::ConstantSpacing,
746            [0.0, 0.0, 100.0, 100.0],
747            50.0,
748            50.0,
749        );
750
751        pattern2.add_rectangle(0.0, 0.0, 50.0, 50.0);
752
753        let name2 = manager.add_pattern(pattern2).unwrap();
754        assert_eq!(name2, "P2");
755    }
756
757    #[test]
758    fn test_pattern_manager_checkerboard() {
759        let mut manager = PatternManager::new();
760        let name = manager
761            .create_checkerboard_pattern(
762                25.0,
763                [1.0, 0.0, 0.0], // Red
764                [0.0, 0.0, 1.0], // Blue
765            )
766            .unwrap();
767
768        let pattern = manager.get_pattern(&name).unwrap();
769        assert_eq!(pattern.x_step, 50.0);
770        assert_eq!(pattern.y_step, 50.0);
771        assert!(!pattern.content_stream.is_empty());
772    }
773
774    #[test]
775    fn test_pattern_manager_stripes() {
776        let mut manager = PatternManager::new();
777        let name = manager
778            .create_stripe_pattern(
779                10.0,
780                45.0,            // 45 degrees
781                [0.0, 1.0, 0.0], // Green
782                [1.0, 1.0, 0.0], // Yellow
783            )
784            .unwrap();
785
786        let pattern = manager.get_pattern(&name).unwrap();
787        assert_eq!(pattern.x_step, 20.0);
788        assert_eq!(pattern.y_step, 20.0);
789        // Should have rotation matrix applied
790        assert_ne!(pattern.matrix.matrix, PatternMatrix::identity().matrix);
791    }
792
793    #[test]
794    fn test_pattern_manager_dots() {
795        let mut manager = PatternManager::new();
796        let name = manager
797            .create_dots_pattern(
798                5.0,             // radius
799                20.0,            // spacing
800                [1.0, 0.0, 1.0], // Magenta
801                [1.0, 1.0, 1.0], // White
802            )
803            .unwrap();
804
805        let pattern = manager.get_pattern(&name).unwrap();
806        assert_eq!(pattern.x_step, 20.0);
807        assert_eq!(pattern.y_step, 20.0);
808
809        let content = String::from_utf8(pattern.content_stream.clone()).unwrap();
810        assert!(content.contains("c")); // Should contain curve commands for circle
811    }
812
813    #[test]
814    fn test_pattern_pdf_dictionary_generation() {
815        let mut pattern = TilingPattern::new(
816            "TestPattern".to_string(),
817            PaintType::Colored,
818            TilingType::ConstantSpacing,
819            [0.0, 0.0, 100.0, 100.0],
820            50.0,
821            50.0,
822        );
823
824        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
825
826        let dict = pattern.to_pdf_dictionary().unwrap();
827
828        // Verify dictionary contents
829        if let Some(Object::Name(type_name)) = dict.get("Type") {
830            assert_eq!(type_name, "Pattern");
831        }
832        if let Some(Object::Integer(pattern_type)) = dict.get("PatternType") {
833            assert_eq!(*pattern_type, 1);
834        }
835        if let Some(Object::Integer(paint_type)) = dict.get("PaintType") {
836            assert_eq!(*paint_type, 1);
837        }
838        if let Some(Object::Array(bbox)) = dict.get("BBox") {
839            assert_eq!(bbox.len(), 4);
840        }
841    }
842
843    #[test]
844    fn test_pattern_manager_clear() {
845        let mut manager = PatternManager::new();
846        let mut pattern = TilingPattern::new(
847            "TestPattern".to_string(),
848            PaintType::Colored,
849            TilingType::ConstantSpacing,
850            [0.0, 0.0, 100.0, 100.0],
851            50.0,
852            50.0,
853        );
854
855        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
856        manager.add_pattern(pattern).unwrap();
857        assert_eq!(manager.count(), 1);
858
859        manager.clear();
860        assert_eq!(manager.count(), 0);
861        assert!(manager.patterns().is_empty());
862    }
863
864    #[test]
865    fn test_pattern_type_values() {
866        assert_eq!(PatternType::Tiling as i32, 1);
867        assert_eq!(PatternType::Shading as i32, 2);
868    }
869
870    #[test]
871    fn test_tiling_type_values() {
872        assert_eq!(TilingType::ConstantSpacing as i32, 1);
873        assert_eq!(TilingType::NoDistortion as i32, 2);
874        assert_eq!(TilingType::ConstantSpacingFaster as i32, 3);
875    }
876
877    #[test]
878    fn test_paint_type_values() {
879        assert_eq!(PaintType::Colored as i32, 1);
880        assert_eq!(PaintType::Uncolored as i32, 2);
881    }
882
883    #[test]
884    fn test_pattern_matrix_rotation() {
885        let angle = std::f64::consts::PI / 2.0; // 90 degrees
886        let matrix = PatternMatrix::rotation(angle);
887
888        // cos(90°) ≈ 0, sin(90°) ≈ 1
889        assert!((matrix.matrix[0]).abs() < 1e-10); // cos(90°) ≈ 0
890        assert!((matrix.matrix[1] - 1.0).abs() < 1e-10); // sin(90°) ≈ 1
891        assert!((matrix.matrix[2] + 1.0).abs() < 1e-10); // -sin(90°) ≈ -1
892        assert!((matrix.matrix[3]).abs() < 1e-10); // cos(90°) ≈ 0
893    }
894
895    #[test]
896    fn test_pattern_matrix_complex_multiply() {
897        let translate = PatternMatrix::translation(10.0, 20.0);
898        let scale = PatternMatrix::scale(2.0, 3.0);
899        let rotate = PatternMatrix::rotation(std::f64::consts::PI / 4.0); // 45 degrees
900
901        let result = translate.multiply(&scale).multiply(&rotate);
902
903        // Verify matrix multiplication was performed
904        assert_ne!(result.matrix, PatternMatrix::identity().matrix);
905    }
906
907    #[test]
908    fn test_tiling_pattern_with_matrix() {
909        let pattern = TilingPattern::new(
910            "TestPattern".to_string(),
911            PaintType::Colored,
912            TilingType::ConstantSpacing,
913            [0.0, 0.0, 100.0, 100.0],
914            50.0,
915            50.0,
916        );
917
918        let matrix = PatternMatrix::scale(2.0, 2.0);
919        let pattern_with_matrix = pattern.with_matrix(matrix);
920
921        assert_eq!(
922            pattern_with_matrix.matrix.matrix,
923            [2.0, 0.0, 0.0, 2.0, 0.0, 0.0]
924        );
925    }
926
927    #[test]
928    fn test_tiling_pattern_with_resources() {
929        let pattern = TilingPattern::new(
930            "TestPattern".to_string(),
931            PaintType::Colored,
932            TilingType::ConstantSpacing,
933            [0.0, 0.0, 100.0, 100.0],
934            50.0,
935            50.0,
936        );
937
938        let mut resources = Dictionary::new();
939        resources.set("Font", Object::Name("F1".to_string()));
940
941        let pattern_with_resources = pattern.with_resources(resources.clone());
942        assert_eq!(pattern_with_resources.resources, Some(resources));
943    }
944
945    #[test]
946    fn test_tiling_pattern_stroke() {
947        let mut pattern = TilingPattern::new(
948            "StrokePattern".to_string(),
949            PaintType::Colored,
950            TilingType::ConstantSpacing,
951            [0.0, 0.0, 100.0, 100.0],
952            100.0,
953            100.0,
954        );
955
956        pattern.add_rectangle(10.0, 10.0, 80.0, 80.0);
957        pattern.stroke();
958
959        let content = String::from_utf8(pattern.content_stream).unwrap();
960        assert!(content.contains("S"));
961        assert!(!content.contains("f")); // Should not contain fill
962    }
963
964    #[test]
965    fn test_tiling_pattern_add_command() {
966        let mut pattern = TilingPattern::new(
967            "CommandPattern".to_string(),
968            PaintType::Colored,
969            TilingType::ConstantSpacing,
970            [0.0, 0.0, 100.0, 100.0],
971            100.0,
972            100.0,
973        );
974
975        pattern.add_command("0.5 0.5 0.5 rg");
976        pattern.add_command("2 w");
977
978        let content = String::from_utf8(pattern.content_stream).unwrap();
979        assert!(content.contains("0.5 0.5 0.5 rg"));
980        assert!(content.contains("2 w"));
981    }
982
983    #[test]
984    fn test_pattern_manager_remove_pattern() {
985        let mut manager = PatternManager::new();
986        let mut pattern = TilingPattern::new(
987            "RemovablePattern".to_string(),
988            PaintType::Colored,
989            TilingType::ConstantSpacing,
990            [0.0, 0.0, 100.0, 100.0],
991            50.0,
992            50.0,
993        );
994
995        pattern.add_rectangle(0.0, 0.0, 50.0, 50.0);
996        manager.add_pattern(pattern).unwrap();
997        assert_eq!(manager.count(), 1);
998
999        let removed = manager.remove_pattern("RemovablePattern");
1000        assert!(removed.is_some());
1001        assert_eq!(manager.count(), 0);
1002
1003        let removed_again = manager.remove_pattern("RemovablePattern");
1004        assert!(removed_again.is_none());
1005    }
1006
1007    #[test]
1008    fn test_pattern_manager_to_resource_dictionary() {
1009        let mut manager = PatternManager::new();
1010
1011        // Empty manager
1012        assert_eq!(manager.to_resource_dictionary().unwrap(), "");
1013
1014        // Add patterns
1015        let mut pattern1 = TilingPattern::new(
1016            "P1".to_string(),
1017            PaintType::Colored,
1018            TilingType::ConstantSpacing,
1019            [0.0, 0.0, 10.0, 10.0],
1020            10.0,
1021            10.0,
1022        );
1023        pattern1.add_rectangle(0.0, 0.0, 10.0, 10.0);
1024        manager.add_pattern(pattern1).unwrap();
1025
1026        let dict = manager.to_resource_dictionary().unwrap();
1027        assert!(dict.starts_with("/Pattern <<"));
1028        assert!(dict.contains("/P1"));
1029        assert!(dict.ends_with(">>"));
1030    }
1031
1032    #[test]
1033    fn test_pattern_manager_default() {
1034        let manager = PatternManager::default();
1035        assert_eq!(manager.count(), 0);
1036        assert!(manager.patterns().is_empty());
1037    }
1038
1039    #[test]
1040    fn test_pattern_graphics_context_extension() {
1041        let mut context = GraphicsContext::new();
1042
1043        // Test fill pattern
1044        context.set_fill_pattern("TestPattern").unwrap();
1045        let commands = context.operations();
1046        assert!(commands.contains("/Pattern cs /TestPattern scn"));
1047
1048        // Test stroke pattern
1049        context.set_stroke_pattern("StrokePattern").unwrap();
1050        let commands = context.operations();
1051        assert!(commands.contains("/Pattern CS /StrokePattern SCN"));
1052    }
1053
1054    #[test]
1055    fn test_tiling_pattern_uncolored() {
1056        let pattern = TilingPattern::new(
1057            "UncoloredPattern".to_string(),
1058            PaintType::Uncolored,
1059            TilingType::NoDistortion,
1060            [0.0, 0.0, 50.0, 50.0],
1061            50.0,
1062            50.0,
1063        );
1064
1065        assert_eq!(pattern.paint_type, PaintType::Uncolored);
1066        assert_eq!(pattern.tiling_type, TilingType::NoDistortion);
1067    }
1068
1069    #[test]
1070    fn test_checkerboard_pattern_content() {
1071        let mut manager = PatternManager::new();
1072        let name = manager
1073            .create_checkerboard_pattern(
1074                10.0,
1075                [1.0, 1.0, 1.0], // White
1076                [0.0, 0.0, 0.0], // Black
1077            )
1078            .unwrap();
1079
1080        let pattern = manager.get_pattern(&name).unwrap();
1081        let content = String::from_utf8(pattern.content_stream.clone()).unwrap();
1082
1083        // Should contain color commands. Pattern emitters now share the
1084        // `.3`-precision format used elsewhere in the pipeline (issue #220
1085        // sanitisation pass), so the wire form is `1.000 1.000 1.000 rg`,
1086        // not the unformatted `1 1 1 rg` the previous emitter produced.
1087        assert!(content.contains("1.000 1.000 1.000 rg")); // White
1088        assert!(content.contains("0.000 0.000 0.000 rg")); // Black
1089                                                           // Should contain rectangles
1090        assert!(content.contains("re"));
1091        assert!(content.contains("f"));
1092    }
1093
1094    #[test]
1095    fn test_stripe_pattern_zero_angle() {
1096        let mut manager = PatternManager::new();
1097        let name = manager
1098            .create_stripe_pattern(
1099                5.0,
1100                0.0, // No rotation
1101                [1.0, 0.0, 0.0],
1102                [0.0, 1.0, 0.0],
1103            )
1104            .unwrap();
1105
1106        let pattern = manager.get_pattern(&name).unwrap();
1107        // With 0 angle, matrix should remain identity
1108        assert_eq!(pattern.matrix.matrix, PatternMatrix::identity().matrix);
1109    }
1110
1111    #[test]
1112    fn test_dots_pattern_content() {
1113        let mut manager = PatternManager::new();
1114        let name = manager
1115            .create_dots_pattern(
1116                3.0,             // Small radius
1117                10.0,            // Spacing
1118                [0.0, 0.0, 0.0], // Black dots
1119                [1.0, 1.0, 1.0], // White background
1120            )
1121            .unwrap();
1122
1123        let pattern = manager.get_pattern(&name).unwrap();
1124        let content = String::from_utf8(pattern.content_stream.clone()).unwrap();
1125
1126        // Should draw background rectangle. Pattern emitters now use
1127        // `.3`-precision (issue #220 sanitisation pass), so the wire form
1128        // is `1.000 1.000 1.000 rg`, not unformatted `1 1 1 rg`.
1129        assert!(content.contains("1.000 1.000 1.000 rg")); // White background
1130        assert!(content.contains("0 0 10 10 re")); // Background rectangle
1131
1132        // Should draw circle
1133        assert!(content.contains("0.000 0.000 0.000 rg")); // Black dot
1134        assert!(content.contains("m")); // Move to
1135        assert!(content.contains("c")); // Curve (for circle)
1136    }
1137
1138    #[test]
1139    fn test_pattern_validation_negative_step() {
1140        let pattern = TilingPattern::new(
1141            "NegativeStep".to_string(),
1142            PaintType::Colored,
1143            TilingType::ConstantSpacing,
1144            [0.0, 0.0, 100.0, 100.0],
1145            50.0,
1146            -50.0, // Negative y_step
1147        );
1148
1149        assert!(pattern.validate().is_err());
1150    }
1151
1152    #[test]
1153    fn test_circle_approximation() {
1154        let mut pattern = TilingPattern::new(
1155            "CircleTest".to_string(),
1156            PaintType::Colored,
1157            TilingType::ConstantSpacing,
1158            [0.0, 0.0, 100.0, 100.0],
1159            100.0,
1160            100.0,
1161        );
1162
1163        pattern.add_circle(50.0, 50.0, 0.0); // Zero radius
1164        let content = String::from_utf8(pattern.content_stream.clone()).unwrap();
1165
1166        // Should still generate move command but minimal curves
1167        assert!(content.contains("50 50 m"));
1168    }
1169
1170    #[test]
1171    fn test_pattern_manager_get_nonexistent() {
1172        let manager = PatternManager::new();
1173        assert!(manager.get_pattern("NonExistent").is_none());
1174    }
1175
1176    #[test]
1177    fn test_pattern_type_debug_clone_eq() {
1178        let pattern_type = PatternType::Tiling;
1179
1180        // Test Debug
1181        let debug_str = format!("{pattern_type:?}");
1182        assert!(debug_str.contains("Tiling"));
1183
1184        // Test Clone
1185        let cloned = pattern_type;
1186        assert_eq!(cloned, PatternType::Tiling);
1187
1188        // Test PartialEq
1189        assert_eq!(PatternType::Tiling, PatternType::Tiling);
1190        assert_ne!(PatternType::Tiling, PatternType::Shading);
1191    }
1192
1193    #[test]
1194    fn test_tiling_pattern_debug_clone() {
1195        let pattern = TilingPattern::new(
1196            "TestPattern".to_string(),
1197            PaintType::Colored,
1198            TilingType::ConstantSpacing,
1199            [0.0, 0.0, 100.0, 100.0],
1200            50.0,
1201            50.0,
1202        );
1203
1204        // Test Debug
1205        let debug_str = format!("{pattern:?}");
1206        assert!(debug_str.contains("TilingPattern"));
1207        assert!(debug_str.contains("TestPattern"));
1208
1209        // Test Clone
1210        let cloned = pattern.clone();
1211        assert_eq!(cloned.name, pattern.name);
1212        assert_eq!(cloned.paint_type, pattern.paint_type);
1213    }
1214
1215    #[test]
1216    fn test_pattern_matrix_debug_clone_eq() {
1217        let matrix = PatternMatrix::translation(5.0, 10.0);
1218
1219        // Test Debug
1220        let debug_str = format!("{matrix:?}");
1221        assert!(debug_str.contains("PatternMatrix"));
1222
1223        // Test Clone
1224        let cloned = matrix.clone();
1225        assert_eq!(cloned.matrix, matrix.matrix);
1226
1227        // Test PartialEq
1228        assert_eq!(matrix, cloned);
1229        assert_ne!(matrix, PatternMatrix::identity());
1230    }
1231
1232    #[test]
1233    fn test_pattern_type() {
1234        assert_eq!(PatternType::Tiling as i32, 1);
1235        assert_eq!(PatternType::Shading as i32, 2);
1236    }
1237
1238    #[test]
1239    fn test_tiling_type() {
1240        assert_eq!(TilingType::ConstantSpacing as i32, 1);
1241        assert_eq!(TilingType::NoDistortion as i32, 2);
1242        assert_eq!(TilingType::ConstantSpacingFaster as i32, 3);
1243    }
1244
1245    #[test]
1246    fn test_paint_type() {
1247        assert_eq!(PaintType::Colored as i32, 1);
1248        assert_eq!(PaintType::Uncolored as i32, 2);
1249    }
1250
1251    #[test]
1252    fn test_pattern_manager() {
1253        let mut manager = PatternManager::new();
1254
1255        let mut pattern = TilingPattern::new(
1256            "P1".to_string(),
1257            PaintType::Colored,
1258            TilingType::ConstantSpacing,
1259            [0.0, 0.0, 10.0, 10.0],
1260            10.0,
1261            10.0,
1262        );
1263        // Add some content to the pattern
1264        pattern.content_stream = vec![b'q', b' ', b'Q']; // Minimal valid PDF content
1265
1266        let name = manager.add_pattern(pattern).unwrap();
1267        assert_eq!(name, "P1");
1268
1269        // PatternManager stores patterns internally
1270        // We can verify it was added by checking the pattern count increases
1271        let mut pattern2 = TilingPattern::new(
1272            "P2".to_string(),
1273            PaintType::Uncolored,
1274            TilingType::NoDistortion,
1275            [0.0, 0.0, 20.0, 20.0],
1276            20.0,
1277            20.0,
1278        );
1279        pattern2.content_stream = vec![b'q', b' ', b'Q']; // Minimal valid PDF content
1280
1281        let name2 = manager.add_pattern(pattern2).unwrap();
1282        assert_eq!(name2, "P2");
1283    }
1284}