Skip to main content

pdfium_render/pdf/document/page/
annotations.rs

1//! Defines the [PdfPageAnnotations] struct, exposing functionality related to the
2//! annotations that have been added to a single `PdfPage`.
3
4use crate::bindgen::{FPDF_ANNOTATION, FPDF_DOCUMENT, FPDF_FORMHANDLE, FPDF_PAGE};
5use crate::bindings::PdfiumLibraryBindings;
6use crate::error::{PdfiumError, PdfiumInternalError};
7use crate::pdf::color::PdfColor;
8use crate::pdf::document::page::annotation::free_text::PdfPageFreeTextAnnotation;
9use crate::pdf::document::page::annotation::highlight::PdfPageHighlightAnnotation;
10use crate::pdf::document::page::annotation::ink::PdfPageInkAnnotation;
11use crate::pdf::document::page::annotation::link::PdfPageLinkAnnotation;
12use crate::pdf::document::page::annotation::private::internal::PdfPageAnnotationPrivate;
13use crate::pdf::document::page::annotation::square::PdfPageSquareAnnotation;
14use crate::pdf::document::page::annotation::squiggly::PdfPageSquigglyAnnotation;
15use crate::pdf::document::page::annotation::stamp::PdfPageStampAnnotation;
16use crate::pdf::document::page::annotation::strikeout::PdfPageStrikeoutAnnotation;
17use crate::pdf::document::page::annotation::text::PdfPageTextAnnotation;
18use crate::pdf::document::page::annotation::underline::PdfPageUnderlineAnnotation;
19use crate::pdf::document::page::annotation::{PdfPageAnnotation, PdfPageAnnotationCommon, PdfPageAnnotationType};
20use crate::pdf::document::page::object::{PdfPageObject, PdfPageObjectCommon};
21use crate::pdf::document::page::{PdfPage, PdfPageContentRegenerationStrategy, PdfPageIndexCache};
22use crate::pdf::quad_points::PdfQuadPoints;
23use chrono::prelude::*;
24use std::ops::Range;
25use std::os::raw::c_int;
26
27/// The zero-based index of a single [PdfPageAnnotation] inside its containing
28/// [PdfPageAnnotations] collection.
29pub type PdfPageAnnotationIndex = usize;
30
31/// The annotations that have been added to a single `PdfPage`.
32pub struct PdfPageAnnotations<'a> {
33    document_handle: FPDF_DOCUMENT,
34    page_handle: FPDF_PAGE,
35    form_handle: Option<FPDF_FORMHANDLE>,
36    bindings: &'a dyn PdfiumLibraryBindings,
37}
38
39impl<'a> PdfPageAnnotations<'a> {
40    #[inline]
41    pub(crate) fn from_pdfium(
42        document_handle: FPDF_DOCUMENT,
43        page_handle: FPDF_PAGE,
44        form_handle: Option<FPDF_FORMHANDLE>,
45        bindings: &'a dyn PdfiumLibraryBindings,
46    ) -> Self {
47        PdfPageAnnotations {
48            document_handle,
49            page_handle,
50            form_handle,
51            bindings,
52        }
53    }
54
55    /// Returns the internal `FPDF_DOCUMENT` handle of the [PdfDocument] containing this
56    /// [PdfPageAnnotations] collection.
57    #[inline]
58    pub(crate) fn document_handle(&self) -> FPDF_DOCUMENT {
59        self.document_handle
60    }
61
62    /// Returns the internal `FPDF_PAGE` handle of the [PdfPage] containing this
63    /// [PdfPageAnnotations] collection.
64    #[inline]
65    pub(crate) fn page_handle(&self) -> FPDF_PAGE {
66        self.page_handle
67    }
68
69    /// Returns the [PdfiumLibraryBindings] used by this [PdfPageAnnotations] collection.
70    #[inline]
71    pub fn bindings(&self) -> &'a dyn PdfiumLibraryBindings {
72        self.bindings
73    }
74
75    /// Returns the total number of annotations that have been added to the containing `PdfPage`.
76    #[inline]
77    pub fn len(&self) -> PdfPageAnnotationIndex {
78        self.bindings().FPDFPage_GetAnnotCount(self.page_handle) as PdfPageAnnotationIndex
79    }
80
81    /// Returns true if this [PdfPageAnnotations] collection is empty.
82    #[inline]
83    pub fn is_empty(&self) -> bool {
84        self.len() == 0
85    }
86
87    /// Returns a Range from 0..(number of annotations) for this [PdfPageAnnotations] collection.
88    #[inline]
89    pub fn as_range(&self) -> Range<PdfPageAnnotationIndex> {
90        0..self.len()
91    }
92
93    /// Returns a single [PdfPageAnnotation] from this [PdfPageAnnotations] collection.
94    pub fn get(&self, index: PdfPageAnnotationIndex) -> Result<PdfPageAnnotation<'a>, PdfiumError> {
95        if index >= self.len() {
96            return Err(PdfiumError::PageAnnotationIndexOutOfBounds);
97        }
98
99        let annotation_handle = self.bindings().FPDFPage_GetAnnot(self.page_handle, index as c_int);
100
101        if annotation_handle.is_null() {
102            Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown))
103        } else {
104            Ok(PdfPageAnnotation::from_pdfium(
105                self.document_handle,
106                self.page_handle,
107                annotation_handle,
108                self.form_handle,
109                self.bindings,
110            ))
111        }
112    }
113
114    /// Returns the first [PdfPageAnnotation] in this [PdfPageAnnotations] collection.
115    #[inline]
116    pub fn first(&self) -> Result<PdfPageAnnotation<'a>, PdfiumError> {
117        if !self.is_empty() {
118            self.get(0)
119        } else {
120            Err(PdfiumError::NoAnnotationsInCollection)
121        }
122    }
123
124    /// Returns the last [PdfPageAnnotation] in this [PdfPageAnnotations] collection.
125    #[inline]
126    pub fn last(&self) -> Result<PdfPageAnnotation<'a>, PdfiumError> {
127        if !self.is_empty() {
128            self.get(self.len() - 1)
129        } else {
130            Err(PdfiumError::NoAnnotationsInCollection)
131        }
132    }
133
134    /// Returns an iterator over all the annotations in this [PdfPageAnnotations] collection.
135    #[inline]
136    pub fn iter(&self) -> PdfPageAnnotationsIterator<'_> {
137        PdfPageAnnotationsIterator::new(self)
138    }
139
140    /// Creates a new annotation of the given [PdfPageAnnotationType] by passing the result of calling
141    /// `FPDFPage_CreateAnnot()` to an annotation constructor function.
142    ///
143    /// If the containing `PdfPage` has a content regeneration strategy of
144    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
145    /// will be triggered on the page.
146    pub(crate) fn create_annotation<T: PdfPageAnnotationCommon>(
147        &mut self,
148        annotation_type: PdfPageAnnotationType,
149        constructor: fn(FPDF_DOCUMENT, FPDF_PAGE, FPDF_ANNOTATION, &'a dyn PdfiumLibraryBindings) -> T,
150    ) -> Result<T, PdfiumError> {
151        let handle = self
152            .bindings()
153            .FPDFPage_CreateAnnot(self.page_handle(), annotation_type.as_pdfium());
154
155        if handle.is_null() {
156            Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown))
157        } else {
158            let mut annotation = constructor(self.document_handle(), self.page_handle(), handle, self.bindings());
159
160            annotation
161                .set_creation_date(Utc::now())
162                .and_then(|()| {
163                    if let Some(content_regeneration_strategy) =
164                        PdfPageIndexCache::get_content_regeneration_strategy_for_page(
165                            self.document_handle(),
166                            self.page_handle(),
167                        )
168                    {
169                        if content_regeneration_strategy == PdfPageContentRegenerationStrategy::AutomaticOnEveryChange {
170                            PdfPage::regenerate_content_immut_for_handle(self.page_handle(), self.bindings())
171                        } else {
172                            Ok(())
173                        }
174                    } else {
175                        Err(PdfiumError::SourcePageIndexNotInCache)
176                    }
177                })
178                .map(|()| annotation)
179        }
180    }
181
182    /// Creates a new [PdfPageFreeTextAnnotation] containing the given text in this
183    /// [PdfPageAnnotations] collection, returning the newly created annotation.
184    ///
185    /// If the containing `PdfPage` has a content regeneration strategy of
186    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
187    /// will be triggered on the page.
188    #[inline]
189    pub fn create_free_text_annotation(&mut self, text: &str) -> Result<PdfPageFreeTextAnnotation<'a>, PdfiumError> {
190        let mut annotation =
191            self.create_annotation(PdfPageAnnotationType::FreeText, PdfPageFreeTextAnnotation::from_pdfium)?;
192
193        annotation.set_contents(text)?;
194
195        Ok(annotation)
196    }
197
198    /// Creates a new [PdfPageHighlightAnnotation] annotation in this [PdfPageAnnotations] collection,
199    /// returning the newly created annotation.
200    ///
201    /// If the containing `PdfPage` has a content regeneration strategy of
202    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
203    /// will be triggered on the page.
204    #[inline]
205    pub fn create_highlight_annotation(&mut self) -> Result<PdfPageHighlightAnnotation<'a>, PdfiumError> {
206        self.create_annotation(
207            PdfPageAnnotationType::Highlight,
208            PdfPageHighlightAnnotation::from_pdfium,
209        )
210    }
211
212    /// Creates a new [PdfPageInkAnnotation] in this [PdfPageAnnotations] collection,
213    /// returning the newly created annotation.
214    ///
215    /// If the containing `PdfPage` has a content regeneration strategy of
216    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
217    /// will be triggered on the page.
218    #[inline]
219    pub fn create_ink_annotation(&mut self) -> Result<PdfPageInkAnnotation<'a>, PdfiumError> {
220        self.create_annotation(PdfPageAnnotationType::Ink, PdfPageInkAnnotation::from_pdfium)
221    }
222
223    /// Creates a new [PdfPageLinkAnnotation] with the given URI in this [PdfPageAnnotations]
224    /// collection, returning the newly created annotation.
225    ///
226    /// If the containing `PdfPage` has a content regeneration strategy of
227    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
228    /// will be triggered on the page.
229    pub fn create_link_annotation(&mut self, uri: &str) -> Result<PdfPageLinkAnnotation<'a>, PdfiumError> {
230        let mut annotation = self.create_annotation(PdfPageAnnotationType::Link, PdfPageLinkAnnotation::from_pdfium)?;
231
232        annotation.set_link(uri)?;
233
234        Ok(annotation)
235    }
236
237    /// Creates a new [PdfPageSquareAnnotation] annotation in this [PdfPageAnnotations] collection,
238    /// returning the newly created annotation.
239    ///
240    /// If the containing `PdfPage` has a content regeneration strategy of
241    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
242    /// will be triggered on the page.
243    #[inline]
244    pub fn create_square_annotation(&mut self) -> Result<PdfPageSquareAnnotation<'a>, PdfiumError> {
245        self.create_annotation(PdfPageAnnotationType::Square, PdfPageSquareAnnotation::from_pdfium)
246    }
247
248    /// Creates a new [PdfPageSquigglyAnnotation] annotation in this [PdfPageAnnotations] collection,
249    /// returning the newly created annotation.
250    ///
251    /// If the containing `PdfPage` has a content regeneration strategy of
252    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
253    /// will be triggered on the page.
254    #[inline]
255    pub fn create_squiggly_annotation(&mut self) -> Result<PdfPageSquigglyAnnotation<'a>, PdfiumError> {
256        self.create_annotation(PdfPageAnnotationType::Squiggly, PdfPageSquigglyAnnotation::from_pdfium)
257    }
258
259    /// Creates a new [PdfPageStampAnnotation] annotation in this [PdfPageAnnotations] collection,
260    /// returning the newly created annotation.
261    ///
262    /// If the containing `PdfPage` has a content regeneration strategy of
263    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
264    /// will be triggered on the page.
265    #[inline]
266    pub fn create_stamp_annotation(&mut self) -> Result<PdfPageStampAnnotation<'a>, PdfiumError> {
267        self.create_annotation(PdfPageAnnotationType::Stamp, PdfPageStampAnnotation::from_pdfium)
268    }
269
270    /// Creates a new [PdfPageStrikeoutAnnotation] annotation in this [PdfPageAnnotations] collection,
271    /// returning the newly created annotation.
272    ///
273    /// If the containing `PdfPage` has a content regeneration strategy of
274    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
275    /// will be triggered on the page.
276    #[inline]
277    pub fn create_strikeout_annotation(&mut self) -> Result<PdfPageStrikeoutAnnotation<'a>, PdfiumError> {
278        self.create_annotation(
279            PdfPageAnnotationType::Strikeout,
280            PdfPageStrikeoutAnnotation::from_pdfium,
281        )
282    }
283
284    /// Creates a new [PdfPageTextAnnotation] containing the given text in this [PdfPageAnnotations]
285    /// collection, returning the newly created annotation.
286    ///
287    /// If the containing `PdfPage` has a content regeneration strategy of
288    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
289    /// will be triggered on the page.
290    #[inline]
291    pub fn create_text_annotation(&mut self, text: &str) -> Result<PdfPageTextAnnotation<'a>, PdfiumError> {
292        let mut annotation = self.create_annotation(PdfPageAnnotationType::Text, PdfPageTextAnnotation::from_pdfium)?;
293
294        annotation.set_contents(text)?;
295
296        Ok(annotation)
297    }
298
299    /// Creates a new [PdfPageUnderlineAnnotation] annotation in this [PdfPageAnnotations] collection,
300    /// returning the newly created annotation.
301    ///
302    /// If the containing `PdfPage` has a content regeneration strategy of
303    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
304    /// will be triggered on the page.
305    #[inline]
306    pub fn create_underline_annotation(&mut self) -> Result<PdfPageUnderlineAnnotation<'a>, PdfiumError> {
307        self.create_annotation(
308            PdfPageAnnotationType::Underline,
309            PdfPageUnderlineAnnotation::from_pdfium,
310        )
311    }
312
313    /// Creates a new [PdfPageSquigglyAnnotation] annotation and positions it underneath the given
314    /// [PdfPageObject], coloring it with the given [PdfColor].
315    ///
316    /// If the given contents string is supplied, the annotation will be additionally configured
317    /// so that when the given [PdfPageObject] is clicked in a conforming PDF viewer, the given
318    /// contents string will be displayed in a popup window.
319    ///
320    /// If the containing `PdfPage` has a content regeneration strategy of
321    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
322    /// will be triggered on the page.
323    #[inline]
324    pub fn create_squiggly_annotation_under_object(
325        &mut self,
326        object: &PdfPageObject,
327        color: PdfColor,
328        contents: Option<&str>,
329    ) -> Result<PdfPageSquigglyAnnotation<'a>, PdfiumError> {
330        let mut annotation = self.create_squiggly_annotation()?;
331
332        let bounds = object.bounds()?;
333
334        annotation.set_position(bounds.left(), bounds.bottom())?;
335        annotation.set_stroke_color(color)?;
336
337        const SQUIGGLY_HEIGHT: f32 = 12.0;
338
339        let annotation_top = bounds.bottom().value - 5.0;
340        let annotation_bottom = annotation_top - SQUIGGLY_HEIGHT;
341
342        annotation
343            .attachment_points_mut()
344            .create_attachment_point_at_end(PdfQuadPoints::new_from_values(
345                bounds.left().value,
346                annotation_bottom,
347                bounds.right().value,
348                annotation_bottom,
349                bounds.right().value,
350                annotation_top,
351                bounds.left().value,
352                annotation_top,
353            ))?;
354
355        if let Some(contents) = contents {
356            annotation.set_width(bounds.width())?;
357            annotation.set_height(bounds.height())?;
358            annotation.set_contents(contents)?;
359        }
360
361        Ok(annotation)
362    }
363
364    /// Creates a new [PdfPageUnderlineAnnotation] annotation and positions it underneath the given
365    /// [PdfPageObject], coloring it with the given [PdfColor].
366    ///
367    /// If the given contents string is supplied, the annotation will be additionally configured
368    /// so that when the given [PdfPageObject] is clicked in a conforming PDF viewer, the given
369    /// contents string will be displayed in a popup window.
370    ///
371    /// If the containing `PdfPage` has a content regeneration strategy of
372    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
373    /// will be triggered on the page.
374    #[inline]
375    pub fn create_underline_annotation_under_object(
376        &mut self,
377        object: &PdfPageObject,
378        color: PdfColor,
379        contents: Option<&str>,
380    ) -> Result<PdfPageUnderlineAnnotation<'a>, PdfiumError> {
381        let mut annotation = self.create_underline_annotation()?;
382
383        let bounds = object.bounds()?;
384
385        annotation.set_position(bounds.left(), bounds.bottom())?;
386        annotation.set_stroke_color(color)?;
387        annotation
388            .attachment_points_mut()
389            .create_attachment_point_at_end(bounds)?;
390
391        if let Some(contents) = contents {
392            annotation.set_width(bounds.width())?;
393            annotation.set_height(bounds.height())?;
394            annotation.set_contents(contents)?;
395        }
396
397        Ok(annotation)
398    }
399
400    /// Creates a new [PdfPageStrikeoutAnnotation] annotation and vertically positions it in the
401    /// center the given [PdfPageObject], coloring it with the given [PdfColor].
402    ///
403    /// If the given contents string is supplied, the annotation will be additionally configured
404    /// so that when the given [PdfPageObject] is clicked in a conforming PDF viewer, the given
405    /// contents string will be displayed in a popup window.
406    ///
407    /// If the containing `PdfPage` has a content regeneration strategy of
408    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
409    /// will be triggered on the page.
410    #[inline]
411    pub fn create_strikeout_annotation_through_object(
412        &mut self,
413        object: &PdfPageObject,
414        color: PdfColor,
415        contents: Option<&str>,
416    ) -> Result<PdfPageStrikeoutAnnotation<'a>, PdfiumError> {
417        let mut annotation = self.create_strikeout_annotation()?;
418
419        let bounds = object.bounds()?;
420
421        annotation.set_position(bounds.left(), bounds.bottom())?;
422        annotation.set_stroke_color(color)?;
423        annotation
424            .attachment_points_mut()
425            .create_attachment_point_at_end(bounds)?;
426
427        if let Some(contents) = contents {
428            annotation.set_width(bounds.width())?;
429            annotation.set_height(bounds.height())?;
430            annotation.set_contents(contents)?;
431        }
432
433        Ok(annotation)
434    }
435
436    /// Creates a new [PdfPageHighlightAnnotation] annotation and positions it so as to cover
437    /// the given [PdfPageObject], coloring it with the given [PdfColor].
438    ///
439    /// If the given contents string is supplied, the annotation will be additionally configured
440    /// so that when the given [PdfPageObject] is clicked in a conforming PDF viewer, the given
441    /// contents string will be displayed in a popup window.
442    ///
443    /// If the containing `PdfPage` has a content regeneration strategy of
444    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
445    /// will be triggered on the page.
446    #[inline]
447    pub fn create_highlight_annotation_over_object(
448        &mut self,
449        object: &PdfPageObject,
450        color: PdfColor,
451        contents: Option<&str>,
452    ) -> Result<PdfPageHighlightAnnotation<'a>, PdfiumError> {
453        let mut annotation = self.create_highlight_annotation()?;
454
455        let bounds = object.bounds()?;
456
457        annotation.set_position(bounds.left(), bounds.bottom())?;
458        annotation.set_stroke_color(color)?;
459        annotation
460            .attachment_points_mut()
461            .create_attachment_point_at_end(bounds)?;
462
463        if let Some(contents) = contents {
464            annotation.set_width(bounds.width())?;
465            annotation.set_height(bounds.height())?;
466            annotation.set_contents(contents)?;
467        }
468
469        Ok(annotation)
470    }
471
472    /// Removes the given [PdfPageAnnotation] from this [PdfPageAnnotations] collection,
473    /// consuming the [PdfPageAnnotation].
474    ///
475    /// If the containing `PdfPage` has a content regeneration strategy of
476    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
477    /// will be triggered on the page.
478    pub fn delete_annotation(&mut self, annotation: PdfPageAnnotation<'a>) -> Result<(), PdfiumError> {
479        let index = self
480            .bindings()
481            .FPDFPage_GetAnnotIndex(self.page_handle(), annotation.handle());
482
483        if index == -1 {
484            return Err(PdfiumError::PageAnnotationIndexOutOfBounds);
485        }
486
487        if self
488            .bindings()
489            .is_true(self.bindings().FPDFPage_RemoveAnnot(self.page_handle(), index))
490        {
491            if let Some(content_regeneration_strategy) = PdfPageIndexCache::get_content_regeneration_strategy_for_page(
492                self.document_handle(),
493                self.page_handle(),
494            ) {
495                if content_regeneration_strategy == PdfPageContentRegenerationStrategy::AutomaticOnEveryChange {
496                    PdfPage::regenerate_content_immut_for_handle(self.page_handle(), self.bindings())
497                } else {
498                    Ok(())
499                }
500            } else {
501                Err(PdfiumError::SourcePageIndexNotInCache)
502            }
503        } else {
504            Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown))
505        }
506    }
507}
508
509/// An iterator over all the [PdfPageAnnotation] objects in a [PdfPageAnnotations] collection.
510pub struct PdfPageAnnotationsIterator<'a> {
511    annotations: &'a PdfPageAnnotations<'a>,
512    next_index: PdfPageAnnotationIndex,
513}
514
515impl<'a> PdfPageAnnotationsIterator<'a> {
516    #[inline]
517    pub(crate) fn new(annotations: &'a PdfPageAnnotations<'a>) -> Self {
518        PdfPageAnnotationsIterator {
519            annotations,
520            next_index: 0,
521        }
522    }
523}
524
525impl<'a> Iterator for PdfPageAnnotationsIterator<'a> {
526    type Item = PdfPageAnnotation<'a>;
527
528    fn next(&mut self) -> Option<Self::Item> {
529        let next = self.annotations.get(self.next_index);
530
531        self.next_index += 1;
532
533        next.ok()
534    }
535}