Skip to main content

oxidize_pdf/annotations/
popup.rs

1//! Popup annotation for displaying text in a pop-up window
2//!
3//! Implements ISO 32000-1 Section 12.5.6.14 (Popup Annotations)
4//! Popup annotations display text in a pop-up window for entering or editing text
5
6use crate::annotations::{Annotation, AnnotationType};
7use crate::error::Result;
8use crate::geometry::{Point, Rectangle};
9use crate::graphics::Color;
10use crate::objects::{Object, ObjectId};
11
12/// Popup annotation - displays text in a pop-up window
13#[derive(Debug, Clone)]
14pub struct PopupAnnotation {
15    /// Rectangle for the popup window
16    pub rect: Rectangle,
17    /// Parent annotation (the annotation this popup is associated with)
18    pub parent: Option<ObjectId>,
19    /// Whether the popup is initially open
20    pub open: bool,
21    /// Contents to display
22    pub contents: Option<String>,
23    /// Background color
24    pub color: Option<Color>,
25    /// Flags for popup behavior
26    pub flags: PopupFlags,
27}
28
29/// Flags for popup annotation behavior
30#[derive(Debug, Clone, Copy, Default)]
31pub struct PopupFlags {
32    /// Popup should not rotate when page is rotated
33    pub no_rotate: bool,
34    /// Popup should not zoom when page is zoomed
35    pub no_zoom: bool,
36}
37
38impl Default for PopupAnnotation {
39    fn default() -> Self {
40        Self {
41            rect: Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 200.0)),
42            parent: None,
43            open: false,
44            contents: None,
45            color: Some(Color::rgb(1.0, 1.0, 0.9)), // Light yellow default
46            flags: PopupFlags::default(),
47        }
48    }
49}
50
51impl PopupAnnotation {
52    /// Create a new popup annotation
53    pub fn new(rect: Rectangle) -> Self {
54        Self {
55            rect,
56            ..Default::default()
57        }
58    }
59
60    /// Associate with a parent annotation
61    pub fn with_parent(mut self, parent: ObjectId) -> Self {
62        self.parent = Some(parent);
63        self
64    }
65
66    /// Set whether popup is initially open
67    pub fn with_open(mut self, open: bool) -> Self {
68        self.open = open;
69        self
70    }
71
72    /// Set popup contents
73    pub fn with_contents(mut self, contents: impl Into<String>) -> Self {
74        self.contents = Some(contents.into());
75        self
76    }
77
78    /// Set background color
79    pub fn with_color(mut self, color: Option<Color>) -> Self {
80        self.color = color;
81        self
82    }
83
84    /// Set no-rotate flag
85    pub fn with_no_rotate(mut self, no_rotate: bool) -> Self {
86        self.flags.no_rotate = no_rotate;
87        self
88    }
89
90    /// Set no-zoom flag
91    pub fn with_no_zoom(mut self, no_zoom: bool) -> Self {
92        self.flags.no_zoom = no_zoom;
93        self
94    }
95
96    /// Set popup flags
97    pub fn with_flags(mut self, flags: PopupFlags) -> Self {
98        self.flags = flags;
99        self
100    }
101
102    /// Convert to PDF annotation
103    pub fn to_annotation(&self) -> Result<Annotation> {
104        let mut annotation = Annotation::new(AnnotationType::Popup, self.rect);
105
106        // Set parent if present
107        if let Some(parent_ref) = &self.parent {
108            annotation
109                .properties
110                .set("Parent", Object::Reference(*parent_ref));
111        }
112
113        // Set open state
114        annotation
115            .properties
116            .set("Open", Object::Boolean(self.open));
117
118        // Set contents if present
119        if let Some(contents) = &self.contents {
120            annotation
121                .properties
122                .set("Contents", Object::String(contents.clone()));
123        }
124
125        // Set background color
126        if let Some(color) = &self.color {
127            annotation.properties.set(
128                "C",
129                Object::Array(vec![
130                    Object::Real(color.r()),
131                    Object::Real(color.g()),
132                    Object::Real(color.b()),
133                ]),
134            );
135        }
136
137        // Set flags
138        let mut flags = 0;
139        if self.flags.no_rotate {
140            flags |= 1 << 4; // NoRotate flag
141        }
142        if self.flags.no_zoom {
143            flags |= 1 << 3; // NoZoom flag
144        }
145
146        if flags != 0 {
147            annotation
148                .properties
149                .set("F", Object::Integer(flags as i64));
150        }
151
152        Ok(annotation)
153    }
154}
155
156/// Create a popup for a text annotation
157pub fn create_text_popup(
158    parent: ObjectId,
159    rect: Rectangle,
160    contents: impl Into<String>,
161) -> Result<Annotation> {
162    PopupAnnotation::new(rect)
163        .with_parent(parent)
164        .with_contents(contents)
165        .with_open(false)
166        .to_annotation()
167}
168
169/// Create a popup for a markup annotation
170pub fn create_markup_popup(
171    parent: ObjectId,
172    position: Point,
173    width: f64,
174    height: f64,
175    contents: impl Into<String>,
176) -> Result<Annotation> {
177    let rect = Rectangle::new(
178        position,
179        Point::new(position.x + width, position.y + height),
180    );
181
182    PopupAnnotation::new(rect)
183        .with_parent(parent)
184        .with_contents(contents)
185        .with_open(false)
186        .with_color(Some(Color::rgb(1.0, 1.0, 0.8))) // Light yellow
187        .to_annotation()
188}
189
190/// Create an initially open popup
191pub fn create_open_popup(
192    parent: ObjectId,
193    rect: Rectangle,
194    contents: impl Into<String>,
195) -> Result<Annotation> {
196    PopupAnnotation::new(rect)
197        .with_parent(parent)
198        .with_contents(contents)
199        .with_open(true)
200        .to_annotation()
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_popup_creation() {
209        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 200.0));
210
211        let popup = PopupAnnotation::new(rect);
212        assert_eq!(popup.rect, rect);
213        assert!(!popup.open);
214        assert!(popup.parent.is_none());
215    }
216
217    #[test]
218    fn test_popup_with_parent() {
219        let parent_ref = ObjectId::new(10, 0);
220        let rect = Rectangle::new(Point::new(200.0, 200.0), Point::new(400.0, 300.0));
221
222        let popup = PopupAnnotation::new(rect).with_parent(parent_ref);
223
224        assert_eq!(popup.parent, Some(parent_ref));
225    }
226
227    #[test]
228    fn test_popup_with_contents() {
229        let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 100.0));
230
231        let popup = PopupAnnotation::new(rect)
232            .with_contents("This is a popup annotation")
233            .with_open(true);
234
235        assert_eq!(
236            popup.contents,
237            Some("This is a popup annotation".to_string())
238        );
239        assert!(popup.open);
240    }
241
242    #[test]
243    fn test_popup_with_color() {
244        let popup = PopupAnnotation::default().with_color(Some(Color::rgb(0.9, 0.9, 1.0)));
245
246        assert_eq!(popup.color, Some(Color::rgb(0.9, 0.9, 1.0)));
247    }
248
249    #[test]
250    fn test_popup_flags() {
251        let flags = PopupFlags {
252            no_rotate: true,
253            no_zoom: true,
254        };
255
256        let popup = PopupAnnotation::default().with_flags(flags);
257
258        assert!(popup.flags.no_rotate);
259        assert!(popup.flags.no_zoom);
260    }
261
262    #[test]
263    fn test_popup_individual_flags() {
264        let popup = PopupAnnotation::default()
265            .with_no_rotate(true)
266            .with_no_zoom(false);
267
268        assert!(popup.flags.no_rotate);
269        assert!(!popup.flags.no_zoom);
270    }
271
272    #[test]
273    fn test_popup_to_annotation() {
274        let parent_ref = ObjectId::new(5, 0);
275        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 200.0));
276
277        let popup = PopupAnnotation::new(rect)
278            .with_parent(parent_ref)
279            .with_contents("Test popup")
280            .with_open(true)
281            .with_color(Some(Color::rgb(1.0, 1.0, 0.0)));
282
283        let annotation = popup.to_annotation();
284        assert!(annotation.is_ok());
285    }
286
287    #[test]
288    fn test_create_text_popup() {
289        let parent = ObjectId::new(1, 0);
290        let rect = Rectangle::new(Point::new(50.0, 50.0), Point::new(250.0, 150.0));
291
292        let popup = create_text_popup(parent, rect, "Text annotation popup");
293        assert!(popup.is_ok());
294    }
295
296    #[test]
297    fn test_create_markup_popup() {
298        let parent = ObjectId::new(2, 0);
299        let position = Point::new(100.0, 200.0);
300
301        let popup = create_markup_popup(parent, position, 200.0, 100.0, "Markup comment");
302        assert!(popup.is_ok());
303    }
304
305    #[test]
306    fn test_create_open_popup() {
307        let parent = ObjectId::new(3, 0);
308        let rect = Rectangle::new(Point::new(150.0, 150.0), Point::new(350.0, 250.0));
309
310        let popup = create_open_popup(parent, rect, "Initially open popup");
311        assert!(popup.is_ok());
312    }
313
314    #[test]
315    fn test_popup_default() {
316        let popup = PopupAnnotation::default();
317
318        assert!(!popup.open);
319        assert!(popup.parent.is_none());
320        assert!(popup.contents.is_none());
321        assert_eq!(popup.color, Some(Color::rgb(1.0, 1.0, 0.9)));
322        assert!(!popup.flags.no_rotate);
323        assert!(!popup.flags.no_zoom);
324    }
325
326    #[test]
327    fn test_popup_flags_default() {
328        let flags = PopupFlags::default();
329
330        assert!(!flags.no_rotate);
331        assert!(!flags.no_zoom);
332    }
333
334    #[test]
335    fn test_popup_complex() {
336        let parent = ObjectId::new(42, 1);
337        let rect = Rectangle::new(Point::new(200.0, 300.0), Point::new(400.0, 450.0));
338
339        let popup = PopupAnnotation::new(rect)
340            .with_parent(parent)
341            .with_contents("Complex popup with all features")
342            .with_open(true)
343            .with_color(Some(Color::rgb(0.8, 0.9, 1.0)))
344            .with_no_rotate(true)
345            .with_no_zoom(true);
346
347        assert_eq!(popup.rect, rect);
348        assert_eq!(popup.parent, Some(parent));
349        assert_eq!(
350            popup.contents,
351            Some("Complex popup with all features".to_string())
352        );
353        assert!(popup.open);
354        assert_eq!(popup.color, Some(Color::rgb(0.8, 0.9, 1.0)));
355        assert!(popup.flags.no_rotate);
356        assert!(popup.flags.no_zoom);
357
358        let annotation = popup.to_annotation();
359        assert!(annotation.is_ok());
360    }
361
362    #[test]
363    fn test_popup_without_parent() {
364        // Popup can exist without parent (though unusual)
365        let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0));
366
367        let popup = PopupAnnotation::new(rect).with_contents("Standalone popup");
368
369        assert!(popup.parent.is_none());
370
371        let annotation = popup.to_annotation();
372        assert!(annotation.is_ok());
373    }
374}