Skip to main content

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