Skip to main content

pdf_annot/
types.rs

1//! Core annotation types shared across all annotation categories.
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 pdf_syntax::object::dict::keys::*;
10use pdf_syntax::object::{Array, Dict, Name, Rect};
11
12/// The type of a PDF annotation (ISO 32000-2 Table 170).
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum AnnotationType {
15    /// A text annotation (sticky note).
16    Text,
17    /// A link annotation.
18    Link,
19    /// A free text annotation.
20    FreeText,
21    /// A line annotation.
22    Line,
23    /// A square annotation.
24    Square,
25    /// A circle annotation.
26    Circle,
27    /// A polygon annotation.
28    Polygon,
29    /// A polyline annotation.
30    PolyLine,
31    /// A highlight annotation.
32    Highlight,
33    /// An underline annotation.
34    Underline,
35    /// A squiggly underline annotation.
36    Squiggly,
37    /// A strikeout annotation.
38    StrikeOut,
39    /// A rubber stamp annotation.
40    Stamp,
41    /// A caret annotation.
42    Caret,
43    /// An ink (freehand) annotation.
44    Ink,
45    /// A popup annotation.
46    Popup,
47    /// A file attachment annotation.
48    FileAttachment,
49    /// A sound annotation.
50    Sound,
51    /// A widget annotation (form field).
52    Widget,
53    /// A watermark annotation.
54    Watermark,
55    /// A redaction annotation.
56    Redact,
57    /// A movie annotation.
58    Movie,
59    /// An unknown annotation type.
60    Unknown,
61}
62
63impl AnnotationType {
64    /// Parse an annotation type from a PDF `/Subtype` name.
65    pub fn from_name(name: &[u8]) -> Self {
66        match name {
67            b"Text" => Self::Text,
68            b"Link" => Self::Link,
69            b"FreeText" => Self::FreeText,
70            b"Line" => Self::Line,
71            b"Square" => Self::Square,
72            b"Circle" => Self::Circle,
73            b"Polygon" => Self::Polygon,
74            b"PolyLine" => Self::PolyLine,
75            b"Highlight" => Self::Highlight,
76            b"Underline" => Self::Underline,
77            b"Squiggly" => Self::Squiggly,
78            b"StrikeOut" => Self::StrikeOut,
79            b"Stamp" => Self::Stamp,
80            b"Caret" => Self::Caret,
81            b"Ink" => Self::Ink,
82            b"Popup" => Self::Popup,
83            b"FileAttachment" => Self::FileAttachment,
84            b"Sound" => Self::Sound,
85            b"Widget" => Self::Widget,
86            b"Watermark" => Self::Watermark,
87            b"Redact" => Self::Redact,
88            b"Movie" => Self::Movie,
89            _ => Self::Unknown,
90        }
91    }
92}
93
94/// Annotation flags (ISO 32000-2 Table 175).
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct AnnotationFlags(pub u32);
97
98impl AnnotationFlags {
99    /// Bit 1: Invisible.
100    pub fn invisible(&self) -> bool {
101        self.0 & (1 << 0) != 0
102    }
103    /// Bit 2: Hidden.
104    pub fn hidden(&self) -> bool {
105        self.0 & (1 << 1) != 0
106    }
107    /// Bit 3: Print.
108    pub fn print(&self) -> bool {
109        self.0 & (1 << 2) != 0
110    }
111    /// Bit 4: No zoom.
112    pub fn no_zoom(&self) -> bool {
113        self.0 & (1 << 3) != 0
114    }
115    /// Bit 5: No rotate.
116    pub fn no_rotate(&self) -> bool {
117        self.0 & (1 << 4) != 0
118    }
119    /// Bit 6: No view.
120    pub fn no_view(&self) -> bool {
121        self.0 & (1 << 5) != 0
122    }
123    /// Bit 7: Read only.
124    pub fn read_only(&self) -> bool {
125        self.0 & (1 << 6) != 0
126    }
127    /// Bit 8: Locked.
128    pub fn locked(&self) -> bool {
129        self.0 & (1 << 7) != 0
130    }
131    /// Bit 9: Toggle no view.
132    pub fn toggle_no_view(&self) -> bool {
133        self.0 & (1 << 8) != 0
134    }
135    /// Bit 10: Locked contents.
136    pub fn locked_contents(&self) -> bool {
137        self.0 & (1 << 9) != 0
138    }
139}
140
141/// Border style (ISO 32000-2 Table 168).
142#[derive(Debug, Clone)]
143pub struct BorderStyle {
144    /// Border width in points.
145    pub width: f32,
146    /// Border style type.
147    pub style: BorderStyleType,
148    /// Dash pattern.
149    pub dash_pattern: Vec<f32>,
150}
151
152/// Border style types.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum BorderStyleType {
155    /// Solid border.
156    Solid,
157    /// Dashed border.
158    Dashed,
159    /// Beveled border.
160    Beveled,
161    /// Inset border.
162    Inset,
163    /// Underline border.
164    Underline,
165}
166
167impl BorderStyle {
168    /// Parse a border style from a `/BS` dictionary.
169    pub fn from_dict(dict: &Dict<'_>) -> Self {
170        let width = dict.get::<f32>(W).unwrap_or(1.0);
171        let style = dict
172            .get::<Name>(S)
173            .map(|n| match n.as_ref() {
174                b"D" => BorderStyleType::Dashed,
175                b"B" => BorderStyleType::Beveled,
176                b"I" => BorderStyleType::Inset,
177                b"U" => BorderStyleType::Underline,
178                _ => BorderStyleType::Solid,
179            })
180            .unwrap_or(BorderStyleType::Solid);
181        let dash_pattern = dict
182            .get::<Array<'_>>(D)
183            .map(|arr| arr.iter::<f32>().collect())
184            .unwrap_or_else(|| vec![3.0]);
185        Self {
186            width,
187            style,
188            dash_pattern,
189        }
190    }
191}
192
193/// A color value.
194#[derive(Debug, Clone)]
195pub enum Color {
196    /// Transparent.
197    Transparent,
198    /// Grayscale.
199    Gray(f32),
200    /// RGB.
201    Rgb(f32, f32, f32),
202    /// CMYK.
203    Cmyk(f32, f32, f32, f32),
204}
205
206impl Color {
207    /// Parse a color from an annotation color array.
208    pub fn from_array(arr: &Array<'_>) -> Self {
209        let values: Vec<f32> = arr.iter::<f32>().collect();
210        match values.len() {
211            0 => Self::Transparent,
212            1 => Self::Gray(values[0]),
213            3 => Self::Rgb(values[0], values[1], values[2]),
214            4 => Self::Cmyk(values[0], values[1], values[2], values[3]),
215            _ => Self::Transparent,
216        }
217    }
218}
219
220/// Border effect.
221#[derive(Debug, Clone)]
222pub struct BorderEffect {
223    /// The effect style.
224    pub style: BorderEffectStyle,
225    /// Effect intensity.
226    pub intensity: f32,
227}
228
229/// Border effect style.
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum BorderEffectStyle {
232    /// No effect.
233    None,
234    /// Cloudy border.
235    Cloudy,
236}
237
238impl BorderEffect {
239    /// Parse from a `/BE` dictionary.
240    pub fn from_dict(dict: &Dict<'_>) -> Self {
241        let style = dict
242            .get::<Name>(S)
243            .map(|n| match n.as_ref() {
244                b"C" => BorderEffectStyle::Cloudy,
245                _ => BorderEffectStyle::None,
246            })
247            .unwrap_or(BorderEffectStyle::None);
248        let intensity = dict.get::<f32>(I).unwrap_or(0.0);
249        Self { style, intensity }
250    }
251}
252
253/// Quadrilateral points for text markup annotations.
254#[derive(Debug, Clone)]
255pub struct QuadPoints {
256    /// Raw coordinate values.
257    pub points: Vec<f32>,
258}
259
260impl QuadPoints {
261    /// Parse quad points from an array.
262    pub fn from_array(arr: &Array<'_>) -> Self {
263        Self {
264            points: arr.iter::<f32>().collect(),
265        }
266    }
267
268    /// Return bounding rectangles.
269    pub fn bounding_rects(&self) -> Vec<Rect> {
270        self.points
271            .chunks_exact(8)
272            .map(|chunk| {
273                let xs = [chunk[0], chunk[2], chunk[4], chunk[6]];
274                let ys = [chunk[1], chunk[3], chunk[5], chunk[7]];
275                let x0 = xs.iter().copied().reduce(f32::min).unwrap_or(0.0) as f64;
276                let y0 = ys.iter().copied().reduce(f32::min).unwrap_or(0.0) as f64;
277                let x1 = xs.iter().copied().reduce(f32::max).unwrap_or(0.0) as f64;
278                let y1 = ys.iter().copied().reduce(f32::max).unwrap_or(0.0) as f64;
279                Rect::new(x0, y0, x1, y1)
280            })
281            .collect()
282    }
283}
284
285/// Line ending styles (ISO 32000-2 Table 179).
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum LineEnding {
288    /// No line ending.
289    None,
290    /// A square.
291    Square,
292    /// A circle.
293    Circle,
294    /// A diamond.
295    Diamond,
296    /// An open arrowhead.
297    OpenArrow,
298    /// A closed arrowhead.
299    ClosedArrow,
300    /// Butt.
301    Butt,
302    /// Reverse open arrowhead.
303    ROpenArrow,
304    /// Reverse closed arrowhead.
305    RClosedArrow,
306    /// A slash.
307    Slash,
308}
309
310impl LineEnding {
311    /// Parse from a PDF name.
312    pub fn from_name(name: &[u8]) -> Self {
313        match name {
314            b"Square" => Self::Square,
315            b"Circle" => Self::Circle,
316            b"Diamond" => Self::Diamond,
317            b"OpenArrow" => Self::OpenArrow,
318            b"ClosedArrow" => Self::ClosedArrow,
319            b"Butt" => Self::Butt,
320            b"ROpenArrow" => Self::ROpenArrow,
321            b"RClosedArrow" => Self::RClosedArrow,
322            b"Slash" => Self::Slash,
323            _ => Self::None,
324        }
325    }
326}