Skip to main content

pdf_oxide/writer/
pattern.rs

1//! Pattern support for PDF generation.
2//!
3//! This module provides builders for PDF pattern resources:
4//! - Tiling patterns (Type 1) - repeating content
5//! - Shading patterns (Type 2) - gradient-based patterns
6//!
7//! # Example
8//!
9//! ```ignore
10//! use pdf_oxide::writer::pattern::{TilingPatternBuilder, PatternPaintType};
11//! use pdf_oxide::layout::Color;
12//!
13//! // Create a striped pattern
14//! let pattern = TilingPatternBuilder::new()
15//!     .bbox(0.0, 0.0, 10.0, 10.0)
16//!     .x_step(10.0)
17//!     .y_step(10.0)
18//!     .colored()
19//!     .content(|builder| {
20//!         builder
21//!             .set_fill_color(1.0, 0.0, 0.0)
22//!             .rect(0.0, 0.0, 5.0, 10.0)
23//!             .fill();
24//!     })
25//!     .build();
26//! ```
27
28use crate::layout::Color;
29use crate::object::Object;
30use std::collections::HashMap;
31
32/// Helper to create a string key for dictionary
33fn key(s: &str) -> String {
34    s.to_string()
35}
36
37/// Pattern paint type.
38#[derive(Debug, Clone, Copy, Default)]
39pub enum PatternPaintType {
40    /// Colored pattern - colors specified in pattern content
41    #[default]
42    Colored = 1,
43    /// Uncolored pattern - color specified when pattern is used
44    Uncolored = 2,
45}
46
47/// Pattern tiling type.
48#[derive(Debug, Clone, Copy, Default)]
49pub enum PatternTilingType {
50    /// Constant spacing - pattern cell spacing is constant
51    #[default]
52    ConstantSpacing = 1,
53    /// No distortion - cell is adjusted to device pixels without distortion
54    NoDistortion = 2,
55    /// Constant spacing and faster tiling
56    ConstantSpacingFaster = 3,
57}
58
59/// Builder for tiling patterns (Type 1).
60///
61/// Tiling patterns paint a cell that is replicated at fixed intervals
62/// to fill the area to be painted.
63#[derive(Debug, Clone)]
64pub struct TilingPatternBuilder {
65    /// Bounding box of the pattern cell
66    bbox: (f32, f32, f32, f32),
67    /// Horizontal spacing between pattern cells
68    x_step: f32,
69    /// Vertical spacing between pattern cells
70    y_step: f32,
71    /// Paint type
72    paint_type: PatternPaintType,
73    /// Tiling type
74    tiling_type: PatternTilingType,
75    /// Pattern content stream
76    content: Vec<u8>,
77    /// Pattern matrix (optional transformation)
78    matrix: Option<[f32; 6]>,
79    /// Resources needed by the pattern
80    resources: HashMap<Vec<u8>, Object>,
81}
82
83impl Default for TilingPatternBuilder {
84    fn default() -> Self {
85        Self {
86            bbox: (0.0, 0.0, 10.0, 10.0),
87            x_step: 10.0,
88            y_step: 10.0,
89            paint_type: PatternPaintType::Colored,
90            tiling_type: PatternTilingType::ConstantSpacing,
91            content: Vec::new(),
92            matrix: None,
93            resources: HashMap::new(),
94        }
95    }
96}
97
98impl TilingPatternBuilder {
99    /// Create a new tiling pattern builder.
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Set the bounding box of the pattern cell.
105    pub fn bbox(mut self, x: f32, y: f32, width: f32, height: f32) -> Self {
106        self.bbox = (x, y, width, height);
107        self
108    }
109
110    /// Set the horizontal step (spacing).
111    pub fn x_step(mut self, step: f32) -> Self {
112        self.x_step = step;
113        self
114    }
115
116    /// Set the vertical step (spacing).
117    pub fn y_step(mut self, step: f32) -> Self {
118        self.y_step = step;
119        self
120    }
121
122    /// Set both steps at once.
123    pub fn step(self, x: f32, y: f32) -> Self {
124        self.x_step(x).y_step(y)
125    }
126
127    /// Set as colored pattern (colors in pattern content).
128    pub fn colored(mut self) -> Self {
129        self.paint_type = PatternPaintType::Colored;
130        self
131    }
132
133    /// Set as uncolored pattern (color specified at use time).
134    pub fn uncolored(mut self) -> Self {
135        self.paint_type = PatternPaintType::Uncolored;
136        self
137    }
138
139    /// Set the tiling type.
140    pub fn tiling_type(mut self, tiling: PatternTilingType) -> Self {
141        self.tiling_type = tiling;
142        self
143    }
144
145    /// Set the pattern transformation matrix.
146    pub fn matrix(mut self, a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self {
147        self.matrix = Some([a, b, c, d, e, f]);
148        self
149    }
150
151    /// Set the raw content stream.
152    pub fn content_bytes(mut self, content: Vec<u8>) -> Self {
153        self.content = content;
154        self
155    }
156
157    /// Build the pattern dictionary and content stream.
158    ///
159    /// Returns (dictionary, content_stream_bytes).
160    pub fn build(&self) -> (Object, Vec<u8>) {
161        let mut dict: HashMap<String, Object> = HashMap::new();
162
163        // Type is always Pattern
164        dict.insert(key("Type"), Object::Name("Pattern".to_string()));
165
166        // PatternType 1 = Tiling
167        dict.insert(key("PatternType"), Object::Integer(1));
168
169        // PaintType
170        dict.insert(key("PaintType"), Object::Integer(self.paint_type as i64));
171
172        // TilingType
173        dict.insert(key("TilingType"), Object::Integer(self.tiling_type as i64));
174
175        // BBox
176        dict.insert(
177            key("BBox"),
178            Object::Array(vec![
179                Object::Real(self.bbox.0 as f64),
180                Object::Real(self.bbox.1 as f64),
181                Object::Real(self.bbox.2 as f64),
182                Object::Real(self.bbox.3 as f64),
183            ]),
184        );
185
186        // XStep and YStep
187        dict.insert(key("XStep"), Object::Real(self.x_step as f64));
188        dict.insert(key("YStep"), Object::Real(self.y_step as f64));
189
190        // Matrix (optional)
191        if let Some(m) = &self.matrix {
192            dict.insert(
193                key("Matrix"),
194                Object::Array(m.iter().map(|&v| Object::Real(v as f64)).collect()),
195            );
196        }
197
198        // Resources (if needed) - convert keys
199        if !self.resources.is_empty() {
200            let converted: HashMap<String, Object> = self
201                .resources
202                .iter()
203                .map(|(k, v)| (String::from_utf8_lossy(k).to_string(), v.clone()))
204                .collect();
205            dict.insert(key("Resources"), Object::Dictionary(converted));
206        }
207
208        (Object::Dictionary(dict), self.content.clone())
209    }
210}
211
212/// Builder for shading patterns (Type 2).
213///
214/// Shading patterns use a shading dictionary to define a gradient fill.
215#[derive(Debug, Clone, Default)]
216pub struct ShadingPatternBuilder {
217    /// Reference to shading dictionary (will be indirect reference)
218    shading_id: Option<u32>,
219    /// Pattern transformation matrix
220    matrix: Option<[f32; 6]>,
221    /// ExtGState for the pattern
222    ext_gstate_id: Option<u32>,
223}
224
225impl ShadingPatternBuilder {
226    /// Create a new shading pattern builder.
227    pub fn new() -> Self {
228        Self::default()
229    }
230
231    /// Set the shading object ID (will be referenced indirectly).
232    pub fn shading_id(mut self, id: u32) -> Self {
233        self.shading_id = Some(id);
234        self
235    }
236
237    /// Set the pattern transformation matrix.
238    pub fn matrix(mut self, a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self {
239        self.matrix = Some([a, b, c, d, e, f]);
240        self
241    }
242
243    /// Set the ExtGState object ID.
244    pub fn ext_gstate_id(mut self, id: u32) -> Self {
245        self.ext_gstate_id = Some(id);
246        self
247    }
248
249    /// Build the pattern dictionary.
250    pub fn build(&self) -> Object {
251        let mut dict: HashMap<String, Object> = HashMap::new();
252
253        // Type is always Pattern
254        dict.insert(key("Type"), Object::Name("Pattern".to_string()));
255
256        // PatternType 2 = Shading
257        dict.insert(key("PatternType"), Object::Integer(2));
258
259        // Shading (indirect reference) - caller must set this
260        // We'll put a placeholder that the caller should replace
261        if let Some(id) = self.shading_id {
262            dict.insert(key("Shading"), Object::Reference(crate::object::ObjectRef::new(id, 0)));
263        }
264
265        // Matrix (optional)
266        if let Some(m) = &self.matrix {
267            dict.insert(
268                key("Matrix"),
269                Object::Array(m.iter().map(|&v| Object::Real(v as f64)).collect()),
270            );
271        }
272
273        // ExtGState (optional)
274        if let Some(id) = self.ext_gstate_id {
275            dict.insert(key("ExtGState"), Object::Reference(crate::object::ObjectRef::new(id, 0)));
276        }
277
278        Object::Dictionary(dict)
279    }
280}
281
282/// Predefined pattern presets.
283pub struct PatternPresets;
284
285impl PatternPresets {
286    /// Create horizontal stripes pattern content.
287    pub fn horizontal_stripes(
288        width: f32,
289        _height: f32,
290        stripe_height: f32,
291        color: Color,
292    ) -> Vec<u8> {
293        format!(
294            "{} {} {} rg\n0 0 {} {} re\nf\n",
295            color.r, color.g, color.b, width, stripe_height
296        )
297        .into_bytes()
298    }
299
300    /// Create vertical stripes pattern content.
301    pub fn vertical_stripes(_width: f32, height: f32, stripe_width: f32, color: Color) -> Vec<u8> {
302        format!(
303            "{} {} {} rg\n0 0 {} {} re\nf\n",
304            color.r, color.g, color.b, stripe_width, height
305        )
306        .into_bytes()
307    }
308
309    /// Create a checkerboard pattern content.
310    pub fn checkerboard(size: f32, color1: Color, color2: Color) -> Vec<u8> {
311        format!(
312            "{} {} {} rg\n0 0 {} {} re\nf\n{} {} {} rg\n{} 0 {} {} re\n0 {} {} {} re\nf\n",
313            color1.r,
314            color1.g,
315            color1.b,
316            size * 2.0,
317            size * 2.0,
318            color2.r,
319            color2.g,
320            color2.b,
321            size,
322            size,
323            size,
324            size,
325            size,
326            size
327        )
328        .into_bytes()
329    }
330
331    /// Create a dot pattern content.
332    pub fn dots(spacing: f32, radius: f32, color: Color) -> Vec<u8> {
333        // Approximate circle with Bézier curves
334        let k = radius * 0.552_284_8;
335        let cx = spacing / 2.0;
336        let cy = spacing / 2.0;
337
338        format!(
339            "{} {} {} rg\n\
340             {} {} m\n\
341             {} {} {} {} {} {} c\n\
342             {} {} {} {} {} {} c\n\
343             {} {} {} {} {} {} c\n\
344             {} {} {} {} {} {} c\n\
345             f\n",
346            color.r,
347            color.g,
348            color.b,
349            cx + radius,
350            cy,
351            cx + radius,
352            cy + k,
353            cx + k,
354            cy + radius,
355            cx,
356            cy + radius,
357            cx - k,
358            cy + radius,
359            cx - radius,
360            cy + k,
361            cx - radius,
362            cy,
363            cx - radius,
364            cy - k,
365            cx - k,
366            cy - radius,
367            cx,
368            cy - radius,
369            cx + k,
370            cy - radius,
371            cx + radius,
372            cy - k,
373            cx + radius,
374            cy
375        )
376        .into_bytes()
377    }
378
379    /// Create diagonal lines pattern content.
380    pub fn diagonal_lines(size: f32, line_width: f32, color: Color) -> Vec<u8> {
381        format!(
382            "{} {} {} RG\n{} w\n0 0 m\n{} {} l\nS\n",
383            color.r, color.g, color.b, line_width, size, size
384        )
385        .into_bytes()
386    }
387
388    /// Create a crosshatch pattern content.
389    pub fn crosshatch(size: f32, line_width: f32, color: Color) -> Vec<u8> {
390        format!(
391            "{} {} {} RG\n{} w\n\
392             0 0 m\n{} {} l\nS\n\
393             {} 0 m\n0 {} l\nS\n",
394            color.r, color.g, color.b, line_width, size, size, size, size
395        )
396        .into_bytes()
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_tiling_pattern_builder() {
406        let content =
407            PatternPresets::horizontal_stripes(10.0, 10.0, 5.0, Color::new(1.0, 0.0, 0.0));
408        let (dict, _content) = TilingPatternBuilder::new()
409            .bbox(0.0, 0.0, 10.0, 10.0)
410            .step(10.0, 10.0)
411            .colored()
412            .content_bytes(content)
413            .build();
414
415        if let Object::Dictionary(d) = dict {
416            assert!(d.contains_key("PatternType"));
417            if let Some(Object::Integer(pt)) = d.get("PatternType") {
418                assert_eq!(*pt, 1);
419            }
420        } else {
421            panic!("Expected dictionary");
422        }
423    }
424
425    #[test]
426    fn test_shading_pattern_builder() {
427        let dict = ShadingPatternBuilder::new()
428            .shading_id(5)
429            .matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
430            .build();
431
432        if let Object::Dictionary(d) = dict {
433            assert!(d.contains_key("PatternType"));
434            if let Some(Object::Integer(pt)) = d.get("PatternType") {
435                assert_eq!(*pt, 2);
436            }
437        } else {
438            panic!("Expected dictionary");
439        }
440    }
441
442    #[test]
443    fn test_pattern_presets() {
444        let _ = PatternPresets::horizontal_stripes(10.0, 10.0, 5.0, Color::new(0.0, 0.0, 1.0));
445        let _ = PatternPresets::checkerboard(5.0, Color::white(), Color::black());
446        let _ = PatternPresets::dots(10.0, 2.0, Color::new(1.0, 0.0, 0.0));
447        let _ = PatternPresets::diagonal_lines(10.0, 0.5, Color::black());
448    }
449}