Skip to main content

pdf_annot/
geometric.rs

1//! Geometric annotations: Line, Square, Circle, Polygon, PolyLine, Ink.
2
3// Copyright (c) 2026 Innovation Trigger B.V.
4//
5// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
6// the PDFluent Commercial Licence. See the LICENSE file in this repository --
7// that file travels with the copy you received, which a URL does not.
8
9use crate::annotation::Annotation;
10use crate::types::*;
11use pdf_syntax::object::dict::keys::*;
12use pdf_syntax::object::{Array, Dict, Name, Rect};
13
14/// A line annotation (ISO 32000-2 §12.5.6.7).
15#[derive(Debug)]
16pub struct LineAnnotation {
17    /// The line endpoints [x1, y1, x2, y2].
18    pub endpoints: Option<[f32; 4]>,
19    /// Line ending styles [start, end].
20    pub line_endings: (LineEnding, LineEnding),
21    /// Whether a caption is shown.
22    pub caption: bool,
23    /// Caption offset [horizontal, vertical].
24    pub caption_offset: Option<[f32; 2]>,
25    /// Leader line length.
26    pub leader_line: f32,
27    /// Leader line extension.
28    pub leader_line_extension: f32,
29    /// Leader line offset.
30    pub leader_line_offset: f32,
31}
32
33impl LineAnnotation {
34    /// Extract line annotation properties.
35    pub fn from_annot(annot: &Annotation<'_>) -> Self {
36        let dict = annot.dict();
37        let endpoints = dict.get::<Array<'_>>(L).and_then(|arr| {
38            let mut iter = arr.iter::<f32>();
39            Some([iter.next()?, iter.next()?, iter.next()?, iter.next()?])
40        });
41        let line_endings = parse_line_endings(dict);
42        let caption = dict.get::<bool>(CAP).unwrap_or(false);
43        let caption_offset = dict.get::<Array<'_>>(CO).and_then(|arr| {
44            let mut iter = arr.iter::<f32>();
45            Some([iter.next()?, iter.next()?])
46        });
47        let leader_line = dict.get::<f32>(LL).unwrap_or(0.0);
48        let leader_line_extension = dict.get::<f32>(LLE).unwrap_or(0.0);
49        let leader_line_offset = dict.get::<f32>(LLO).unwrap_or(0.0);
50        Self {
51            endpoints,
52            line_endings,
53            caption,
54            caption_offset,
55            leader_line,
56            leader_line_extension,
57            leader_line_offset,
58        }
59    }
60}
61
62/// A Square or Circle annotation.
63#[derive(Debug)]
64pub struct SquareCircleAnnotation {
65    /// `true` for Circle, `false` for Square.
66    pub is_circle: bool,
67    /// Rectangle differences.
68    pub rd: Option<Rect>,
69}
70
71impl SquareCircleAnnotation {
72    /// Extract square/circle annotation properties.
73    pub fn from_annot(annot: &Annotation<'_>) -> Self {
74        let is_circle = annot.annotation_type() == AnnotationType::Circle;
75        let rd = annot.dict().get::<Rect>(RD);
76        Self { is_circle, rd }
77    }
78}
79
80/// A Polygon or PolyLine annotation.
81#[derive(Debug)]
82pub struct PolygonAnnotation {
83    /// `true` for Polygon (closed), `false` for PolyLine (open).
84    pub closed: bool,
85    /// The vertices as pairs of coordinates.
86    pub vertices: Vec<f32>,
87    /// Line ending styles [start, end].
88    pub line_endings: (LineEnding, LineEnding),
89}
90
91impl PolygonAnnotation {
92    /// Extract polygon/polyline annotation properties.
93    pub fn from_annot(annot: &Annotation<'_>) -> Self {
94        let dict = annot.dict();
95        let closed = annot.annotation_type() == AnnotationType::Polygon;
96        let vertices = dict
97            .get::<Array<'_>>(VERTICES)
98            .map(|arr| arr.iter::<f32>().collect())
99            .unwrap_or_default();
100        let line_endings = parse_line_endings(dict);
101        Self {
102            closed,
103            vertices,
104            line_endings,
105        }
106    }
107}
108
109/// An Ink (freehand drawing) annotation.
110#[derive(Debug)]
111pub struct InkAnnotation {
112    /// List of ink strokes.
113    pub ink_list: Vec<Vec<f32>>,
114}
115
116impl InkAnnotation {
117    /// Extract ink annotation properties.
118    pub fn from_annot(annot: &Annotation<'_>) -> Self {
119        let dict = annot.dict();
120        let ink_list = dict
121            .get::<Array<'_>>(INKLIST)
122            .map(|outer| {
123                outer
124                    .iter::<Array<'_>>()
125                    .map(|inner| inner.iter::<f32>().collect())
126                    .collect()
127            })
128            .unwrap_or_default();
129        Self { ink_list }
130    }
131}
132
133/// Parse line ending styles from `/LE` array.
134fn parse_line_endings(dict: &Dict<'_>) -> (LineEnding, LineEnding) {
135    dict.get::<Array<'_>>(LE)
136        .and_then(|arr| {
137            let mut iter = arr.iter::<Name>();
138            let start = iter.next().map(|n| LineEnding::from_name(n.as_ref()))?;
139            let end = iter.next().map(|n| LineEnding::from_name(n.as_ref()))?;
140            Some((start, end))
141        })
142        .unwrap_or((LineEnding::None, LineEnding::None))
143}