1use crate::annotations::Annotation;
4use crate::geometry::{Point, Rectangle};
5use crate::graphics::Color;
6use crate::objects::Object;
7use crate::text::Font;
8
9#[derive(Debug, Clone)]
11pub struct FreeTextAnnotation {
12 pub annotation: Annotation,
14 pub default_appearance: String,
16 pub quadding: i32,
18 pub rich_text: Option<String>,
20 pub default_style: Option<String>,
22}
23
24impl FreeTextAnnotation {
25 pub fn new(rect: Rectangle, text: impl Into<String>) -> Self {
27 let mut annotation = Annotation::new(crate::annotations::AnnotationType::FreeText, rect);
28 annotation.contents = Some(text.into());
29
30 Self {
31 annotation,
32 default_appearance: "/Helv 12 Tf 0 g".to_string(),
33 quadding: 0,
34 rich_text: None,
35 default_style: None,
36 }
37 }
38
39 pub fn with_font(mut self, font: Font, size: f64, color: Color) -> Self {
41 let color_str = crate::graphics::color::fill_color_op(color);
45
46 self.default_appearance = format!("/{} {size} Tf {color_str}", font.pdf_name());
47 self
48 }
49
50 pub fn with_justification(mut self, quadding: i32) -> Self {
52 self.quadding = quadding.clamp(0, 2);
53 self
54 }
55
56 pub fn to_annotation(self) -> Annotation {
58 let mut annotation = self.annotation;
59
60 annotation
61 .properties
62 .set("DA", Object::String(self.default_appearance));
63 annotation
64 .properties
65 .set("Q", Object::Integer(self.quadding as i64));
66
67 if let Some(rich_text) = self.rich_text {
68 annotation.properties.set("RC", Object::String(rich_text));
69 }
70
71 if let Some(style) = self.default_style {
72 annotation.properties.set("DS", Object::String(style));
73 }
74
75 annotation
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct LineAnnotation {
82 pub annotation: Annotation,
84 pub start: Point,
86 pub end: Point,
88 pub start_style: LineEndingStyle,
90 pub end_style: LineEndingStyle,
92 pub interior_color: Option<Color>,
94}
95
96#[derive(Debug, Clone, Copy)]
98pub enum LineEndingStyle {
99 None,
101 Square,
103 Circle,
105 Diamond,
107 OpenArrow,
109 ClosedArrow,
111 Butt,
113 ROpenArrow,
115 RClosedArrow,
117 Slash,
119}
120
121impl LineEndingStyle {
122 pub fn pdf_name(&self) -> &'static str {
124 match self {
125 LineEndingStyle::None => "None",
126 LineEndingStyle::Square => "Square",
127 LineEndingStyle::Circle => "Circle",
128 LineEndingStyle::Diamond => "Diamond",
129 LineEndingStyle::OpenArrow => "OpenArrow",
130 LineEndingStyle::ClosedArrow => "ClosedArrow",
131 LineEndingStyle::Butt => "Butt",
132 LineEndingStyle::ROpenArrow => "ROpenArrow",
133 LineEndingStyle::RClosedArrow => "RClosedArrow",
134 LineEndingStyle::Slash => "Slash",
135 }
136 }
137}
138
139impl LineAnnotation {
140 pub fn new(start: Point, end: Point) -> Self {
142 let rect = Rectangle::new(
143 Point::new(start.x.min(end.x), start.y.min(end.y)),
144 Point::new(start.x.max(end.x), start.y.max(end.y)),
145 );
146
147 let annotation = Annotation::new(crate::annotations::AnnotationType::Line, rect);
148
149 Self {
150 annotation,
151 start,
152 end,
153 start_style: LineEndingStyle::None,
154 end_style: LineEndingStyle::None,
155 interior_color: None,
156 }
157 }
158
159 pub fn with_endings(mut self, start: LineEndingStyle, end: LineEndingStyle) -> Self {
161 self.start_style = start;
162 self.end_style = end;
163 self
164 }
165
166 pub fn with_interior_color(mut self, color: Color) -> Self {
168 self.interior_color = Some(color);
169 self
170 }
171
172 pub fn to_annotation(self) -> Annotation {
174 let mut annotation = self.annotation;
175
176 annotation.properties.set(
178 "L",
179 Object::Array(vec![
180 Object::Real(self.start.x),
181 Object::Real(self.start.y),
182 Object::Real(self.end.x),
183 Object::Real(self.end.y),
184 ]),
185 );
186
187 annotation.properties.set(
189 "LE",
190 Object::Array(vec![
191 Object::Name(self.start_style.pdf_name().to_string()),
192 Object::Name(self.end_style.pdf_name().to_string()),
193 ]),
194 );
195
196 if let Some(color) = self.interior_color {
198 let ic = match color {
199 Color::Rgb(r, g, b) => vec![Object::Real(r), Object::Real(g), Object::Real(b)],
200 Color::Gray(g) => vec![Object::Real(g)],
201 Color::Cmyk(c, m, y, k) => vec![
202 Object::Real(c),
203 Object::Real(m),
204 Object::Real(y),
205 Object::Real(k),
206 ],
207 };
208 annotation.properties.set("IC", Object::Array(ic));
209 }
210
211 annotation
212 }
213}
214
215#[derive(Debug, Clone)]
217pub struct SquareAnnotation {
218 pub annotation: Annotation,
220 pub interior_color: Option<Color>,
222 pub border_effect: Option<BorderEffect>,
224}
225
226#[derive(Debug, Clone)]
228pub struct BorderEffect {
229 pub style: BorderEffectStyle,
231 pub intensity: f64,
233}
234
235#[derive(Debug, Clone, Copy)]
236pub enum BorderEffectStyle {
237 Solid,
239 Cloudy,
241}
242
243impl SquareAnnotation {
244 pub fn new(rect: Rectangle) -> Self {
246 let annotation = Annotation::new(crate::annotations::AnnotationType::Square, rect);
247
248 Self {
249 annotation,
250 interior_color: None,
251 border_effect: None,
252 }
253 }
254
255 pub fn with_interior_color(mut self, color: Color) -> Self {
257 self.interior_color = Some(color);
258 self
259 }
260
261 pub fn with_cloudy_border(mut self, intensity: f64) -> Self {
263 self.border_effect = Some(BorderEffect {
264 style: BorderEffectStyle::Cloudy,
265 intensity: intensity.clamp(0.0, 2.0),
266 });
267 self
268 }
269
270 pub fn to_annotation(self) -> Annotation {
272 let mut annotation = self.annotation;
273
274 if let Some(color) = self.interior_color {
276 let ic = match color {
277 Color::Rgb(r, g, b) => vec![Object::Real(r), Object::Real(g), Object::Real(b)],
278 Color::Gray(g) => vec![Object::Real(g)],
279 Color::Cmyk(c, m, y, k) => vec![
280 Object::Real(c),
281 Object::Real(m),
282 Object::Real(y),
283 Object::Real(k),
284 ],
285 };
286 annotation.properties.set("IC", Object::Array(ic));
287 }
288
289 if let Some(effect) = self.border_effect {
291 let mut be_dict = crate::objects::Dictionary::new();
292 match effect.style {
293 BorderEffectStyle::Solid => be_dict.set("S", Object::Name("S".to_string())),
294 BorderEffectStyle::Cloudy => {
295 be_dict.set("S", Object::Name("C".to_string()));
296 be_dict.set("I", Object::Real(effect.intensity));
297 }
298 }
299 annotation.properties.set("BE", Object::Dictionary(be_dict));
300 }
301
302 annotation
303 }
304}
305
306#[derive(Debug, Clone)]
308pub struct StampAnnotation {
309 pub annotation: Annotation,
311 pub stamp_name: StampName,
313}
314
315#[derive(Debug, Clone)]
317pub enum StampName {
318 Approved,
320 Experimental,
322 NotApproved,
324 AsIs,
326 Expired,
328 NotForPublicRelease,
330 Confidential,
332 Final,
334 Sold,
336 Departmental,
338 ForComment,
340 TopSecret,
342 Draft,
344 ForPublicRelease,
346 Custom(String),
348}
349
350impl StampName {
351 pub fn pdf_name(&self) -> String {
353 match self {
354 StampName::Approved => "Approved".to_string(),
355 StampName::Experimental => "Experimental".to_string(),
356 StampName::NotApproved => "NotApproved".to_string(),
357 StampName::AsIs => "AsIs".to_string(),
358 StampName::Expired => "Expired".to_string(),
359 StampName::NotForPublicRelease => "NotForPublicRelease".to_string(),
360 StampName::Confidential => "Confidential".to_string(),
361 StampName::Final => "Final".to_string(),
362 StampName::Sold => "Sold".to_string(),
363 StampName::Departmental => "Departmental".to_string(),
364 StampName::ForComment => "ForComment".to_string(),
365 StampName::TopSecret => "TopSecret".to_string(),
366 StampName::Draft => "Draft".to_string(),
367 StampName::ForPublicRelease => "ForPublicRelease".to_string(),
368 StampName::Custom(name) => name.clone(),
369 }
370 }
371}
372
373impl StampAnnotation {
374 pub fn new(rect: Rectangle, stamp_name: StampName) -> Self {
376 let annotation = Annotation::new(crate::annotations::AnnotationType::Stamp, rect);
377
378 Self {
379 annotation,
380 stamp_name,
381 }
382 }
383
384 pub fn to_annotation(self) -> Annotation {
386 let mut annotation = self.annotation;
387 annotation
388 .properties
389 .set("Name", Object::Name(self.stamp_name.pdf_name()));
390 annotation
391 }
392}
393
394#[derive(Debug, Clone)]
396pub struct InkAnnotation {
397 pub annotation: Annotation,
399 pub ink_lists: Vec<Vec<Point>>,
401}
402
403impl Default for InkAnnotation {
404 fn default() -> Self {
405 Self::new()
406 }
407}
408
409impl InkAnnotation {
410 pub fn new() -> Self {
412 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(0.0, 0.0));
414 let annotation = Annotation::new(crate::annotations::AnnotationType::Ink, rect);
415
416 Self {
417 annotation,
418 ink_lists: Vec::new(),
419 }
420 }
421
422 pub fn add_stroke(mut self, points: Vec<Point>) -> Self {
424 self.ink_lists.push(points);
425 self
426 }
427
428 pub fn to_annotation(mut self) -> Annotation {
430 if !self.ink_lists.is_empty() {
432 let mut min_x = f64::MAX;
433 let mut min_y = f64::MAX;
434 let mut max_x = f64::MIN;
435 let mut max_y = f64::MIN;
436
437 for list in &self.ink_lists {
438 for point in list {
439 min_x = min_x.min(point.x);
440 min_y = min_y.min(point.y);
441 max_x = max_x.max(point.x);
442 max_y = max_y.max(point.y);
443 }
444 }
445
446 self.annotation.rect =
447 Rectangle::new(Point::new(min_x, min_y), Point::new(max_x, max_y));
448 }
449
450 let ink_array: Vec<Object> = self
452 .ink_lists
453 .into_iter()
454 .map(|list| {
455 let points: Vec<Object> = list
456 .into_iter()
457 .flat_map(|p| vec![Object::Real(p.x), Object::Real(p.y)])
458 .collect();
459 Object::Array(points)
460 })
461 .collect();
462
463 self.annotation
464 .properties
465 .set("InkList", Object::Array(ink_array));
466 self.annotation
467 }
468}
469
470#[derive(Debug, Clone)]
472pub struct HighlightAnnotation {
473 pub annotation: Annotation,
475 pub quad_points: crate::annotations::QuadPoints,
477}
478
479impl HighlightAnnotation {
480 pub fn new(rect: Rectangle) -> Self {
482 let annotation = Annotation::new(crate::annotations::AnnotationType::Highlight, rect);
483 let quad_points = crate::annotations::QuadPoints::from_rect(&rect);
484
485 Self {
486 annotation,
487 quad_points,
488 }
489 }
490
491 pub fn to_annotation(self) -> Annotation {
493 let mut annotation = self.annotation;
494 annotation
495 .properties
496 .set("QuadPoints", self.quad_points.to_array());
497 annotation
498 }
499}
500
501#[derive(Debug, Clone)]
503pub struct CircleAnnotation {
504 pub annotation: Annotation,
506 pub interior_color: Option<Color>,
508 pub border_effect: Option<BorderEffect>,
510}
511
512impl CircleAnnotation {
513 pub fn new(rect: Rectangle) -> Self {
515 let annotation = Annotation::new(crate::annotations::AnnotationType::Circle, rect);
516
517 Self {
518 annotation,
519 interior_color: None,
520 border_effect: None,
521 }
522 }
523
524 pub fn with_interior_color(mut self, color: Color) -> Self {
526 self.interior_color = Some(color);
527 self
528 }
529
530 pub fn with_cloudy_border(mut self, intensity: f64) -> Self {
532 self.border_effect = Some(BorderEffect {
533 style: BorderEffectStyle::Cloudy,
534 intensity: intensity.clamp(0.0, 2.0),
535 });
536 self
537 }
538
539 pub fn to_annotation(self) -> Annotation {
541 let mut annotation = self.annotation;
542
543 if let Some(color) = self.interior_color {
545 let ic = match color {
546 Color::Rgb(r, g, b) => vec![Object::Real(r), Object::Real(g), Object::Real(b)],
547 Color::Gray(g) => vec![Object::Real(g)],
548 Color::Cmyk(c, m, y, k) => vec![
549 Object::Real(c),
550 Object::Real(m),
551 Object::Real(y),
552 Object::Real(k),
553 ],
554 };
555 annotation.properties.set("IC", Object::Array(ic));
556 }
557
558 if let Some(effect) = self.border_effect {
560 let mut be_dict = crate::objects::Dictionary::new();
561 match effect.style {
562 BorderEffectStyle::Solid => be_dict.set("S", Object::Name("S".to_string())),
563 BorderEffectStyle::Cloudy => {
564 be_dict.set("S", Object::Name("C".to_string()));
565 be_dict.set("I", Object::Real(effect.intensity));
566 }
567 }
568 annotation.properties.set("BE", Object::Dictionary(be_dict));
569 }
570
571 annotation
572 }
573}
574
575#[derive(Debug, Clone)]
577pub struct FileAttachmentAnnotation {
578 pub annotation: Annotation,
580 pub file_name: String,
582 pub file_data: Vec<u8>,
584 pub mime_type: Option<String>,
586 pub icon: FileAttachmentIcon,
588}
589
590#[derive(Debug, Clone)]
592pub enum FileAttachmentIcon {
593 Graph,
595 Paperclip,
597 PushPin,
599 Tag,
601}
602
603impl FileAttachmentIcon {
604 pub fn pdf_name(&self) -> &'static str {
606 match self {
607 FileAttachmentIcon::Graph => "Graph",
608 FileAttachmentIcon::Paperclip => "Paperclip",
609 FileAttachmentIcon::PushPin => "PushPin",
610 FileAttachmentIcon::Tag => "Tag",
611 }
612 }
613}
614
615impl FileAttachmentAnnotation {
616 pub fn new(rect: Rectangle, file_name: String, file_data: Vec<u8>) -> Self {
618 let annotation = Annotation::new(crate::annotations::AnnotationType::FileAttachment, rect);
619
620 Self {
621 annotation,
622 file_name,
623 file_data,
624 mime_type: None,
625 icon: FileAttachmentIcon::Paperclip,
626 }
627 }
628
629 pub fn with_mime_type(mut self, mime_type: String) -> Self {
631 self.mime_type = Some(mime_type);
632 self
633 }
634
635 pub fn with_icon(mut self, icon: FileAttachmentIcon) -> Self {
637 self.icon = icon;
638 self
639 }
640
641 pub fn to_annotation(self) -> Annotation {
643 let mut annotation = self.annotation;
644
645 annotation
647 .properties
648 .set("Name", Object::Name(self.icon.pdf_name().to_string()));
649
650 let mut fs_dict = crate::objects::Dictionary::new();
652 fs_dict.set("Type", Object::Name("Filespec".to_string()));
653 fs_dict.set("F", Object::String(self.file_name.clone()));
654 fs_dict.set("UF", Object::String(self.file_name.clone()));
655
656 let mut ef_dict = crate::objects::Dictionary::new();
658 let mut stream_dict = crate::objects::Dictionary::new();
659 stream_dict.set("Type", Object::Name("EmbeddedFile".to_string()));
660 stream_dict.set("Length", Object::Integer(self.file_data.len() as i64));
661
662 if let Some(mime) = self.mime_type {
663 stream_dict.set("Subtype", Object::Name(mime));
664 }
665
666 ef_dict.set("F", Object::Dictionary(stream_dict));
669 fs_dict.set("EF", Object::Dictionary(ef_dict));
670
671 annotation.properties.set("FS", Object::Dictionary(fs_dict));
672
673 annotation
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use crate::geometry::Point;
681
682 #[test]
683 fn test_free_text_annotation() {
684 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 150.0));
685 let free_text = FreeTextAnnotation::new(rect, "Sample text")
686 .with_font(Font::Helvetica, 14.0, Color::black())
687 .with_justification(1);
688
689 assert_eq!(free_text.quadding, 1);
690 assert!(free_text.default_appearance.contains("/Helvetica 14"));
691 }
692
693 #[test]
694 fn test_line_annotation() {
695 let start = Point::new(100.0, 100.0);
696 let end = Point::new(200.0, 200.0);
697
698 let line = LineAnnotation::new(start, end)
699 .with_endings(LineEndingStyle::OpenArrow, LineEndingStyle::Circle);
700
701 assert!(matches!(line.start_style, LineEndingStyle::OpenArrow));
702 assert!(matches!(line.end_style, LineEndingStyle::Circle));
703 }
704
705 #[test]
706 fn test_stamp_names() {
707 assert_eq!(StampName::Approved.pdf_name(), "Approved");
708 assert_eq!(StampName::Draft.pdf_name(), "Draft");
709 assert_eq!(
710 StampName::Custom("MyStamp".to_string()).pdf_name(),
711 "MyStamp"
712 );
713 }
714
715 #[test]
716 fn test_ink_annotation() {
717 let mut ink = InkAnnotation::new();
718 ink = ink.add_stroke(vec![
719 Point::new(100.0, 100.0),
720 Point::new(110.0, 105.0),
721 Point::new(120.0, 110.0),
722 ]);
723
724 assert_eq!(ink.ink_lists.len(), 1);
725 assert_eq!(ink.ink_lists[0].len(), 3);
726 }
727
728 #[test]
729 fn test_free_text_annotation_justification() {
730 let rect = Rectangle::new(Point::new(100.0, 200.0), Point::new(400.0, 300.0));
731
732 for quadding in 0..=2 {
734 let free_text = FreeTextAnnotation::new(rect, "Test text").with_justification(quadding);
735
736 assert_eq!(free_text.quadding, quadding);
737
738 let annotation = free_text.to_annotation();
739 let dict = annotation.to_dict();
740
741 assert_eq!(dict.get("Q"), Some(&Object::Integer(quadding as i64)));
742 }
743
744 let clamped_low = FreeTextAnnotation::new(rect, "Test").with_justification(-1);
746 assert_eq!(clamped_low.quadding, 0);
747
748 let clamped_high = FreeTextAnnotation::new(rect, "Test").with_justification(5);
749 assert_eq!(clamped_high.quadding, 2);
750 }
751
752 #[test]
753 fn test_free_text_font_variations() {
754 let rect = Rectangle::new(Point::new(50.0, 50.0), Point::new(350.0, 150.0));
755
756 let fonts_and_sizes = [
757 (Font::Helvetica, 12.0),
758 (Font::TimesRoman, 10.0),
759 (Font::Courier, 14.0),
760 ];
761
762 let colors = [
763 Color::Gray(0.0),
764 Color::Rgb(1.0, 0.0, 0.0),
765 Color::Cmyk(0.0, 1.0, 1.0, 0.0),
766 ];
767
768 for ((font, size), color) in fonts_and_sizes.iter().zip(colors.iter()) {
769 let free_text =
770 FreeTextAnnotation::new(rect, "Test text").with_font(font.clone(), *size, *color);
771
772 let annotation = free_text.to_annotation();
773 let dict = annotation.to_dict();
774
775 if let Some(Object::String(da)) = dict.get("DA") {
776 assert!(da.contains(&font.pdf_name()));
777 assert!(da.contains(&format!("{size} Tf")));
778 } else {
779 panic!("DA field not found");
780 }
781 }
782 }
783
784 #[test]
785 fn test_free_text_rich_text() {
786 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 200.0));
787
788 let mut free_text = FreeTextAnnotation::new(rect, "Plain text content");
789 free_text.rich_text = Some("<p>Rich <b>text</b> content</p>".to_string());
790 free_text.default_style = Some("font-family: Arial; font-size: 12pt;".to_string());
791
792 let annotation = free_text.to_annotation();
793 let dict = annotation.to_dict();
794
795 assert_eq!(
796 dict.get("RC"),
797 Some(&Object::String(
798 "<p>Rich <b>text</b> content</p>".to_string()
799 ))
800 );
801 assert_eq!(
802 dict.get("DS"),
803 Some(&Object::String(
804 "font-family: Arial; font-size: 12pt;".to_string()
805 ))
806 );
807 }
808
809 #[test]
810 fn test_line_ending_styles_comprehensive() {
811 let styles = [
812 LineEndingStyle::None,
813 LineEndingStyle::Square,
814 LineEndingStyle::Circle,
815 LineEndingStyle::Diamond,
816 LineEndingStyle::OpenArrow,
817 LineEndingStyle::ClosedArrow,
818 LineEndingStyle::Butt,
819 LineEndingStyle::ROpenArrow,
820 LineEndingStyle::RClosedArrow,
821 LineEndingStyle::Slash,
822 ];
823
824 let expected_names = [
825 "None",
826 "Square",
827 "Circle",
828 "Diamond",
829 "OpenArrow",
830 "ClosedArrow",
831 "Butt",
832 "ROpenArrow",
833 "RClosedArrow",
834 "Slash",
835 ];
836
837 for (style, expected) in styles.iter().zip(expected_names.iter()) {
838 assert_eq!(style.pdf_name(), *expected);
839 }
840 }
841
842 #[test]
843 fn test_line_annotation_comprehensive() {
844 let start = Point::new(50.0, 100.0);
845 let end = Point::new(250.0, 300.0);
846
847 let line = LineAnnotation::new(start, end)
848 .with_endings(LineEndingStyle::Diamond, LineEndingStyle::OpenArrow)
849 .with_interior_color(Color::Rgb(0.5, 0.5, 1.0));
850
851 assert_eq!(line.annotation.rect.lower_left.x, 50.0);
853 assert_eq!(line.annotation.rect.lower_left.y, 100.0);
854 assert_eq!(line.annotation.rect.upper_right.x, 250.0);
855 assert_eq!(line.annotation.rect.upper_right.y, 300.0);
856
857 let annotation = line.to_annotation();
858 let dict = annotation.to_dict();
859
860 if let Some(Object::Array(coords)) = dict.get("L") {
862 assert_eq!(coords.len(), 4);
863 assert_eq!(coords[0], Object::Real(50.0));
864 assert_eq!(coords[1], Object::Real(100.0));
865 assert_eq!(coords[2], Object::Real(250.0));
866 assert_eq!(coords[3], Object::Real(300.0));
867 }
868
869 if let Some(Object::Array(endings)) = dict.get("LE") {
871 assert_eq!(endings[0], Object::Name("Diamond".to_string()));
872 assert_eq!(endings[1], Object::Name("OpenArrow".to_string()));
873 }
874
875 if let Some(Object::Array(color)) = dict.get("IC") {
877 assert_eq!(color.len(), 3);
878 assert_eq!(color[0], Object::Real(0.5));
879 assert_eq!(color[1], Object::Real(0.5));
880 assert_eq!(color[2], Object::Real(1.0));
881 }
882 }
883
884 #[test]
885 fn test_line_annotation_edge_cases() {
886 let point = Point::new(100.0, 100.0);
888 let zero_line = LineAnnotation::new(point, point);
889 assert_eq!(zero_line.annotation.rect.lower_left, point);
890 assert_eq!(zero_line.annotation.rect.upper_right, point);
891
892 let neg_start = Point::new(-100.0, -200.0);
894 let neg_end = Point::new(-50.0, -150.0);
895 let neg_line = LineAnnotation::new(neg_start, neg_end);
896 assert_eq!(neg_line.annotation.rect.lower_left.x, -100.0);
897 assert_eq!(neg_line.annotation.rect.lower_left.y, -200.0);
898
899 let reversed_line = LineAnnotation::new(Point::new(200.0, 300.0), Point::new(100.0, 200.0));
901 assert_eq!(reversed_line.annotation.rect.lower_left.x, 100.0);
902 assert_eq!(reversed_line.annotation.rect.lower_left.y, 200.0);
903 assert_eq!(reversed_line.annotation.rect.upper_right.x, 200.0);
904 assert_eq!(reversed_line.annotation.rect.upper_right.y, 300.0);
905 }
906
907 #[test]
908 fn test_square_annotation_border_effects() {
909 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 200.0));
910
911 let plain_square = SquareAnnotation::new(rect);
913 assert!(plain_square.border_effect.is_none());
914
915 let annotation = plain_square.to_annotation();
916 let dict = annotation.to_dict();
917 assert!(!dict.contains_key("BE"));
918
919 let cloudy_square = SquareAnnotation::new(rect).with_cloudy_border(1.5);
921
922 assert!(cloudy_square.border_effect.is_some());
923 if let Some(effect) = &cloudy_square.border_effect {
924 assert!(matches!(effect.style, BorderEffectStyle::Cloudy));
925 assert_eq!(effect.intensity, 1.5);
926 }
927
928 let annotation = cloudy_square.to_annotation();
929 let dict = annotation.to_dict();
930
931 if let Some(Object::Dictionary(be_dict)) = dict.get("BE") {
932 assert_eq!(be_dict.get("S"), Some(&Object::Name("C".to_string())));
933 assert_eq!(be_dict.get("I"), Some(&Object::Real(1.5)));
934 }
935 }
936
937 #[test]
938 fn test_square_annotation_interior_colors() {
939 let rect = Rectangle::new(Point::new(50.0, 50.0), Point::new(150.0, 150.0));
940
941 let colors = vec![
942 Color::Gray(0.75),
943 Color::Rgb(0.9, 0.9, 1.0),
944 Color::Cmyk(0.05, 0.05, 0.0, 0.0),
945 ];
946
947 for color in colors {
948 let square = SquareAnnotation::new(rect).with_interior_color(color);
949
950 let annotation = square.to_annotation();
951 let dict = annotation.to_dict();
952
953 if let Some(Object::Array(ic_array)) = dict.get("IC") {
954 match color {
955 Color::Gray(_) => assert_eq!(ic_array.len(), 1),
956 Color::Rgb(_, _, _) => assert_eq!(ic_array.len(), 3),
957 Color::Cmyk(_, _, _, _) => assert_eq!(ic_array.len(), 4),
958 }
959 }
960 }
961 }
962
963 #[test]
964 fn test_border_effect_intensity_clamping() {
965 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
966
967 let low_intensity = SquareAnnotation::new(rect).with_cloudy_border(-1.0);
969 if let Some(effect) = &low_intensity.border_effect {
970 assert_eq!(effect.intensity, 0.0);
971 }
972
973 let high_intensity = SquareAnnotation::new(rect).with_cloudy_border(5.0);
975 if let Some(effect) = &high_intensity.border_effect {
976 assert_eq!(effect.intensity, 2.0);
977 }
978
979 let valid_intensity = SquareAnnotation::new(rect).with_cloudy_border(1.0);
981 if let Some(effect) = &valid_intensity.border_effect {
982 assert_eq!(effect.intensity, 1.0);
983 }
984 }
985
986 #[test]
987 fn test_all_stamp_names() {
988 let stamps = vec![
989 StampName::Approved,
990 StampName::Experimental,
991 StampName::NotApproved,
992 StampName::AsIs,
993 StampName::Expired,
994 StampName::NotForPublicRelease,
995 StampName::Confidential,
996 StampName::Final,
997 StampName::Sold,
998 StampName::Departmental,
999 StampName::ForComment,
1000 StampName::TopSecret,
1001 StampName::Draft,
1002 StampName::ForPublicRelease,
1003 StampName::Custom("MyCustomStamp".to_string()),
1004 ];
1005
1006 let expected_names = vec![
1007 "Approved",
1008 "Experimental",
1009 "NotApproved",
1010 "AsIs",
1011 "Expired",
1012 "NotForPublicRelease",
1013 "Confidential",
1014 "Final",
1015 "Sold",
1016 "Departmental",
1017 "ForComment",
1018 "TopSecret",
1019 "Draft",
1020 "ForPublicRelease",
1021 "MyCustomStamp",
1022 ];
1023
1024 for (stamp, expected) in stamps.iter().zip(expected_names.iter()) {
1025 assert_eq!(stamp.pdf_name(), *expected);
1026 }
1027 }
1028
1029 #[test]
1030 fn test_stamp_annotation_variations() {
1031 let rect = Rectangle::new(Point::new(400.0, 700.0), Point::new(500.0, 750.0));
1032
1033 let standard_stamp = StampAnnotation::new(rect, StampName::Confidential);
1035 let annotation = standard_stamp.to_annotation();
1036 let dict = annotation.to_dict();
1037 assert_eq!(
1038 dict.get("Name"),
1039 Some(&Object::Name("Confidential".to_string()))
1040 );
1041
1042 let custom_stamp =
1044 StampAnnotation::new(rect, StampName::Custom("ReviewedByManager".to_string()));
1045 let annotation = custom_stamp.to_annotation();
1046 let dict = annotation.to_dict();
1047 assert_eq!(
1048 dict.get("Name"),
1049 Some(&Object::Name("ReviewedByManager".to_string()))
1050 );
1051 }
1052
1053 #[test]
1054 fn test_ink_annotation_bounding_box() {
1055 let mut ink = InkAnnotation::new();
1056
1057 ink = ink.add_stroke(vec![
1059 Point::new(100.0, 100.0),
1060 Point::new(150.0, 120.0),
1061 Point::new(200.0, 100.0),
1062 ]);
1063
1064 ink = ink.add_stroke(vec![
1065 Point::new(120.0, 80.0),
1066 Point::new(180.0, 90.0),
1067 Point::new(220.0, 110.0),
1068 ]);
1069
1070 ink = ink.add_stroke(vec![Point::new(90.0, 95.0), Point::new(210.0, 105.0)]);
1071
1072 let annotation = ink.to_annotation();
1073
1074 assert_eq!(annotation.rect.lower_left.x, 90.0); assert_eq!(annotation.rect.lower_left.y, 80.0); assert_eq!(annotation.rect.upper_right.x, 220.0); assert_eq!(annotation.rect.upper_right.y, 120.0); let dict = annotation.to_dict();
1081
1082 if let Some(Object::Array(ink_list)) = dict.get("InkList") {
1083 assert_eq!(ink_list.len(), 3); if let Object::Array(stroke1) = &ink_list[0] {
1087 assert_eq!(stroke1.len(), 6); assert_eq!(stroke1[0], Object::Real(100.0));
1089 assert_eq!(stroke1[1], Object::Real(100.0));
1090 }
1091 }
1092 }
1093
1094 #[test]
1095 fn test_ink_annotation_empty_strokes() {
1096 let ink = InkAnnotation::new();
1097 let annotation = ink.to_annotation();
1098
1099 assert_eq!(annotation.rect.lower_left.x, 0.0);
1101 assert_eq!(annotation.rect.lower_left.y, 0.0);
1102 assert_eq!(annotation.rect.upper_right.x, 0.0);
1103 assert_eq!(annotation.rect.upper_right.y, 0.0);
1104 }
1105
1106 #[test]
1107 fn test_highlight_annotation_convenience() {
1108 let rect = Rectangle::new(Point::new(100.0, 500.0), Point::new(400.0, 515.0));
1109 let highlight = HighlightAnnotation::new(rect);
1110
1111 assert_eq!(
1112 highlight.annotation.annotation_type,
1113 crate::annotations::AnnotationType::Highlight
1114 );
1115
1116 let annotation = highlight.to_annotation();
1117 let dict = annotation.to_dict();
1118
1119 assert_eq!(
1120 dict.get("Subtype"),
1121 Some(&Object::Name("Highlight".to_string()))
1122 );
1123 assert!(dict.get("QuadPoints").is_some());
1124
1125 if let Some(Object::Array(points)) = dict.get("QuadPoints") {
1127 assert_eq!(points.len(), 8);
1128 assert_eq!(points[0], Object::Real(100.0));
1129 assert_eq!(points[1], Object::Real(500.0));
1130 assert_eq!(points[4], Object::Real(400.0));
1131 assert_eq!(points[5], Object::Real(515.0));
1132 }
1133 }
1134
1135 #[test]
1136 fn test_free_text_debug_clone() {
1137 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 100.0));
1138 let free_text = FreeTextAnnotation::new(rect, "Debug test")
1139 .with_font(Font::Helvetica, 14.0, Color::black())
1140 .with_justification(1);
1141
1142 let debug_str = format!("{free_text:?}");
1143 assert!(debug_str.contains("FreeTextAnnotation"));
1144 assert!(debug_str.contains("Debug test"));
1145
1146 let cloned = free_text;
1147 assert_eq!(cloned.quadding, 1);
1148 assert_eq!(cloned.annotation.contents, Some("Debug test".to_string()));
1149 }
1150
1151 #[test]
1152 fn test_line_annotation_debug_clone() {
1153 let line = LineAnnotation::new(Point::new(0.0, 0.0), Point::new(100.0, 100.0))
1154 .with_endings(LineEndingStyle::Circle, LineEndingStyle::Square);
1155
1156 let debug_str = format!("{line:?}");
1157 assert!(debug_str.contains("LineAnnotation"));
1158
1159 let cloned = line;
1160 assert!(matches!(cloned.start_style, LineEndingStyle::Circle));
1161 assert!(matches!(cloned.end_style, LineEndingStyle::Square));
1162 }
1163
1164 #[test]
1165 fn test_border_effect_debug_clone() {
1166 let effect = BorderEffect {
1167 style: BorderEffectStyle::Cloudy,
1168 intensity: 1.2,
1169 };
1170
1171 let debug_str = format!("{effect:?}");
1172 assert!(debug_str.contains("BorderEffect"));
1173 assert!(debug_str.contains("Cloudy"));
1174
1175 let cloned = effect;
1176 assert!(matches!(cloned.style, BorderEffectStyle::Cloudy));
1177 assert_eq!(cloned.intensity, 1.2);
1178 }
1179
1180 #[test]
1181 fn test_stamp_name_debug_clone() {
1182 let stamp = StampName::TopSecret;
1183
1184 let debug_str = format!("{stamp:?}");
1185 assert!(debug_str.contains("TopSecret"));
1186
1187 let cloned = stamp;
1188 assert!(matches!(cloned, StampName::TopSecret));
1189
1190 let custom = StampName::Custom("TestStamp".to_string());
1191 let custom_clone = custom;
1192 if let StampName::Custom(name) = custom_clone {
1193 assert_eq!(name, "TestStamp");
1194 }
1195 }
1196
1197 #[test]
1198 fn test_ink_annotation_default() {
1199 let default_ink = InkAnnotation::default();
1200 assert!(default_ink.ink_lists.is_empty());
1201 assert_eq!(
1202 default_ink.annotation.annotation_type,
1203 crate::annotations::AnnotationType::Ink
1204 );
1205 }
1206
1207 #[test]
1208 fn test_all_annotations_to_dict() {
1209 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 150.0));
1210
1211 let annotations: Vec<Annotation> = vec![
1213 FreeTextAnnotation::new(rect, "Test").to_annotation(),
1214 LineAnnotation::new(Point::new(100.0, 100.0), Point::new(200.0, 150.0)).to_annotation(),
1215 SquareAnnotation::new(rect).to_annotation(),
1216 StampAnnotation::new(rect, StampName::Draft).to_annotation(),
1217 InkAnnotation::new()
1218 .add_stroke(vec![Point::new(100.0, 100.0), Point::new(200.0, 150.0)])
1219 .to_annotation(),
1220 HighlightAnnotation::new(rect).to_annotation(),
1221 ];
1222
1223 for annotation in annotations {
1224 let dict = annotation.to_dict();
1225 assert!(dict.contains_key("Type"));
1226 assert!(dict.contains_key("Subtype"));
1227 assert!(dict.contains_key("Rect"));
1228 }
1229 }
1230
1231 #[test]
1232 fn test_line_ending_style_debug_clone_copy() {
1233 let style = LineEndingStyle::ClosedArrow;
1234
1235 let debug_str = format!("{style:?}");
1236 assert!(debug_str.contains("ClosedArrow"));
1237
1238 let cloned = style;
1239 assert!(matches!(cloned, LineEndingStyle::ClosedArrow));
1240
1241 let copied: LineEndingStyle = style;
1242 assert!(matches!(copied, LineEndingStyle::ClosedArrow));
1243 }
1244
1245 #[test]
1246 fn test_border_effect_style_debug_clone_copy() {
1247 let style = BorderEffectStyle::Cloudy;
1248
1249 let debug_str = format!("{style:?}");
1250 assert!(debug_str.contains("Cloudy"));
1251
1252 let cloned = style;
1253 assert!(matches!(cloned, BorderEffectStyle::Cloudy));
1254
1255 let copied: BorderEffectStyle = style;
1256 assert!(matches!(copied, BorderEffectStyle::Cloudy));
1257 }
1258}