Skip to main content

pdf_oxide/writer/
shape_annotations.rs

1//! Shape annotations (Line, Square, Circle, Polygon, PolyLine) for PDF generation.
2//!
3//! This module provides support for shape annotations per PDF spec:
4//! - Line: Section 12.5.6.7
5//! - Square/Circle: Section 12.5.6.8
6//! - Polygon/PolyLine: Section 12.5.6.9
7//!
8//! # Example
9//!
10//! ```ignore
11//! use pdf_oxide::writer::{LineAnnotation, ShapeAnnotation, PolygonAnnotation};
12//! use pdf_oxide::geometry::Rect;
13//! use pdf_oxide::annotation_types::LineEndingStyle;
14//!
15//! // Draw a line with arrow
16//! let line = LineAnnotation::new((100.0, 100.0), (200.0, 200.0))
17//!     .with_line_endings(LineEndingStyle::OpenArrow, LineEndingStyle::None);
18//!
19//! // Draw a rectangle
20//! let rect = ShapeAnnotation::square(Rect::new(72.0, 600.0, 100.0, 80.0))
21//!     .with_stroke_color(0.0, 0.0, 1.0)  // Blue border
22//!     .with_fill_color(0.9, 0.9, 1.0);   // Light blue fill
23//!
24//! // Draw a polygon
25//! let polygon = PolygonAnnotation::polygon(vec![(100.0, 100.0), (150.0, 150.0), (100.0, 150.0)]);
26//! ```
27
28use crate::annotation_types::{
29    AnnotationColor, AnnotationFlags, BorderEffect, BorderStyleType, LineEndingStyle,
30};
31use crate::geometry::Rect;
32use crate::object::{Object, ObjectRef};
33use std::collections::HashMap;
34
35// ============================================================================
36// Line Annotation
37// ============================================================================
38
39/// A Line annotation per PDF spec Section 12.5.6.7.
40///
41/// Displays a single straight line on the page.
42#[derive(Debug, Clone)]
43pub struct LineAnnotation {
44    /// Start point (x, y)
45    pub start: (f64, f64),
46    /// End point (x, y)
47    pub end: (f64, f64),
48    /// Line ending styles (start, end)
49    pub line_endings: (LineEndingStyle, LineEndingStyle),
50    /// Stroke color
51    pub color: Option<AnnotationColor>,
52    /// Interior color (for filled line endings)
53    pub interior_color: Option<AnnotationColor>,
54    /// Opacity (0.0 = transparent, 1.0 = opaque)
55    pub opacity: Option<f32>,
56    /// Border style type
57    pub border_style: Option<BorderStyleType>,
58    /// Line width
59    pub line_width: Option<f32>,
60    /// Leader line length (positive = extends from endpoints)
61    pub leader_line: Option<f64>,
62    /// Leader line offset (distance from endpoints)
63    pub leader_line_offset: Option<f64>,
64    /// Leader line extension (beyond leader line)
65    pub leader_line_extension: Option<f64>,
66    /// Whether to show caption
67    pub caption: bool,
68    /// Caption content
69    pub contents: Option<String>,
70    /// Caption positioning (Inline or Top)
71    pub caption_position: CaptionPosition,
72    /// Author
73    pub author: Option<String>,
74    /// Subject
75    pub subject: Option<String>,
76    /// Annotation flags
77    pub flags: AnnotationFlags,
78}
79
80/// Caption position for line annotations.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum CaptionPosition {
83    /// Caption centered inside the line
84    #[default]
85    Inline,
86    /// Caption above the line
87    Top,
88}
89
90impl CaptionPosition {
91    /// Get PDF name.
92    pub fn pdf_name(&self) -> &'static str {
93        match self {
94            Self::Inline => "Inline",
95            Self::Top => "Top",
96        }
97    }
98}
99
100impl LineAnnotation {
101    /// Create a new line annotation.
102    pub fn new(start: (f64, f64), end: (f64, f64)) -> Self {
103        Self {
104            start,
105            end,
106            line_endings: (LineEndingStyle::None, LineEndingStyle::None),
107            color: Some(AnnotationColor::black()),
108            interior_color: None,
109            opacity: None,
110            border_style: None,
111            line_width: Some(1.0),
112            leader_line: None,
113            leader_line_offset: None,
114            leader_line_extension: None,
115            caption: false,
116            contents: None,
117            caption_position: CaptionPosition::Inline,
118            author: None,
119            subject: None,
120            flags: AnnotationFlags::printable(),
121        }
122    }
123
124    /// Create a line with arrow at the end.
125    pub fn arrow(start: (f64, f64), end: (f64, f64)) -> Self {
126        Self::new(start, end).with_line_endings(LineEndingStyle::None, LineEndingStyle::OpenArrow)
127    }
128
129    /// Create a double-headed arrow line.
130    pub fn double_arrow(start: (f64, f64), end: (f64, f64)) -> Self {
131        Self::new(start, end)
132            .with_line_endings(LineEndingStyle::OpenArrow, LineEndingStyle::OpenArrow)
133    }
134
135    /// Create a dimension line (with leader lines).
136    pub fn dimension(start: (f64, f64), end: (f64, f64), leader_length: f64) -> Self {
137        Self::new(start, end)
138            .with_line_endings(LineEndingStyle::OpenArrow, LineEndingStyle::OpenArrow)
139            .with_leader_line(leader_length)
140    }
141
142    /// Set line endings.
143    pub fn with_line_endings(mut self, start: LineEndingStyle, end: LineEndingStyle) -> Self {
144        self.line_endings = (start, end);
145        self
146    }
147
148    /// Set stroke color (RGB).
149    pub fn with_stroke_color(mut self, r: f32, g: f32, b: f32) -> Self {
150        self.color = Some(AnnotationColor::Rgb(r, g, b));
151        self
152    }
153
154    /// Set interior color for filled line endings (RGB).
155    pub fn with_fill_color(mut self, r: f32, g: f32, b: f32) -> Self {
156        self.interior_color = Some(AnnotationColor::Rgb(r, g, b));
157        self
158    }
159
160    /// Set line width.
161    pub fn with_line_width(mut self, width: f32) -> Self {
162        self.line_width = Some(width);
163        self
164    }
165
166    /// Set border style.
167    pub fn with_border_style(mut self, style: BorderStyleType) -> Self {
168        self.border_style = Some(style);
169        self
170    }
171
172    /// Set leader line length.
173    pub fn with_leader_line(mut self, length: f64) -> Self {
174        self.leader_line = Some(length);
175        self
176    }
177
178    /// Set leader line offset.
179    pub fn with_leader_offset(mut self, offset: f64) -> Self {
180        self.leader_line_offset = Some(offset);
181        self
182    }
183
184    /// Set caption text.
185    pub fn with_caption(mut self, text: impl Into<String>) -> Self {
186        self.caption = true;
187        self.contents = Some(text.into());
188        self
189    }
190
191    /// Set caption position.
192    pub fn with_caption_position(mut self, position: CaptionPosition) -> Self {
193        self.caption_position = position;
194        self
195    }
196
197    /// Set opacity.
198    pub fn with_opacity(mut self, opacity: f32) -> Self {
199        self.opacity = Some(opacity.clamp(0.0, 1.0));
200        self
201    }
202
203    /// Set author.
204    pub fn with_author(mut self, author: impl Into<String>) -> Self {
205        self.author = Some(author.into());
206        self
207    }
208
209    /// Set subject.
210    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
211        self.subject = Some(subject.into());
212        self
213    }
214
215    /// Set annotation flags.
216    pub fn with_flags(mut self, flags: AnnotationFlags) -> Self {
217        self.flags = flags;
218        self
219    }
220
221    /// Calculate bounding rectangle.
222    pub fn calculate_rect(&self) -> Rect {
223        let min_x = self.start.0.min(self.end.0);
224        let max_x = self.start.0.max(self.end.0);
225        let min_y = self.start.1.min(self.end.1);
226        let max_y = self.start.1.max(self.end.1);
227
228        // Add margin for line endings
229        let margin = 10.0;
230        Rect::new(
231            (min_x - margin) as f32,
232            (min_y - margin) as f32,
233            (max_x - min_x + 2.0 * margin) as f32,
234            (max_y - min_y + 2.0 * margin) as f32,
235        )
236    }
237
238    /// Build the annotation dictionary.
239    pub fn build(&self, _page_refs: &[ObjectRef]) -> HashMap<String, Object> {
240        let mut dict = HashMap::new();
241
242        // Required entries
243        dict.insert("Type".to_string(), Object::Name("Annot".to_string()));
244        dict.insert("Subtype".to_string(), Object::Name("Line".to_string()));
245
246        // Rectangle
247        let rect = self.calculate_rect();
248        dict.insert(
249            "Rect".to_string(),
250            Object::Array(vec![
251                Object::Real(rect.x as f64),
252                Object::Real(rect.y as f64),
253                Object::Real((rect.x + rect.width) as f64),
254                Object::Real((rect.y + rect.height) as f64),
255            ]),
256        );
257
258        // Line coordinates (L entry) - required for Line annotations
259        dict.insert(
260            "L".to_string(),
261            Object::Array(vec![
262                Object::Real(self.start.0),
263                Object::Real(self.start.1),
264                Object::Real(self.end.0),
265                Object::Real(self.end.1),
266            ]),
267        );
268
269        // Line endings (LE entry)
270        if self.line_endings != (LineEndingStyle::None, LineEndingStyle::None) {
271            dict.insert(
272                "LE".to_string(),
273                Object::Array(vec![
274                    Object::Name(self.line_endings.0.pdf_name().to_string()),
275                    Object::Name(self.line_endings.1.pdf_name().to_string()),
276                ]),
277            );
278        }
279
280        // Contents
281        if let Some(ref contents) = self.contents {
282            dict.insert("Contents".to_string(), Object::String(contents.as_bytes().to_vec()));
283        }
284
285        // Flags
286        if self.flags.bits() != 0 {
287            dict.insert("F".to_string(), Object::Integer(self.flags.bits() as i64));
288        }
289
290        // Color (C entry) - stroke color
291        if let Some(ref color) = self.color {
292            if let Some(color_array) = color.to_array() {
293                if !color_array.is_empty() {
294                    dict.insert(
295                        "C".to_string(),
296                        Object::Array(
297                            color_array
298                                .into_iter()
299                                .map(|v| Object::Real(v as f64))
300                                .collect(),
301                        ),
302                    );
303                }
304            }
305        }
306
307        // Interior color (IC entry)
308        if let Some(ref color) = self.interior_color {
309            if let Some(color_array) = color.to_array() {
310                if !color_array.is_empty() {
311                    dict.insert(
312                        "IC".to_string(),
313                        Object::Array(
314                            color_array
315                                .into_iter()
316                                .map(|v| Object::Real(v as f64))
317                                .collect(),
318                        ),
319                    );
320                }
321            }
322        }
323
324        // Opacity
325        if let Some(opacity) = self.opacity {
326            dict.insert("CA".to_string(), Object::Real(opacity as f64));
327        }
328
329        // Border style (BS entry)
330        if self.border_style.is_some() || self.line_width.is_some() {
331            let mut bs = HashMap::new();
332            bs.insert("Type".to_string(), Object::Name("Border".to_string()));
333            if let Some(width) = self.line_width {
334                bs.insert("W".to_string(), Object::Real(width as f64));
335            }
336            if let Some(ref style) = self.border_style {
337                let style_char = match style {
338                    BorderStyleType::Solid => "S",
339                    BorderStyleType::Dashed => "D",
340                    BorderStyleType::Beveled => "B",
341                    BorderStyleType::Inset => "I",
342                    BorderStyleType::Underline => "U",
343                };
344                bs.insert("S".to_string(), Object::Name(style_char.to_string()));
345            }
346            dict.insert("BS".to_string(), Object::Dictionary(bs));
347        }
348
349        // Leader line (LL entry)
350        if let Some(ll) = self.leader_line {
351            dict.insert("LL".to_string(), Object::Real(ll));
352        }
353
354        // Leader line offset (LLO entry)
355        if let Some(llo) = self.leader_line_offset {
356            dict.insert("LLO".to_string(), Object::Real(llo));
357        }
358
359        // Leader line extension (LLE entry)
360        if let Some(lle) = self.leader_line_extension {
361            dict.insert("LLE".to_string(), Object::Real(lle));
362        }
363
364        // Caption (Cap entry)
365        if self.caption {
366            dict.insert("Cap".to_string(), Object::Boolean(true));
367        }
368
369        // Caption position (CP entry)
370        if self.caption && self.caption_position != CaptionPosition::Inline {
371            dict.insert(
372                "CP".to_string(),
373                Object::Name(self.caption_position.pdf_name().to_string()),
374            );
375        }
376
377        // Author
378        if let Some(ref author) = self.author {
379            dict.insert("T".to_string(), Object::String(author.as_bytes().to_vec()));
380        }
381
382        // Subject
383        if let Some(ref subject) = self.subject {
384            dict.insert("Subj".to_string(), Object::String(subject.as_bytes().to_vec()));
385        }
386
387        dict
388    }
389}
390
391// ============================================================================
392// Shape Annotation (Square/Circle)
393// ============================================================================
394
395/// Shape type for Square/Circle annotations.
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum ShapeType {
398    /// Square/Rectangle
399    Square,
400    /// Circle/Ellipse
401    Circle,
402}
403
404impl ShapeType {
405    /// Get PDF subtype name.
406    pub fn pdf_name(&self) -> &'static str {
407        match self {
408            Self::Square => "Square",
409            Self::Circle => "Circle",
410        }
411    }
412}
413
414/// A Shape annotation (Square or Circle) per PDF spec Section 12.5.6.8.
415#[derive(Debug, Clone)]
416pub struct ShapeAnnotation {
417    /// Bounding rectangle
418    pub rect: Rect,
419    /// Shape type
420    pub shape_type: ShapeType,
421    /// Stroke color
422    pub color: Option<AnnotationColor>,
423    /// Interior (fill) color
424    pub interior_color: Option<AnnotationColor>,
425    /// Opacity
426    pub opacity: Option<f32>,
427    /// Border style
428    pub border_style: Option<BorderStyleType>,
429    /// Border width
430    pub border_width: Option<f32>,
431    /// Border effect
432    pub border_effect: Option<BorderEffect>,
433    /// Rectangle differences (inner content area)
434    pub rect_differences: Option<[f32; 4]>,
435    /// Contents/comment
436    pub contents: Option<String>,
437    /// Author
438    pub author: Option<String>,
439    /// Subject
440    pub subject: Option<String>,
441    /// Annotation flags
442    pub flags: AnnotationFlags,
443}
444
445impl ShapeAnnotation {
446    /// Create a new shape annotation.
447    pub fn new(rect: Rect, shape_type: ShapeType) -> Self {
448        Self {
449            rect,
450            shape_type,
451            color: Some(AnnotationColor::black()),
452            interior_color: None,
453            opacity: None,
454            border_style: None,
455            border_width: Some(1.0),
456            border_effect: None,
457            rect_differences: None,
458            contents: None,
459            author: None,
460            subject: None,
461            flags: AnnotationFlags::printable(),
462        }
463    }
464
465    /// Create a square/rectangle annotation.
466    pub fn square(rect: Rect) -> Self {
467        Self::new(rect, ShapeType::Square)
468    }
469
470    /// Create a circle/ellipse annotation.
471    pub fn circle(rect: Rect) -> Self {
472        Self::new(rect, ShapeType::Circle)
473    }
474
475    /// Set stroke color (RGB).
476    pub fn with_stroke_color(mut self, r: f32, g: f32, b: f32) -> Self {
477        self.color = Some(AnnotationColor::Rgb(r, g, b));
478        self
479    }
480
481    /// Set fill color (RGB).
482    pub fn with_fill_color(mut self, r: f32, g: f32, b: f32) -> Self {
483        self.interior_color = Some(AnnotationColor::Rgb(r, g, b));
484        self
485    }
486
487    /// Set no fill (transparent interior).
488    pub fn with_no_fill(mut self) -> Self {
489        self.interior_color = None;
490        self
491    }
492
493    /// Set border width.
494    pub fn with_border_width(mut self, width: f32) -> Self {
495        self.border_width = Some(width);
496        self
497    }
498
499    /// Set border style.
500    pub fn with_border_style(mut self, style: BorderStyleType) -> Self {
501        self.border_style = Some(style);
502        self
503    }
504
505    /// Set border effect (cloudy effect).
506    pub fn with_border_effect(mut self, effect: BorderEffect) -> Self {
507        self.border_effect = Some(effect);
508        self
509    }
510
511    /// Set opacity.
512    pub fn with_opacity(mut self, opacity: f32) -> Self {
513        self.opacity = Some(opacity.clamp(0.0, 1.0));
514        self
515    }
516
517    /// Set content/comment.
518    pub fn with_contents(mut self, contents: impl Into<String>) -> Self {
519        self.contents = Some(contents.into());
520        self
521    }
522
523    /// Set author.
524    pub fn with_author(mut self, author: impl Into<String>) -> Self {
525        self.author = Some(author.into());
526        self
527    }
528
529    /// Set subject.
530    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
531        self.subject = Some(subject.into());
532        self
533    }
534
535    /// Set annotation flags.
536    pub fn with_flags(mut self, flags: AnnotationFlags) -> Self {
537        self.flags = flags;
538        self
539    }
540
541    /// Build the annotation dictionary.
542    pub fn build(&self, _page_refs: &[ObjectRef]) -> HashMap<String, Object> {
543        let mut dict = HashMap::new();
544
545        // Required entries
546        dict.insert("Type".to_string(), Object::Name("Annot".to_string()));
547        dict.insert("Subtype".to_string(), Object::Name(self.shape_type.pdf_name().to_string()));
548
549        // Rectangle
550        dict.insert(
551            "Rect".to_string(),
552            Object::Array(vec![
553                Object::Real(self.rect.x as f64),
554                Object::Real(self.rect.y as f64),
555                Object::Real((self.rect.x + self.rect.width) as f64),
556                Object::Real((self.rect.y + self.rect.height) as f64),
557            ]),
558        );
559
560        // Contents
561        if let Some(ref contents) = self.contents {
562            dict.insert("Contents".to_string(), Object::String(contents.as_bytes().to_vec()));
563        }
564
565        // Flags
566        if self.flags.bits() != 0 {
567            dict.insert("F".to_string(), Object::Integer(self.flags.bits() as i64));
568        }
569
570        // Stroke color (C entry)
571        if let Some(ref color) = self.color {
572            if let Some(color_array) = color.to_array() {
573                if !color_array.is_empty() {
574                    dict.insert(
575                        "C".to_string(),
576                        Object::Array(
577                            color_array
578                                .into_iter()
579                                .map(|v| Object::Real(v as f64))
580                                .collect(),
581                        ),
582                    );
583                }
584            }
585        }
586
587        // Interior color (IC entry)
588        if let Some(ref color) = self.interior_color {
589            if let Some(color_array) = color.to_array() {
590                if !color_array.is_empty() {
591                    dict.insert(
592                        "IC".to_string(),
593                        Object::Array(
594                            color_array
595                                .into_iter()
596                                .map(|v| Object::Real(v as f64))
597                                .collect(),
598                        ),
599                    );
600                }
601            }
602        }
603
604        // Opacity
605        if let Some(opacity) = self.opacity {
606            dict.insert("CA".to_string(), Object::Real(opacity as f64));
607        }
608
609        // Border style (BS entry)
610        if self.border_style.is_some() || self.border_width.is_some() {
611            let mut bs = HashMap::new();
612            bs.insert("Type".to_string(), Object::Name("Border".to_string()));
613            if let Some(width) = self.border_width {
614                bs.insert("W".to_string(), Object::Real(width as f64));
615            }
616            if let Some(ref style) = self.border_style {
617                let style_char = match style {
618                    BorderStyleType::Solid => "S",
619                    BorderStyleType::Dashed => "D",
620                    BorderStyleType::Beveled => "B",
621                    BorderStyleType::Inset => "I",
622                    BorderStyleType::Underline => "U",
623                };
624                bs.insert("S".to_string(), Object::Name(style_char.to_string()));
625            }
626            dict.insert("BS".to_string(), Object::Dictionary(bs));
627        }
628
629        // Border effect (BE entry)
630        if let Some(ref be) = self.border_effect {
631            let mut be_dict = HashMap::new();
632            be_dict.insert("S".to_string(), Object::Name(be.style.pdf_name().to_string()));
633            if be.intensity > 0.0 {
634                be_dict.insert("I".to_string(), Object::Real(be.intensity as f64));
635            }
636            dict.insert("BE".to_string(), Object::Dictionary(be_dict));
637        }
638
639        // Rectangle differences (RD entry)
640        if let Some(rd) = self.rect_differences {
641            dict.insert(
642                "RD".to_string(),
643                Object::Array(vec![
644                    Object::Real(rd[0] as f64),
645                    Object::Real(rd[1] as f64),
646                    Object::Real(rd[2] as f64),
647                    Object::Real(rd[3] as f64),
648                ]),
649            );
650        }
651
652        // Author
653        if let Some(ref author) = self.author {
654            dict.insert("T".to_string(), Object::String(author.as_bytes().to_vec()));
655        }
656
657        // Subject
658        if let Some(ref subject) = self.subject {
659            dict.insert("Subj".to_string(), Object::String(subject.as_bytes().to_vec()));
660        }
661
662        dict
663    }
664}
665
666// ============================================================================
667// Polygon/PolyLine Annotation
668// ============================================================================
669
670/// Polygon type.
671#[derive(Debug, Clone, Copy, PartialEq, Eq)]
672pub enum PolygonType {
673    /// Closed polygon
674    Polygon,
675    /// Open polyline
676    PolyLine,
677}
678
679impl PolygonType {
680    /// Get PDF subtype name.
681    pub fn pdf_name(&self) -> &'static str {
682        match self {
683            Self::Polygon => "Polygon",
684            Self::PolyLine => "PolyLine",
685        }
686    }
687}
688
689/// A Polygon or PolyLine annotation per PDF spec Section 12.5.6.9.
690#[derive(Debug, Clone)]
691pub struct PolygonAnnotation {
692    /// Vertices as (x, y) coordinate pairs
693    pub vertices: Vec<(f64, f64)>,
694    /// Whether this is a closed polygon or open polyline
695    pub polygon_type: PolygonType,
696    /// Line endings for PolyLine (start, end)
697    pub line_endings: Option<(LineEndingStyle, LineEndingStyle)>,
698    /// Stroke color
699    pub color: Option<AnnotationColor>,
700    /// Interior (fill) color (for Polygon)
701    pub interior_color: Option<AnnotationColor>,
702    /// Opacity
703    pub opacity: Option<f32>,
704    /// Border style
705    pub border_style: Option<BorderStyleType>,
706    /// Border width
707    pub border_width: Option<f32>,
708    /// Border effect
709    pub border_effect: Option<BorderEffect>,
710    /// Contents/comment
711    pub contents: Option<String>,
712    /// Author
713    pub author: Option<String>,
714    /// Subject
715    pub subject: Option<String>,
716    /// Annotation flags
717    pub flags: AnnotationFlags,
718}
719
720impl PolygonAnnotation {
721    /// Create a closed polygon.
722    pub fn polygon(vertices: Vec<(f64, f64)>) -> Self {
723        Self {
724            vertices,
725            polygon_type: PolygonType::Polygon,
726            line_endings: None,
727            color: Some(AnnotationColor::black()),
728            interior_color: None,
729            opacity: None,
730            border_style: None,
731            border_width: Some(1.0),
732            border_effect: None,
733            contents: None,
734            author: None,
735            subject: None,
736            flags: AnnotationFlags::printable(),
737        }
738    }
739
740    /// Create an open polyline.
741    pub fn polyline(vertices: Vec<(f64, f64)>) -> Self {
742        Self {
743            vertices,
744            polygon_type: PolygonType::PolyLine,
745            line_endings: None,
746            color: Some(AnnotationColor::black()),
747            interior_color: None,
748            opacity: None,
749            border_style: None,
750            border_width: Some(1.0),
751            border_effect: None,
752            contents: None,
753            author: None,
754            subject: None,
755            flags: AnnotationFlags::printable(),
756        }
757    }
758
759    /// Set line endings (for PolyLine only).
760    pub fn with_line_endings(mut self, start: LineEndingStyle, end: LineEndingStyle) -> Self {
761        self.line_endings = Some((start, end));
762        self
763    }
764
765    /// Set stroke color (RGB).
766    pub fn with_stroke_color(mut self, r: f32, g: f32, b: f32) -> Self {
767        self.color = Some(AnnotationColor::Rgb(r, g, b));
768        self
769    }
770
771    /// Set fill color (RGB).
772    pub fn with_fill_color(mut self, r: f32, g: f32, b: f32) -> Self {
773        self.interior_color = Some(AnnotationColor::Rgb(r, g, b));
774        self
775    }
776
777    /// Set no fill.
778    pub fn with_no_fill(mut self) -> Self {
779        self.interior_color = None;
780        self
781    }
782
783    /// Set border width.
784    pub fn with_border_width(mut self, width: f32) -> Self {
785        self.border_width = Some(width);
786        self
787    }
788
789    /// Set border style.
790    pub fn with_border_style(mut self, style: BorderStyleType) -> Self {
791        self.border_style = Some(style);
792        self
793    }
794
795    /// Set border effect.
796    pub fn with_border_effect(mut self, effect: BorderEffect) -> Self {
797        self.border_effect = Some(effect);
798        self
799    }
800
801    /// Set opacity.
802    pub fn with_opacity(mut self, opacity: f32) -> Self {
803        self.opacity = Some(opacity.clamp(0.0, 1.0));
804        self
805    }
806
807    /// Set contents/comment.
808    pub fn with_contents(mut self, contents: impl Into<String>) -> Self {
809        self.contents = Some(contents.into());
810        self
811    }
812
813    /// Set author.
814    pub fn with_author(mut self, author: impl Into<String>) -> Self {
815        self.author = Some(author.into());
816        self
817    }
818
819    /// Set subject.
820    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
821        self.subject = Some(subject.into());
822        self
823    }
824
825    /// Set annotation flags.
826    pub fn with_flags(mut self, flags: AnnotationFlags) -> Self {
827        self.flags = flags;
828        self
829    }
830
831    /// Calculate bounding rectangle from vertices.
832    pub fn calculate_rect(&self) -> Rect {
833        if self.vertices.is_empty() {
834            return Rect::new(0.0, 0.0, 0.0, 0.0);
835        }
836
837        let mut min_x = f64::MAX;
838        let mut max_x = f64::MIN;
839        let mut min_y = f64::MAX;
840        let mut max_y = f64::MIN;
841
842        for (x, y) in &self.vertices {
843            min_x = min_x.min(*x);
844            max_x = max_x.max(*x);
845            min_y = min_y.min(*y);
846            max_y = max_y.max(*y);
847        }
848
849        // Add small margin
850        let margin = 5.0;
851        Rect::new(
852            (min_x - margin) as f32,
853            (min_y - margin) as f32,
854            (max_x - min_x + 2.0 * margin) as f32,
855            (max_y - min_y + 2.0 * margin) as f32,
856        )
857    }
858
859    /// Build the annotation dictionary.
860    pub fn build(&self, _page_refs: &[ObjectRef]) -> HashMap<String, Object> {
861        let mut dict = HashMap::new();
862
863        // Required entries
864        dict.insert("Type".to_string(), Object::Name("Annot".to_string()));
865        dict.insert("Subtype".to_string(), Object::Name(self.polygon_type.pdf_name().to_string()));
866
867        // Rectangle (calculated from vertices)
868        let rect = self.calculate_rect();
869        dict.insert(
870            "Rect".to_string(),
871            Object::Array(vec![
872                Object::Real(rect.x as f64),
873                Object::Real(rect.y as f64),
874                Object::Real((rect.x + rect.width) as f64),
875                Object::Real((rect.y + rect.height) as f64),
876            ]),
877        );
878
879        // Vertices (required)
880        let vertices: Vec<Object> = self
881            .vertices
882            .iter()
883            .flat_map(|(x, y)| vec![Object::Real(*x), Object::Real(*y)])
884            .collect();
885        dict.insert("Vertices".to_string(), Object::Array(vertices));
886
887        // Contents
888        if let Some(ref contents) = self.contents {
889            dict.insert("Contents".to_string(), Object::String(contents.as_bytes().to_vec()));
890        }
891
892        // Flags
893        if self.flags.bits() != 0 {
894            dict.insert("F".to_string(), Object::Integer(self.flags.bits() as i64));
895        }
896
897        // Line endings (for PolyLine)
898        if let Some((start, end)) = &self.line_endings {
899            if self.polygon_type == PolygonType::PolyLine {
900                dict.insert(
901                    "LE".to_string(),
902                    Object::Array(vec![
903                        Object::Name(start.pdf_name().to_string()),
904                        Object::Name(end.pdf_name().to_string()),
905                    ]),
906                );
907            }
908        }
909
910        // Stroke color (C entry)
911        if let Some(ref color) = self.color {
912            if let Some(color_array) = color.to_array() {
913                if !color_array.is_empty() {
914                    dict.insert(
915                        "C".to_string(),
916                        Object::Array(
917                            color_array
918                                .into_iter()
919                                .map(|v| Object::Real(v as f64))
920                                .collect(),
921                        ),
922                    );
923                }
924            }
925        }
926
927        // Interior color (IC entry)
928        if let Some(ref color) = self.interior_color {
929            if let Some(color_array) = color.to_array() {
930                if !color_array.is_empty() {
931                    dict.insert(
932                        "IC".to_string(),
933                        Object::Array(
934                            color_array
935                                .into_iter()
936                                .map(|v| Object::Real(v as f64))
937                                .collect(),
938                        ),
939                    );
940                }
941            }
942        }
943
944        // Opacity
945        if let Some(opacity) = self.opacity {
946            dict.insert("CA".to_string(), Object::Real(opacity as f64));
947        }
948
949        // Border style (BS entry)
950        if self.border_style.is_some() || self.border_width.is_some() {
951            let mut bs = HashMap::new();
952            bs.insert("Type".to_string(), Object::Name("Border".to_string()));
953            if let Some(width) = self.border_width {
954                bs.insert("W".to_string(), Object::Real(width as f64));
955            }
956            if let Some(ref style) = self.border_style {
957                let style_char = match style {
958                    BorderStyleType::Solid => "S",
959                    BorderStyleType::Dashed => "D",
960                    BorderStyleType::Beveled => "B",
961                    BorderStyleType::Inset => "I",
962                    BorderStyleType::Underline => "U",
963                };
964                bs.insert("S".to_string(), Object::Name(style_char.to_string()));
965            }
966            dict.insert("BS".to_string(), Object::Dictionary(bs));
967        }
968
969        // Border effect (BE entry)
970        if let Some(ref be) = self.border_effect {
971            let mut be_dict = HashMap::new();
972            be_dict.insert("S".to_string(), Object::Name(be.style.pdf_name().to_string()));
973            if be.intensity > 0.0 {
974                be_dict.insert("I".to_string(), Object::Real(be.intensity as f64));
975            }
976            dict.insert("BE".to_string(), Object::Dictionary(be_dict));
977        }
978
979        // Author
980        if let Some(ref author) = self.author {
981            dict.insert("T".to_string(), Object::String(author.as_bytes().to_vec()));
982        }
983
984        // Subject
985        if let Some(ref subject) = self.subject {
986            dict.insert("Subj".to_string(), Object::String(subject.as_bytes().to_vec()));
987        }
988
989        dict
990    }
991}
992
993#[cfg(test)]
994mod tests {
995    use super::*;
996    use crate::annotation_types::BorderEffectStyle;
997
998    // ========== Line Annotation Tests ==========
999
1000    #[test]
1001    fn test_line_annotation_new() {
1002        let line = LineAnnotation::new((100.0, 100.0), (200.0, 200.0));
1003        assert_eq!(line.start, (100.0, 100.0));
1004        assert_eq!(line.end, (200.0, 200.0));
1005        assert_eq!(line.line_endings, (LineEndingStyle::None, LineEndingStyle::None));
1006    }
1007
1008    #[test]
1009    fn test_line_annotation_arrow() {
1010        let line = LineAnnotation::arrow((0.0, 0.0), (100.0, 100.0));
1011        assert_eq!(line.line_endings.1, LineEndingStyle::OpenArrow);
1012    }
1013
1014    #[test]
1015    fn test_line_annotation_double_arrow() {
1016        let line = LineAnnotation::double_arrow((0.0, 0.0), (100.0, 100.0));
1017        assert_eq!(line.line_endings.0, LineEndingStyle::OpenArrow);
1018        assert_eq!(line.line_endings.1, LineEndingStyle::OpenArrow);
1019    }
1020
1021    #[test]
1022    fn test_line_annotation_build() {
1023        let line = LineAnnotation::new((100.0, 200.0), (300.0, 400.0))
1024            .with_stroke_color(1.0, 0.0, 0.0)
1025            .with_line_endings(LineEndingStyle::None, LineEndingStyle::ClosedArrow);
1026
1027        let dict = line.build(&[]);
1028
1029        assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
1030        assert_eq!(dict.get("Subtype"), Some(&Object::Name("Line".to_string())));
1031        assert!(dict.contains_key("L")); // Line coordinates
1032        assert!(dict.contains_key("LE")); // Line endings
1033        assert!(dict.contains_key("C")); // Color
1034    }
1035
1036    #[test]
1037    fn test_line_with_caption() {
1038        let line = LineAnnotation::new((100.0, 100.0), (200.0, 100.0))
1039            .with_caption("10 cm")
1040            .with_caption_position(CaptionPosition::Top);
1041
1042        assert!(line.caption);
1043        assert_eq!(line.contents, Some("10 cm".to_string()));
1044
1045        let dict = line.build(&[]);
1046        assert_eq!(dict.get("Cap"), Some(&Object::Boolean(true)));
1047        assert_eq!(dict.get("CP"), Some(&Object::Name("Top".to_string())));
1048    }
1049
1050    // ========== Shape Annotation Tests ==========
1051
1052    #[test]
1053    fn test_shape_annotation_square() {
1054        let rect = Rect::new(72.0, 600.0, 100.0, 80.0);
1055        let shape = ShapeAnnotation::square(rect);
1056
1057        assert_eq!(shape.shape_type, ShapeType::Square);
1058    }
1059
1060    #[test]
1061    fn test_shape_annotation_circle() {
1062        let rect = Rect::new(72.0, 600.0, 100.0, 100.0);
1063        let shape = ShapeAnnotation::circle(rect);
1064
1065        assert_eq!(shape.shape_type, ShapeType::Circle);
1066    }
1067
1068    #[test]
1069    fn test_shape_annotation_build() {
1070        let rect = Rect::new(100.0, 500.0, 150.0, 100.0);
1071        let shape = ShapeAnnotation::square(rect)
1072            .with_stroke_color(0.0, 0.0, 1.0)
1073            .with_fill_color(0.8, 0.8, 1.0)
1074            .with_border_width(2.0);
1075
1076        let dict = shape.build(&[]);
1077
1078        assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
1079        assert_eq!(dict.get("Subtype"), Some(&Object::Name("Square".to_string())));
1080        assert!(dict.contains_key("C")); // Stroke color
1081        assert!(dict.contains_key("IC")); // Interior color
1082        assert!(dict.contains_key("BS")); // Border style
1083    }
1084
1085    #[test]
1086    fn test_shape_annotation_circle_build() {
1087        let rect = Rect::new(200.0, 400.0, 80.0, 80.0);
1088        let shape = ShapeAnnotation::circle(rect).with_stroke_color(1.0, 0.0, 0.0);
1089
1090        let dict = shape.build(&[]);
1091
1092        assert_eq!(dict.get("Subtype"), Some(&Object::Name("Circle".to_string())));
1093    }
1094
1095    // ========== Polygon Annotation Tests ==========
1096
1097    #[test]
1098    fn test_polygon_annotation_triangle() {
1099        let vertices = vec![(100.0, 100.0), (150.0, 200.0), (50.0, 200.0)];
1100        let polygon = PolygonAnnotation::polygon(vertices.clone());
1101
1102        assert_eq!(polygon.vertices, vertices);
1103        assert_eq!(polygon.polygon_type, PolygonType::Polygon);
1104    }
1105
1106    #[test]
1107    fn test_polyline_annotation() {
1108        let vertices = vec![(100.0, 100.0), (200.0, 150.0), (300.0, 100.0)];
1109        let polyline = PolygonAnnotation::polyline(vertices);
1110
1111        assert_eq!(polyline.polygon_type, PolygonType::PolyLine);
1112    }
1113
1114    #[test]
1115    fn test_polygon_build() {
1116        let vertices = vec![(100.0, 100.0), (200.0, 100.0), (150.0, 200.0)];
1117        let polygon = PolygonAnnotation::polygon(vertices)
1118            .with_stroke_color(0.0, 0.5, 0.0)
1119            .with_fill_color(0.8, 1.0, 0.8);
1120
1121        let dict = polygon.build(&[]);
1122
1123        assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
1124        assert_eq!(dict.get("Subtype"), Some(&Object::Name("Polygon".to_string())));
1125        assert!(dict.contains_key("Vertices"));
1126        assert!(dict.contains_key("C")); // Stroke color
1127        assert!(dict.contains_key("IC")); // Interior color
1128    }
1129
1130    #[test]
1131    fn test_polyline_with_line_endings() {
1132        let vertices = vec![(100.0, 100.0), (200.0, 150.0), (300.0, 100.0)];
1133        let polyline = PolygonAnnotation::polyline(vertices)
1134            .with_line_endings(LineEndingStyle::Circle, LineEndingStyle::OpenArrow);
1135
1136        let dict = polyline.build(&[]);
1137
1138        assert_eq!(dict.get("Subtype"), Some(&Object::Name("PolyLine".to_string())));
1139        assert!(dict.contains_key("LE")); // Line endings
1140    }
1141
1142    #[test]
1143    fn test_polygon_calculate_rect() {
1144        let vertices = vec![(50.0, 50.0), (150.0, 50.0), (100.0, 150.0)];
1145        let polygon = PolygonAnnotation::polygon(vertices);
1146
1147        let rect = polygon.calculate_rect();
1148
1149        // Should contain all vertices with margin
1150        assert!(rect.x < 50.0);
1151        assert!(rect.y < 50.0);
1152        assert!(rect.x + rect.width > 150.0);
1153        assert!(rect.y + rect.height > 150.0);
1154    }
1155
1156    // ========== Additional Coverage Tests ==========
1157
1158    // --- CaptionPosition tests ---
1159
1160    #[test]
1161    fn test_caption_position_pdf_name() {
1162        assert_eq!(CaptionPosition::Inline.pdf_name(), "Inline");
1163        assert_eq!(CaptionPosition::Top.pdf_name(), "Top");
1164    }
1165
1166    #[test]
1167    fn test_caption_position_default() {
1168        let pos = CaptionPosition::default();
1169        assert_eq!(pos, CaptionPosition::Inline);
1170    }
1171
1172    // --- ShapeType tests ---
1173
1174    #[test]
1175    fn test_shape_type_pdf_name() {
1176        assert_eq!(ShapeType::Square.pdf_name(), "Square");
1177        assert_eq!(ShapeType::Circle.pdf_name(), "Circle");
1178    }
1179
1180    // --- PolygonType tests ---
1181
1182    #[test]
1183    fn test_polygon_type_pdf_name() {
1184        assert_eq!(PolygonType::Polygon.pdf_name(), "Polygon");
1185        assert_eq!(PolygonType::PolyLine.pdf_name(), "PolyLine");
1186    }
1187
1188    // --- LineAnnotation builder method tests ---
1189
1190    #[test]
1191    fn test_line_annotation_dimension() {
1192        let line = LineAnnotation::dimension((50.0, 50.0), (200.0, 50.0), 20.0);
1193        assert_eq!(line.line_endings.0, LineEndingStyle::OpenArrow);
1194        assert_eq!(line.line_endings.1, LineEndingStyle::OpenArrow);
1195        assert_eq!(line.leader_line, Some(20.0));
1196    }
1197
1198    #[test]
1199    fn test_line_annotation_with_stroke_color() {
1200        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_stroke_color(1.0, 0.0, 0.0);
1201        match line.color {
1202            Some(AnnotationColor::Rgb(r, g, b)) => {
1203                assert!((r - 1.0).abs() < 0.001);
1204                assert!((g - 0.0).abs() < 0.001);
1205                assert!((b - 0.0).abs() < 0.001);
1206            },
1207            _ => panic!("Expected RGB color"),
1208        }
1209    }
1210
1211    #[test]
1212    fn test_line_annotation_with_fill_color() {
1213        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_fill_color(0.0, 1.0, 0.0);
1214        assert!(line.interior_color.is_some());
1215    }
1216
1217    #[test]
1218    fn test_line_annotation_with_line_width() {
1219        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_line_width(3.0);
1220        assert_eq!(line.line_width, Some(3.0));
1221    }
1222
1223    #[test]
1224    fn test_line_annotation_with_border_style() {
1225        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0))
1226            .with_border_style(BorderStyleType::Dashed);
1227        assert_eq!(line.border_style, Some(BorderStyleType::Dashed));
1228    }
1229
1230    #[test]
1231    fn test_line_annotation_with_leader_line() {
1232        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_leader_line(15.0);
1233        assert_eq!(line.leader_line, Some(15.0));
1234    }
1235
1236    #[test]
1237    fn test_line_annotation_with_leader_offset() {
1238        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_leader_offset(5.0);
1239        assert_eq!(line.leader_line_offset, Some(5.0));
1240    }
1241
1242    #[test]
1243    fn test_line_annotation_with_opacity() {
1244        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_opacity(0.5);
1245        assert_eq!(line.opacity, Some(0.5));
1246    }
1247
1248    #[test]
1249    fn test_line_annotation_opacity_clamped() {
1250        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_opacity(1.5);
1251        assert_eq!(line.opacity, Some(1.0)); // Clamped to 1.0
1252
1253        let line2 = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_opacity(-0.5);
1254        assert_eq!(line2.opacity, Some(0.0)); // Clamped to 0.0
1255    }
1256
1257    #[test]
1258    fn test_line_annotation_with_author() {
1259        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_author("John");
1260        assert_eq!(line.author, Some("John".to_string()));
1261    }
1262
1263    #[test]
1264    fn test_line_annotation_with_subject() {
1265        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_subject("Measurement");
1266        assert_eq!(line.subject, Some("Measurement".to_string()));
1267    }
1268
1269    #[test]
1270    fn test_line_annotation_with_flags() {
1271        let flags = AnnotationFlags::new(AnnotationFlags::PRINT | AnnotationFlags::LOCKED);
1272        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_flags(flags);
1273        assert_eq!(line.flags.bits(), AnnotationFlags::PRINT | AnnotationFlags::LOCKED);
1274    }
1275
1276    #[test]
1277    fn test_line_annotation_calculate_rect() {
1278        let line = LineAnnotation::new((50.0, 100.0), (200.0, 300.0));
1279        let rect = line.calculate_rect();
1280        // Should encompass both points with margin
1281        assert!(rect.x < 50.0);
1282        assert!(rect.y < 100.0);
1283        assert!(rect.x as f64 + rect.width as f64 > 200.0);
1284        assert!(rect.y as f64 + rect.height as f64 > 300.0);
1285    }
1286
1287    #[test]
1288    fn test_line_annotation_build_full() {
1289        let line = LineAnnotation::new((100.0, 200.0), (300.0, 400.0))
1290            .with_stroke_color(1.0, 0.0, 0.0)
1291            .with_fill_color(0.0, 1.0, 0.0)
1292            .with_line_width(2.0)
1293            .with_border_style(BorderStyleType::Solid)
1294            .with_opacity(0.8)
1295            .with_leader_line(10.0)
1296            .with_leader_offset(5.0)
1297            .with_caption("Test caption")
1298            .with_caption_position(CaptionPosition::Top)
1299            .with_author("Author")
1300            .with_subject("Subject")
1301            .with_line_endings(LineEndingStyle::OpenArrow, LineEndingStyle::ClosedArrow);
1302
1303        let dict = line.build(&[]);
1304
1305        assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
1306        assert_eq!(dict.get("Subtype"), Some(&Object::Name("Line".to_string())));
1307        assert!(dict.contains_key("L"));
1308        assert!(dict.contains_key("LE"));
1309        assert!(dict.contains_key("C"));
1310        assert!(dict.contains_key("IC"));
1311        assert!(dict.contains_key("CA"));
1312        assert!(dict.contains_key("BS"));
1313        assert!(dict.contains_key("LL"));
1314        assert!(dict.contains_key("LLO"));
1315        assert!(dict.contains_key("Cap"));
1316        assert!(dict.contains_key("CP"));
1317        assert!(dict.contains_key("Contents"));
1318        assert!(dict.contains_key("T")); // Author
1319        assert!(dict.contains_key("Subj")); // Subject
1320        assert!(dict.contains_key("F")); // Flags
1321    }
1322
1323    #[test]
1324    fn test_line_annotation_build_leader_line_extension() {
1325        let mut line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0));
1326        line.leader_line_extension = Some(5.0);
1327        let dict = line.build(&[]);
1328        assert_eq!(dict.get("LLE"), Some(&Object::Real(5.0)));
1329    }
1330
1331    #[test]
1332    fn test_line_annotation_build_no_caption_cp() {
1333        // Without caption enabled, CP should not appear
1334        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0))
1335            .with_caption_position(CaptionPosition::Top);
1336        // caption is false
1337        let dict = line.build(&[]);
1338        assert!(!dict.contains_key("Cap"));
1339        assert!(!dict.contains_key("CP"));
1340    }
1341
1342    #[test]
1343    fn test_line_annotation_build_caption_inline_no_cp() {
1344        // With caption and inline position, CP should not appear
1345        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_caption("Test");
1346        // caption_position defaults to Inline
1347        let dict = line.build(&[]);
1348        assert!(dict.contains_key("Cap"));
1349        assert!(!dict.contains_key("CP")); // Inline is default, don't write CP
1350    }
1351
1352    #[test]
1353    fn test_line_annotation_build_all_border_styles() {
1354        for style in &[
1355            BorderStyleType::Solid,
1356            BorderStyleType::Dashed,
1357            BorderStyleType::Beveled,
1358            BorderStyleType::Inset,
1359            BorderStyleType::Underline,
1360        ] {
1361            let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0)).with_border_style(*style);
1362            let dict = line.build(&[]);
1363            assert!(dict.contains_key("BS"));
1364            match dict.get("BS") {
1365                Some(Object::Dictionary(bs)) => {
1366                    assert!(bs.contains_key("S"));
1367                },
1368                _ => panic!("Expected BS dictionary"),
1369            }
1370        }
1371    }
1372
1373    #[test]
1374    fn test_line_annotation_build_no_line_endings() {
1375        // Default line endings are None/None, so LE should not appear
1376        let line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0));
1377        let dict = line.build(&[]);
1378        assert!(!dict.contains_key("LE"));
1379    }
1380
1381    // --- ShapeAnnotation builder method tests ---
1382
1383    #[test]
1384    fn test_shape_annotation_with_no_fill() {
1385        let shape = ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0))
1386            .with_fill_color(1.0, 0.0, 0.0)
1387            .with_no_fill();
1388        assert!(shape.interior_color.is_none());
1389    }
1390
1391    #[test]
1392    fn test_shape_annotation_with_border_width() {
1393        let shape =
1394            ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0)).with_border_width(3.0);
1395        assert_eq!(shape.border_width, Some(3.0));
1396    }
1397
1398    #[test]
1399    fn test_shape_annotation_with_border_style() {
1400        let shape = ShapeAnnotation::circle(Rect::new(0.0, 0.0, 100.0, 100.0))
1401            .with_border_style(BorderStyleType::Dashed);
1402        assert_eq!(shape.border_style, Some(BorderStyleType::Dashed));
1403    }
1404
1405    #[test]
1406    fn test_shape_annotation_with_border_effect() {
1407        let effect = BorderEffect {
1408            style: BorderEffectStyle::Cloudy,
1409            intensity: 1.5,
1410        };
1411        let shape =
1412            ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0)).with_border_effect(effect);
1413        assert!(shape.border_effect.is_some());
1414    }
1415
1416    #[test]
1417    fn test_shape_annotation_with_opacity() {
1418        let shape = ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0)).with_opacity(0.5);
1419        assert_eq!(shape.opacity, Some(0.5));
1420    }
1421
1422    #[test]
1423    fn test_shape_annotation_opacity_clamped() {
1424        let shape = ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0)).with_opacity(2.0);
1425        assert_eq!(shape.opacity, Some(1.0));
1426    }
1427
1428    #[test]
1429    fn test_shape_annotation_with_contents() {
1430        let shape =
1431            ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0)).with_contents("A note");
1432        assert_eq!(shape.contents, Some("A note".to_string()));
1433    }
1434
1435    #[test]
1436    fn test_shape_annotation_with_author() {
1437        let shape = ShapeAnnotation::circle(Rect::new(0.0, 0.0, 100.0, 100.0)).with_author("Jane");
1438        assert_eq!(shape.author, Some("Jane".to_string()));
1439    }
1440
1441    #[test]
1442    fn test_shape_annotation_with_subject() {
1443        let shape =
1444            ShapeAnnotation::circle(Rect::new(0.0, 0.0, 100.0, 100.0)).with_subject("Review");
1445        assert_eq!(shape.subject, Some("Review".to_string()));
1446    }
1447
1448    #[test]
1449    fn test_shape_annotation_with_flags() {
1450        let flags = AnnotationFlags::new(AnnotationFlags::HIDDEN);
1451        let shape = ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0)).with_flags(flags);
1452        assert_eq!(shape.flags.bits(), AnnotationFlags::HIDDEN);
1453    }
1454
1455    #[test]
1456    fn test_shape_annotation_build_full() {
1457        let effect = BorderEffect {
1458            style: BorderEffectStyle::Cloudy,
1459            intensity: 1.0,
1460        };
1461        let shape = ShapeAnnotation::square(Rect::new(50.0, 50.0, 200.0, 150.0))
1462            .with_stroke_color(0.0, 0.0, 1.0)
1463            .with_fill_color(0.9, 0.9, 1.0)
1464            .with_border_width(2.0)
1465            .with_border_style(BorderStyleType::Solid)
1466            .with_border_effect(effect)
1467            .with_opacity(0.7)
1468            .with_contents("Comment text")
1469            .with_author("Author")
1470            .with_subject("Subject");
1471
1472        let dict = shape.build(&[]);
1473
1474        assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
1475        assert_eq!(dict.get("Subtype"), Some(&Object::Name("Square".to_string())));
1476        assert!(dict.contains_key("Rect"));
1477        assert!(dict.contains_key("C"));
1478        assert!(dict.contains_key("IC"));
1479        assert!(dict.contains_key("CA"));
1480        assert!(dict.contains_key("BS"));
1481        assert!(dict.contains_key("BE"));
1482        assert!(dict.contains_key("Contents"));
1483        assert!(dict.contains_key("T"));
1484        assert!(dict.contains_key("Subj"));
1485        assert!(dict.contains_key("F"));
1486    }
1487
1488    #[test]
1489    fn test_shape_annotation_build_border_effect_no_intensity() {
1490        let effect = BorderEffect {
1491            style: BorderEffectStyle::None,
1492            intensity: 0.0,
1493        };
1494        let shape =
1495            ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0)).with_border_effect(effect);
1496
1497        let dict = shape.build(&[]);
1498        match dict.get("BE") {
1499            Some(Object::Dictionary(be)) => {
1500                // Intensity is 0.0, should not appear
1501                assert!(!be.contains_key("I"));
1502            },
1503            _ => panic!("Expected BE dictionary"),
1504        }
1505    }
1506
1507    #[test]
1508    fn test_shape_annotation_rect_differences() {
1509        let mut shape = ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0));
1510        shape.rect_differences = Some([5.0, 5.0, 5.0, 5.0]);
1511
1512        let dict = shape.build(&[]);
1513        assert!(dict.contains_key("RD"));
1514        match dict.get("RD") {
1515            Some(Object::Array(arr)) => assert_eq!(arr.len(), 4),
1516            _ => panic!("Expected RD array"),
1517        }
1518    }
1519
1520    #[test]
1521    fn test_shape_annotation_build_all_border_styles() {
1522        for style in &[
1523            BorderStyleType::Solid,
1524            BorderStyleType::Dashed,
1525            BorderStyleType::Beveled,
1526            BorderStyleType::Inset,
1527            BorderStyleType::Underline,
1528        ] {
1529            let shape = ShapeAnnotation::circle(Rect::new(0.0, 0.0, 100.0, 100.0))
1530                .with_border_style(*style);
1531            let dict = shape.build(&[]);
1532            assert!(dict.contains_key("BS"));
1533        }
1534    }
1535
1536    // --- PolygonAnnotation builder method tests ---
1537
1538    #[test]
1539    fn test_polygon_with_stroke_color() {
1540        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 0.0), (50.0, 100.0)])
1541            .with_stroke_color(1.0, 0.0, 0.0);
1542        assert!(polygon.color.is_some());
1543    }
1544
1545    #[test]
1546    fn test_polygon_with_fill_color() {
1547        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 0.0), (50.0, 100.0)])
1548            .with_fill_color(0.0, 1.0, 0.0);
1549        assert!(polygon.interior_color.is_some());
1550    }
1551
1552    #[test]
1553    fn test_polygon_with_no_fill() {
1554        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 0.0), (50.0, 100.0)])
1555            .with_fill_color(1.0, 0.0, 0.0)
1556            .with_no_fill();
1557        assert!(polygon.interior_color.is_none());
1558    }
1559
1560    #[test]
1561    fn test_polygon_with_border_width() {
1562        let polygon =
1563            PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 0.0)]).with_border_width(3.0);
1564        assert_eq!(polygon.border_width, Some(3.0));
1565    }
1566
1567    #[test]
1568    fn test_polygon_with_border_style() {
1569        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 0.0)])
1570            .with_border_style(BorderStyleType::Beveled);
1571        assert_eq!(polygon.border_style, Some(BorderStyleType::Beveled));
1572    }
1573
1574    #[test]
1575    fn test_polygon_with_border_effect() {
1576        let effect = BorderEffect {
1577            style: BorderEffectStyle::Cloudy,
1578            intensity: 2.0,
1579        };
1580        let polygon =
1581            PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 0.0)]).with_border_effect(effect);
1582        assert!(polygon.border_effect.is_some());
1583    }
1584
1585    #[test]
1586    fn test_polygon_with_opacity() {
1587        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0)]).with_opacity(0.3);
1588        assert_eq!(polygon.opacity, Some(0.3));
1589    }
1590
1591    #[test]
1592    fn test_polygon_opacity_clamped() {
1593        let polygon = PolygonAnnotation::polygon(vec![]).with_opacity(-1.0);
1594        assert_eq!(polygon.opacity, Some(0.0));
1595    }
1596
1597    #[test]
1598    fn test_polygon_with_contents() {
1599        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0)]).with_contents("Note");
1600        assert_eq!(polygon.contents, Some("Note".to_string()));
1601    }
1602
1603    #[test]
1604    fn test_polygon_with_author() {
1605        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0)]).with_author("Bob");
1606        assert_eq!(polygon.author, Some("Bob".to_string()));
1607    }
1608
1609    #[test]
1610    fn test_polygon_with_subject() {
1611        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0)]).with_subject("Area");
1612        assert_eq!(polygon.subject, Some("Area".to_string()));
1613    }
1614
1615    #[test]
1616    fn test_polygon_with_flags() {
1617        let flags = AnnotationFlags::new(AnnotationFlags::READ_ONLY);
1618        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0)]).with_flags(flags);
1619        assert_eq!(polygon.flags.bits(), AnnotationFlags::READ_ONLY);
1620    }
1621
1622    #[test]
1623    fn test_polygon_calculate_rect_empty() {
1624        let polygon = PolygonAnnotation::polygon(vec![]);
1625        let rect = polygon.calculate_rect();
1626        assert_eq!(rect.x, 0.0);
1627        assert_eq!(rect.y, 0.0);
1628        assert_eq!(rect.width, 0.0);
1629        assert_eq!(rect.height, 0.0);
1630    }
1631
1632    #[test]
1633    fn test_polygon_build_full() {
1634        let effect = BorderEffect {
1635            style: BorderEffectStyle::Cloudy,
1636            intensity: 1.5,
1637        };
1638        let polygon =
1639            PolygonAnnotation::polygon(vec![(100.0, 100.0), (200.0, 100.0), (150.0, 200.0)])
1640                .with_stroke_color(0.0, 0.0, 1.0)
1641                .with_fill_color(0.8, 0.8, 1.0)
1642                .with_border_width(2.0)
1643                .with_border_style(BorderStyleType::Dashed)
1644                .with_border_effect(effect)
1645                .with_opacity(0.9)
1646                .with_contents("Comment")
1647                .with_author("Author")
1648                .with_subject("Subject");
1649
1650        let dict = polygon.build(&[]);
1651
1652        assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
1653        assert_eq!(dict.get("Subtype"), Some(&Object::Name("Polygon".to_string())));
1654        assert!(dict.contains_key("Vertices"));
1655        assert!(dict.contains_key("C"));
1656        assert!(dict.contains_key("IC"));
1657        assert!(dict.contains_key("CA"));
1658        assert!(dict.contains_key("BS"));
1659        assert!(dict.contains_key("BE"));
1660        assert!(dict.contains_key("Contents"));
1661        assert!(dict.contains_key("T"));
1662        assert!(dict.contains_key("Subj"));
1663        assert!(dict.contains_key("F"));
1664    }
1665
1666    #[test]
1667    fn test_polyline_build_full() {
1668        let polyline = PolygonAnnotation::polyline(vec![(0.0, 0.0), (50.0, 100.0), (100.0, 0.0)])
1669            .with_line_endings(LineEndingStyle::Square, LineEndingStyle::Diamond)
1670            .with_stroke_color(1.0, 0.0, 0.0);
1671
1672        let dict = polyline.build(&[]);
1673
1674        assert_eq!(dict.get("Subtype"), Some(&Object::Name("PolyLine".to_string())));
1675        assert!(dict.contains_key("LE"));
1676        assert!(dict.contains_key("Vertices"));
1677    }
1678
1679    #[test]
1680    fn test_polygon_line_endings_ignored_for_polygon() {
1681        // Line endings should only appear for PolyLine, not Polygon
1682        let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 0.0), (50.0, 100.0)])
1683            .with_line_endings(LineEndingStyle::OpenArrow, LineEndingStyle::ClosedArrow);
1684
1685        let dict = polygon.build(&[]);
1686        // LE should NOT be in the dict for Polygon type
1687        assert!(!dict.contains_key("LE"));
1688    }
1689
1690    #[test]
1691    fn test_polygon_build_all_border_styles() {
1692        for style in &[
1693            BorderStyleType::Solid,
1694            BorderStyleType::Dashed,
1695            BorderStyleType::Beveled,
1696            BorderStyleType::Inset,
1697            BorderStyleType::Underline,
1698        ] {
1699            let polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 100.0)])
1700                .with_border_style(*style);
1701            let dict = polygon.build(&[]);
1702            assert!(dict.contains_key("BS"));
1703        }
1704    }
1705
1706    #[test]
1707    fn test_polygon_build_border_effect_no_intensity() {
1708        let effect = BorderEffect {
1709            style: BorderEffectStyle::None,
1710            intensity: 0.0,
1711        };
1712        let polygon =
1713            PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 100.0)]).with_border_effect(effect);
1714        let dict = polygon.build(&[]);
1715        match dict.get("BE") {
1716            Some(Object::Dictionary(be)) => {
1717                assert!(!be.contains_key("I")); // intensity is 0
1718            },
1719            _ => panic!("Expected BE dictionary"),
1720        }
1721    }
1722
1723    #[test]
1724    fn test_polygon_build_vertices_array() {
1725        let polygon = PolygonAnnotation::polygon(vec![(10.0, 20.0), (30.0, 40.0), (50.0, 60.0)]);
1726        let dict = polygon.build(&[]);
1727        match dict.get("Vertices") {
1728            Some(Object::Array(arr)) => {
1729                // 3 vertices x 2 coords = 6 values
1730                assert_eq!(arr.len(), 6);
1731                assert_eq!(arr[0], Object::Real(10.0));
1732                assert_eq!(arr[1], Object::Real(20.0));
1733                assert_eq!(arr[4], Object::Real(50.0));
1734                assert_eq!(arr[5], Object::Real(60.0));
1735            },
1736            _ => panic!("Expected Vertices array"),
1737        }
1738    }
1739
1740    #[test]
1741    fn test_line_annotation_build_l_entry() {
1742        let line = LineAnnotation::new((10.0, 20.0), (30.0, 40.0));
1743        let dict = line.build(&[]);
1744        match dict.get("L") {
1745            Some(Object::Array(arr)) => {
1746                assert_eq!(arr.len(), 4);
1747                assert_eq!(arr[0], Object::Real(10.0));
1748                assert_eq!(arr[1], Object::Real(20.0));
1749                assert_eq!(arr[2], Object::Real(30.0));
1750                assert_eq!(arr[3], Object::Real(40.0));
1751            },
1752            _ => panic!("Expected L array"),
1753        }
1754    }
1755
1756    #[test]
1757    fn test_shape_annotation_circle_rect_values() {
1758        let rect = Rect::new(100.0, 200.0, 50.0, 50.0);
1759        let shape = ShapeAnnotation::circle(rect);
1760        let dict = shape.build(&[]);
1761        match dict.get("Rect") {
1762            Some(Object::Array(arr)) => {
1763                assert_eq!(arr.len(), 4);
1764                assert_eq!(arr[0], Object::Real(100.0));
1765                assert_eq!(arr[1], Object::Real(200.0));
1766                assert_eq!(arr[2], Object::Real(150.0)); // x + width
1767                assert_eq!(arr[3], Object::Real(250.0)); // y + height
1768            },
1769            _ => panic!("Expected Rect array"),
1770        }
1771    }
1772
1773    #[test]
1774    fn test_shape_annotation_no_color() {
1775        let mut shape = ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0));
1776        shape.color = None;
1777        let dict = shape.build(&[]);
1778        assert!(!dict.contains_key("C"));
1779    }
1780
1781    #[test]
1782    fn test_line_annotation_no_color() {
1783        let mut line = LineAnnotation::new((0.0, 0.0), (100.0, 100.0));
1784        line.color = None;
1785        let dict = line.build(&[]);
1786        assert!(!dict.contains_key("C"));
1787    }
1788
1789    #[test]
1790    fn test_polygon_no_color() {
1791        let mut polygon = PolygonAnnotation::polygon(vec![(0.0, 0.0), (100.0, 100.0)]);
1792        polygon.color = None;
1793        let dict = polygon.build(&[]);
1794        assert!(!dict.contains_key("C"));
1795    }
1796
1797    #[test]
1798    fn test_shape_annotation_flags_zero() {
1799        let mut shape = ShapeAnnotation::square(Rect::new(0.0, 0.0, 100.0, 100.0));
1800        shape.flags = AnnotationFlags::new(0);
1801        let dict = shape.build(&[]);
1802        assert!(!dict.contains_key("F"));
1803    }
1804}