1use crate::error::Result;
8use crate::geometry::Rectangle;
9use crate::objects::{Dictionary, Object, ObjectId, Stream};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
14pub struct FormXObject {
15 pub bbox: Rectangle,
17 pub matrix: Option<[f64; 6]>,
19 pub resources: Dictionary,
21 pub content: Vec<u8>,
23 pub group: Option<TransparencyGroup>,
25 pub reference: Option<ObjectId>,
27 pub metadata: Option<Dictionary>,
29}
30
31#[derive(Debug, Clone)]
33pub struct TransparencyGroup {
34 pub color_space: String,
36 pub isolated: bool,
38 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 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 pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
68 self.matrix = Some(matrix);
69 self
70 }
71
72 pub fn with_resources(mut self, resources: Dictionary) -> Self {
74 self.resources = resources;
75 self
76 }
77
78 pub fn with_content(mut self, content: Vec<u8>) -> Self {
80 self.content = content;
81 self
82 }
83
84 pub fn with_transparency_group(mut self, group: TransparencyGroup) -> Self {
86 self.group = Some(group);
87 self
88 }
89
90 pub fn with_metadata(mut self, metadata: Dictionary) -> Self {
92 self.metadata = Some(metadata);
93 self
94 }
95
96 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 pub fn to_stream(&self) -> Result<Stream> {
111 let mut dict = Dictionary::new();
112
113 dict.set("Type", Object::Name("XObject".to_string()));
115 dict.set("Subtype", Object::Name("Form".to_string()));
116
117 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 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 dict.set("Resources", Object::Dictionary(self.resources.clone()));
138
139 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 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 pub fn get_bbox(&self) -> &Rectangle {
166 &self.bbox
167 }
168
169 pub fn has_transparency(&self) -> bool {
171 self.group.is_some()
172 }
173}
174
175pub 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 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 pub fn matrix(mut self, matrix: [f64; 6]) -> Self {
198 self.matrix = Some(matrix);
199 self
200 }
201
202 pub fn add_operation(mut self, op: impl Into<String>) -> Self {
204 self.operations.push(op.into());
205 self
206 }
207
208 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 pub fn move_to(mut self, x: f64, y: f64) -> Self {
217 self.operations.push(format!("{} {} m", x, y));
218 self
219 }
220
221 pub fn line_to(mut self, x: f64, y: f64) -> Self {
223 self.operations.push(format!("{} {} l", x, y));
224 self
225 }
226
227 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 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 pub fn fill(mut self) -> Self {
241 self.operations.push("f".to_string());
242 self
243 }
244
245 pub fn stroke(mut self) -> Self {
247 self.operations.push("S".to_string());
248 self
249 }
250
251 pub fn fill_stroke(mut self) -> Self {
253 self.operations.push("B".to_string());
254 self
255 }
256
257 pub fn save_state(mut self) -> Self {
259 self.operations.push("q".to_string());
260 self
261 }
262
263 pub fn restore_state(mut self) -> Self {
265 self.operations.push("Q".to_string());
266 self
267 }
268
269 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 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
295pub struct FormTemplates;
297
298impl FormTemplates {
299 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 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 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 let k = 0.5522847498; let cp = radius * k; 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 builder = builder
345 .move_to(size, radius)
346 .add_operation(format!(
347 "{} {} {} {} {} {} c", size,
349 radius + cp,
350 radius + cp,
351 size,
352 radius,
353 size
354 ))
355 .add_operation(format!(
356 "{} {} {} {} {} {} c", radius - cp,
358 size,
359 0.0,
360 radius + cp,
361 0.0,
362 radius
363 ))
364 .add_operation(format!(
365 "{} {} {} {} {} {} c", 0.0,
367 radius - cp,
368 radius - cp,
369 0.0,
370 radius,
371 0.0
372 ))
373 .add_operation(format!(
374 "{} {} {} {} {} {} c", 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 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 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 .stroke_color(0.5, 0.5, 0.5)
430 .rectangle(1.0, 1.0, width - 2.0, height - 2.0)
431 .stroke()
432 .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#[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 pub fn new() -> Self {
462 Self::default()
463 }
464
465 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 pub fn get_form(&self, name: &str) -> Option<&FormXObject> {
479 self.forms.get(name)
480 }
481
482 pub fn get_all_forms(&self) -> &HashMap<String, FormXObject> {
484 &self.forms
485 }
486
487 pub fn remove_form(&mut self, name: &str) -> Option<FormXObject> {
489 self.forms.remove(name)
490 }
491
492 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]; 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")); }
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")); }
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")); assert!(stroked_content.contains("S")); }
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")); }
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}