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        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    /// Set justification
52    pub fn with_justification(mut self, quadding: i32) -> Self {
53        self.quadding = quadding.clamp(0, 2);
54        self
55    }
56
57    /// Convert to annotation
58    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/// Line annotation
81#[derive(Debug, Clone)]
82pub struct LineAnnotation {
83    /// Base annotation
84    pub annotation: Annotation,
85    /// Line start point
86    pub start: Point,
87    /// Line end point
88    pub end: Point,
89    /// Line ending style for start
90    pub start_style: LineEndingStyle,
91    /// Line ending style for end
92    pub end_style: LineEndingStyle,
93    /// Interior color
94    pub interior_color: Option<Color>,
95}
96
97/// Line ending styles
98#[derive(Debug, Clone, Copy)]
99pub enum LineEndingStyle {
100    /// No ending
101    None,
102    /// Square
103    Square,
104    /// Circle
105    Circle,
106    /// Diamond
107    Diamond,
108    /// Open arrow
109    OpenArrow,
110    /// Closed arrow
111    ClosedArrow,
112    /// Butt
113    Butt,
114    /// Right open arrow
115    ROpenArrow,
116    /// Right closed arrow
117    RClosedArrow,
118    /// Slash
119    Slash,
120}
121
122impl LineEndingStyle {
123    /// Get PDF name
124    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    /// Create a new line annotation
142    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    /// Set line ending styles
161    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    /// Set interior color
168    pub fn with_interior_color(mut self, color: Color) -> Self {
169        self.interior_color = Some(color);
170        self
171    }
172
173    /// Convert to annotation
174    pub fn to_annotation(self) -> Annotation {
175        let mut annotation = self.annotation;
176
177        // Line coordinates
178        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        // Line endings
189        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        // Interior color
198        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/// Square annotation
217#[derive(Debug, Clone)]
218pub struct SquareAnnotation {
219    /// Base annotation
220    pub annotation: Annotation,
221    /// Interior color
222    pub interior_color: Option<Color>,
223    /// Border effect
224    pub border_effect: Option<BorderEffect>,
225}
226
227/// Border effect
228#[derive(Debug, Clone)]
229pub struct BorderEffect {
230    /// Style: S (no effect) or C (cloudy)
231    pub style: BorderEffectStyle,
232    /// Intensity (0-2 for cloudy)
233    pub intensity: f64,
234}
235
236#[derive(Debug, Clone, Copy)]
237pub enum BorderEffectStyle {
238    /// No effect
239    Solid,
240    /// Cloudy border
241    Cloudy,
242}
243
244impl SquareAnnotation {
245    /// Create a new square annotation
246    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    /// Set interior color
257    pub fn with_interior_color(mut self, color: Color) -> Self {
258        self.interior_color = Some(color);
259        self
260    }
261
262    /// Set cloudy border
263    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    /// Convert to annotation
272    pub fn to_annotation(self) -> Annotation {
273        let mut annotation = self.annotation;
274
275        // Interior color
276        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        // Border effect
291        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/// Stamp annotation
308#[derive(Debug, Clone)]
309pub struct StampAnnotation {
310    /// Base annotation
311    pub annotation: Annotation,
312    /// Stamp name
313    pub stamp_name: StampName,
314}
315
316/// Standard stamp names
317#[derive(Debug, Clone)]
318pub enum StampName {
319    /// Approved
320    Approved,
321    /// Experimental
322    Experimental,
323    /// Not approved
324    NotApproved,
325    /// As is
326    AsIs,
327    /// Expired
328    Expired,
329    /// Not for public release
330    NotForPublicRelease,
331    /// Confidential
332    Confidential,
333    /// Final
334    Final,
335    /// Sold
336    Sold,
337    /// Departmental
338    Departmental,
339    /// For comment
340    ForComment,
341    /// Top secret
342    TopSecret,
343    /// Draft
344    Draft,
345    /// For public release
346    ForPublicRelease,
347    /// Custom stamp
348    Custom(String),
349}
350
351impl StampName {
352    /// Get PDF name
353    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    /// Create a new stamp annotation
376    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    /// Convert to annotation
386    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/// Ink annotation (freehand drawing)
396#[derive(Debug, Clone)]
397pub struct InkAnnotation {
398    /// Base annotation
399    pub annotation: Annotation,
400    /// Ink lists (each list is a series of points)
401    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    /// Create a new ink annotation
412    pub fn new() -> Self {
413        // Initial rect will be calculated from points
414        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    /// Add an ink stroke
424    pub fn add_stroke(mut self, points: Vec<Point>) -> Self {
425        self.ink_lists.push(points);
426        self
427    }
428
429    /// Convert to annotation
430    pub fn to_annotation(mut self) -> Annotation {
431        // Calculate bounding box from all points
432        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        // Convert ink lists to array
452        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/// Highlight annotation
472#[derive(Debug, Clone)]
473pub struct HighlightAnnotation {
474    /// Base annotation
475    pub annotation: Annotation,
476    /// Quad points defining highlighted areas
477    pub quad_points: crate::annotations::QuadPoints,
478}
479
480impl HighlightAnnotation {
481    /// Create a new highlight annotation
482    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    /// Convert to annotation
493    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/// Circle annotation
503#[derive(Debug, Clone)]
504pub struct CircleAnnotation {
505    /// Base annotation
506    pub annotation: Annotation,
507    /// Interior color (fill color)
508    pub interior_color: Option<Color>,
509    /// Border effect
510    pub border_effect: Option<BorderEffect>,
511}
512
513impl CircleAnnotation {
514    /// Create a new circle annotation
515    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    /// Set interior color
526    pub fn with_interior_color(mut self, color: Color) -> Self {
527        self.interior_color = Some(color);
528        self
529    }
530
531    /// Set cloudy border
532    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    /// Convert to annotation
541    pub fn to_annotation(self) -> Annotation {
542        let mut annotation = self.annotation;
543
544        // Interior color
545        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        // Border effect
560        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/// File attachment annotation
577#[derive(Debug, Clone)]
578pub struct FileAttachmentAnnotation {
579    /// Base annotation
580    pub annotation: Annotation,
581    /// File name
582    pub file_name: String,
583    /// File data
584    pub file_data: Vec<u8>,
585    /// MIME type
586    pub mime_type: Option<String>,
587    /// Icon name
588    pub icon: FileAttachmentIcon,
589}
590
591/// File attachment icon types
592#[derive(Debug, Clone)]
593pub enum FileAttachmentIcon {
594    /// Graph icon
595    Graph,
596    /// Paperclip icon
597    Paperclip,
598    /// Push pin icon
599    PushPin,
600    /// Tag icon
601    Tag,
602}
603
604impl FileAttachmentIcon {
605    /// Get PDF name
606    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    /// Create a new file attachment annotation
618    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    /// Set MIME type
631    pub fn with_mime_type(mut self, mime_type: String) -> Self {
632        self.mime_type = Some(mime_type);
633        self
634    }
635
636    /// Set icon
637    pub fn with_icon(mut self, icon: FileAttachmentIcon) -> Self {
638        self.icon = icon;
639        self
640    }
641
642    /// Convert to annotation
643    pub fn to_annotation(self) -> Annotation {
644        let mut annotation = self.annotation;
645
646        // Set icon name
647        annotation
648            .properties
649            .set("Name", Object::Name(self.icon.pdf_name().to_string()));
650
651        // Create file specification dictionary
652        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        // Create embedded file stream
658        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        // Note: In a real implementation, we'd create a proper stream object
668        // For now, we'll just reference it
669        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        // Test all justification values
734        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        // Test clamping of invalid values
746        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        // Verify bounding rectangle is calculated correctly
853        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        // Verify line coordinates
862        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        // Verify line endings
871        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        // Verify interior color
877        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        // Test with same start and end point (zero-length line)
888        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        // Test with negative coordinates
894        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        // Test with reversed coordinates (end < start)
901        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        // Test without border effect
913        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        // Test with cloudy border
921        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        // Test clamping to 0.0
969        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        // Test clamping to 2.0
975        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        // Test valid intensity
981        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        // Test standard stamp
1035        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        // Test custom stamp
1044        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        // Add multiple strokes
1059        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        // Verify bounding box encompasses all points
1076        assert_eq!(annotation.rect.lower_left.x, 90.0); // min x
1077        assert_eq!(annotation.rect.lower_left.y, 80.0); // min y
1078        assert_eq!(annotation.rect.upper_right.x, 220.0); // max x
1079        assert_eq!(annotation.rect.upper_right.y, 120.0); // max y
1080
1081        let dict = annotation.to_dict();
1082
1083        if let Some(Object::Array(ink_list)) = dict.get("InkList") {
1084            assert_eq!(ink_list.len(), 3); // 3 strokes
1085
1086            // Check first stroke
1087            if let Object::Array(stroke1) = &ink_list[0] {
1088                assert_eq!(stroke1.len(), 6); // 3 points * 2 coords
1089                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        // With no strokes, rect should be at origin
1101        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        // Verify QuadPoints match the rectangle
1127        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        // Test each annotation type produces valid dictionary
1213        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}