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