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