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 {
231 self.operations.push(crate::graphics::color::fill_color_op(
232 crate::graphics::Color::Rgb(r, g, b),
233 ));
234 self
235 }
236
237 pub fn stroke_color(mut self, r: f64, g: f64, b: f64) -> Self {
239 self.operations
240 .push(crate::graphics::color::stroke_color_op(
241 crate::graphics::Color::Rgb(r, g, b),
242 ));
243 self
244 }
245
246 pub fn fill(mut self) -> Self {
248 self.operations.push("f".to_string());
249 self
250 }
251
252 pub fn stroke(mut self) -> Self {
254 self.operations.push("S".to_string());
255 self
256 }
257
258 pub fn fill_stroke(mut self) -> Self {
260 self.operations.push("B".to_string());
261 self
262 }
263
264 pub fn save_state(mut self) -> Self {
266 self.operations.push("q".to_string());
267 self
268 }
269
270 pub fn restore_state(mut self) -> Self {
272 self.operations.push("Q".to_string());
273 self
274 }
275
276 pub fn transparency_group(mut self, isolated: bool, knockout: bool) -> Self {
278 self.group = Some(TransparencyGroup {
279 color_space: "DeviceRGB".to_string(),
280 isolated,
281 knockout,
282 });
283 self
284 }
285
286 pub fn build(self) -> FormXObject {
288 let content = self.operations.join("\n").into_bytes();
289
290 FormXObject {
291 bbox: self.bbox,
292 matrix: self.matrix,
293 resources: self.resources,
294 content,
295 group: self.group,
296 reference: None,
297 metadata: None,
298 }
299 }
300}
301
302pub struct FormTemplates;
304
305impl FormTemplates {
306 pub fn checkmark(size: f64) -> FormXObject {
308 let bbox = Rectangle::from_position_and_size(0.0, 0.0, size, size);
309
310 FormXObjectBuilder::new(bbox)
311 .stroke_color(0.0, 0.5, 0.0)
312 .move_to(size * 0.2, size * 0.5)
313 .line_to(size * 0.4, size * 0.3)
314 .line_to(size * 0.8, size * 0.7)
315 .stroke()
316 .build()
317 }
318
319 pub fn cross(size: f64) -> FormXObject {
321 let bbox = Rectangle::from_position_and_size(0.0, 0.0, size, size);
322
323 FormXObjectBuilder::new(bbox)
324 .stroke_color(0.8, 0.0, 0.0)
325 .move_to(size * 0.2, size * 0.2)
326 .line_to(size * 0.8, size * 0.8)
327 .move_to(size * 0.2, size * 0.8)
328 .line_to(size * 0.8, size * 0.2)
329 .stroke()
330 .build()
331 }
332
333 pub fn circle(radius: f64, filled: bool) -> FormXObject {
335 let size = radius * 2.0;
336 let bbox = Rectangle::from_position_and_size(0.0, 0.0, size, size);
337
338 let k = 0.5522847498; let cp = radius * k; let mut builder = FormXObjectBuilder::new(bbox);
343
344 if filled {
345 builder = builder.fill_color(0.0, 0.0, 1.0);
346 } else {
347 builder = builder.stroke_color(0.0, 0.0, 1.0);
348 }
349
350 builder = builder
352 .move_to(size, radius)
353 .add_operation(format!(
354 "{} {} {} {} {} {} c", size,
356 radius + cp,
357 radius + cp,
358 size,
359 radius,
360 size
361 ))
362 .add_operation(format!(
363 "{} {} {} {} {} {} c", radius - cp,
365 size,
366 0.0,
367 radius + cp,
368 0.0,
369 radius
370 ))
371 .add_operation(format!(
372 "{} {} {} {} {} {} c", 0.0,
374 radius - cp,
375 radius - cp,
376 0.0,
377 radius,
378 0.0
379 ))
380 .add_operation(format!(
381 "{} {} {} {} {} {} c", radius + cp,
383 0.0,
384 size,
385 radius - cp,
386 size,
387 radius
388 ));
389
390 if filled {
391 builder.fill()
392 } else {
393 builder.stroke()
394 }
395 .build()
396 }
397
398 pub fn star(size: f64, points: usize) -> FormXObject {
400 let bbox = Rectangle::from_position_and_size(0.0, 0.0, size, size);
401 let center = size / 2.0;
402 let outer_radius = size / 2.0 * 0.9;
403 let inner_radius = outer_radius * 0.4;
404
405 let mut builder = FormXObjectBuilder::new(bbox).fill_color(1.0, 0.8, 0.0);
406
407 let angle_step = std::f64::consts::PI * 2.0 / (points * 2) as f64;
408
409 for i in 0..(points * 2) {
410 let angle = i as f64 * angle_step - std::f64::consts::PI / 2.0;
411 let radius = if i % 2 == 0 {
412 outer_radius
413 } else {
414 inner_radius
415 };
416 let x = center + radius * angle.cos();
417 let y = center + radius * angle.sin();
418
419 if i == 0 {
420 builder = builder.move_to(x, y);
421 } else {
422 builder = builder.line_to(x, y);
423 }
424 }
425
426 builder.add_operation("h".to_string()).fill().build()
427 }
428
429 pub fn logo_placeholder(width: f64, height: f64) -> FormXObject {
431 let bbox = Rectangle::from_position_and_size(0.0, 0.0, width, height);
432
433 FormXObjectBuilder::new(bbox)
434 .save_state()
435 .stroke_color(0.5, 0.5, 0.5)
437 .rectangle(1.0, 1.0, width - 2.0, height - 2.0)
438 .stroke()
439 .move_to(0.0, 0.0)
441 .line_to(width, height)
442 .move_to(0.0, height)
443 .line_to(width, 0.0)
444 .stroke()
445 .restore_state()
446 .build()
447 }
448}
449
450#[derive(Debug, Clone)]
452pub struct FormXObjectManager {
453 forms: HashMap<String, FormXObject>,
454 next_id: usize,
455}
456
457impl Default for FormXObjectManager {
458 fn default() -> Self {
459 Self {
460 forms: HashMap::new(),
461 next_id: 1,
462 }
463 }
464}
465
466impl FormXObjectManager {
467 pub fn new() -> Self {
469 Self::default()
470 }
471
472 pub fn add_form(&mut self, name: Option<String>, form: FormXObject) -> String {
474 let name = name.unwrap_or_else(|| {
475 let id = format!("Fm{}", self.next_id);
476 self.next_id += 1;
477 id
478 });
479
480 self.forms.insert(name.clone(), form);
481 name
482 }
483
484 pub fn get_form(&self, name: &str) -> Option<&FormXObject> {
486 self.forms.get(name)
487 }
488
489 pub fn get_all_forms(&self) -> &HashMap<String, FormXObject> {
491 &self.forms
492 }
493
494 pub fn remove_form(&mut self, name: &str) -> Option<FormXObject> {
496 self.forms.remove(name)
497 }
498
499 pub fn clear(&mut self) {
501 self.forms.clear();
502 self.next_id = 1;
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use crate::geometry::Point;
510
511 #[test]
512 fn test_form_xobject_creation() {
513 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
514 let form = FormXObject::new(bbox.clone());
515
516 assert_eq!(form.bbox, bbox);
517 assert!(form.matrix.is_none());
518 assert!(form.content.is_empty());
519 }
520
521 #[test]
522 fn test_form_xobject_with_matrix() {
523 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(50.0, 50.0));
524 let matrix = [2.0, 0.0, 0.0, 2.0, 10.0, 10.0]; let form = FormXObject::new(bbox).with_matrix(matrix);
527
528 assert_eq!(form.matrix, Some(matrix));
529 }
530
531 #[test]
532 fn test_form_xobject_from_graphics_ops() {
533 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
534 let ops = "0 0 100 100 re\nf";
535
536 let form = FormXObject::from_graphics_ops(bbox.clone(), ops);
537
538 assert_eq!(form.bbox, bbox);
539 assert_eq!(form.content, ops.as_bytes());
540 }
541
542 #[test]
543 fn test_form_xobject_to_stream() {
544 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 100.0));
545 let form = FormXObject::new(bbox).with_content(b"q\n1 0 0 1 0 0 cm\nQ".to_vec());
546
547 let stream = form.to_stream();
548 assert!(stream.is_ok());
549
550 let stream = stream.unwrap();
551 let dict = stream.dictionary();
552
553 assert_eq!(dict.get("Type"), Some(&Object::Name("XObject".to_string())));
554 assert_eq!(dict.get("Subtype"), Some(&Object::Name("Form".to_string())));
555 assert!(dict.get("BBox").is_some());
556 }
557
558 #[test]
559 fn test_transparency_group() {
560 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
561 let group = TransparencyGroup {
562 color_space: "DeviceCMYK".to_string(),
563 isolated: true,
564 knockout: false,
565 };
566
567 let form = FormXObject::new(bbox).with_transparency_group(group);
568
569 assert!(form.has_transparency());
570 assert_eq!(form.group.as_ref().unwrap().color_space, "DeviceCMYK");
571 assert!(form.group.as_ref().unwrap().isolated);
572 }
573
574 #[test]
575 fn test_form_builder_basic() {
576 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
577
578 let form = FormXObjectBuilder::new(bbox)
579 .fill_color(1.0, 0.0, 0.0)
580 .rectangle(10.0, 10.0, 80.0, 80.0)
581 .fill()
582 .build();
583
584 let content = String::from_utf8(form.content).unwrap();
585 assert!(content.contains("1.000 0.000 0.000 rg"));
588 assert!(content.contains("10 10 80 80 re"));
589 assert!(content.contains("f"));
590 }
591
592 #[test]
593 fn test_form_builder_complex() {
594 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 200.0));
595
596 let form = FormXObjectBuilder::new(bbox)
597 .save_state()
598 .stroke_color(0.0, 0.0, 1.0)
599 .move_to(50.0, 50.0)
600 .line_to(150.0, 150.0)
601 .stroke()
602 .restore_state()
603 .transparency_group(true, false)
604 .build();
605
606 let content = String::from_utf8(form.content.clone()).unwrap();
607 assert!(content.contains("q"));
608 assert!(content.contains("Q"));
609 assert!(content.contains("0.000 0.000 1.000 RG"));
610 assert!(form.has_transparency());
611 }
612
613 #[test]
614 fn test_form_templates_checkmark() {
615 let form = FormTemplates::checkmark(20.0);
616
617 assert_eq!(form.bbox.width(), 20.0);
618 assert_eq!(form.bbox.height(), 20.0);
619
620 let content = String::from_utf8(form.content).unwrap();
621 assert!(content.contains("0.000 0.500 0.000 RG")); }
623
624 #[test]
625 fn test_form_templates_cross() {
626 let form = FormTemplates::cross(30.0);
627
628 assert_eq!(form.bbox.width(), 30.0);
629
630 let content = String::from_utf8(form.content).unwrap();
631 assert!(content.contains("0.800 0.000 0.000 RG")); }
633
634 #[test]
635 fn test_form_templates_circle() {
636 let filled_circle = FormTemplates::circle(25.0, true);
637 let stroked_circle = FormTemplates::circle(25.0, false);
638
639 assert_eq!(filled_circle.bbox.width(), 50.0);
640 assert_eq!(stroked_circle.bbox.width(), 50.0);
641
642 let filled_content = String::from_utf8(filled_circle.content).unwrap();
643 let stroked_content = String::from_utf8(stroked_circle.content).unwrap();
644
645 assert!(filled_content.contains("f")); assert!(stroked_content.contains("S")); }
648
649 #[test]
650 fn test_form_templates_star() {
651 let star = FormTemplates::star(100.0, 5);
652
653 assert_eq!(star.bbox.width(), 100.0);
654
655 let content = String::from_utf8(star.content).unwrap();
656 assert!(content.contains("1.000 0.800 0.000 rg")); }
658
659 #[test]
660 fn test_form_xobject_manager() {
661 let mut manager = FormXObjectManager::new();
662
663 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(50.0, 50.0));
664 let form1 = FormXObject::new(bbox.clone());
665 let form2 = FormXObject::new(bbox);
666
667 let name1 = manager.add_form(Some("custom".to_string()), form1);
668 let name2 = manager.add_form(None, form2);
669
670 assert_eq!(name1, "custom");
671 assert!(name2.starts_with("Fm"));
672
673 assert!(manager.get_form("custom").is_some());
674 assert!(manager.get_form(&name2).is_some());
675 assert!(manager.get_form("nonexistent").is_none());
676 }
677
678 #[test]
679 fn test_form_xobject_manager_operations() {
680 let mut manager = FormXObjectManager::new();
681
682 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
683 let form = FormXObject::new(bbox);
684
685 manager.add_form(Some("test".to_string()), form.clone());
686 assert_eq!(manager.get_all_forms().len(), 1);
687
688 let removed = manager.remove_form("test");
689 assert!(removed.is_some());
690 assert_eq!(manager.get_all_forms().len(), 0);
691
692 manager.add_form(None, form);
693 manager.clear();
694 assert_eq!(manager.get_all_forms().len(), 0);
695 }
696}