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

1//! Defines the [PdfPageImageObject] struct, exposing functionality related to a single page
2//! object defining an image, where the image data is sourced from a [PdfBitmap] buffer.
3
4use crate::bindgen::{
5    fpdf_page_t__, FPDF_DOCUMENT, FPDF_IMAGEOBJ_METADATA, FPDF_PAGE, FPDF_PAGEOBJECT,
6};
7use crate::bindings::PdfiumLibraryBindings;
8use crate::error::{PdfiumError, PdfiumInternalError};
9use crate::pdf::bitmap::PdfBitmap;
10use crate::pdf::bitmap::Pixels;
11use crate::pdf::color_space::PdfColorSpace;
12use crate::pdf::document::page::object::private::internal::PdfPageObjectPrivate;
13use crate::pdf::document::page::object::{PdfPageObject, PdfPageObjectOwnership};
14use crate::pdf::document::PdfDocument;
15use crate::pdf::matrix::{PdfMatrix, PdfMatrixValue};
16use crate::pdf::points::PdfPoints;
17use crate::utils::mem::create_byte_buffer;
18use crate::{create_transform_getters, create_transform_setters};
19use std::convert::TryInto;
20use std::ops::{Range, RangeInclusive};
21use std::os::raw::{c_int, c_void};
22
23#[cfg(not(target_arch = "wasm32"))]
24use {
25    crate::utils::files::get_pdfium_file_accessor_from_reader,
26    std::fs::File,
27    std::io::{Read, Seek},
28    std::path::Path,
29};
30
31#[cfg(feature = "image_api")]
32use {
33    crate::pdf::bitmap::PdfBitmapFormat,
34    crate::utils::pixels::{
35        aligned_bgr_to_rgba, aligned_grayscale_to_unaligned, bgra_to_rgba, rgba_to_bgra,
36    },
37};
38
39#[cfg(feature = "image_025")]
40use image_025::{DynamicImage, EncodableLayout, GrayImage, RgbaImage};
41
42#[cfg(feature = "image_024")]
43use image_024::{DynamicImage, EncodableLayout, GrayImage, RgbaImage};
44
45#[cfg(feature = "image_023")]
46use image_023::{DynamicImage, EncodableLayout, GenericImageView, GrayImage, RgbaImage};
47
48#[cfg(doc)]
49use {
50    crate::pdf::document::page::object::PdfPageObjectType,
51    crate::pdf::document::page::objects::common::PdfPageObjectsCommon,
52    crate::pdf::document::page::PdfPage,
53};
54
55/// A single [PdfPageObject] of type [PdfPageObjectType::Image]. The page object defines a
56/// single image, where the image data is sourced from a [PdfBitmap] buffer.
57///
58/// Page objects can be created either attached to a [PdfPage] (in which case the page object's
59/// memory is owned by the containing page) or detached from any page (in which case the page
60/// object's memory is owned by the object). Page objects are not rendered until they are
61/// attached to a page; page objects that are never attached to a page will be lost when they
62/// fall out of scope.
63///
64/// The simplest way to create a page image object that is immediately attached to a page
65/// is to call the [PdfPageObjectsCommon::create_image_object()] function.
66///
67/// Creating a detached page image object offers more scope for customization, but you must
68/// add the object to a containing [PdfPage] manually. To create a detached page image object,
69/// use the [PdfPageImageObject::new()] or [PdfPageImageObject::new_from_jpeg_file()] functions.
70/// The detached page image object can later be attached to a page by using the
71/// [PdfPageObjectsCommon::add_image_object()] function.
72pub struct PdfPageImageObject<'a> {
73    object_handle: FPDF_PAGEOBJECT,
74    ownership: PdfPageObjectOwnership,
75    bindings: &'a dyn PdfiumLibraryBindings,
76}
77
78impl<'a> PdfPageImageObject<'a> {
79    #[inline]
80    pub(crate) fn from_pdfium(
81        object_handle: FPDF_PAGEOBJECT,
82        ownership: PdfPageObjectOwnership,
83        bindings: &'a dyn PdfiumLibraryBindings,
84    ) -> Self {
85        PdfPageImageObject {
86            object_handle,
87            ownership,
88            bindings,
89        }
90    }
91
92    /// Creates a new [PdfPageImageObject] from the given [DynamicImage]. The returned
93    /// page object will not be rendered until it is added to a [PdfPage] using the
94    /// [PdfPageObjectsCommon::add_image_object()] function.
95    ///
96    /// The returned page object will have its width and height both set to 1.0 points.
97    /// Use the [PdfPageImageObject::scale()] function to apply a horizontal and vertical scale
98    /// to the object after it is created, or use one of the [PdfPageImageObject::new_with_width()],
99    /// [PdfPageImageObject::new_with_height()], or [PdfPageImageObject::new_with_size()] functions
100    /// to scale the page object to a specific width and/or height at the time the object is created.
101    ///
102    /// This function is only available when this crate's `image` feature is enabled.
103    #[cfg(feature = "image_api")]
104    #[inline]
105    pub fn new(document: &PdfDocument<'a>, image: &DynamicImage) -> Result<Self, PdfiumError> {
106        let mut result = Self::new_from_handle(document.handle(), document.bindings());
107
108        if let Ok(result) = result.as_mut() {
109            result.set_image(image)?;
110        }
111
112        result
113    }
114
115    /// Creates a new [PdfPageImageObject]. The returned page object will not be
116    /// rendered until it is added to a [PdfPage] using the
117    /// [PdfPageObjects::add_image_object()] function.
118    ///
119    /// Use the [PdfPageImageObject::set_bitmap()] function to apply image data to
120    /// the empty object.
121    ///
122    /// The returned page object will have its width and height both set to 1.0 points.
123    /// Use the [WriteTransforms::scale()] function to apply a horizontal and vertical scale
124    /// to the object after it is created.
125    #[cfg(not(feature = "image_api"))]
126    pub fn new(document: &PdfDocument<'a>) -> Result<Self, PdfiumError> {
127        Self::new_from_handle(document.handle(), document.bindings())
128    }
129
130    /// Creates a new [PdfPageImageObject] containing JPEG image data loaded from the
131    /// given file path. The returned page object will not be rendered until it is added to
132    /// a [PdfPage] using the [PdfPageObjectsCommon::add_image_object()] function.
133    ///
134    /// The returned page object will have its width and height both set to 1.0 points.
135    /// Use the [PdfPageImageObject::scale] function to apply a horizontal and vertical scale
136    /// to the object after it is created, or use one of the [PdfPageImageObject::new_with_width()],
137    /// [PdfPageImageObject::new_with_height()], or [PdfPageImageObject::new_with_size()] functions
138    /// to scale the page object to a specific width and/or height at the time the object is created.
139    ///
140    /// This function is not available when compiling to WASM.
141    #[cfg(not(target_arch = "wasm32"))]
142    pub fn new_from_jpeg_file(
143        document: &PdfDocument<'a>,
144        path: &(impl AsRef<Path> + ?Sized),
145    ) -> Result<Self, PdfiumError> {
146        Self::new_from_jpeg_reader(document, File::open(path).map_err(PdfiumError::IoError)?)
147    }
148
149    /// Creates a new [PdfPageImageObject] containing JPEG image data loaded from the
150    /// given reader. Because Pdfium must know the total content length in advance prior to
151    /// loading any portion of it, the given reader must implement the [Seek] trait
152    /// as well as the [Read] trait.
153    ///
154    /// The returned page object will not be rendered until it is added to
155    /// a [PdfPage] using the [PdfPageObjectsCommon::add_image_object()] function.
156    ///
157    /// The returned page object will have its width and height both set to 1.0 points.
158    /// Use the [PdfPageImageObject::scale] function to apply a horizontal and vertical scale
159    /// to the object after it is created, or use one of the [PdfPageImageObject::new_with_width()],
160    /// [PdfPageImageObject::new_with_height()], or [PdfPageImageObject::new_with_size()] functions
161    /// to scale the page object to a specific width and/or height at the time the object is created.
162    ///
163    /// This function is not available when compiling to WASM.
164    #[cfg(not(target_arch = "wasm32"))]
165    pub fn new_from_jpeg_reader<R: Read + Seek>(
166        document: &PdfDocument<'a>,
167        reader: R,
168    ) -> Result<Self, PdfiumError> {
169        let object = Self::new_from_handle(document.handle(), document.bindings())?;
170
171        let mut reader = get_pdfium_file_accessor_from_reader(reader);
172
173        let result = document.bindings().FPDFImageObj_LoadJpegFileInline(
174            std::ptr::null_mut(),
175            0,
176            object.object_handle(),
177            reader.as_fpdf_file_access_mut_ptr(),
178        );
179
180        if object.bindings.is_true(result) {
181            Ok(object)
182        } else {
183            Err(PdfiumError::PdfiumLibraryInternalError(
184                PdfiumInternalError::Unknown,
185            ))
186        }
187    }
188
189    // Takes a raw `FPDF_DOCUMENT` handle to avoid cascading lifetime problems
190    // associated with borrowing `PdfDocument<'a>`.
191    pub(crate) fn new_from_handle(
192        document: FPDF_DOCUMENT,
193        bindings: &'a dyn PdfiumLibraryBindings,
194    ) -> Result<Self, PdfiumError> {
195        let handle = bindings.FPDFPageObj_NewImageObj(document);
196
197        if handle.is_null() {
198            Err(PdfiumError::PdfiumLibraryInternalError(
199                PdfiumInternalError::Unknown,
200            ))
201        } else {
202            Ok(PdfPageImageObject {
203                object_handle: handle,
204                ownership: PdfPageObjectOwnership::unowned(),
205                bindings,
206            })
207        }
208    }
209
210    /// Creates a new [PdfPageImageObject] from the given arguments. The page object will be scaled
211    /// horizontally to match the given width; its height will be adjusted to maintain the aspect
212    /// ratio of the given image. The returned page object will not be rendered until it is
213    /// added to a [PdfPage] using the [PdfPageObjectsCommon::add_image_object()] function.
214    ///
215    /// This function is only available when this crate's `image` feature is enabled.
216    #[cfg(feature = "image_api")]
217    pub fn new_with_width(
218        document: &PdfDocument<'a>,
219        image: &DynamicImage,
220        width: PdfPoints,
221    ) -> Result<Self, PdfiumError> {
222        let aspect_ratio = image.height() as f32 / image.width() as f32;
223
224        let height = width * aspect_ratio;
225
226        Self::new_with_size(document, image, width, height)
227    }
228
229    /// Creates a new [PdfPageImageObject] from the given arguments. The page object will be scaled
230    /// vertically to match the given height; its width will be adjusted to maintain the aspect
231    /// ratio of the given image. The returned page object will not be rendered until it is
232    /// added to a [PdfPage] using the [PdfPageObjectsCommon::add_image_object()] function.
233    ///
234    /// This function is only available when this crate's `image` feature is enabled.
235    #[cfg(feature = "image_api")]
236    pub fn new_with_height(
237        document: &PdfDocument<'a>,
238        image: &DynamicImage,
239        height: PdfPoints,
240    ) -> Result<Self, PdfiumError> {
241        let aspect_ratio = image.height() as f32 / image.width() as f32;
242
243        let width = height / aspect_ratio;
244
245        Self::new_with_size(document, image, width, height)
246    }
247
248    /// Creates a new [PdfPageImageObject] from the given arguments. The page object will be scaled to
249    /// match the given width and height. The returned page object will not be rendered until it is
250    /// added to a [PdfPage] using the [PdfPageObjectsCommon::add_image_object()] function.
251    ///
252    /// This function is only available when this crate's `image` feature is enabled.
253    #[cfg(feature = "image_api")]
254    #[inline]
255    pub fn new_with_size(
256        document: &PdfDocument<'a>,
257        image: &DynamicImage,
258        width: PdfPoints,
259        height: PdfPoints,
260    ) -> Result<Self, PdfiumError> {
261        let mut result = Self::new(document, image)?;
262
263        result.scale(width.value, height.value)?;
264
265        Ok(result)
266    }
267
268    /// Returns a new [PdfBitmap] created from the bitmap buffer backing
269    /// this [PdfPageImageObject], ignoring any image filters, image mask, or object
270    /// transforms applied to this page object.
271    pub fn get_raw_bitmap(&self) -> Result<PdfBitmap<'_>, PdfiumError> {
272        Ok(PdfBitmap::from_pdfium(
273            self.bindings().FPDFImageObj_GetBitmap(self.object_handle()),
274            self.bindings(),
275        ))
276    }
277
278    /// Returns a new [DynamicImage] created from the bitmap buffer backing
279    /// this [PdfPageImageObject], ignoring any image filters, image mask, or object
280    /// transforms applied to this page object.
281    ///
282    /// This function is only available when this crate's `image` feature is enabled.
283    #[cfg(feature = "image_api")]
284    #[inline]
285    pub fn get_raw_image(&self) -> Result<DynamicImage, PdfiumError> {
286        self.get_image_from_bitmap(&self.get_raw_bitmap()?)
287    }
288
289    /// Returns a new [PdfBitmap] created from the bitmap buffer backing
290    /// this [PdfPageImageObject], taking into account any image filters, image mask, and
291    /// object transforms applied to this page object.
292    #[inline]
293    pub fn get_processed_bitmap(
294        &self,
295        document: &PdfDocument,
296    ) -> Result<PdfBitmap<'_>, PdfiumError> {
297        let (width, height) = self.get_current_width_and_height_from_metadata()?;
298
299        self.get_processed_bitmap_with_size(document, width, height)
300    }
301
302    /// Returns a new [DynamicImage] created from the bitmap buffer backing
303    /// this [PdfPageImageObject], taking into account any image filters, image mask, and
304    /// object transforms applied to this page object.
305    ///
306    /// This function is only available when this crate's `image` feature is enabled.
307    #[cfg(feature = "image_api")]
308    #[inline]
309    pub fn get_processed_image(&self, document: &PdfDocument) -> Result<DynamicImage, PdfiumError> {
310        let (width, height) = self.get_current_width_and_height_from_metadata()?;
311
312        self.get_processed_image_with_size(document, width, height)
313    }
314
315    /// Returns a new [PdfBitmap] created from the bitmap buffer backing
316    /// this [PdfPageImageObject], taking into account any image filters, image mask, and
317    /// object transforms applied to this page object.
318    ///
319    /// The returned bitmap will be scaled during rendering so its width matches the given target width.
320    #[inline]
321    pub fn get_processed_bitmap_with_width(
322        &self,
323        document: &PdfDocument,
324        width: Pixels,
325    ) -> Result<PdfBitmap<'_>, PdfiumError> {
326        let (current_width, current_height) = self.get_current_width_and_height_from_metadata()?;
327
328        let aspect_ratio = current_width as f32 / current_height as f32;
329
330        self.get_processed_bitmap_with_size(
331            document,
332            width,
333            ((width as f32 / aspect_ratio) as u32)
334                .try_into()
335                .map_err(|_| PdfiumError::ImageSizeOutOfBounds)?,
336        )
337    }
338
339    /// Returns a new [DynamicImage] created from the bitmap buffer backing
340    /// this [PdfPageImageObject], taking into account any image filters, image mask, and
341    /// object transforms applied to this page object.
342    ///
343    /// The returned image will be scaled during rendering so its width matches the given target width.
344    ///
345    /// This function is only available when this crate's `image` feature is enabled.
346    #[cfg(feature = "image_api")]
347    #[inline]
348    pub fn get_processed_image_with_width(
349        &self,
350        document: &PdfDocument,
351        width: Pixels,
352    ) -> Result<DynamicImage, PdfiumError> {
353        let (current_width, current_height) = self.get_current_width_and_height_from_metadata()?;
354
355        let aspect_ratio = current_width as f32 / current_height as f32;
356
357        self.get_processed_image_with_size(
358            document,
359            width,
360            ((width as f32 / aspect_ratio) as u32)
361                .try_into()
362                .map_err(|_| PdfiumError::ImageSizeOutOfBounds)?,
363        )
364    }
365
366    /// Returns a new [PdfBitmap] created from the bitmap buffer backing
367    /// this [PdfPageImageObject], taking into account any image filters, image mask, and
368    /// object transforms applied to this page object.
369    ///
370    /// The returned bitmap will be scaled during rendering so its height matches the given target height.
371    #[inline]
372    pub fn get_processed_bitmap_with_height(
373        &self,
374        document: &PdfDocument,
375        height: Pixels,
376    ) -> Result<PdfBitmap<'_>, PdfiumError> {
377        let (current_width, current_height) = self.get_current_width_and_height_from_metadata()?;
378
379        let aspect_ratio = current_width as f32 / current_height as f32;
380
381        self.get_processed_bitmap_with_size(
382            document,
383            ((height as f32 * aspect_ratio) as u32)
384                .try_into()
385                .map_err(|_| PdfiumError::ImageSizeOutOfBounds)?,
386            height,
387        )
388    }
389
390    /// Returns a new [DynamicImage] created from the bitmap buffer backing
391    /// this [PdfPageImageObject], taking into account any image filters, image mask, and
392    /// object transforms applied to this page object.
393    ///
394    /// The returned image will be scaled during rendering so its height matches the given target height.
395    ///
396    /// This function is only available when this crate's `image` feature is enabled.
397    #[cfg(feature = "image_api")]
398    #[inline]
399    pub fn get_processed_image_with_height(
400        &self,
401        document: &PdfDocument,
402        height: Pixels,
403    ) -> Result<DynamicImage, PdfiumError> {
404        let (current_width, current_height) = self.get_current_width_and_height_from_metadata()?;
405
406        let aspect_ratio = current_width as f32 / current_height as f32;
407
408        self.get_processed_image_with_size(
409            document,
410            ((height as f32 * aspect_ratio) as u32)
411                .try_into()
412                .map_err(|_| PdfiumError::ImageSizeOutOfBounds)?,
413            height,
414        )
415    }
416
417    /// Returns a new [PdfBitmap] created from the bitmap buffer backing
418    /// this [PdfPageImageObject], taking into account any image filters, image mask, and
419    /// object transforms applied to this page object.
420    ///
421    /// The returned bitmap will be scaled during rendering so its width and height match
422    /// the given target dimensions.
423    pub fn get_processed_bitmap_with_size(
424        &self,
425        document: &PdfDocument,
426        width: Pixels,
427        height: Pixels,
428    ) -> Result<PdfBitmap<'_>, PdfiumError> {
429        // We attempt to work around two separate problems in Pdfium's
430        // FPDFImageObj_GetRenderedBitmap() function.
431
432        // First, the call to FPDFImageObj_GetRenderedBitmap() can fail, returning
433        // a null FPDF_BITMAP handle, if the image object's transformation matrix includes
434        // negative values for either the matrix.a or matrix.d values. We flip those values
435        // in the transformation matrix if they are negative, and we make sure we restore
436        // the original values before we return to the caller.
437
438        // Second, Pdfium seems to often return a rendered bitmap that is much smaller
439        // than the image object's metadata suggests. We look at the dimensions of the bitmap
440        // returned from FPDFImageObj_GetRenderedBitmap(), and we apply a scale factor to the
441        // image object's transformation matrix if the bitmap is not the expected size.
442
443        // For more details, see: https://github.com/ajrcarey/pdfium-render/issues/52
444
445        let mut matrix = self.matrix()?;
446
447        let original_matrix = matrix; // We'll restore the matrix to this before we return.
448
449        // Ensure the matrix.a and matrix.d values are not negative.
450
451        if matrix.a() < 0f32 {
452            matrix.set_a(-matrix.a());
453            self.reset_matrix_impl(matrix)?;
454        }
455
456        if matrix.d() < 0f32 {
457            matrix.set_d(-matrix.d());
458            self.reset_matrix_impl(matrix)?;
459        }
460
461        let page_handle = match self.ownership() {
462            PdfPageObjectOwnership::Page(ownership) => Some(ownership.page_handle()),
463            PdfPageObjectOwnership::AttachedAnnotation(ownership) => Some(ownership.page_handle()),
464            _ => None,
465        };
466
467        let bitmap_handle = match page_handle {
468            Some(page_handle) => self.bindings().FPDFImageObj_GetRenderedBitmap(
469                document.handle(),
470                page_handle,
471                self.object_handle(),
472            ),
473            None => self.bindings.FPDFImageObj_GetRenderedBitmap(
474                document.handle(),
475                std::ptr::null_mut::<fpdf_page_t__>(),
476                self.object_handle(),
477            ),
478        };
479
480        if bitmap_handle.is_null() {
481            // Restore the original transformation matrix values before we return the error
482            // to the caller.
483
484            self.reset_matrix_impl(original_matrix)?;
485            return Err(PdfiumError::PdfiumLibraryInternalError(
486                PdfiumInternalError::Unknown,
487            ));
488        }
489
490        let result = PdfBitmap::from_pdfium(bitmap_handle, self.bindings());
491
492        if width == result.width() && height == result.height() {
493            // The bitmap generated by Pdfium is already at the caller's requested dimensions.
494            // Restore the original transformation matrix values before we return to the caller.
495
496            self.reset_matrix_impl(original_matrix)?;
497
498            Ok(result)
499        } else {
500            // The bitmap generated by Pdfium is not at the caller's requested dimensions.
501            // We apply a scale transform to the page object to encourage Pdfium to generate
502            // a bitmap matching the caller's requested dimensions.
503
504            self.transform_impl(
505                width as PdfMatrixValue / result.width() as PdfMatrixValue,
506                0.0,
507                0.0,
508                height as PdfMatrixValue / result.height() as PdfMatrixValue,
509                0.0,
510                0.0,
511            )?;
512
513            // Generate the bitmap again at the new scale.
514
515            let result = PdfBitmap::from_pdfium(
516                match page_handle {
517                    Some(page_handle) => self.bindings().FPDFImageObj_GetRenderedBitmap(
518                        document.handle(),
519                        page_handle,
520                        self.object_handle(),
521                    ),
522                    None => self.bindings.FPDFImageObj_GetRenderedBitmap(
523                        document.handle(),
524                        std::ptr::null_mut::<fpdf_page_t__>(),
525                        self.object_handle(),
526                    ),
527                },
528                self.bindings,
529            );
530
531            // Restore the original transformation matrix values before we return to the caller.
532
533            self.reset_matrix_impl(original_matrix)?;
534
535            Ok(result)
536        }
537    }
538
539    /// Returns a new [DynamicImage] created from the bitmap buffer backing
540    /// this [PdfPageImageObject], taking into account any image filters, image mask, and
541    /// object transforms applied to this page object.
542    ///
543    /// The returned image will be scaled during rendering so its width and height match
544    /// the given target dimensions.
545    ///
546    /// This function is only available when this crate's `image` feature is enabled.
547    #[cfg(feature = "image_api")]
548    #[inline]
549    pub fn get_processed_image_with_size(
550        &self,
551        document: &PdfDocument,
552        width: Pixels,
553        height: Pixels,
554    ) -> Result<DynamicImage, PdfiumError> {
555        self.get_processed_bitmap_with_size(document, width, height)
556            .and_then(|bitmap| self.get_image_from_bitmap(&bitmap))
557    }
558
559    #[cfg(feature = "image_api")]
560    pub(crate) fn get_image_from_bitmap(
561        &self,
562        bitmap: &PdfBitmap,
563    ) -> Result<DynamicImage, PdfiumError> {
564        let handle = bitmap.handle();
565
566        let width = self.bindings.FPDFBitmap_GetWidth(handle);
567
568        let height = self.bindings.FPDFBitmap_GetHeight(handle);
569
570        let stride = self.bindings.FPDFBitmap_GetStride(handle);
571
572        let format =
573            PdfBitmapFormat::from_pdfium(self.bindings.FPDFBitmap_GetFormat(handle) as u32)?;
574
575        #[cfg(not(target_arch = "wasm32"))]
576        let buffer = self.bindings.FPDFBitmap_GetBuffer_as_slice(handle);
577
578        #[cfg(target_arch = "wasm32")]
579        let buffer_vec = self.bindings.FPDFBitmap_GetBuffer_as_vec(handle);
580        #[cfg(target_arch = "wasm32")]
581        let buffer = buffer_vec.as_slice();
582
583        match format {
584            #[allow(deprecated)]
585            PdfBitmapFormat::BGRA | PdfBitmapFormat::BRGx | PdfBitmapFormat::BGRx => {
586                RgbaImage::from_raw(width as u32, height as u32, bgra_to_rgba(buffer))
587                    .map(DynamicImage::ImageRgba8)
588            }
589            PdfBitmapFormat::BGR => RgbaImage::from_raw(
590                width as u32,
591                height as u32,
592                aligned_bgr_to_rgba(buffer, width as usize, stride as usize),
593            )
594            .map(DynamicImage::ImageRgba8),
595            PdfBitmapFormat::Gray => GrayImage::from_raw(
596                width as u32,
597                height as u32,
598                aligned_grayscale_to_unaligned(buffer, width as usize, stride as usize),
599            )
600            .map(DynamicImage::ImageLuma8),
601        }
602        .ok_or(PdfiumError::ImageError)
603    }
604
605    /// Returns the raw image data backing this [PdfPageImageObject] exactly as it is stored
606    /// in the containing PDF without applying any of the image's filters.
607    ///
608    /// The returned byte buffer may be empty if the image object does not contain any data.
609    pub fn get_raw_image_data(&self) -> Result<Vec<u8>, PdfiumError> {
610        let buffer_length = self.bindings().FPDFImageObj_GetImageDataRaw(
611            self.object_handle(),
612            std::ptr::null_mut(),
613            0,
614        );
615
616        if buffer_length == 0 {
617            return Ok(Vec::new());
618        }
619
620        let mut buffer = create_byte_buffer(buffer_length as usize);
621
622        let result = self.bindings().FPDFImageObj_GetImageDataRaw(
623            self.object_handle(),
624            buffer.as_mut_ptr() as *mut c_void,
625            buffer_length,
626        );
627
628        assert_eq!(result, buffer_length);
629
630        Ok(buffer)
631    }
632
633    /// Returns the expected pixel width and height of the processed image from Pdfium's metadata.
634    pub(crate) fn get_current_width_and_height_from_metadata(
635        &self,
636    ) -> Result<(Pixels, Pixels), PdfiumError> {
637        let width = self.get_raw_metadata().and_then(|metadata| {
638            metadata
639                .width
640                .try_into()
641                .map_err(|_| PdfiumError::ImageSizeOutOfBounds)
642        })?;
643
644        let height = self.get_raw_metadata().and_then(|metadata| {
645            metadata
646                .height
647                .try_into()
648                .map_err(|_| PdfiumError::ImageSizeOutOfBounds)
649        })?;
650
651        Ok((width, height))
652    }
653
654    /// Returns the expected pixel width of the processed image for this [PdfPageImageObject],
655    /// taking into account any image filters, image mask, and object transforms applied
656    /// to this page object.
657    #[inline]
658    pub fn width(&self) -> Result<Pixels, PdfiumError> {
659        self.get_current_width_and_height_from_metadata()
660            .map(|(width, _height)| width)
661    }
662
663    /// Returns the expected pixel height of the processed image for this [PdfPageImageObject],
664    /// taking into account any image filters, image mask, and object transforms applied
665    /// to this page object.
666    #[inline]
667    pub fn height(&self) -> Result<Pixels, PdfiumError> {
668        self.get_current_width_and_height_from_metadata()
669            .map(|(_width, height)| height)
670    }
671
672    /// Applies the byte data in the given [DynamicImage] to this [PdfPageImageObject].
673    ///
674    /// This function is only available when this crate's `image` feature is enabled.
675    #[cfg(feature = "image_api")]
676    pub fn set_image(&mut self, image: &DynamicImage) -> Result<(), PdfiumError> {
677        let width: Pixels = image
678            .width()
679            .try_into()
680            .map_err(|_| PdfiumError::ImageSizeOutOfBounds)?;
681
682        let height: Pixels = image
683            .height()
684            .try_into()
685            .map_err(|_| PdfiumError::ImageSizeOutOfBounds)?;
686
687        let bitmap = PdfBitmap::empty(width, height, PdfBitmapFormat::BGRA, self.bindings)?;
688
689        let buffer = if let Some(image) = image.as_rgba8() {
690            // The given image is already in RGBA format.
691
692            rgba_to_bgra(image.as_bytes())
693        } else {
694            // The image must be converted to RGBA first.
695
696            let image = image.to_rgba8();
697
698            rgba_to_bgra(image.as_bytes())
699        };
700
701        if !self
702            .bindings
703            .FPDFBitmap_SetBuffer(bitmap.handle(), buffer.as_slice())
704        {
705            return Err(PdfiumError::PdfiumLibraryInternalError(
706                PdfiumInternalError::Unknown,
707            ));
708        }
709
710        self.set_bitmap(&bitmap)
711    }
712
713    /// Applies the byte data in the given [PdfBitmap] to this [PdfPageImageObject].
714    pub fn set_bitmap(&mut self, bitmap: &PdfBitmap) -> Result<(), PdfiumError> {
715        if self
716            .bindings
717            .is_true(self.bindings().FPDFImageObj_SetBitmap(
718                std::ptr::null_mut::<FPDF_PAGE>(),
719                0,
720                self.object_handle(),
721                bitmap.handle(),
722            ))
723        {
724            Ok(())
725        } else {
726            Err(PdfiumError::PdfiumLibraryInternalError(
727                PdfiumInternalError::Unknown,
728            ))
729        }
730    }
731
732    /// Returns all internal metadata for this [PdfPageImageObject].
733    pub(crate) fn get_raw_metadata(&self) -> Result<FPDF_IMAGEOBJ_METADATA, PdfiumError> {
734        let mut metadata = FPDF_IMAGEOBJ_METADATA {
735            width: 0,
736            height: 0,
737            horizontal_dpi: 0.0,
738            vertical_dpi: 0.0,
739            bits_per_pixel: 0,
740            colorspace: 0,
741            marked_content_id: 0,
742        };
743
744        let page_handle = match self.ownership() {
745            PdfPageObjectOwnership::Page(ownership) => Some(ownership.page_handle()),
746            PdfPageObjectOwnership::AttachedAnnotation(ownership) => Some(ownership.page_handle()),
747            _ => None,
748        };
749
750        let result = self.bindings().FPDFImageObj_GetImageMetadata(
751            self.object_handle(),
752            match page_handle {
753                Some(page_handle) => page_handle,
754                None => std::ptr::null_mut::<fpdf_page_t__>(),
755            },
756            &mut metadata,
757        );
758
759        if self.bindings().is_true(result) {
760            Ok(metadata)
761        } else {
762            Err(PdfiumError::PdfiumLibraryInternalError(
763                PdfiumInternalError::Unknown,
764            ))
765        }
766    }
767
768    /// Returns the horizontal dots per inch resolution of the image assigned to this
769    /// [PdfPageImageObject], based on the intrinsic resolution of the assigned image
770    /// and the dimensions of this object.
771    #[inline]
772    pub fn horizontal_dpi(&self) -> Result<f32, PdfiumError> {
773        self.get_raw_metadata()
774            .map(|metadata| metadata.horizontal_dpi)
775    }
776
777    /// Returns the vertical dots per inch resolution of the image assigned to this
778    /// [PdfPageImageObject], based on the intrinsic resolution of the assigned image
779    /// and the dimensions of this object.
780    #[inline]
781    pub fn vertical_dpi(&self) -> Result<f32, PdfiumError> {
782        self.get_raw_metadata()
783            .map(|metadata| metadata.vertical_dpi)
784    }
785
786    /// Returns the bits per pixel for the image assigned to this [PdfPageImageObject].
787    ///
788    /// This value is not available if this object has not been attached to a `PdfPage`.
789    #[inline]
790    pub fn bits_per_pixel(&self) -> Result<u8, PdfiumError> {
791        self.get_raw_metadata()
792            .map(|metadata| metadata.bits_per_pixel as u8)
793    }
794
795    /// Returns the color space for the image assigned to this [PdfPageImageObject].
796    ///
797    /// This value is not available if this object has not been attached to a `PdfPage`.
798    #[inline]
799    pub fn color_space(&self) -> Result<PdfColorSpace, PdfiumError> {
800        self.get_raw_metadata()
801            .and_then(|metadata| PdfColorSpace::from_pdfium(metadata.colorspace as u32))
802    }
803
804    /// Returns the collection of image filters currently applied to this [PdfPageImageObject].
805    #[inline]
806    pub fn filters(&self) -> PdfPageImageObjectFilters<'_> {
807        PdfPageImageObjectFilters::new(self)
808    }
809
810    create_transform_setters!(
811        &mut Self,
812        Result<(), PdfiumError>,
813        "this [PdfPageImageObject]",
814        "this [PdfPageImageObject].",
815        "this [PdfPageImageObject],"
816    );
817
818    // The transform_impl() function required by the create_transform_setters!() macro
819    // is provided by the PdfPageObjectPrivate trait.
820
821    create_transform_getters!(
822        "this [PdfPageImageObject]",
823        "this [PdfPageImageObject].",
824        "this [PdfPageImageObject],"
825    );
826
827    // The get_matrix_impl() function required by the create_transform_getters!() macro
828    // is provided by the PdfPageObjectPrivate trait.
829}
830
831impl<'a> PdfPageObjectPrivate<'a> for PdfPageImageObject<'a> {
832    #[inline]
833    fn object_handle(&self) -> FPDF_PAGEOBJECT {
834        self.object_handle
835    }
836
837    #[inline]
838    fn ownership(&self) -> &PdfPageObjectOwnership {
839        &self.ownership
840    }
841
842    #[inline]
843    fn set_ownership(&mut self, ownership: PdfPageObjectOwnership) {
844        self.ownership = ownership;
845    }
846
847    #[inline]
848    fn bindings(&self) -> &dyn PdfiumLibraryBindings {
849        self.bindings
850    }
851
852    #[inline]
853    fn is_copyable_impl(&self) -> bool {
854        // Image filters cannot be copied.
855
856        self.filters().is_empty()
857    }
858
859    #[inline]
860    fn try_copy_impl<'b>(
861        &self,
862        document: FPDF_DOCUMENT,
863        bindings: &'b dyn PdfiumLibraryBindings,
864    ) -> Result<PdfPageObject<'b>, PdfiumError> {
865        if !self.filters().is_empty() {
866            // Image filters cannot be copied.
867
868            return Err(PdfiumError::ImageObjectFiltersNotCopyable);
869        }
870
871        let mut copy = PdfPageImageObject::new_from_handle(document, bindings)?;
872
873        copy.set_bitmap(&self.get_raw_bitmap()?)?;
874        copy.reset_matrix(self.matrix()?)?;
875
876        Ok(PdfPageObject::Image(copy))
877    }
878}
879
880/// The zero-based index of a single [PdfPageImageObjectFilter] inside its containing
881/// [PdfPageImageObjectFilters] collection.
882pub type PdfPageImageObjectFilterIndex = usize;
883
884/// A collection of all the image filters applied to a [PdfPageImageObject].
885pub struct PdfPageImageObjectFilters<'a> {
886    object: &'a PdfPageImageObject<'a>,
887}
888
889impl<'a> PdfPageImageObjectFilters<'a> {
890    #[inline]
891    pub(crate) fn new(object: &'a PdfPageImageObject<'a>) -> Self {
892        PdfPageImageObjectFilters { object }
893    }
894
895    /// Returns the number of image filters applied to the parent [PdfPageImageObject].
896    pub fn len(&self) -> usize {
897        self.object
898            .bindings()
899            .FPDFImageObj_GetImageFilterCount(self.object.object_handle()) as usize
900    }
901
902    /// Returns true if this [PdfPageImageObjectFilters] collection is empty.
903    #[inline]
904    pub fn is_empty(&self) -> bool {
905        self.len() == 0
906    }
907
908    /// Returns a Range from `0..(number of filters)` for this [PdfPageImageObjectFilters] collection.
909    #[inline]
910    pub fn as_range(&self) -> Range<PdfPageImageObjectFilterIndex> {
911        0..self.len()
912    }
913
914    /// Returns an inclusive Range from `0..=(number of filters - 1)` for this [PdfPageImageObjectFilters] collection.
915    #[inline]
916    pub fn as_range_inclusive(&self) -> RangeInclusive<PdfPageImageObjectFilterIndex> {
917        if self.is_empty() {
918            0..=0
919        } else {
920            0..=(self.len() - 1)
921        }
922    }
923
924    /// Returns a single [PdfPageImageObjectFilter] from this [PdfPageImageObjectFilters] collection.
925    pub fn get(
926        &self,
927        index: PdfPageImageObjectFilterIndex,
928    ) -> Result<PdfPageImageObjectFilter, PdfiumError> {
929        if index >= self.len() {
930            return Err(PdfiumError::ImageObjectFilterIndexOutOfBounds);
931        }
932
933        // Retrieving the image filter name from Pdfium is a two-step operation. First, we call
934        // FPDFImageObj_GetImageFilter() with a null buffer; this will retrieve the length of
935        // the image filter name in bytes. If the length is zero, then there is no image filter name.
936
937        // If the length is non-zero, then we reserve a byte buffer of the given
938        // length and call FPDFImageObj_GetImageFilter() again with a pointer to the buffer;
939        // this will write the font name into the buffer. Unlike most text handling in
940        // Pdfium, image filter names are returned in UTF-8 format.
941
942        let buffer_length = self.object.bindings().FPDFImageObj_GetImageFilter(
943            self.object.object_handle(),
944            index as c_int,
945            std::ptr::null_mut(),
946            0,
947        );
948
949        if buffer_length == 0 {
950            // The image filter name is not present.
951
952            return Err(PdfiumError::ImageObjectFilterIndexInBoundsButFilterUndefined);
953        }
954
955        let mut buffer = create_byte_buffer(buffer_length as usize);
956
957        let result = self.object.bindings().FPDFImageObj_GetImageFilter(
958            self.object.object_handle(),
959            index as c_int,
960            buffer.as_mut_ptr() as *mut c_void,
961            buffer_length,
962        );
963
964        assert_eq!(result, buffer_length);
965
966        Ok(PdfPageImageObjectFilter::new(
967            String::from_utf8(buffer)
968                // Trim any trailing nulls. All strings returned from Pdfium are generally terminated
969                // by one null byte.
970                .map(|str| str.trim_end_matches(char::from(0)).to_owned())
971                .unwrap_or_default(),
972        ))
973    }
974
975    /// Returns an iterator over all the [PdfPageImageObjectFilter] objects in this
976    /// [PdfPageImageObjectFilters] collection.
977    #[inline]
978    pub fn iter(&self) -> PdfPageImageObjectFiltersIterator<'_> {
979        PdfPageImageObjectFiltersIterator::new(self)
980    }
981}
982
983/// A single image filter applied to a [PdfPageImageObject].
984pub struct PdfPageImageObjectFilter {
985    name: String,
986}
987
988impl PdfPageImageObjectFilter {
989    #[inline]
990    pub(crate) fn new(name: String) -> Self {
991        PdfPageImageObjectFilter { name }
992    }
993
994    /// Returns the name of this [PdfPageImageObjectFilter].
995    pub fn name(&self) -> &str {
996        self.name.as_str()
997    }
998}
999
1000/// An iterator over all the [PdfPageImageObjectFilter] objects in a
1001/// [PdfPageImageObjectFilters] collection.
1002pub struct PdfPageImageObjectFiltersIterator<'a> {
1003    filters: &'a PdfPageImageObjectFilters<'a>,
1004    next_index: PdfPageImageObjectFilterIndex,
1005}
1006
1007impl<'a> PdfPageImageObjectFiltersIterator<'a> {
1008    #[inline]
1009    pub(crate) fn new(filters: &'a PdfPageImageObjectFilters<'a>) -> Self {
1010        PdfPageImageObjectFiltersIterator {
1011            filters,
1012            next_index: 0,
1013        }
1014    }
1015}
1016
1017impl<'a> Iterator for PdfPageImageObjectFiltersIterator<'a> {
1018    type Item = PdfPageImageObjectFilter;
1019
1020    fn next(&mut self) -> Option<Self::Item> {
1021        let next = self.filters.get(self.next_index);
1022
1023        self.next_index += 1;
1024
1025        next.ok()
1026    }
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031    use super::*;
1032    use crate::prelude::*;
1033    use crate::utils::test::test_bind_to_pdfium;
1034
1035    #[test]
1036    fn test_page_image_object_retains_format() -> Result<(), PdfiumError> {
1037        // Make sure the format of the image we pass into a new PdfPageImageObject is the
1038        // same when we later retrieve it.
1039
1040        let pdfium = test_bind_to_pdfium();
1041
1042        let image = pdfium
1043            .load_pdf_from_file("./test/path-test.pdf", None)?
1044            .pages()
1045            .get(0)?
1046            .render_with_config(&PdfRenderConfig::new().set_target_width(1000))?
1047            .as_image();
1048
1049        let mut document = pdfium.create_new_pdf()?;
1050
1051        let mut page = document
1052            .pages_mut()
1053            .create_page_at_end(PdfPagePaperSize::a4())?;
1054
1055        let object = page.objects_mut().create_image_object(
1056            PdfPoints::new(100.0),
1057            PdfPoints::new(100.0),
1058            &image,
1059            Some(PdfPoints::new(image.width() as f32)),
1060            Some(PdfPoints::new(image.height() as f32)),
1061        )?;
1062
1063        // Since the object has no image filters applied, both the raw and processed images should
1064        // be identical to the source image we assigned to the object. The processed image will
1065        // take the object's scale factors into account, but we made sure to set those to the actual
1066        // pixel dimensions of the source image.
1067
1068        // A visual inspection can be carried out by uncommenting the PNG save commands below.
1069
1070        let raw_image = object.as_image_object().unwrap().get_raw_image()?;
1071
1072        // raw_image
1073        //     .save_with_format("./test/1.png", ImageFormat::Png)
1074        //     .unwrap();
1075
1076        let processed_image = object
1077            .as_image_object()
1078            .unwrap()
1079            .get_processed_image(&document)?;
1080
1081        // processed_image
1082        //     .save_with_format("./test/2.png", ImageFormat::Png)
1083        //     .unwrap();
1084
1085        assert!(compare_equality_of_byte_arrays(
1086            image.as_bytes(),
1087            raw_image.into_rgba8().as_raw().as_slice()
1088        ));
1089
1090        assert!(compare_equality_of_byte_arrays(
1091            image.as_bytes(),
1092            processed_image.into_rgba8().as_raw().as_slice()
1093        ));
1094
1095        Ok(())
1096    }
1097
1098    fn compare_equality_of_byte_arrays(a: &[u8], b: &[u8]) -> bool {
1099        if a.len() != b.len() {
1100            return false;
1101        }
1102
1103        for index in 0..a.len() {
1104            if a[index] != b[index] {
1105                return false;
1106            }
1107        }
1108
1109        true
1110    }
1111
1112    #[test]
1113    fn test_image_scaling_keeps_aspect_ratio() -> Result<(), PdfiumError> {
1114        let pdfium = test_bind_to_pdfium();
1115
1116        let mut document = pdfium.create_new_pdf()?;
1117
1118        let mut page = document
1119            .pages_mut()
1120            .create_page_at_end(PdfPagePaperSize::a4())?;
1121
1122        let image = DynamicImage::new_rgb8(100, 200);
1123
1124        let object = page.objects_mut().create_image_object(
1125            PdfPoints::new(0.0),
1126            PdfPoints::new(0.0),
1127            &image,
1128            Some(PdfPoints::new(image.width() as f32)),
1129            Some(PdfPoints::new(image.height() as f32)),
1130        )?;
1131
1132        let image_object = object.as_image_object().unwrap();
1133
1134        assert_eq!(
1135            image_object
1136                .get_processed_bitmap_with_width(&document, 50)?
1137                .height(),
1138            100
1139        );
1140        assert_eq!(
1141            image_object
1142                .get_processed_image_with_width(&document, 50)?
1143                .height(),
1144            100
1145        );
1146        assert_eq!(
1147            image_object
1148                .get_processed_bitmap_with_height(&document, 50)?
1149                .width(),
1150            25
1151        );
1152        assert_eq!(
1153            image_object
1154                .get_processed_image_with_height(&document, 50)?
1155                .width(),
1156            25
1157        );
1158
1159        Ok(())
1160    }
1161}
1162
1163impl<'a> Drop for PdfPageImageObject<'a> {
1164    /// Closes this [PdfPageImageObject], releasing held memory.
1165    fn drop(&mut self) {
1166        self.drop_impl();
1167    }
1168}