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