Skip to main content

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