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(),
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(),
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(), 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) =
802                PdfPageObject::from_pdfium(*object_handle, *self.ownership(), self.bindings())
803                    .bounds()
804            {
805                empty = false;
806
807                if object_bounds.bottom() < bottom {
808                    bottom = object_bounds.bottom();
809                }
810
811                if object_bounds.left() < left {
812                    left = object_bounds.left();
813                }
814
815                if object_bounds.top() > top {
816                    top = object_bounds.top();
817                }
818
819                if object_bounds.right() > right {
820                    right = object_bounds.right();
821                }
822            }
823        });
824
825        if empty {
826            Err(PdfiumError::EmptyPageObjectGroup)
827        } else {
828            Ok(PdfRect::new(bottom, left, top, right))
829        }
830    }
831
832    /// Sets the blend mode that will be applied when painting every [PdfPageObject] in this group.
833    #[inline]
834    pub fn set_blend_mode(
835        &mut self,
836        blend_mode: PdfPageObjectBlendMode,
837    ) -> Result<(), PdfiumError> {
838        self.apply_to_each(|object| object.set_blend_mode(blend_mode))
839    }
840
841    /// Sets the color of any filled paths in every [PdfPageObject] in this group.
842    #[inline]
843    pub fn set_fill_color(&mut self, fill_color: PdfColor) -> Result<(), PdfiumError> {
844        self.apply_to_each(|object| object.set_fill_color(fill_color))
845    }
846
847    /// Sets the color of any stroked lines in every [PdfPageObject] in this group.
848    ///
849    /// Even if an object's path is set with a visible color and a non-zero stroke width,
850    /// the object's stroke mode must be set in order for strokes to actually be visible.
851    #[inline]
852    pub fn set_stroke_color(&mut self, stroke_color: PdfColor) -> Result<(), PdfiumError> {
853        self.apply_to_each(|object| object.set_stroke_color(stroke_color))
854    }
855
856    /// Sets the width of any stroked lines in every [PdfPageObject] in this group.
857    ///
858    /// A line width of 0 denotes the thinnest line that can be rendered at device resolution:
859    /// 1 device pixel wide. However, some devices cannot reproduce 1-pixel lines,
860    /// and on high-resolution devices, they are nearly invisible. Since the results of rendering
861    /// such zero-width lines are device-dependent, their use is not recommended.
862    ///
863    /// Even if an object's path is set with a visible color and a non-zero stroke width,
864    /// the object's stroke mode must be set in order for strokes to actually be visible.
865    #[inline]
866    pub fn set_stroke_width(&mut self, stroke_width: PdfPoints) -> Result<(), PdfiumError> {
867        self.apply_to_each(|object| object.set_stroke_width(stroke_width))
868    }
869
870    /// Sets the line join style that will be used when painting stroked path segments
871    /// in every [PdfPageObject] in this group.
872    #[inline]
873    pub fn set_line_join(&mut self, line_join: PdfPageObjectLineJoin) -> Result<(), PdfiumError> {
874        self.apply_to_each(|object| object.set_line_join(line_join))
875    }
876
877    /// Sets the line cap style that will be used when painting stroked path segments
878    /// in every [PdfPageObject] in this group.
879    #[inline]
880    pub fn set_line_cap(&mut self, line_cap: PdfPageObjectLineCap) -> Result<(), PdfiumError> {
881        self.apply_to_each(|object| object.set_line_cap(line_cap))
882    }
883
884    /// Sets the method used to determine which sub-paths of any path in a [PdfPageObject]
885    /// should be filled, and whether or not any path in a [PdfPageObject] should be stroked,
886    /// for every [PdfPageObject] in this group.
887    ///
888    /// Even if an object's path is set to be stroked, the stroke must be configured with
889    /// a visible color and a non-zero width in order to actually be visible.
890    #[inline]
891    pub fn set_fill_and_stroke_mode(
892        &mut self,
893        fill_mode: PdfPathFillMode,
894        do_stroke: bool,
895    ) -> Result<(), PdfiumError> {
896        self.apply_to_each(|object| {
897            if let Some(object) = object.as_path_object_mut() {
898                object.set_fill_and_stroke_mode(fill_mode, do_stroke)
899            } else {
900                Ok(())
901            }
902        })
903    }
904
905    /// Applies the given closure to each [PdfPageObject] in this group.
906    #[inline]
907    pub(crate) fn apply_to_each<F, T>(&mut self, mut f: F) -> Result<(), PdfiumError>
908    where
909        F: FnMut(&mut PdfPageObject<'a>) -> Result<T, PdfiumError>,
910    {
911        let mut error = None;
912
913        self.object_handles.iter().for_each(|handle| {
914            if let Err(err) = f(&mut self.get_object_from_handle(handle)) {
915                error = Some(err)
916            }
917        });
918
919        match error {
920            Some(err) => Err(err),
921            None => Ok(()),
922        }
923    }
924
925    /// Calls the given closure on each [PdfPageObject] in this group.
926    #[inline]
927    pub(crate) fn for_each<F>(&self, mut f: F)
928    where
929        F: FnMut(&mut PdfPageObject<'a>),
930    {
931        self.object_handles.iter().for_each(|handle| {
932            f(&mut self.get_object_from_handle(handle));
933        });
934    }
935
936    /// Inflates an internal `FPDF_PAGEOBJECT` handle into a [PdfPageObject].
937    #[inline]
938    pub(crate) fn get_object_from_handle(&self, handle: &FPDF_PAGEOBJECT) -> PdfPageObject<'a> {
939        PdfPageObject::from_pdfium(*handle, *self.ownership(), self.bindings())
940    }
941
942    create_transform_setters!(
943        &mut Self,
944        Result<(), PdfiumError>,
945        "every [PdfPageObject] in this group",
946        "every [PdfPageObject] in this group.",
947        "every [PdfPageObject] in this group,"
948    );
949
950    // The internal implementation of the transform() function used by the create_transform_setters!() macro.
951    fn transform_impl(
952        &mut self,
953        a: PdfMatrixValue,
954        b: PdfMatrixValue,
955        c: PdfMatrixValue,
956        d: PdfMatrixValue,
957        e: PdfMatrixValue,
958        f: PdfMatrixValue,
959    ) -> Result<(), PdfiumError> {
960        self.apply_to_each(|object| object.transform(a, b, c, d, e, f))
961    }
962
963    // The internal implementation of the reset_matrix() function used by the create_transform_setters!() macro.
964    fn reset_matrix_impl(&mut self, matrix: PdfMatrix) -> Result<(), PdfiumError> {
965        self.apply_to_each(|object| object.reset_matrix_impl(matrix))
966    }
967}
968
969/// An iterator over all the [PdfPageObject] objects in a [PdfPageGroupObject] group.
970pub struct PdfPageGroupObjectIterator<'a> {
971    group: &'a PdfPageGroupObject<'a>,
972    next_index: PdfPageObjectIndex,
973}
974
975impl<'a> PdfPageGroupObjectIterator<'a> {
976    #[inline]
977    pub(crate) fn new(group: &'a PdfPageGroupObject<'a>) -> Self {
978        PdfPageGroupObjectIterator {
979            group,
980            next_index: 0,
981        }
982    }
983}
984
985impl<'a> Iterator for PdfPageGroupObjectIterator<'a> {
986    type Item = PdfPageObject<'a>;
987
988    fn next(&mut self) -> Option<Self::Item> {
989        let next = self.group.get(self.next_index);
990
991        self.next_index += 1;
992
993        next.ok()
994    }
995}
996
997#[cfg(test)]
998mod test {
999    use crate::prelude::*;
1000    use crate::utils::test::test_bind_to_pdfium;
1001
1002    #[test]
1003    fn test_group_bounds() -> Result<(), PdfiumError> {
1004        let pdfium = test_bind_to_pdfium();
1005
1006        let document = pdfium.load_pdf_from_file("./test/export-test.pdf", None)?;
1007
1008        // Form a group of all text objects in the top half of the first page of music ...
1009
1010        let page = document.pages().get(2)?;
1011
1012        let mut group = page.objects().create_empty_group();
1013
1014        group.append(
1015            page.objects()
1016                .iter()
1017                .filter(|object| {
1018                    object.object_type() == PdfPageObjectType::Text
1019                        && object.bounds().unwrap().bottom() > page.height() / 2.0
1020                })
1021                .collect::<Vec<_>>()
1022                .as_mut_slice(),
1023        )?;
1024
1025        // ... and confirm the group's bounds are restricted to the top half of the page.
1026
1027        let bounds = group.bounds()?;
1028
1029        assert_eq!(bounds.bottom().value, 428.31033);
1030        assert_eq!(bounds.left().value, 62.60526);
1031        assert_eq!(bounds.top().value, 807.8812);
1032        assert_eq!(bounds.right().value, 544.48096);
1033
1034        Ok(())
1035    }
1036
1037    #[test]
1038    fn test_group_text() -> Result<(), PdfiumError> {
1039        let pdfium = test_bind_to_pdfium();
1040
1041        let document = pdfium.load_pdf_from_file("./test/export-test.pdf", None)?;
1042
1043        // Form a group of all text objects in the bottom half of the last page of music ...
1044
1045        let page = document.pages().get(5)?;
1046
1047        let mut group = page.objects().create_empty_group();
1048
1049        group.append(
1050            page.objects()
1051                .iter()
1052                .filter(|object| {
1053                    object.object_type() == PdfPageObjectType::Text
1054                        && object.bounds().unwrap().bottom() < page.height() / 2.0
1055                })
1056                .collect::<Vec<_>>()
1057                .as_mut_slice(),
1058        )?;
1059
1060        // ... and extract the text from the group.
1061
1062        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.");
1063
1064        Ok(())
1065    }
1066
1067    #[test]
1068    fn test_group_apply() -> Result<(), PdfiumError> {
1069        // Measure the bounds of a group of objects, translate the group, and confirm the
1070        // bounds have changed.
1071
1072        let pdfium = test_bind_to_pdfium();
1073
1074        let mut document = pdfium.create_new_pdf()?;
1075
1076        let mut page = document
1077            .pages_mut()
1078            .create_page_at_start(PdfPagePaperSize::a4())?;
1079
1080        page.objects_mut().create_path_object_rect(
1081            PdfRect::new_from_values(100.0, 100.0, 200.0, 200.0),
1082            None,
1083            None,
1084            Some(PdfColor::RED),
1085        )?;
1086
1087        page.objects_mut().create_path_object_rect(
1088            PdfRect::new_from_values(150.0, 150.0, 250.0, 250.0),
1089            None,
1090            None,
1091            Some(PdfColor::GREEN),
1092        )?;
1093
1094        page.objects_mut().create_path_object_rect(
1095            PdfRect::new_from_values(200.0, 200.0, 300.0, 300.0),
1096            None,
1097            None,
1098            Some(PdfColor::BLUE),
1099        )?;
1100
1101        let mut group = PdfPageGroupObject::new(&page, |_| true)?;
1102
1103        let bounds = group.bounds()?;
1104
1105        assert_eq!(bounds.bottom().value, 100.0);
1106        assert_eq!(bounds.left().value, 100.0);
1107        assert_eq!(bounds.top().value, 300.0);
1108        assert_eq!(bounds.right().value, 300.0);
1109
1110        group.translate(PdfPoints::new(150.0), PdfPoints::new(200.0))?;
1111
1112        let bounds = group.bounds()?;
1113
1114        assert_eq!(bounds.bottom().value, 300.0);
1115        assert_eq!(bounds.left().value, 250.0);
1116        assert_eq!(bounds.top().value, 500.0);
1117        assert_eq!(bounds.right().value, 450.0);
1118
1119        Ok(())
1120    }
1121}