Skip to main content

oxidize_pdf/annotations/
annotation_type.rs

1//! Additional annotation types
2
3use crate::annotations::Annotation;
4use crate::geometry::{Point, Rectangle};
5use crate::graphics::Color;
6use crate::objects::Object;
7use crate::text::Font;
8
9/// Free text annotation
10#[derive(Debug, Clone)]
11pub struct FreeTextAnnotation {
12    /// Base annotation
13    pub annotation: Annotation,
14    /// Default appearance string
15    pub default_appearance: String,
16    /// Quadding (justification): 0=left, 1=center, 2=right
17    pub quadding: i32,
18    /// Rich text string
19    pub rich_text: Option<String>,
20    /// Default style string
21    pub default_style: Option<String>,
22}
23
24impl FreeTextAnnotation {
25    /// Create a new free text annotation
26    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    /// Set font and size
40    pub fn with_font(mut self, font: Font, size: f64, color: Color) -> Self {
41        // Use the shared NaN-sanitising helper (issues #220, #221) — emits
42        // `.3`-precision tokens so the /DA string never carries
43        // `NaN`/`inf` that ISO 32000-1 §7.3.3 rejects.
44        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    /// Set justification
51    pub fn with_justification(mut self, quadding: i32) -> Self {
52        self.quadding = quadding.clamp(0, 2);
53        self
54    }
55
56    /// Convert to annotation
57    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/// Line annotation
80#[derive(Debug, Clone)]
81pub struct LineAnnotation {
82    /// Base annotation
83    pub annotation: Annotation,
84    /// Line start point
85    pub start: Point,
86    /// Line end point
87    pub end: Point,
88    /// Line ending style for start
89    pub start_style: LineEndingStyle,
90    /// Line ending style for end
91    pub end_style: LineEndingStyle,
92    /// Interior color
93    pub interior_color: Option<Color>,
94}
95
96/// Line ending styles
97#[derive(Debug, Clone, Copy)]
98pub enum LineEndingStyle {
99    /// No ending
100    None,
101    /// Square
102    Square,
103    /// Circle
104    Circle,
105    /// Diamond
106    Diamond,
107    /// Open arrow
108    OpenArrow,
109    /// Closed arrow
110    ClosedArrow,
111    /// Butt
112    Butt,
113    /// Right open arrow
114    ROpenArrow,
115    /// Right closed arrow
116    RClosedArrow,
117    /// Slash
118    Slash,
119}
120
121impl LineEndingStyle {
122    /// Get PDF name
123    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    /// Create a new line annotation
141    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    /// Set line ending styles
160    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    /// Set interior color
167    pub fn with_interior_color(mut self, color: Color) -> Self {
168        self.interior_color = Some(color);
169        self
170    }
171
172    /// Convert to annotation
173    pub fn to_annotation(self) -> Annotation {
174        let mut annotation = self.annotation;
175
176        // Line coordinates
177        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        // Line endings
188        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        // Interior color
197        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/// Square annotation
216#[derive(Debug, Clone)]
217pub struct SquareAnnotation {
218    /// Base annotation
219    pub annotation: Annotation,
220    /// Interior color
221    pub interior_color: Option<Color>,
222    /// Border effect
223    pub border_effect: Option<BorderEffect>,
224}
225
226/// Border effect
227#[derive(Debug, Clone)]
228pub struct BorderEffect {
229    /// Style: S (no effect) or C (cloudy)
230    pub style: BorderEffectStyle,
231    /// Intensity (0-2 for cloudy)
232    pub intensity: f64,
233}
234
235#[derive(Debug, Clone, Copy)]
236pub enum BorderEffectStyle {
237    /// No effect
238    Solid,
239    /// Cloudy border
240    Cloudy,
241}
242
243impl SquareAnnotation {
244    /// Create a new square annotation
245    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    /// Set interior color
256    pub fn with_interior_color(mut self, color: Color) -> Self {
257        self.interior_color = Some(color);
258        self
259    }
260
261    /// Set cloudy border
262    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    /// Convert to annotation
271    pub fn to_annotation(self) -> Annotation {
272        let mut annotation = self.annotation;
273
274        // Interior color
275        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        // Border effect
290        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/// Stamp annotation
307#[derive(Debug, Clone)]
308pub struct StampAnnotation {
309    /// Base annotation
310    pub annotation: Annotation,
311    /// Stamp name
312    pub stamp_name: StampName,
313}
314
315/// Standard stamp names
316#[derive(Debug, Clone)]
317pub enum StampName {
318    /// Approved
319    Approved,
320    /// Experimental
321    Experimental,
322    /// Not approved
323    NotApproved,
324    /// As is
325    AsIs,
326    /// Expired
327    Expired,
328    /// Not for public release
329    NotForPublicRelease,
330    /// Confidential
331    Confidential,
332    /// Final
333    Final,
334    /// Sold
335    Sold,
336    /// Departmental
337    Departmental,
338    /// For comment
339    ForComment,
340    /// Top secret
341    TopSecret,
342    /// Draft
343    Draft,
344    /// For public release
345    ForPublicRelease,
346    /// Custom stamp
347    Custom(String),
348}
349
350impl StampName {
351    /// Get PDF name
352    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    /// Create a new stamp annotation
375    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    /// Convert to annotation
385    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/// Ink annotation (freehand drawing)
395#[derive(Debug, Clone)]
396pub struct InkAnnotation {
397    /// Base annotation
398    pub annotation: Annotation,
399    /// Ink lists (each list is a series of points)
400    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    /// Create a new ink annotation
411    pub fn new() -> Self {
412        // Initial rect will be calculated from points
413        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    /// Add an ink stroke
423    pub fn add_stroke(mut self, points: Vec<Point>) -> Self {
424        self.ink_lists.push(points);
425        self
426    }
427
428    /// Convert to annotation
429    pub fn to_annotation(mut self) -> Annotation {
430        // Calculate bounding box from all points
431        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        // Convert ink lists to array
451        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/// Highlight annotation
471#[derive(Debug, Clone)]
472pub struct HighlightAnnotation {
473    /// Base annotation
474    pub annotation: Annotation,
475    /// Quad points defining highlighted areas
476    pub quad_points: crate::annotations::QuadPoints,
477}
478
479impl HighlightAnnotation {
480    /// Create a new highlight annotation
481    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    /// Convert to annotation
492    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/// Circle annotation
502#[derive(Debug, Clone)]
503pub struct CircleAnnotation {
504    /// Base annotation
505    pub annotation: Annotation,
506    /// Interior color (fill color)
507    pub interior_color: Option<Color>,
508    /// Border effect
509    pub border_effect: Option<BorderEffect>,
510}
511
512impl CircleAnnotation {
513    /// Create a new circle annotation
514    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    /// Set interior color
525    pub fn with_interior_color(mut self, color: Color) -> Self {
526        self.interior_color = Some(color);
527        self
528    }
529
530    /// Set cloudy border
531    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    /// Convert to annotation
540    pub fn to_annotation(self) -> Annotation {
541        let mut annotation = self.annotation;
542
543        // Interior color
544        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        // Border effect
559        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/// File attachment annotation
576#[derive(Debug, Clone)]
577pub struct FileAttachmentAnnotation {
578    /// Base annotation
579    pub annotation: Annotation,
580    /// File name
581    pub file_name: String,
582    /// File data
583    pub file_data: Vec<u8>,
584    /// MIME type
585    pub mime_type: Option<String>,
586    /// Icon name
587    pub icon: FileAttachmentIcon,
588}
589
590/// File attachment icon types
591#[derive(Debug, Clone)]
592pub enum FileAttachmentIcon {
593    /// Graph icon
594    Graph,
595    /// Paperclip icon
596    Paperclip,
597    /// Push pin icon
598    PushPin,
599    /// Tag icon
600    Tag,
601}
602
603impl FileAttachmentIcon {
604    /// Get PDF name
605    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    /// Create a new file attachment annotation
617    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    /// Set MIME type
630    pub fn with_mime_type(mut self, mime_type: String) -> Self {
631        self.mime_type = Some(mime_type);
632        self
633    }
634
635    /// Set icon
636    pub fn with_icon(mut self, icon: FileAttachmentIcon) -> Self {
637        self.icon = icon;
638        self
639    }
640
641    /// Convert to annotation
642    pub fn to_annotation(self) -> Annotation {
643        let mut annotation = self.annotation;
644
645        // Set icon name
646        annotation
647            .properties
648            .set("Name", Object::Name(self.icon.pdf_name().to_string()));
649
650        // Create file specification dictionary
651        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        // Create embedded file stream
657        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        // Note: In a real implementation, we'd create a proper stream object
667        // For now, we'll just reference it
668        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        // Test all justification values
733        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        // Test clamping of invalid values
745        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        // Verify bounding rectangle is calculated correctly
852        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        // Verify line coordinates
861        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        // Verify line endings
870        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        // Verify interior color
876        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        // Test with same start and end point (zero-length line)
887        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        // Test with negative coordinates
893        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        // Test with reversed coordinates (end < start)
900        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        // Test without border effect
912        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        // Test with cloudy border
920        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        // Test clamping to 0.0
968        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        // Test clamping to 2.0
974        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        // Test valid intensity
980        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        // Test standard stamp
1034        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        // Test custom stamp
1043        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        // Add multiple strokes
1058        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        // Verify bounding box encompasses all points
1075        assert_eq!(annotation.rect.lower_left.x, 90.0); // min x
1076        assert_eq!(annotation.rect.lower_left.y, 80.0); // min y
1077        assert_eq!(annotation.rect.upper_right.x, 220.0); // max x
1078        assert_eq!(annotation.rect.upper_right.y, 120.0); // max y
1079
1080        let dict = annotation.to_dict();
1081
1082        if let Some(Object::Array(ink_list)) = dict.get("InkList") {
1083            assert_eq!(ink_list.len(), 3); // 3 strokes
1084
1085            // Check first stroke
1086            if let Object::Array(stroke1) = &ink_list[0] {
1087                assert_eq!(stroke1.len(), 6); // 3 points * 2 coords
1088                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        // With no strokes, rect should be at origin
1100        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        // Verify QuadPoints match the rectangle
1126        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        // Test each annotation type produces valid dictionary
1212        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}