Skip to main content

oxidize_pdf/graphics/
form_xobject.rs

1//! Form XObjects for reusable graphics content
2//!
3//! Implements ISO 32000-1 Section 8.10 (Form XObjects)
4//! Form XObjects are self-contained descriptions of graphics objects that can be
5//! painted multiple times on different pages or at different locations.
6
7use crate::error::Result;
8use crate::geometry::Rectangle;
9use crate::objects::{Dictionary, Object, ObjectId, Stream};
10use std::collections::HashMap;
11
12/// Form XObject - reusable graphics content
13#[derive(Debug, Clone)]
14pub struct FormXObject {
15    /// Bounding box of the form
16    pub bbox: Rectangle,
17    /// Optional transformation matrix
18    pub matrix: Option<[f64; 6]>,
19    /// Resources used by the form
20    pub resources: Dictionary,
21    /// Graphics operations content
22    pub content: Vec<u8>,
23    /// Optional group attributes for transparency
24    pub group: Option<TransparencyGroup>,
25    /// Optional reference to external XObject
26    pub reference: Option<ObjectId>,
27    /// Metadata
28    pub metadata: Option<Dictionary>,
29}
30
31/// Transparency group attributes
32#[derive(Debug, Clone)]
33pub struct TransparencyGroup {
34    /// Color space for group
35    pub color_space: String,
36    /// Whether group is isolated
37    pub isolated: bool,
38    /// Whether group is knockout
39    pub knockout: bool,
40}
41
42impl Default for TransparencyGroup {
43    fn default() -> Self {
44        Self {
45            color_space: "DeviceRGB".to_string(),
46            isolated: false,
47            knockout: false,
48        }
49    }
50}
51
52impl FormXObject {
53    /// Create a new form XObject
54    pub fn new(bbox: Rectangle) -> Self {
55        Self {
56            bbox,
57            matrix: None,
58            resources: Dictionary::new(),
59            content: Vec::new(),
60            group: None,
61            reference: None,
62            metadata: None,
63        }
64    }
65
66    /// Set transformation matrix
67    pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
68        self.matrix = Some(matrix);
69        self
70    }
71
72    /// Set resources
73    pub fn with_resources(mut self, resources: Dictionary) -> Self {
74        self.resources = resources;
75        self
76    }
77
78    /// Set content stream
79    pub fn with_content(mut self, content: Vec<u8>) -> Self {
80        self.content = content;
81        self
82    }
83
84    /// Add transparency group
85    pub fn with_transparency_group(mut self, group: TransparencyGroup) -> Self {
86        self.group = Some(group);
87        self
88    }
89
90    /// Set metadata
91    pub fn with_metadata(mut self, metadata: Dictionary) -> Self {
92        self.metadata = Some(metadata);
93        self
94    }
95
96    /// Create a form XObject from graphics operations
97    pub fn from_graphics_ops(bbox: Rectangle, ops: &str) -> Self {
98        Self {
99            bbox,
100            matrix: None,
101            resources: Dictionary::new(),
102            content: ops.as_bytes().to_vec(),
103            group: None,
104            reference: None,
105            metadata: None,
106        }
107    }
108
109    /// Convert to PDF stream object
110    pub fn to_stream(&self) -> Result<Stream> {
111        let mut dict = Dictionary::new();
112
113        // Required entries
114        dict.set("Type", Object::Name("XObject".to_string()));
115        dict.set("Subtype", Object::Name("Form".to_string()));
116
117        // BBox is required
118        dict.set(
119            "BBox",
120            Object::Array(vec![
121                Object::Real(self.bbox.lower_left.x),
122                Object::Real(self.bbox.lower_left.y),
123                Object::Real(self.bbox.upper_right.x),
124                Object::Real(self.bbox.upper_right.y),
125            ]),
126        );
127
128        // Optional matrix
129        if let Some(matrix) = &self.matrix {
130            dict.set(
131                "Matrix",
132                Object::Array(matrix.iter().map(|&v| Object::Real(v)).collect()),
133            );
134        }
135
136        // Resources
137        dict.set("Resources", Object::Dictionary(self.resources.clone()));
138
139        // Transparency group if present
140        if let Some(group) = &self.group {
141            let mut group_dict = Dictionary::new();
142            group_dict.set("Type", Object::Name("Group".to_string()));
143            group_dict.set("S", Object::Name("Transparency".to_string()));
144            group_dict.set("CS", Object::Name(group.color_space.clone()));
145
146            if group.isolated {
147                group_dict.set("I", Object::Boolean(true));
148            }
149            if group.knockout {
150                group_dict.set("K", Object::Boolean(true));
151            }
152
153            dict.set("Group", Object::Dictionary(group_dict));
154        }
155
156        // Optional metadata
157        if let Some(metadata) = &self.metadata {
158            dict.set("Metadata", Object::Dictionary(metadata.clone()));
159        }
160
161        Ok(Stream::with_dictionary(dict, self.content.clone()))
162    }
163
164    /// Get the bounding box
165    pub fn get_bbox(&self) -> &Rectangle {
166        &self.bbox
167    }
168
169    /// Check if form has transparency
170    pub fn has_transparency(&self) -> bool {
171        self.group.is_some()
172    }
173}
174
175/// Builder for creating form XObjects with graphics operations
176pub struct FormXObjectBuilder {
177    bbox: Rectangle,
178    matrix: Option<[f64; 6]>,
179    resources: Dictionary,
180    operations: Vec<String>,
181    group: Option<TransparencyGroup>,
182}
183
184impl FormXObjectBuilder {
185    /// Create a new builder
186    pub fn new(bbox: Rectangle) -> Self {
187        Self {
188            bbox,
189            matrix: None,
190            resources: Dictionary::new(),
191            operations: Vec::new(),
192            group: None,
193        }
194    }
195
196    /// Set transformation matrix
197    pub fn matrix(mut self, matrix: [f64; 6]) -> Self {
198        self.matrix = Some(matrix);
199        self
200    }
201
202    /// Add a graphics operation
203    pub fn add_operation(mut self, op: impl Into<String>) -> Self {
204        self.operations.push(op.into());
205        self
206    }
207
208    /// Draw a rectangle
209    pub fn rectangle(mut self, x: f64, y: f64, width: f64, height: f64) -> Self {
210        self.operations
211            .push(format!("{} {} {} {} re", x, y, width, height));
212        self
213    }
214
215    /// Move to point
216    pub fn move_to(mut self, x: f64, y: f64) -> Self {
217        self.operations.push(format!("{} {} m", x, y));
218        self
219    }
220
221    /// Line to point
222    pub fn line_to(mut self, x: f64, y: f64) -> Self {
223        self.operations.push(format!("{} {} l", x, y));
224        self
225    }
226
227    /// Set fill color (RGB)
228    pub fn fill_color(mut self, r: f64, g: f64, b: f64) -> Self {
229        self.operations.push(format!("{} {} {} rg", r, g, b));
230        self
231    }
232
233    /// Set stroke color (RGB)
234    pub fn stroke_color(mut self, r: f64, g: f64, b: f64) -> Self {
235        self.operations.push(format!("{} {} {} RG", r, g, b));
236        self
237    }
238
239    /// Fill path
240    pub fn fill(mut self) -> Self {
241        self.operations.push("f".to_string());
242        self
243    }
244
245    /// Stroke path
246    pub fn stroke(mut self) -> Self {
247        self.operations.push("S".to_string());
248        self
249    }
250
251    /// Fill and stroke path
252    pub fn fill_stroke(mut self) -> Self {
253        self.operations.push("B".to_string());
254        self
255    }
256
257    /// Save graphics state
258    pub fn save_state(mut self) -> Self {
259        self.operations.push("q".to_string());
260        self
261    }
262
263    /// Restore graphics state
264    pub fn restore_state(mut self) -> Self {
265        self.operations.push("Q".to_string());
266        self
267    }
268
269    /// Add transparency group
270    pub fn transparency_group(mut self, isolated: bool, knockout: bool) -> Self {
271        self.group = Some(TransparencyGroup {
272            color_space: "DeviceRGB".to_string(),
273            isolated,
274            knockout,
275        });
276        self
277    }
278
279    /// Build the form XObject
280    pub fn build(self) -> FormXObject {
281        let content = self.operations.join("\n").into_bytes();
282
283        FormXObject {
284            bbox: self.bbox,
285            matrix: self.matrix,
286            resources: self.resources,
287            content,
288            group: self.group,
289            reference: None,
290            metadata: None,
291        }
292    }
293}
294
295/// Template form XObject for common shapes
296pub struct FormTemplates;
297
298impl FormTemplates {
299    /// Create a checkmark form
300    pub fn checkmark(size: f64) -> FormXObject {
301        let bbox = Rectangle::from_position_and_size(0.0, 0.0, size, size);
302
303        FormXObjectBuilder::new(bbox)
304            .stroke_color(0.0, 0.5, 0.0)
305            .move_to(size * 0.2, size * 0.5)
306            .line_to(size * 0.4, size * 0.3)
307            .line_to(size * 0.8, size * 0.7)
308            .stroke()
309            .build()
310    }
311
312    /// Create a cross/X form
313    pub fn cross(size: f64) -> FormXObject {
314        let bbox = Rectangle::from_position_and_size(0.0, 0.0, size, size);
315
316        FormXObjectBuilder::new(bbox)
317            .stroke_color(0.8, 0.0, 0.0)
318            .move_to(size * 0.2, size * 0.2)
319            .line_to(size * 0.8, size * 0.8)
320            .move_to(size * 0.2, size * 0.8)
321            .line_to(size * 0.8, size * 0.2)
322            .stroke()
323            .build()
324    }
325
326    /// Create a circle form
327    pub fn circle(radius: f64, filled: bool) -> FormXObject {
328        let size = radius * 2.0;
329        let bbox = Rectangle::from_position_and_size(0.0, 0.0, size, size);
330
331        // Approximate circle with Bézier curves
332        let k = 0.5522847498; // Magic constant for circle approximation
333        let cp = radius * k; // Control point offset
334
335        let mut builder = FormXObjectBuilder::new(bbox);
336
337        if filled {
338            builder = builder.fill_color(0.0, 0.0, 1.0);
339        } else {
340            builder = builder.stroke_color(0.0, 0.0, 1.0);
341        }
342
343        // Move to right point
344        builder = builder
345            .move_to(size, radius)
346            .add_operation(format!(
347                "{} {} {} {} {} {} c", // Top right quadrant
348                size,
349                radius + cp,
350                radius + cp,
351                size,
352                radius,
353                size
354            ))
355            .add_operation(format!(
356                "{} {} {} {} {} {} c", // Top left quadrant
357                radius - cp,
358                size,
359                0.0,
360                radius + cp,
361                0.0,
362                radius
363            ))
364            .add_operation(format!(
365                "{} {} {} {} {} {} c", // Bottom left quadrant
366                0.0,
367                radius - cp,
368                radius - cp,
369                0.0,
370                radius,
371                0.0
372            ))
373            .add_operation(format!(
374                "{} {} {} {} {} {} c", // Bottom right quadrant
375                radius + cp,
376                0.0,
377                size,
378                radius - cp,
379                size,
380                radius
381            ));
382
383        if filled {
384            builder.fill()
385        } else {
386            builder.stroke()
387        }
388        .build()
389    }
390
391    /// Create a star form
392    pub fn star(size: f64, points: usize) -> FormXObject {
393        let bbox = Rectangle::from_position_and_size(0.0, 0.0, size, size);
394        let center = size / 2.0;
395        let outer_radius = size / 2.0 * 0.9;
396        let inner_radius = outer_radius * 0.4;
397
398        let mut builder = FormXObjectBuilder::new(bbox).fill_color(1.0, 0.8, 0.0);
399
400        let angle_step = std::f64::consts::PI * 2.0 / (points * 2) as f64;
401
402        for i in 0..(points * 2) {
403            let angle = i as f64 * angle_step - std::f64::consts::PI / 2.0;
404            let radius = if i % 2 == 0 {
405                outer_radius
406            } else {
407                inner_radius
408            };
409            let x = center + radius * angle.cos();
410            let y = center + radius * angle.sin();
411
412            if i == 0 {
413                builder = builder.move_to(x, y);
414            } else {
415                builder = builder.line_to(x, y);
416            }
417        }
418
419        builder.add_operation("h".to_string()).fill().build()
420    }
421
422    /// Create a logo placeholder form
423    pub fn logo_placeholder(width: f64, height: f64) -> FormXObject {
424        let bbox = Rectangle::from_position_and_size(0.0, 0.0, width, height);
425
426        FormXObjectBuilder::new(bbox)
427            .save_state()
428            // Border
429            .stroke_color(0.5, 0.5, 0.5)
430            .rectangle(1.0, 1.0, width - 2.0, height - 2.0)
431            .stroke()
432            // Diagonal lines
433            .move_to(0.0, 0.0)
434            .line_to(width, height)
435            .move_to(0.0, height)
436            .line_to(width, 0.0)
437            .stroke()
438            .restore_state()
439            .build()
440    }
441}
442
443/// Manager for form XObjects in a document
444#[derive(Debug, Clone)]
445pub struct FormXObjectManager {
446    forms: HashMap<String, FormXObject>,
447    next_id: usize,
448}
449
450impl Default for FormXObjectManager {
451    fn default() -> Self {
452        Self {
453            forms: HashMap::new(),
454            next_id: 1,
455        }
456    }
457}
458
459impl FormXObjectManager {
460    /// Create a new manager
461    pub fn new() -> Self {
462        Self::default()
463    }
464
465    /// Add a form XObject
466    pub fn add_form(&mut self, name: Option<String>, form: FormXObject) -> String {
467        let name = name.unwrap_or_else(|| {
468            let id = format!("Fm{}", self.next_id);
469            self.next_id += 1;
470            id
471        });
472
473        self.forms.insert(name.clone(), form);
474        name
475    }
476
477    /// Get a form XObject
478    pub fn get_form(&self, name: &str) -> Option<&FormXObject> {
479        self.forms.get(name)
480    }
481
482    /// Get all forms
483    pub fn get_all_forms(&self) -> &HashMap<String, FormXObject> {
484        &self.forms
485    }
486
487    /// Remove a form
488    pub fn remove_form(&mut self, name: &str) -> Option<FormXObject> {
489        self.forms.remove(name)
490    }
491
492    /// Clear all forms
493    pub fn clear(&mut self) {
494        self.forms.clear();
495        self.next_id = 1;
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502    use crate::geometry::Point;
503
504    #[test]
505    fn test_form_xobject_creation() {
506        let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
507        let form = FormXObject::new(bbox.clone());
508
509        assert_eq!(form.bbox, bbox);
510        assert!(form.matrix.is_none());
511        assert!(form.content.is_empty());
512    }
513
514    #[test]
515    fn test_form_xobject_with_matrix() {
516        let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(50.0, 50.0));
517        let matrix = [2.0, 0.0, 0.0, 2.0, 10.0, 10.0]; // Scale by 2, translate by (10, 10)
518
519        let form = FormXObject::new(bbox).with_matrix(matrix);
520
521        assert_eq!(form.matrix, Some(matrix));
522    }
523
524    #[test]
525    fn test_form_xobject_from_graphics_ops() {
526        let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
527        let ops = "0 0 100 100 re\nf";
528
529        let form = FormXObject::from_graphics_ops(bbox.clone(), ops);
530
531        assert_eq!(form.bbox, bbox);
532        assert_eq!(form.content, ops.as_bytes());
533    }
534
535    #[test]
536    fn test_form_xobject_to_stream() {
537        let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 100.0));
538        let form = FormXObject::new(bbox).with_content(b"q\n1 0 0 1 0 0 cm\nQ".to_vec());
539
540        let stream = form.to_stream();
541        assert!(stream.is_ok());
542
543        let stream = stream.unwrap();
544        let dict = stream.dictionary();
545
546        assert_eq!(dict.get("Type"), Some(&Object::Name("XObject".to_string())));
547        assert_eq!(dict.get("Subtype"), Some(&Object::Name("Form".to_string())));
548        assert!(dict.get("BBox").is_some());
549    }
550
551    #[test]
552    fn test_transparency_group() {
553        let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
554        let group = TransparencyGroup {
555            color_space: "DeviceCMYK".to_string(),
556            isolated: true,
557            knockout: false,
558        };
559
560        let form = FormXObject::new(bbox).with_transparency_group(group);
561
562        assert!(form.has_transparency());
563        assert_eq!(form.group.as_ref().unwrap().color_space, "DeviceCMYK");
564        assert!(form.group.as_ref().unwrap().isolated);
565    }
566
567    #[test]
568    fn test_form_builder_basic() {
569        let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
570
571        let form = FormXObjectBuilder::new(bbox)
572            .fill_color(1.0, 0.0, 0.0)
573            .rectangle(10.0, 10.0, 80.0, 80.0)
574            .fill()
575            .build();
576
577        let content = String::from_utf8(form.content).unwrap();
578        assert!(content.contains("1 0 0 rg"));
579        assert!(content.contains("10 10 80 80 re"));
580        assert!(content.contains("f"));
581    }
582
583    #[test]
584    fn test_form_builder_complex() {
585        let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 200.0));
586
587        let form = FormXObjectBuilder::new(bbox)
588            .save_state()
589            .stroke_color(0.0, 0.0, 1.0)
590            .move_to(50.0, 50.0)
591            .line_to(150.0, 150.0)
592            .stroke()
593            .restore_state()
594            .transparency_group(true, false)
595            .build();
596
597        let content = String::from_utf8(form.content.clone()).unwrap();
598        assert!(content.contains("q"));
599        assert!(content.contains("Q"));
600        assert!(content.contains("0 0 1 RG"));
601        assert!(form.has_transparency());
602    }
603
604    #[test]
605    fn test_form_templates_checkmark() {
606        let form = FormTemplates::checkmark(20.0);
607
608        assert_eq!(form.bbox.width(), 20.0);
609        assert_eq!(form.bbox.height(), 20.0);
610
611        let content = String::from_utf8(form.content).unwrap();
612        assert!(content.contains("0 0.5 0 RG")); // Green color
613    }
614
615    #[test]
616    fn test_form_templates_cross() {
617        let form = FormTemplates::cross(30.0);
618
619        assert_eq!(form.bbox.width(), 30.0);
620
621        let content = String::from_utf8(form.content).unwrap();
622        assert!(content.contains("0.8 0 0 RG")); // Red color
623    }
624
625    #[test]
626    fn test_form_templates_circle() {
627        let filled_circle = FormTemplates::circle(25.0, true);
628        let stroked_circle = FormTemplates::circle(25.0, false);
629
630        assert_eq!(filled_circle.bbox.width(), 50.0);
631        assert_eq!(stroked_circle.bbox.width(), 50.0);
632
633        let filled_content = String::from_utf8(filled_circle.content).unwrap();
634        let stroked_content = String::from_utf8(stroked_circle.content).unwrap();
635
636        assert!(filled_content.contains("f")); // Fill
637        assert!(stroked_content.contains("S")); // Stroke
638    }
639
640    #[test]
641    fn test_form_templates_star() {
642        let star = FormTemplates::star(100.0, 5);
643
644        assert_eq!(star.bbox.width(), 100.0);
645
646        let content = String::from_utf8(star.content).unwrap();
647        assert!(content.contains("1 0.8 0 rg")); // Gold color
648    }
649
650    #[test]
651    fn test_form_xobject_manager() {
652        let mut manager = FormXObjectManager::new();
653
654        let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(50.0, 50.0));
655        let form1 = FormXObject::new(bbox.clone());
656        let form2 = FormXObject::new(bbox);
657
658        let name1 = manager.add_form(Some("custom".to_string()), form1);
659        let name2 = manager.add_form(None, form2);
660
661        assert_eq!(name1, "custom");
662        assert!(name2.starts_with("Fm"));
663
664        assert!(manager.get_form("custom").is_some());
665        assert!(manager.get_form(&name2).is_some());
666        assert!(manager.get_form("nonexistent").is_none());
667    }
668
669    #[test]
670    fn test_form_xobject_manager_operations() {
671        let mut manager = FormXObjectManager::new();
672
673        let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
674        let form = FormXObject::new(bbox);
675
676        manager.add_form(Some("test".to_string()), form.clone());
677        assert_eq!(manager.get_all_forms().len(), 1);
678
679        let removed = manager.remove_form("test");
680        assert!(removed.is_some());
681        assert_eq!(manager.get_all_forms().len(), 0);
682
683        manager.add_form(None, form);
684        manager.clear();
685        assert_eq!(manager.get_all_forms().len(), 0);
686    }
687}