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