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