pdfium_render/pdf/document/page/object/
group.rs

1//! Defines the [PdfPageGroupObject] struct, exposing functionality related to a group of
2//! page objects contained in the same `PdfPageObjects` collection.
3
4use crate::bindgen::{FPDF_DOCUMENT, FPDF_PAGE, FPDF_PAGEOBJECT};
5use crate::bindings::PdfiumLibraryBindings;
6use crate::create_transform_setters;
7use crate::error::PdfiumError;
8use crate::pdf::color::PdfColor;
9use crate::pdf::document::page::annotation::PdfPageAnnotation;
10use crate::pdf::document::page::index_cache::PdfPageIndexCache;
11use crate::pdf::document::page::object::path::PdfPathFillMode;
12use crate::pdf::document::page::object::private::internal::PdfPageObjectPrivate;
13use crate::pdf::document::page::object::{
14    PdfPageObject, PdfPageObjectBlendMode, PdfPageObjectCommon, PdfPageObjectLineCap,
15    PdfPageObjectLineJoin,
16};
17use crate::pdf::document::page::objects::common::{PdfPageObjectIndex, PdfPageObjectsCommon};
18use crate::pdf::document::page::{
19    PdfPage, PdfPageContentRegenerationStrategy, PdfPageObjectOwnership,
20};
21use crate::pdf::document::pages::{PdfPageIndex, PdfPages};
22use crate::pdf::document::PdfDocument;
23use crate::pdf::matrix::{PdfMatrix, PdfMatrixValue};
24use crate::pdf::points::PdfPoints;
25use crate::pdf::quad_points::PdfQuadPoints;
26use crate::pdf::rect::PdfRect;
27use crate::pdfium::Pdfium;
28use crate::prelude::PdfPageXObjectFormObject;
29use std::collections::HashMap;
30use std::ffi::c_double;
31
32#[cfg(doc)]
33use crate::pdf::document::page::object::text::PdfPageTextObject;
34
35/// A group of [PdfPageObject] objects contained in the same `PdfPageObjects` collection.
36/// The page objects contained in the group can be manipulated and transformed together
37/// as if they were a single object.
38///
39/// Groups are bound to specific pages in the document. To create an empty group, use either the
40/// `PdfPageObjects::create_new_group()` function or the [PdfPageGroupObject::empty()] function.
41/// To create a populated group, use one of the [PdfPageGroupObject::new()],
42/// [PdfPageGroupObject::from_vec()], or [PdfPageGroupObject::from_slice()] functions.
43pub struct PdfPageGroupObject<'a> {
44    document_handle: FPDF_DOCUMENT,
45    page_handle: FPDF_PAGE,
46    ownership: PdfPageObjectOwnership,
47    object_handles: Vec<FPDF_PAGEOBJECT>,
48    bindings: &'a dyn PdfiumLibraryBindings,
49}
50
51impl<'a> PdfPageGroupObject<'a> {
52    #[inline]
53    pub(crate) fn from_pdfium(
54        document_handle: FPDF_DOCUMENT,
55        page_handle: FPDF_PAGE,
56        bindings: &'a dyn PdfiumLibraryBindings,
57    ) -> Self {
58        PdfPageGroupObject {
59            page_handle,
60            document_handle,
61            ownership: PdfPageObjectOwnership::owned_by_page(document_handle, page_handle),
62            object_handles: Vec::new(),
63            bindings,
64        }
65    }
66
67    /// Creates a new, empty [PdfPageGroupObject] that can be used to hold any page objects
68    /// on the given [PdfPage].
69    pub fn empty(page: &'a PdfPage) -> Self {
70        Self::from_pdfium(page.document_handle(), page.page_handle(), page.bindings())
71    }
72
73    /// Creates a new [PdfPageGroupObject] that includes any page objects on the given [PdfPage]
74    /// matching the given predicate function.
75    pub fn new<F>(page: &'a PdfPage, predicate: F) -> Result<Self, PdfiumError>
76    where
77        F: FnMut(&PdfPageObject) -> bool,
78    {
79        let mut result =
80            Self::from_pdfium(page.document_handle(), page.page_handle(), page.bindings());
81
82        for mut object in page.objects().iter().filter(predicate) {
83            result.push(&mut object)?;
84        }
85
86        Ok(result)
87    }
88
89    /// Creates a new [PdfPageGroupObject] that includes the given page objects on the
90    /// given [PdfPage].
91    #[inline]
92    pub fn from_vec(
93        page: &PdfPage<'a>,
94        mut objects: Vec<PdfPageObject<'a>>,
95    ) -> Result<Self, PdfiumError> {
96        Self::from_slice(page, objects.as_mut_slice())
97    }
98
99    /// Creates a new [PdfPageGroupObject] that includes the given page objects on the
100    /// given [PdfPage].
101    pub fn from_slice(
102        page: &PdfPage<'a>,
103        objects: &mut [PdfPageObject<'a>],
104    ) -> Result<Self, PdfiumError> {
105        let mut result =
106            Self::from_pdfium(page.document_handle(), page.page_handle(), page.bindings());
107
108        for object in objects.iter_mut() {
109            result.push(object)?;
110        }
111
112        Ok(result)
113    }
114
115    /// Returns the internal `FPDF_DOCUMENT` handle for this group.
116    #[inline]
117    pub(crate) fn document_handle(&self) -> FPDF_DOCUMENT {
118        self.document_handle
119    }
120
121    /// Returns the internal `FPDF_PAGE` handle for this group.
122    #[inline]
123    pub(crate) fn page_handle(&self) -> FPDF_PAGE {
124        self.page_handle
125    }
126
127    /// Returns the ownership hierarchy for this group.
128    #[inline]
129    pub(crate) fn ownership(&self) -> &PdfPageObjectOwnership {
130        &self.ownership
131    }
132
133    /// Returns the [PdfiumLibraryBindings] used by this group.
134    #[inline]
135    pub fn bindings(&self) -> &'a dyn PdfiumLibraryBindings {
136        self.bindings
137    }
138
139    /// Returns the number of page objects in this group.
140    #[inline]
141    pub fn len(&self) -> usize {
142        self.object_handles.len()
143    }
144
145    /// Returns `true` if this group contains no page objects.
146    #[inline]
147    pub fn is_empty(&self) -> bool {
148        self.len() == 0
149    }
150
151    /// Returns `true` if this group already contains the given page object.
152    #[inline]
153    pub fn contains(&self, object: &PdfPageObject) -> bool {
154        self.object_handles.contains(&object.object_handle())
155    }
156
157    /// Adds a single [PdfPageObject] to this group.
158    pub fn push(&mut self, object: &mut PdfPageObject<'a>) -> Result<(), PdfiumError> {
159        let page_handle = match object.ownership() {
160            PdfPageObjectOwnership::Page(ownership) => Some(ownership.page_handle()),
161            _ => None,
162        };
163
164        if let Some(page_handle) = page_handle {
165            if page_handle != self.page_handle() {
166                // The object is attached to a different page.
167
168                // In theory, transferring ownership of the page object from its current
169                // page to the page referenced by this group should be possible:
170
171                // object.remove_object_from_page()?;
172                // object.add_object_to_page_handle(self.page)?;
173
174                // But in practice, as per https://github.com/ajrcarey/pdfium-render/issues/18,
175                // transferring memory ownership of a page object from one page to another
176                // generally segfaults Pdfium. Instead, return an error.
177                // TODO: AJRC - 26/5/25 - this may not be the case where the pages are in the
178                // same document. Refer to https://github.com/ajrcarey/pdfium-render/issues/18
179                // and test. We may be able to relax this restriction. It would be necessary
180                // to rethink the ownership hierarchy of the group, since it would no longer
181                // necessarily be fixed to a single page.
182
183                return Err(PdfiumError::OwnershipAlreadyAttachedToDifferentPage);
184            } else {
185                // The object is already attached to this group's parent page.
186
187                true
188            }
189        } else {
190            // The object isn't attached to a page.
191
192            object.add_object_to_page_handle(self.document_handle(), self.page_handle())?;
193
194            false
195        };
196
197        self.object_handles.push(object.object_handle());
198
199        Ok(())
200    }
201
202    /// Adds all the given [PdfPageObject] objects to this group.
203    pub fn append(&mut self, objects: &mut [PdfPageObject<'a>]) -> Result<(), PdfiumError> {
204        // Hold off regenerating page content until all objects have been processed.
205
206        let content_regeneration_strategy =
207            PdfPageIndexCache::get_content_regeneration_strategy_for_page(
208                self.document_handle(),
209                self.page_handle(),
210            )
211            .unwrap_or(PdfPageContentRegenerationStrategy::AutomaticOnEveryChange);
212
213        let page_index =
214            PdfPageIndexCache::get_index_for_page(self.document_handle(), self.page_handle());
215
216        if let Some(page_index) = page_index {
217            PdfPageIndexCache::cache_props_for_page(
218                self.document_handle(),
219                self.page_handle(),
220                page_index,
221                PdfPageContentRegenerationStrategy::Manual,
222            );
223        }
224
225        for object in objects.iter_mut() {
226            self.push(object)?;
227        }
228
229        // Regenerate page content now, if necessary.
230
231        if let Some(page_index) = page_index {
232            PdfPageIndexCache::cache_props_for_page(
233                self.document_handle(),
234                self.page_handle(),
235                page_index,
236                content_regeneration_strategy,
237            );
238        }
239
240        if content_regeneration_strategy
241            == PdfPageContentRegenerationStrategy::AutomaticOnEveryChange
242        {
243            PdfPage::regenerate_content_immut_for_handle(self.page_handle(), self.bindings())?;
244        }
245
246        Ok(())
247    }
248
249    /// Removes every [PdfPageObject] in this group from the group's containing [PdfPage]
250    /// and from this group, consuming the group.
251    ///
252    /// Each object's memory ownership will be removed from the `PdfPageObjects` collection for
253    /// this group's containing [PdfPage]. The objects will also be removed from this group,
254    /// and the memory owned by each object will be freed.
255    ///
256    /// If the containing [PdfPage] has a content regeneration strategy of
257    /// `PdfPageContentRegenerationStrategy::AutomaticOnEveryChange` then content regeneration
258    /// will be triggered on the page.
259    pub fn remove_objects_from_page(mut self) -> Result<(), PdfiumError> {
260        // Hold off regenerating page content until all objects have been processed.
261
262        let content_regeneration_strategy =
263            PdfPageIndexCache::get_content_regeneration_strategy_for_page(
264                self.document_handle(),
265                self.page_handle(),
266            )
267            .unwrap_or(PdfPageContentRegenerationStrategy::AutomaticOnEveryChange);
268
269        let page_index =
270            PdfPageIndexCache::get_index_for_page(self.document_handle(), self.page_handle());
271
272        if let Some(page_index) = page_index {
273            PdfPageIndexCache::cache_props_for_page(
274                self.document_handle(),
275                self.page_handle(),
276                page_index,
277                PdfPageContentRegenerationStrategy::Manual,
278            );
279        }
280
281        // Remove the selected objects from the source page.
282
283        self.apply_to_each(|object| object.remove_object_from_page())?;
284        self.object_handles.clear();
285
286        // A curious upstream bug in Pdfium means that any objects _not_ removed from the page
287        // may be vertically reflected and translated. Attempt to mitigate this.
288        // For more details, see: https://github.com/ajrcarey/pdfium-render/issues/60
289
290        let page_height = PdfPoints::new(self.bindings().FPDF_GetPageHeightF(self.page_handle()));
291
292        for index in 0..self.bindings().FPDFPage_CountObjects(self.page_handle()) {
293            let mut object = PdfPageObject::from_pdfium(
294                self.bindings()
295                    .FPDFPage_GetObject(self.page_handle(), index),
296                self.ownership().clone(),
297                self.bindings(),
298            );
299
300            // Undo the reflection effect.
301            // TODO: AJRC - 28/1/23 - it is not clear that _all_ objects need to be unreflected.
302            // The challenge here is detecting which objects, if any, have been affected by
303            // the Pdfium reflection bug. Testing suggests that comparing object transformation matrices
304            // before and after object removal doesn't result in any detectable change to the matrices,
305            // so that approach doesn't work.
306
307            object.flip_vertically()?;
308            object.translate(PdfPoints::ZERO, page_height)?;
309        }
310
311        // Regenerate page content now, if necessary.
312
313        if let Some(page_index) = page_index {
314            PdfPageIndexCache::cache_props_for_page(
315                self.document_handle,
316                self.page_handle,
317                page_index,
318                content_regeneration_strategy,
319            );
320        }
321
322        if content_regeneration_strategy
323            == PdfPageContentRegenerationStrategy::AutomaticOnEveryChange
324        {
325            PdfPage::regenerate_content_immut_for_handle(self.page_handle(), self.bindings())?;
326        }
327
328        Ok(())
329    }
330
331    /// Returns a single [PdfPageObject] from this group.
332    #[inline]
333    pub fn get(&self, index: PdfPageObjectIndex) -> Result<PdfPageObject, PdfiumError> {
334        if let Some(handle) = self.object_handles.get(index) {
335            Ok(self.get_object_from_handle(handle))
336        } else {
337            Err(PdfiumError::PageObjectIndexOutOfBounds)
338        }
339    }
340
341    /// Retains only the [PdfPageObject] objects in this group specified by the given predicate function.
342    ///
343    /// Non-retained objects are only removed from this group. They remain on the source [PdfPage] that
344    /// currently contains them.
345    pub fn retain<F>(&mut self, f: F)
346    where
347        F: Fn(&PdfPageObject) -> bool,
348    {
349        // The naive approach of using self.object_handles.retain() directly like so:
350
351        // self.object_handles.retain(|handle| f(&self.get_object_from_handle(handle)));
352
353        // does not work, due to self being borrowed both mutably and immutably simultaneously.
354        // Instead, we build a separate list indicating whether each object should be retained
355        // or discarded ...
356
357        let mut do_retain = vec![false; self.object_handles.len()];
358
359        for (index, handle) in self.object_handles.iter().enumerate() {
360            do_retain[index] = f(&self.get_object_from_handle(handle));
361        }
362
363        // ... and then we use that marker list in our call to self.object_handles.retain().
364
365        let mut index = 0;
366
367        self.object_handles.retain(|_| {
368            // Should the object at index position |index| be retained?
369
370            let do_retain = do_retain[index];
371
372            index += 1;
373
374            do_retain
375        });
376    }
377
378    #[inline]
379    #[deprecated(
380        since = "0.8.32",
381        note = "This function is no longer relevant, as the PdfPageGroupObject::copy_to_page() function can copy all object types."
382    )]
383    /// Retains only the [PdfPageObject] objects in this group that can be copied.
384    ///
385    /// Objects that cannot be copied are only removed from this group. They remain on the source
386    /// [PdfPage] that currently contains them.
387    pub fn retain_if_copyable(&mut self) {
388        #[allow(deprecated)]
389        self.retain(|object| object.is_copyable());
390    }
391
392    #[inline]
393    #[deprecated(
394        since = "0.8.32",
395        note = "This function is no longer relevant, as the PdfPageGroupObject::copy_to_page() function can copy all object types."
396    )]
397    /// Returns `true` if all the [PdfPageObject] objects in this group can be copied.
398    pub fn is_copyable(&self) -> bool {
399        #[allow(deprecated)]
400        self.iter().all(|object| object.is_copyable())
401    }
402
403    #[deprecated(
404        since = "0.8.32",
405        note = "This function is no longer relevant, as the PdfPageGroupObject::copy_to_page() function can copy all object types."
406    )]
407    /// Attempts to copy all the [PdfPageObject] objects in this group, placing the copied objects
408    /// onto the given existing destination [PdfPage].
409    ///
410    /// This function can only copy page objects supported by the [PdfPageObjectCommon::try_copy()]
411    /// function. For a different approach that supports more page object types but is more limited
412    /// in where the copied objects can be placed, see the [PdfPageGroupObject::copy_onto_new_page_at_start()],
413    /// [PdfPageGroupObject::copy_onto_new_page_at_end()], and
414    /// [PdfPageGroupObject::copy_onto_new_page_at_index()] functions.
415    ///
416    /// If all objects were copied successfully, then a new [PdfPageGroupObject] containing the clones
417    /// is returned, allowing the new objects to be manipulated as a group.
418    pub fn try_copy_onto_existing_page<'b>(
419        &self,
420        destination: &mut PdfPage<'b>,
421    ) -> Result<PdfPageGroupObject<'b>, PdfiumError> {
422        #[allow(deprecated)]
423        if !self.is_copyable() {
424            return Err(PdfiumError::GroupContainsNonCopyablePageObjects);
425        }
426
427        let mut group = destination.objects_mut().create_empty_group();
428
429        for handle in self.object_handles.iter() {
430            let source = self.get_object_from_handle(handle);
431
432            let clone =
433                source.try_copy_impl(destination.document_handle(), destination.bindings())?;
434
435            group.push(&mut destination.objects_mut().add_object(clone)?)?;
436        }
437
438        Ok(group)
439    }
440
441    /// Moves the ownership of all the [PdfPageObject] objects in this group to the given
442    /// [PdfPage], consuming the group. Page content will be regenerated as necessary.
443    ///
444    /// An error will be returned if the destination page is in a different [PdfDocument]
445    /// than the source objects. Pdfium only supports safely moving objects within the
446    /// same document, not across documents.
447    pub fn move_to_page(mut self, page: &mut PdfPage) -> Result<(), PdfiumError> {
448        self.apply_to_each(|object| object.move_to_page(page))?;
449        self.object_handles.clear();
450        Ok(())
451    }
452
453    /// Moves the ownership of all the [PdfPageObject] objects in this group to the given
454    /// [PdfPageAnnotation], consuming the group. Page content will be regenerated as necessary.
455    ///
456    /// An error will be returned if the destination annotation is in a different [PdfDocument]
457    /// than the source objects. Pdfium only supports safely moving objects within the
458    /// same document, not across documents.
459    pub fn move_to_annotation(
460        mut self,
461        annotation: &mut PdfPageAnnotation,
462    ) -> Result<(), PdfiumError> {
463        self.apply_to_each(|object| object.move_to_annotation(annotation))?;
464        self.object_handles.clear();
465        Ok(())
466    }
467
468    /// Copies all the [PdfPageObject] objects in this group into a new [PdfPageXObjectFormObject],
469    /// then adds the new form object to the page objects collection of the given [PdfPage],
470    /// returning the new form object.
471    pub fn copy_to_page(
472        &mut self,
473        page: &mut PdfPage<'a>,
474    ) -> Result<PdfPageObject<'a>, PdfiumError> {
475        let mut object = self.copy_into_x_object_form_object_from_handles(
476            page.document_handle(),
477            page.width(),
478            page.height(),
479        )?;
480
481        object.move_to_page(page)?;
482
483        Ok(object)
484    }
485
486    /// Creates a new [PdfPageXObjectFormObject] object from the page objects in this group,
487    /// ready to use in the given destination [PdfDocument].
488    pub fn copy_into_x_object_form_object(
489        &mut self,
490        destination: &mut PdfDocument<'a>,
491    ) -> Result<PdfPageObject<'a>, PdfiumError> {
492        self.copy_into_x_object_form_object_from_handles(
493            destination.handle(),
494            PdfPoints::new(self.bindings().FPDF_GetPageWidthF(self.page_handle())),
495            PdfPoints::new(self.bindings().FPDF_GetPageHeightF(self.page_handle())),
496        )
497    }
498
499    pub(crate) fn copy_into_x_object_form_object_from_handles(
500        &mut self,
501        destination_document_handle: FPDF_DOCUMENT,
502        destination_page_width: PdfPoints,
503        destination_page_height: PdfPoints,
504    ) -> Result<PdfPageObject<'a>, PdfiumError> {
505        // Since the PdfPageXObjectForm can only create a form from an entire page, we first
506        // prepare a temporary page containing just the items in this group. Once we have
507        // prepared that page, then we can create the form object.
508
509        let src_doc_handle = self.document_handle();
510        let src_page_handle = self.page_handle();
511
512        // First, create a new temporary page in the source document...
513
514        let tmp_page_index = self.bindings().FPDF_GetPageCount(src_doc_handle);
515
516        let tmp_page = self.bindings().FPDFPage_New(
517            src_doc_handle,
518            tmp_page_index,
519            destination_page_width.value as c_double,
520            destination_page_height.value as c_double,
521        );
522
523        PdfPageIndexCache::cache_props_for_page(
524            src_doc_handle,
525            tmp_page,
526            tmp_page_index as u16,
527            PdfPageContentRegenerationStrategy::AutomaticOnEveryChange,
528        );
529
530        // ... move the objects in this group across to the temporary page...
531
532        self.apply_to_each(|object| {
533            match object.ownership() {
534                PdfPageObjectOwnership::Page(_) => object.remove_object_from_page()?,
535                PdfPageObjectOwnership::AttachedAnnotation(_)
536                | PdfPageObjectOwnership::UnattachedAnnotation(_) => {
537                    object.remove_object_from_annotation()?
538                }
539                _ => {}
540            }
541
542            object.add_object_to_page_handle(src_doc_handle, tmp_page)?;
543
544            Ok(())
545        })?;
546        PdfPage::regenerate_content_immut_for_handle(self.page_handle(), self.bindings())?;
547        PdfPage::regenerate_content_immut_for_handle(tmp_page, self.bindings())?;
548
549        // ... create the form object from the temporary page...
550
551        let x_object = self.bindings().FPDF_NewXObjectFromPage(
552            destination_document_handle,
553            src_doc_handle,
554            tmp_page_index,
555        );
556
557        let object_handle = self.bindings().FPDF_NewFormObjectFromXObject(x_object);
558        if object_handle.is_null() {
559            return Err(PdfiumError::PdfiumLibraryInternalError(
560                crate::error::PdfiumInternalError::Unknown,
561            ));
562        }
563
564        let object = PdfPageXObjectFormObject::from_pdfium(
565            object_handle,
566            PdfPageObjectOwnership::owned_by_document(destination_document_handle),
567            self.bindings(),
568        );
569
570        self.bindings().FPDF_CloseXObject(x_object);
571
572        // ... and move objects on the temporary page back to their original locations.
573
574        self.apply_to_each(|object| {
575            match object.ownership() {
576                PdfPageObjectOwnership::Page(ownership) => {
577                    if ownership.page_handle() != src_page_handle {
578                        object.remove_object_from_page()?
579                    }
580                }
581                PdfPageObjectOwnership::AttachedAnnotation(_)
582                | PdfPageObjectOwnership::UnattachedAnnotation(_) => {
583                    object.remove_object_from_annotation()?
584                }
585                _ => {}
586            }
587            object.add_object_to_page_handle(src_doc_handle, src_page_handle)?;
588
589            Ok(())
590        })?;
591        PdfPage::regenerate_content_immut_for_handle(tmp_page, self.bindings())?;
592        PdfPage::regenerate_content_immut_for_handle(self.page_handle(), self.bindings())?;
593
594        PdfPageIndexCache::remove_index_for_page(src_doc_handle, tmp_page);
595        self.bindings()
596            .FPDFPage_Delete(src_doc_handle, tmp_page_index);
597
598        Ok(PdfPageObject::XObjectForm(object))
599    }
600
601    #[deprecated(
602        since = "0.8.32",
603        note = "This function has been retired in favour of the PdfPageGroupObject::copy_to_page() function."
604    )]
605    #[inline]
606    /// Copies all the [PdfPageObject] objects in this group by copying the page containing the
607    /// objects in this group into a new page at the start of the given destination [PdfDocument]
608    /// then removing all objects from the new page _not_ in this group.
609    ///
610    /// This function differs internally from [PdfPageGroupObject::try_copy_onto_existing_page()]
611    /// in that it uses `Pdfium` to copy page objects instead of the [PdfPageObjectCommon::try_copy()]
612    /// method provided by `pdfium-render`. As a result, this function can copy some objects that
613    /// [PdfPageGroupObject::try_copy_onto_existing_page()] cannot; for example, it can copy
614    /// path objects containing Bézier curves. However, it can only copy objects onto a new page,
615    /// not an existing page, and it cannot return a new [PdfPageGroupObject] containing the
616    /// newly created objects.
617    ///
618    /// The new page will have the same size and bounding box configuration as the page containing
619    /// the objects in this group.
620    pub fn copy_onto_new_page_at_start(
621        &self,
622        destination: &PdfDocument,
623    ) -> Result<(), PdfiumError> {
624        #[allow(deprecated)]
625        self.copy_onto_new_page_at_index(0, destination)
626    }
627
628    #[deprecated(
629        since = "0.8.32",
630        note = "This function has been retired in favour of the PdfPageGroupObject::copy_to_page() function."
631    )]
632    #[inline]
633    /// Copies all the [PdfPageObject] objects in this group by copying the page containing the
634    /// objects in this group into a new page at the end of the given destination [PdfDocument]
635    /// then removing all objects from the new page _not_ in this group.
636    ///
637    /// This function differs internally from [PdfPageGroupObject::try_copy_onto_existing_page()]
638    /// in that it uses `Pdfium` to copy page objects instead of the [PdfPageObjectCommon::try_copy()]
639    /// method provided by `pdfium-render`. As a result, this function can copy some objects that
640    /// [PdfPageGroupObject::try_copy_onto_existing_page()] cannot; for example, it can copy
641    /// path objects containing Bézier curves. However, it can only copy objects onto a new page,
642    /// not an existing page, and it cannot return a new [PdfPageGroupObject] containing the
643    /// newly created objects.
644    ///
645    /// The new page will have the same size and bounding box configuration as the page containing
646    /// the objects in this group.
647    pub fn copy_onto_new_page_at_end(&self, destination: &PdfDocument) -> Result<(), PdfiumError> {
648        #[allow(deprecated)]
649        self.copy_onto_new_page_at_index(destination.pages().len(), destination)
650    }
651
652    #[deprecated(
653        since = "0.8.32",
654        note = "This function has been retired in favour of the PdfPageGroupObject::copy_to_page() function."
655    )]
656    /// Copies all the [PdfPageObject] objects in this group by copying the page containing the
657    /// objects in this group into a new page in the given destination [PdfDocument] at the given
658    /// page index, then removing all objects from the new page _not_ in this group.
659    ///
660    /// This function differs internally from [PdfPageGroupObject::try_copy_onto_existing_page()]
661    /// in that it uses `Pdfium` to copy page objects instead of the [PdfPageObjectCommon::try_copy()]
662    /// method provided by `pdfium-render`. As a result, this function can copy some objects that
663    /// [PdfPageGroupObject::try_copy_onto_existing_page()] cannot; for example, it can copy
664    /// path objects containing Bézier curves. However, it can only copy objects onto a new page,
665    /// not an existing page, and it cannot return a new [PdfPageGroupObject] containing the
666    /// newly created objects.
667    ///
668    /// The new page will have the same size and bounding box configuration as the page containing
669    /// the objects in this group.
670    pub fn copy_onto_new_page_at_index(
671        &self,
672        index: PdfPageIndex,
673        destination: &PdfDocument,
674    ) -> Result<(), PdfiumError> {
675        // Pdfium provides the FPDF_ImportPages() function for copying one or more pages
676        // from one document into another. Using this function as a substitute for true
677        // page object cloning allows us to copy some objects (such as path objects containing
678        // Bézier curves) that PdfPageObject::try_copy() cannot.
679
680        // To use FPDF_ImportPages() as a cloning substitute, we take the following approach:
681
682        // First, we create a new in-memory document and import the source page for this
683        // page object group into that new document.
684
685        let temp = Pdfium::pdfium_document_handle_to_result(
686            self.bindings.FPDF_CreateNewDocument(),
687            self.bindings,
688        )?;
689
690        if let Some(source_page_index) =
691            PdfPageIndexCache::get_index_for_page(self.document_handle, self.page_handle)
692        {
693            PdfPages::copy_page_range_between_documents(
694                self.document_handle,
695                source_page_index..=source_page_index,
696                temp.handle(),
697                0,
698                self.bindings,
699            )?;
700        } else {
701            return Err(PdfiumError::SourcePageIndexNotInCache);
702        }
703
704        // Next, we remove all page objects from the in-memory document _except_ the ones in this group.
705
706        // We cannot compare object references across documents. Instead, we build a map of
707        // the types of objects, their positions, their bounds, and their transformation matrices,
708        // and use this map to determine which objects should be removed from the in-memory page.
709
710        let mut objects_to_discard = HashMap::new();
711
712        for index in 0..self.bindings.FPDFPage_CountObjects(self.page_handle) {
713            let object = PdfPageObject::from_pdfium(
714                self.bindings().FPDFPage_GetObject(self.page_handle, index),
715                self.ownership().clone(),
716                self.bindings(),
717            );
718
719            if !self.contains(&object) {
720                objects_to_discard.insert(
721                    (object.bounds()?, object.matrix()?, object.object_type()),
722                    true,
723                );
724            }
725        }
726
727        // We now have a map of objects that should be removed from the in-memory page; after
728        // we remove them, only the copies of the objects in this group will remain on the page.
729
730        temp.pages()
731            .get(0)?
732            .objects()
733            .create_group(|object| {
734                objects_to_discard.contains_key(&(
735                    object.bounds().unwrap_or(PdfQuadPoints::ZERO),
736                    object.matrix().unwrap_or(PdfMatrix::IDENTITY),
737                    object.object_type(),
738                ))
739            })?
740            .remove_objects_from_page()?;
741
742        // Finally, with only the copies of the objects in this group left on the in-memory page,
743        // we now copy the page back into the given destination.
744
745        PdfPages::copy_page_range_between_documents(
746            temp.handle(),
747            0..=0,
748            destination.handle(),
749            index,
750            self.bindings,
751        )?;
752
753        Ok(())
754    }
755
756    /// Returns an iterator over all the [PdfPageObject] objects in this group.
757    #[inline]
758    pub fn iter(&'a self) -> PdfPageGroupObjectIterator<'a> {
759        PdfPageGroupObjectIterator::new(self)
760    }
761
762    /// Returns the text contained within all [PdfPageTextObject] objects in this group.
763    #[inline]
764    pub fn text(&self) -> String {
765        self.text_separated("")
766    }
767
768    /// Returns the text contained within all [PdfPageTextObject] objects in this group,
769    /// separating each text fragment with the given separator.
770    pub fn text_separated(&self, separator: &str) -> String {
771        let mut strings = Vec::with_capacity(self.len());
772
773        self.for_each(|object| {
774            if let Some(object) = object.as_text_object() {
775                strings.push(object.text());
776            }
777        });
778
779        strings.join(separator)
780    }
781
782    /// Returns `true` if any [PdfPageObject] in this group contains transparency.
783    #[inline]
784    pub fn has_transparency(&self) -> bool {
785        self.object_handles.iter().any(|object_handle| {
786            PdfPageObject::from_pdfium(*object_handle, self.ownership().clone(), self.bindings())
787                .has_transparency()
788        })
789    }
790
791    /// Returns the bounding box of this group of objects. Since the bounds of every object in the
792    /// group must be considered, this function has runtime complexity of O(n).
793    pub fn bounds(&self) -> Result<PdfRect, PdfiumError> {
794        let mut bottom = PdfPoints::MAX;
795        let mut top = PdfPoints::MIN;
796        let mut left = PdfPoints::MAX;
797        let mut right = PdfPoints::MIN;
798        let mut empty = true;
799
800        self.object_handles.iter().for_each(|object_handle| {
801            if let Ok(object_bounds) = PdfPageObject::from_pdfium(
802                *object_handle,
803                self.ownership().clone(),
804                self.bindings(),
805            )
806            .bounds()
807            {
808                empty = false;
809
810                if object_bounds.bottom() < bottom {
811                    bottom = object_bounds.bottom();
812                }
813
814                if object_bounds.left() < left {
815                    left = object_bounds.left();
816                }
817
818                if object_bounds.top() > top {
819                    top = object_bounds.top();
820                }
821
822                if object_bounds.right() > right {
823                    right = object_bounds.right();
824                }
825            }
826        });
827
828        if empty {
829            Err(PdfiumError::EmptyPageObjectGroup)
830        } else {
831            Ok(PdfRect::new(bottom, left, top, right))
832        }
833    }
834
835    /// Sets the blend mode that will be applied when painting every [PdfPageObject] in this group.
836    #[inline]
837    pub fn set_blend_mode(
838        &mut self,
839        blend_mode: PdfPageObjectBlendMode,
840    ) -> Result<(), PdfiumError> {
841        self.apply_to_each(|object| object.set_blend_mode(blend_mode))
842    }
843
844    /// Sets the color of any filled paths in every [PdfPageObject] in this group.
845    #[inline]
846    pub fn set_fill_color(&mut self, fill_color: PdfColor) -> Result<(), PdfiumError> {
847        self.apply_to_each(|object| object.set_fill_color(fill_color))
848    }
849
850    /// Sets the color of any stroked lines in every [PdfPageObject] in this group.
851    ///
852    /// Even if an object's path is set with a visible color and a non-zero stroke width,
853    /// the object's stroke mode must be set in order for strokes to actually be visible.
854    #[inline]
855    pub fn set_stroke_color(&mut self, stroke_color: PdfColor) -> Result<(), PdfiumError> {
856        self.apply_to_each(|object| object.set_stroke_color(stroke_color))
857    }
858
859    /// Sets the width of any stroked lines in every [PdfPageObject] in this group.
860    ///
861    /// A line width of 0 denotes the thinnest line that can be rendered at device resolution:
862    /// 1 device pixel wide. However, some devices cannot reproduce 1-pixel lines,
863    /// and on high-resolution devices, they are nearly invisible. Since the results of rendering
864    /// such zero-width lines are device-dependent, their use is not recommended.
865    ///
866    /// Even if an object's path is set with a visible color and a non-zero stroke width,
867    /// the object's stroke mode must be set in order for strokes to actually be visible.
868    #[inline]
869    pub fn set_stroke_width(&mut self, stroke_width: PdfPoints) -> Result<(), PdfiumError> {
870        self.apply_to_each(|object| object.set_stroke_width(stroke_width))
871    }
872
873    /// Sets the line join style that will be used when painting stroked path segments
874    /// in every [PdfPageObject] in this group.
875    #[inline]
876    pub fn set_line_join(&mut self, line_join: PdfPageObjectLineJoin) -> Result<(), PdfiumError> {
877        self.apply_to_each(|object| object.set_line_join(line_join))
878    }
879
880    /// Sets the line cap style that will be used when painting stroked path segments
881    /// in every [PdfPageObject] in this group.
882    #[inline]
883    pub fn set_line_cap(&mut self, line_cap: PdfPageObjectLineCap) -> Result<(), PdfiumError> {
884        self.apply_to_each(|object| object.set_line_cap(line_cap))
885    }
886
887    /// Sets the method used to determine which sub-paths of any path in a [PdfPageObject]
888    /// should be filled, and whether or not any path in a [PdfPageObject] should be stroked,
889    /// for every [PdfPageObject] in this group.
890    ///
891    /// Even if an object's path is set to be stroked, the stroke must be configured with
892    /// a visible color and a non-zero width in order to actually be visible.
893    #[inline]
894    pub fn set_fill_and_stroke_mode(
895        &mut self,
896        fill_mode: PdfPathFillMode,
897        do_stroke: bool,
898    ) -> Result<(), PdfiumError> {
899        self.apply_to_each(|object| {
900            if let Some(object) = object.as_path_object_mut() {
901                object.set_fill_and_stroke_mode(fill_mode, do_stroke)
902            } else {
903                Ok(())
904            }
905        })
906    }
907
908    /// Applies the given closure to each [PdfPageObject] in this group.
909    #[inline]
910    pub(crate) fn apply_to_each<F, T>(&mut self, mut f: F) -> Result<(), PdfiumError>
911    where
912        F: FnMut(&mut PdfPageObject<'a>) -> Result<T, PdfiumError>,
913    {
914        let mut error = None;
915
916        self.object_handles.iter().for_each(|handle| {
917            if let Err(err) = f(&mut self.get_object_from_handle(handle)) {
918                error = Some(err)
919            }
920        });
921
922        match error {
923            Some(err) => Err(err),
924            None => Ok(()),
925        }
926    }
927
928    /// Calls the given closure on each [PdfPageObject] in this group.
929    #[inline]
930    pub(crate) fn for_each<F>(&self, mut f: F)
931    where
932        F: FnMut(&mut PdfPageObject<'a>),
933    {
934        self.object_handles.iter().for_each(|handle| {
935            f(&mut self.get_object_from_handle(handle));
936        });
937    }
938
939    /// Inflates an internal `FPDF_PAGEOBJECT` handle into a [PdfPageObject].
940    #[inline]
941    pub(crate) fn get_object_from_handle(&self, handle: &FPDF_PAGEOBJECT) -> PdfPageObject<'a> {
942        PdfPageObject::from_pdfium(*handle, self.ownership().clone(), self.bindings())
943    }
944
945    create_transform_setters!(
946        &mut Self,
947        Result<(), PdfiumError>,
948        "every [PdfPageObject] in this group",
949        "every [PdfPageObject] in this group.",
950        "every [PdfPageObject] in this group,"
951    );
952
953    // The internal implementation of the transform() function used by the create_transform_setters!() macro.
954    fn transform_impl(
955        &mut self,
956        a: PdfMatrixValue,
957        b: PdfMatrixValue,
958        c: PdfMatrixValue,
959        d: PdfMatrixValue,
960        e: PdfMatrixValue,
961        f: PdfMatrixValue,
962    ) -> Result<(), PdfiumError> {
963        self.apply_to_each(|object| object.transform(a, b, c, d, e, f))
964    }
965
966    // The internal implementation of the reset_matrix() function used by the create_transform_setters!() macro.
967    fn reset_matrix_impl(&mut self, matrix: PdfMatrix) -> Result<(), PdfiumError> {
968        self.apply_to_each(|object| object.reset_matrix_impl(matrix))
969    }
970}
971
972/// An iterator over all the [PdfPageObject] objects in a [PdfPageGroupObject] group.
973pub struct PdfPageGroupObjectIterator<'a> {
974    group: &'a PdfPageGroupObject<'a>,
975    next_index: PdfPageObjectIndex,
976}
977
978impl<'a> PdfPageGroupObjectIterator<'a> {
979    #[inline]
980    pub(crate) fn new(group: &'a PdfPageGroupObject<'a>) -> Self {
981        PdfPageGroupObjectIterator {
982            group,
983            next_index: 0,
984        }
985    }
986}
987
988impl<'a> Iterator for PdfPageGroupObjectIterator<'a> {
989    type Item = PdfPageObject<'a>;
990
991    fn next(&mut self) -> Option<Self::Item> {
992        let next = self.group.get(self.next_index);
993
994        self.next_index += 1;
995
996        next.ok()
997    }
998}
999
1000#[cfg(test)]
1001mod test {
1002    use crate::prelude::*;
1003    use crate::utils::test::test_bind_to_pdfium;
1004
1005    #[test]
1006    fn test_group_bounds() -> Result<(), PdfiumError> {
1007        let pdfium = test_bind_to_pdfium();
1008
1009        let document = pdfium.load_pdf_from_file("./test/export-test.pdf", None)?;
1010
1011        // Form a group of all text objects in the top half of the first page of music ...
1012
1013        let page = document.pages().get(2)?;
1014
1015        let mut group = page.objects().create_empty_group();
1016
1017        group.append(
1018            page.objects()
1019                .iter()
1020                .filter(|object| {
1021                    object.object_type() == PdfPageObjectType::Text
1022                        && object.bounds().unwrap().bottom() > page.height() / 2.0
1023                })
1024                .collect::<Vec<_>>()
1025                .as_mut_slice(),
1026        )?;
1027
1028        // ... and confirm the group's bounds are restricted to the top half of the page.
1029
1030        let bounds = group.bounds()?;
1031
1032        assert_eq!(bounds.bottom().value, 428.31033);
1033        assert_eq!(bounds.left().value, 62.60526);
1034        assert_eq!(bounds.top().value, 807.8812);
1035        assert_eq!(bounds.right().value, 544.48096);
1036
1037        Ok(())
1038    }
1039
1040    #[test]
1041    fn test_group_text() -> Result<(), PdfiumError> {
1042        let pdfium = test_bind_to_pdfium();
1043
1044        let document = pdfium.load_pdf_from_file("./test/export-test.pdf", None)?;
1045
1046        // Form a group of all text objects in the bottom half of the last page of music ...
1047
1048        let page = document.pages().get(5)?;
1049
1050        let mut group = page.objects().create_empty_group();
1051
1052        group.append(
1053            page.objects()
1054                .iter()
1055                .filter(|object| {
1056                    object.object_type() == PdfPageObjectType::Text
1057                        && object.bounds().unwrap().bottom() < page.height() / 2.0
1058                })
1059                .collect::<Vec<_>>()
1060                .as_mut_slice(),
1061        )?;
1062
1063        // ... and extract the text from the group.
1064
1065        assert_eq!(group.text_separated(" "), "Cento Concerti Ecclesiastici a Una, a Due, a Tre, e   a Quattro voci Giacomo Vincenti, Venice, 1605 Edited by Alastair Carey Source is the 1605 reprint of the original 1602 publication.  Item #2 in the source. Folio pages f5r (binding B1) in both Can to and Basso partbooks. The Basso partbook is barred; the Canto par tbook is not. The piece is marked ™Canto solo, Û Tenoreº in the  Basso partbook, indicating it can be sung either by a Soprano or by a  Tenor down an octave. V.  Quem vidistis, pastores, dicite, annuntiate nobis: in terris quis apparuit? R.  Natum vidimus, et choros angelorum collaudantes Dominum. Alleluia. What did you see, shepherds, speak, tell us: who has appeared on earth? We saw the new-born, and choirs of angels praising the Lord. Alleluia. Third responsory at Matins on Christmas Day 2  Basso, bar 47: one tone lower in source.");
1066
1067        Ok(())
1068    }
1069
1070    #[test]
1071    fn test_group_apply() -> Result<(), PdfiumError> {
1072        // Measure the bounds of a group of objects, translate the group, and confirm the
1073        // bounds have changed.
1074
1075        let pdfium = test_bind_to_pdfium();
1076
1077        let mut document = pdfium.create_new_pdf()?;
1078
1079        let mut page = document
1080            .pages_mut()
1081            .create_page_at_start(PdfPagePaperSize::a4())?;
1082
1083        page.objects_mut().create_path_object_rect(
1084            PdfRect::new_from_values(100.0, 100.0, 200.0, 200.0),
1085            None,
1086            None,
1087            Some(PdfColor::RED),
1088        )?;
1089
1090        page.objects_mut().create_path_object_rect(
1091            PdfRect::new_from_values(150.0, 150.0, 250.0, 250.0),
1092            None,
1093            None,
1094            Some(PdfColor::GREEN),
1095        )?;
1096
1097        page.objects_mut().create_path_object_rect(
1098            PdfRect::new_from_values(200.0, 200.0, 300.0, 300.0),
1099            None,
1100            None,
1101            Some(PdfColor::BLUE),
1102        )?;
1103
1104        let mut group = PdfPageGroupObject::new(&page, |_| true)?;
1105
1106        let bounds = group.bounds()?;
1107
1108        assert_eq!(bounds.bottom().value, 100.0);
1109        assert_eq!(bounds.left().value, 100.0);
1110        assert_eq!(bounds.top().value, 300.0);
1111        assert_eq!(bounds.right().value, 300.0);
1112
1113        group.translate(PdfPoints::new(150.0), PdfPoints::new(200.0))?;
1114
1115        let bounds = group.bounds()?;
1116
1117        assert_eq!(bounds.bottom().value, 300.0);
1118        assert_eq!(bounds.left().value, 250.0);
1119        assert_eq!(bounds.top().value, 500.0);
1120        assert_eq!(bounds.right().value, 450.0);
1121
1122        Ok(())
1123    }
1124}