Skip to main content

oxidize_pdf/annotations/
polygon.rs

1//! Polygon and Polyline annotations for drawing multi-point shapes
2//!
3//! Implements ISO 32000-1 Section 12.5.6.9 (Polygon annotations) and
4//! Section 12.5.6.10 (Polyline annotations)
5
6use crate::annotations::{Annotation, AnnotationType};
7use crate::error::Result;
8use crate::geometry::{Point, Rectangle};
9use crate::graphics::Color;
10use crate::objects::{Dictionary, Object};
11
12/// Polygon annotation - displays a closed polygon on the page
13#[derive(Debug, Clone)]
14pub struct PolygonAnnotation {
15    /// Vertices of the polygon
16    pub vertices: Vec<Point>,
17    /// Line color
18    pub line_color: Option<Color>,
19    /// Fill color
20    pub fill_color: Option<Color>,
21    /// Line width in points
22    pub line_width: f64,
23    /// Border style
24    pub border_style: BorderStyle,
25    /// Opacity (0.0 to 1.0)
26    pub opacity: f64,
27}
28
29/// Polyline annotation - displays an open polyline on the page
30#[derive(Debug, Clone)]
31pub struct PolylineAnnotation {
32    /// Vertices of the polyline
33    pub vertices: Vec<Point>,
34    /// Line color
35    pub line_color: Option<Color>,
36    /// Line width in points
37    pub line_width: f64,
38    /// Line ending style at start
39    pub start_style: LineEndingStyle,
40    /// Line ending style at end
41    pub end_style: LineEndingStyle,
42    /// Interior color for line endings
43    pub interior_color: Option<Color>,
44    /// Border style
45    pub border_style: BorderStyle,
46    /// Opacity (0.0 to 1.0)
47    pub opacity: f64,
48}
49
50/// Line ending styles
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub enum LineEndingStyle {
53    None,
54    Square,
55    Circle,
56    Diamond,
57    OpenArrow,
58    ClosedArrow,
59    Butt,
60    ROpenArrow,
61    RClosedArrow,
62    Slash,
63}
64
65impl LineEndingStyle {
66    pub fn to_pdf_name(self) -> &'static str {
67        match self {
68            LineEndingStyle::None => "None",
69            LineEndingStyle::Square => "Square",
70            LineEndingStyle::Circle => "Circle",
71            LineEndingStyle::Diamond => "Diamond",
72            LineEndingStyle::OpenArrow => "OpenArrow",
73            LineEndingStyle::ClosedArrow => "ClosedArrow",
74            LineEndingStyle::Butt => "Butt",
75            LineEndingStyle::ROpenArrow => "ROpenArrow",
76            LineEndingStyle::RClosedArrow => "RClosedArrow",
77            LineEndingStyle::Slash => "Slash",
78        }
79    }
80}
81
82/// Border style
83#[derive(Debug, Clone)]
84pub struct BorderStyle {
85    /// Border width
86    pub width: f64,
87    /// Border style type
88    pub style: BorderStyleType,
89    /// Dash pattern for dashed borders
90    pub dash_pattern: Option<Vec<f64>>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
94pub enum BorderStyleType {
95    Solid,
96    Dashed,
97    Beveled,
98    Inset,
99    Underline,
100}
101
102impl BorderStyleType {
103    pub fn to_pdf_name(self) -> &'static str {
104        match self {
105            BorderStyleType::Solid => "S",
106            BorderStyleType::Dashed => "D",
107            BorderStyleType::Beveled => "B",
108            BorderStyleType::Inset => "I",
109            BorderStyleType::Underline => "U",
110        }
111    }
112}
113
114impl Default for BorderStyle {
115    fn default() -> Self {
116        Self {
117            width: 1.0,
118            style: BorderStyleType::Solid,
119            dash_pattern: None,
120        }
121    }
122}
123
124impl Default for PolygonAnnotation {
125    fn default() -> Self {
126        Self {
127            vertices: Vec::new(),
128            line_color: Some(Color::rgb(0.0, 0.0, 0.0)),
129            fill_color: None,
130            line_width: 1.0,
131            border_style: BorderStyle::default(),
132            opacity: 1.0,
133        }
134    }
135}
136
137impl Default for PolylineAnnotation {
138    fn default() -> Self {
139        Self {
140            vertices: Vec::new(),
141            line_color: Some(Color::rgb(0.0, 0.0, 0.0)),
142            line_width: 1.0,
143            start_style: LineEndingStyle::None,
144            end_style: LineEndingStyle::None,
145            interior_color: None,
146            border_style: BorderStyle::default(),
147            opacity: 1.0,
148        }
149    }
150}
151
152impl PolygonAnnotation {
153    /// Create a new polygon annotation
154    pub fn new(vertices: Vec<Point>) -> Self {
155        Self {
156            vertices,
157            ..Default::default()
158        }
159    }
160
161    /// Set line color
162    pub fn with_line_color(mut self, color: Option<Color>) -> Self {
163        self.line_color = color;
164        self
165    }
166
167    /// Set fill color
168    pub fn with_fill_color(mut self, color: Option<Color>) -> Self {
169        self.fill_color = color;
170        self
171    }
172
173    /// Set line width
174    pub fn with_line_width(mut self, width: f64) -> Self {
175        self.line_width = width;
176        self
177    }
178
179    /// Set border style
180    pub fn with_border_style(mut self, style: BorderStyle) -> Self {
181        self.border_style = style;
182        self
183    }
184
185    /// Set opacity
186    pub fn with_opacity(mut self, opacity: f64) -> Self {
187        self.opacity = opacity.clamp(0.0, 1.0);
188        self
189    }
190
191    /// Calculate bounding rectangle
192    pub fn calculate_rect(&self) -> Rectangle {
193        if self.vertices.is_empty() {
194            return Rectangle::new(Point::new(0.0, 0.0), Point::new(0.0, 0.0));
195        }
196
197        let mut min_x = self.vertices[0].x;
198        let mut min_y = self.vertices[0].y;
199        let mut max_x = self.vertices[0].x;
200        let mut max_y = self.vertices[0].y;
201
202        for vertex in &self.vertices[1..] {
203            min_x = min_x.min(vertex.x);
204            min_y = min_y.min(vertex.y);
205            max_x = max_x.max(vertex.x);
206            max_y = max_y.max(vertex.y);
207        }
208
209        // Add padding for line width
210        let padding = self.line_width;
211        Rectangle::new(
212            Point::new(min_x - padding, min_y - padding),
213            Point::new(max_x + padding, max_y + padding),
214        )
215    }
216
217    /// Convert to PDF annotation
218    pub fn to_annotation(&self) -> Result<Annotation> {
219        let rect = self.calculate_rect();
220        let mut annotation = Annotation::new(AnnotationType::Polygon, rect);
221
222        // Set vertices
223        let mut vertices_array = Vec::new();
224        for vertex in &self.vertices {
225            vertices_array.push(Object::Real(vertex.x));
226            vertices_array.push(Object::Real(vertex.y));
227        }
228        annotation
229            .properties
230            .set("Vertices", Object::Array(vertices_array));
231
232        // Set line color
233        if let Some(color) = &self.line_color {
234            annotation.properties.set(
235                "C",
236                Object::Array(vec![
237                    Object::Real(color.r()),
238                    Object::Real(color.g()),
239                    Object::Real(color.b()),
240                ]),
241            );
242        }
243
244        // Set fill color
245        if let Some(color) = &self.fill_color {
246            annotation.properties.set(
247                "IC",
248                Object::Array(vec![
249                    Object::Real(color.r()),
250                    Object::Real(color.g()),
251                    Object::Real(color.b()),
252                ]),
253            );
254        }
255
256        // Set border style
257        let mut bs_dict = Dictionary::new();
258        bs_dict.set("W", Object::Real(self.border_style.width));
259        bs_dict.set(
260            "S",
261            Object::Name(self.border_style.style.to_pdf_name().to_string()),
262        );
263
264        if let Some(dash) = &self.border_style.dash_pattern {
265            bs_dict.set(
266                "D",
267                Object::Array(dash.iter().map(|&d| Object::Real(d)).collect()),
268            );
269        }
270
271        annotation.properties.set("BS", Object::Dictionary(bs_dict));
272
273        // Set opacity if not default
274        if self.opacity < 1.0 {
275            annotation.properties.set("CA", Object::Real(self.opacity));
276        }
277
278        Ok(annotation)
279    }
280}
281
282impl PolylineAnnotation {
283    /// Create a new polyline annotation
284    pub fn new(vertices: Vec<Point>) -> Self {
285        Self {
286            vertices,
287            ..Default::default()
288        }
289    }
290
291    /// Set line color
292    pub fn with_line_color(mut self, color: Option<Color>) -> Self {
293        self.line_color = color;
294        self
295    }
296
297    /// Set line width
298    pub fn with_line_width(mut self, width: f64) -> Self {
299        self.line_width = width;
300        self
301    }
302
303    /// Set line ending styles
304    pub fn with_endings(mut self, start: LineEndingStyle, end: LineEndingStyle) -> Self {
305        self.start_style = start;
306        self.end_style = end;
307        self
308    }
309
310    /// Set interior color for line endings
311    pub fn with_interior_color(mut self, color: Option<Color>) -> Self {
312        self.interior_color = color;
313        self
314    }
315
316    /// Set border style
317    pub fn with_border_style(mut self, style: BorderStyle) -> Self {
318        self.border_style = style;
319        self
320    }
321
322    /// Set opacity
323    pub fn with_opacity(mut self, opacity: f64) -> Self {
324        self.opacity = opacity.clamp(0.0, 1.0);
325        self
326    }
327
328    /// Calculate bounding rectangle
329    pub fn calculate_rect(&self) -> Rectangle {
330        if self.vertices.is_empty() {
331            return Rectangle::new(Point::new(0.0, 0.0), Point::new(0.0, 0.0));
332        }
333
334        let mut min_x = self.vertices[0].x;
335        let mut min_y = self.vertices[0].y;
336        let mut max_x = self.vertices[0].x;
337        let mut max_y = self.vertices[0].y;
338
339        for vertex in &self.vertices[1..] {
340            min_x = min_x.min(vertex.x);
341            min_y = min_y.min(vertex.y);
342            max_x = max_x.max(vertex.x);
343            max_y = max_y.max(vertex.y);
344        }
345
346        // Add padding for line width
347        let padding = self.line_width;
348        Rectangle::new(
349            Point::new(min_x - padding, min_y - padding),
350            Point::new(max_x + padding, max_y + padding),
351        )
352    }
353
354    /// Convert to PDF annotation
355    pub fn to_annotation(&self) -> Result<Annotation> {
356        let rect = self.calculate_rect();
357        let mut annotation = Annotation::new(AnnotationType::PolyLine, rect);
358
359        // Set vertices
360        let mut vertices_array = Vec::new();
361        for vertex in &self.vertices {
362            vertices_array.push(Object::Real(vertex.x));
363            vertices_array.push(Object::Real(vertex.y));
364        }
365        annotation
366            .properties
367            .set("Vertices", Object::Array(vertices_array));
368
369        // Set line color
370        if let Some(color) = &self.line_color {
371            annotation.properties.set(
372                "C",
373                Object::Array(vec![
374                    Object::Real(color.r()),
375                    Object::Real(color.g()),
376                    Object::Real(color.b()),
377                ]),
378            );
379        }
380
381        // Set line endings
382        annotation.properties.set(
383            "LE",
384            Object::Array(vec![
385                Object::Name(self.start_style.to_pdf_name().to_string()),
386                Object::Name(self.end_style.to_pdf_name().to_string()),
387            ]),
388        );
389
390        // Set interior color for endings
391        if let Some(color) = &self.interior_color {
392            annotation.properties.set(
393                "IC",
394                Object::Array(vec![
395                    Object::Real(color.r()),
396                    Object::Real(color.g()),
397                    Object::Real(color.b()),
398                ]),
399            );
400        }
401
402        // Set border style
403        let mut bs_dict = Dictionary::new();
404        bs_dict.set("W", Object::Real(self.border_style.width));
405        bs_dict.set(
406            "S",
407            Object::Name(self.border_style.style.to_pdf_name().to_string()),
408        );
409
410        if let Some(dash) = &self.border_style.dash_pattern {
411            bs_dict.set(
412                "D",
413                Object::Array(dash.iter().map(|&d| Object::Real(d)).collect()),
414            );
415        }
416
417        annotation.properties.set("BS", Object::Dictionary(bs_dict));
418
419        // Set opacity if not default
420        if self.opacity < 1.0 {
421            annotation.properties.set("CA", Object::Real(self.opacity));
422        }
423
424        Ok(annotation)
425    }
426}
427
428/// Helper function to create a rectangle annotation from four points
429pub fn create_rectangle_polygon(
430    top_left: Point,
431    top_right: Point,
432    bottom_right: Point,
433    bottom_left: Point,
434) -> PolygonAnnotation {
435    PolygonAnnotation::new(vec![top_left, top_right, bottom_right, bottom_left])
436}
437
438/// Helper function to create a triangle annotation
439pub fn create_triangle(p1: Point, p2: Point, p3: Point) -> PolygonAnnotation {
440    PolygonAnnotation::new(vec![p1, p2, p3])
441}
442
443/// Helper function to create a regular polygon
444pub fn create_regular_polygon(center: Point, radius: f64, sides: usize) -> PolygonAnnotation {
445    let mut vertices = Vec::new();
446    let angle_step = 2.0 * std::f64::consts::PI / sides as f64;
447
448    for i in 0..sides {
449        let angle = i as f64 * angle_step;
450        let x = center.x + radius * angle.cos();
451        let y = center.y + radius * angle.sin();
452        vertices.push(Point::new(x, y));
453    }
454
455    PolygonAnnotation::new(vertices)
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    #[test]
463    fn test_polygon_creation() {
464        let vertices = vec![
465            Point::new(100.0, 100.0),
466            Point::new(200.0, 100.0),
467            Point::new(200.0, 200.0),
468            Point::new(100.0, 200.0),
469        ];
470
471        let polygon = PolygonAnnotation::new(vertices.clone());
472        assert_eq!(polygon.vertices.len(), 4);
473        assert_eq!(polygon.vertices[0], vertices[0]);
474    }
475
476    #[test]
477    fn test_polyline_creation() {
478        let vertices = vec![
479            Point::new(50.0, 50.0),
480            Point::new(100.0, 75.0),
481            Point::new(150.0, 50.0),
482            Point::new(200.0, 100.0),
483        ];
484
485        let polyline = PolylineAnnotation::new(vertices.clone());
486        assert_eq!(polyline.vertices.len(), 4);
487        assert_eq!(polyline.vertices[0], vertices[0]);
488    }
489
490    #[test]
491    fn test_polygon_with_fill() {
492        let vertices = vec![
493            Point::new(0.0, 0.0),
494            Point::new(100.0, 0.0),
495            Point::new(50.0, 86.6),
496        ];
497
498        let polygon = PolygonAnnotation::new(vertices)
499            .with_line_color(Some(Color::rgb(1.0, 0.0, 0.0)))
500            .with_fill_color(Some(Color::rgb(1.0, 1.0, 0.0)))
501            .with_line_width(2.0);
502
503        assert_eq!(polygon.line_color, Some(Color::rgb(1.0, 0.0, 0.0)));
504        assert_eq!(polygon.fill_color, Some(Color::rgb(1.0, 1.0, 0.0)));
505        assert_eq!(polygon.line_width, 2.0);
506    }
507
508    #[test]
509    fn test_polyline_with_endings() {
510        let vertices = vec![
511            Point::new(100.0, 100.0),
512            Point::new(200.0, 150.0),
513            Point::new(300.0, 100.0),
514        ];
515
516        let polyline = PolylineAnnotation::new(vertices)
517            .with_endings(LineEndingStyle::Circle, LineEndingStyle::ClosedArrow)
518            .with_interior_color(Some(Color::rgb(0.0, 1.0, 0.0)));
519
520        assert_eq!(polyline.start_style, LineEndingStyle::Circle);
521        assert_eq!(polyline.end_style, LineEndingStyle::ClosedArrow);
522        assert_eq!(polyline.interior_color, Some(Color::rgb(0.0, 1.0, 0.0)));
523    }
524
525    #[test]
526    fn test_calculate_rect() {
527        let vertices = vec![
528            Point::new(50.0, 50.0),
529            Point::new(150.0, 100.0),
530            Point::new(100.0, 200.0),
531        ];
532
533        let polygon = PolygonAnnotation::new(vertices).with_line_width(5.0);
534        let rect = polygon.calculate_rect();
535
536        // Should encompass all vertices with padding
537        assert_eq!(rect.lower_left.x, 45.0);
538        assert_eq!(rect.lower_left.y, 45.0);
539        assert_eq!(rect.upper_right.x, 155.0);
540        assert_eq!(rect.upper_right.y, 205.0);
541    }
542
543    #[test]
544    fn test_border_style() {
545        let border = BorderStyle {
546            width: 3.0,
547            style: BorderStyleType::Dashed,
548            dash_pattern: Some(vec![5.0, 3.0]),
549        };
550
551        assert_eq!(border.width, 3.0);
552        assert_eq!(border.style.to_pdf_name(), "D");
553        assert_eq!(border.dash_pattern, Some(vec![5.0, 3.0]));
554    }
555
556    #[test]
557    fn test_line_ending_styles() {
558        assert_eq!(LineEndingStyle::None.to_pdf_name(), "None");
559        assert_eq!(LineEndingStyle::Circle.to_pdf_name(), "Circle");
560        assert_eq!(LineEndingStyle::ClosedArrow.to_pdf_name(), "ClosedArrow");
561        assert_eq!(LineEndingStyle::Diamond.to_pdf_name(), "Diamond");
562    }
563
564    #[test]
565    fn test_polygon_to_annotation() {
566        let vertices = vec![
567            Point::new(100.0, 100.0),
568            Point::new(200.0, 100.0),
569            Point::new(150.0, 200.0),
570        ];
571
572        let polygon = PolygonAnnotation::new(vertices)
573            .with_line_color(Some(Color::rgb(0.0, 0.0, 1.0)))
574            .with_opacity(0.5);
575
576        let annotation = polygon.to_annotation();
577        assert!(annotation.is_ok());
578    }
579
580    #[test]
581    fn test_polyline_to_annotation() {
582        let vertices = vec![
583            Point::new(50.0, 50.0),
584            Point::new(100.0, 100.0),
585            Point::new(150.0, 50.0),
586        ];
587
588        let polyline = PolylineAnnotation::new(vertices)
589            .with_endings(LineEndingStyle::OpenArrow, LineEndingStyle::OpenArrow);
590
591        let annotation = polyline.to_annotation();
592        assert!(annotation.is_ok());
593    }
594
595    #[test]
596    fn test_create_rectangle_polygon() {
597        let rect_poly = create_rectangle_polygon(
598            Point::new(100.0, 200.0), // top_left
599            Point::new(200.0, 200.0), // top_right
600            Point::new(200.0, 100.0), // bottom_right
601            Point::new(100.0, 100.0), // bottom_left
602        );
603
604        assert_eq!(rect_poly.vertices.len(), 4);
605        assert_eq!(rect_poly.vertices[0], Point::new(100.0, 200.0));
606    }
607
608    #[test]
609    fn test_create_triangle() {
610        let triangle = create_triangle(
611            Point::new(100.0, 100.0),
612            Point::new(200.0, 100.0),
613            Point::new(150.0, 186.6),
614        );
615
616        assert_eq!(triangle.vertices.len(), 3);
617    }
618
619    #[test]
620    fn test_create_regular_polygon() {
621        // Create a hexagon
622        let hexagon = create_regular_polygon(Point::new(100.0, 100.0), 50.0, 6);
623        assert_eq!(hexagon.vertices.len(), 6);
624
625        // Create a pentagon
626        let pentagon = create_regular_polygon(Point::new(200.0, 200.0), 30.0, 5);
627        assert_eq!(pentagon.vertices.len(), 5);
628    }
629
630    #[test]
631    fn test_opacity_clamping() {
632        let polygon = PolygonAnnotation::new(vec![]).with_opacity(1.5); // Should be clamped to 1.0
633        assert_eq!(polygon.opacity, 1.0);
634
635        let polygon2 = PolygonAnnotation::new(vec![]).with_opacity(-0.5); // Should be clamped to 0.0
636        assert_eq!(polygon2.opacity, 0.0);
637    }
638
639    #[test]
640    fn test_empty_vertices() {
641        let polygon = PolygonAnnotation::new(vec![]);
642        let rect = polygon.calculate_rect();
643
644        assert_eq!(rect.lower_left, Point::new(0.0, 0.0));
645        assert_eq!(rect.upper_right, Point::new(0.0, 0.0));
646    }
647
648    #[test]
649    fn test_border_style_types() {
650        assert_eq!(BorderStyleType::Solid.to_pdf_name(), "S");
651        assert_eq!(BorderStyleType::Dashed.to_pdf_name(), "D");
652        assert_eq!(BorderStyleType::Beveled.to_pdf_name(), "B");
653        assert_eq!(BorderStyleType::Inset.to_pdf_name(), "I");
654        assert_eq!(BorderStyleType::Underline.to_pdf_name(), "U");
655    }
656}