pdfium_render/pdf/document/page.rs
1//! Defines the [PdfPage] struct, exposing functionality related to a single page in a
2//! [PdfPages] collection.
3
4pub mod annotation;
5pub mod annotations;
6pub mod boundaries;
7pub mod field;
8pub(crate) mod index_cache;
9pub mod links;
10pub mod object;
11pub mod objects;
12pub mod render_config;
13pub mod size;
14pub mod text;
15
16#[cfg(feature = "paragraph")]
17pub mod paragraph;
18
19#[cfg(feature = "flatten")]
20mod flatten; // Keep internal flatten operation private.
21
22use object::ownership::PdfPageObjectOwnership;
23
24use crate::bindgen::{
25 FLATTEN_FAIL, FLATTEN_NOTHINGTODO, FLATTEN_SUCCESS, FLAT_PRINT, FPDF_DOCUMENT, FPDF_FORMHANDLE,
26 FPDF_PAGE,
27};
28use crate::bindings::PdfiumLibraryBindings;
29use crate::create_transform_setters;
30use crate::error::{PdfiumError, PdfiumInternalError};
31use crate::pdf::bitmap::{PdfBitmap, PdfBitmapFormat, Pixels};
32use crate::pdf::document::page::annotations::PdfPageAnnotations;
33use crate::pdf::document::page::boundaries::PdfPageBoundaries;
34use crate::pdf::document::page::index_cache::PdfPageIndexCache;
35use crate::pdf::document::page::links::PdfPageLinks;
36use crate::pdf::document::page::objects::common::PdfPageObjectsCommon;
37use crate::pdf::document::page::objects::PdfPageObjects;
38use crate::pdf::document::page::render_config::{PdfPageRenderSettings, PdfRenderConfig};
39use crate::pdf::document::page::size::PdfPagePaperSize;
40use crate::pdf::document::page::text::PdfPageText;
41use crate::pdf::font::PdfFont;
42use crate::pdf::matrix::{PdfMatrix, PdfMatrixValue};
43use crate::pdf::points::PdfPoints;
44use crate::pdf::rect::PdfRect;
45use crate::pdfium::PdfiumLibraryBindingsAccessor;
46use std::collections::{hash_map::Entry, HashMap};
47use std::f32::consts::{FRAC_PI_2, PI};
48use std::marker::PhantomData;
49use std::os::raw::{c_double, c_int};
50
51#[cfg(doc)]
52use crate::pdf::document::{PdfDocument, PdfPages};
53
54/// The orientation of a [PdfPage].
55#[derive(Copy, Clone, Debug, PartialEq)]
56pub enum PdfPageOrientation {
57 Portrait,
58 Landscape,
59}
60
61impl PdfPageOrientation {
62 #[inline]
63 pub(crate) fn from_width_and_height(width: PdfPoints, height: PdfPoints) -> Self {
64 if width.value > height.value {
65 PdfPageOrientation::Landscape
66 } else {
67 PdfPageOrientation::Portrait
68 }
69 }
70}
71
72/// A rotation transformation that should be applied to a [PdfPage] when it is rendered
73/// into a [PdfBitmap].
74#[derive(Copy, Clone, Debug, PartialEq)]
75pub enum PdfPageRenderRotation {
76 None,
77 Degrees90,
78 Degrees180,
79 Degrees270,
80}
81
82impl PdfPageRenderRotation {
83 #[inline]
84 pub(crate) fn from_pdfium(value: i32) -> Result<Self, PdfiumError> {
85 match value {
86 0 => Ok(PdfPageRenderRotation::None),
87 1 => Ok(PdfPageRenderRotation::Degrees90),
88 2 => Ok(PdfPageRenderRotation::Degrees180),
89 3 => Ok(PdfPageRenderRotation::Degrees270),
90 _ => Err(PdfiumError::UnknownBitmapRotation),
91 }
92 }
93
94 #[inline]
95 pub(crate) fn as_pdfium(&self) -> i32 {
96 match self {
97 PdfPageRenderRotation::None => 0,
98 PdfPageRenderRotation::Degrees90 => 1,
99 PdfPageRenderRotation::Degrees180 => 2,
100 PdfPageRenderRotation::Degrees270 => 3,
101 }
102 }
103
104 /// Returns the equivalent clockwise rotation of this [PdfPageRenderRotation] variant, in degrees.
105 #[inline]
106 pub const fn as_degrees(&self) -> f32 {
107 match self {
108 PdfPageRenderRotation::None => 0.0,
109 PdfPageRenderRotation::Degrees90 => 90.0,
110 PdfPageRenderRotation::Degrees180 => 180.0,
111 PdfPageRenderRotation::Degrees270 => 270.0,
112 }
113 }
114
115 pub(crate) const DEGREES_90_AS_RADIANS: f32 = FRAC_PI_2;
116
117 pub(crate) const DEGREES_180_AS_RADIANS: f32 = PI;
118
119 pub(crate) const DEGREES_270_AS_RADIANS: f32 = FRAC_PI_2 + PI;
120
121 /// Returns the equivalent clockwise rotation of this [PdfPageRenderRotation] variant, in radians.
122 #[inline]
123 pub const fn as_radians(&self) -> f32 {
124 match self {
125 PdfPageRenderRotation::None => 0.0,
126 PdfPageRenderRotation::Degrees90 => Self::DEGREES_90_AS_RADIANS,
127 PdfPageRenderRotation::Degrees180 => Self::DEGREES_180_AS_RADIANS,
128 PdfPageRenderRotation::Degrees270 => Self::DEGREES_270_AS_RADIANS,
129 }
130 }
131}
132
133/// Content regeneration strategies that instruct `pdfium-render` when, if ever, it should
134/// automatically regenerate the content of a [PdfPage].
135///
136/// Updates to a [PdfPage] are not committed to the underlying [PdfDocument] until the page's
137/// content is regenerated. If a page is reloaded or closed without regenerating the page's
138/// content, any changes not applied are lost.
139///
140/// By default, `pdfium-render` will trigger content regeneration on any change to a [PdfPage];
141/// this removes the possibility of data loss, and ensures changes can be read back from other
142/// data structures as soon as they are made. However, if many changes are made to a page at once,
143/// then regenerating the content after every change is inefficient; it is faster to stage
144/// all changes first, then regenerate the page's content just once. In this case,
145/// changing the content regeneration strategy for a [PdfPage] can improve performance,
146/// but you must be careful not to forget to commit your changes before the [PdfPage] moves out of scope.
147#[derive(Copy, Clone, Debug, PartialEq)]
148pub enum PdfPageContentRegenerationStrategy {
149 /// `pdfium-render` will call the [PdfPage::regenerate_content()] function on any
150 /// change to this [PdfPage]. This is the default setting.
151 AutomaticOnEveryChange,
152
153 /// `pdfium-render` will call the [PdfPage::regenerate_content()] function only when
154 /// this [PdfPage] is about to move out of scope.
155 AutomaticOnDrop,
156
157 /// `pdfium-render` will never call the [PdfPage::regenerate_content()] function.
158 /// You must do so manually after staging your changes, or your changes will be lost
159 /// when this [PdfPage] moves out of scope.
160 Manual,
161}
162
163/// A single page in a `PdfDocument`.
164///
165/// In addition to its own intrinsic properties, a [PdfPage] serves as the entry point
166/// to all object collections related to a single page in a document. These collections include:
167/// * [PdfPage::annotations()], an immutable collection of all the user annotations attached to the [PdfPage].
168/// * [PdfPage::annotations_mut()], a mutable collection of all the user annotations attached to the [PdfPage].
169/// * [PdfPage::boundaries()], an immutable collection of the boundary boxes relating to the [PdfPage].
170/// * [PdfPage::boundaries_mut()], a mutable collection of the boundary boxes relating to the [PdfPage].
171/// * [PdfPage::links()], an immutable collection of the links on the [PdfPage].
172/// * [PdfPage::links_mut()], a mutable collection of the links on the [PdfPage].
173/// * [PdfPage::objects()], an immutable collection of all the displayable objects on the [PdfPage].
174/// * [PdfPage::objects_mut()], a mutable collection of all the displayable objects on the [PdfPage].
175pub struct PdfPage<'a> {
176 document_handle: FPDF_DOCUMENT,
177 page_handle: FPDF_PAGE,
178 form_handle: Option<FPDF_FORMHANDLE>,
179 label: Option<String>,
180 regeneration_strategy: PdfPageContentRegenerationStrategy,
181 is_content_regeneration_required: bool,
182 annotations: PdfPageAnnotations<'a>,
183 boundaries: PdfPageBoundaries<'a>,
184 links: PdfPageLinks<'a>,
185 objects: PdfPageObjects<'a>,
186 lifetime: PhantomData<&'a FPDF_PAGE>,
187}
188
189impl<'a> PdfPage<'a> {
190 /// The default content regeneration strategy used by `pdfium-render`. This can be overridden
191 /// on a page-by-page basis using the [PdfPage::set_content_regeneration_strategy()] function.
192 const DEFAULT_CONTENT_REGENERATION_STRATEGY: PdfPageContentRegenerationStrategy =
193 PdfPageContentRegenerationStrategy::AutomaticOnEveryChange;
194
195 #[inline]
196 pub(crate) fn from_pdfium(
197 document_handle: FPDF_DOCUMENT,
198 page_handle: FPDF_PAGE,
199 form_handle: Option<FPDF_FORMHANDLE>,
200 label: Option<String>,
201 ) -> Self {
202 let mut result = PdfPage {
203 document_handle,
204 page_handle,
205 form_handle,
206 label,
207 regeneration_strategy: PdfPageContentRegenerationStrategy::Manual,
208 is_content_regeneration_required: false,
209 annotations: PdfPageAnnotations::from_pdfium(document_handle, page_handle, form_handle),
210 boundaries: PdfPageBoundaries::from_pdfium(page_handle),
211 links: PdfPageLinks::from_pdfium(page_handle, document_handle),
212 objects: PdfPageObjects::from_pdfium(document_handle, page_handle),
213 lifetime: PhantomData,
214 };
215
216 // Make sure the default content regeneration strategy is applied to child containers.
217
218 result.set_content_regeneration_strategy(Self::DEFAULT_CONTENT_REGENERATION_STRATEGY);
219
220 result
221 }
222
223 /// Returns the internal `FPDF_PAGE` handle for this [PdfPage].
224 #[inline]
225 pub(crate) fn page_handle(&self) -> FPDF_PAGE {
226 self.page_handle
227 }
228
229 /// Returns the internal `FPDF_DOCUMENT` handle of the [PdfDocument] containing this [PdfPage].
230 #[inline]
231 pub(crate) fn document_handle(&self) -> FPDF_DOCUMENT {
232 self.document_handle
233 }
234
235 /// Returns the label assigned to this [PdfPage], if any.
236 #[inline]
237 pub fn label(&self) -> Option<&str> {
238 self.label.as_deref()
239 }
240
241 /// Returns the width of this [PdfPage] in device-independent points.
242 /// One point is 1/72 inches, roughly 0.358 mm.
243 #[inline]
244 pub fn width(&self) -> PdfPoints {
245 PdfPoints::new(unsafe { self.bindings().FPDF_GetPageWidthF(self.page_handle) })
246 }
247
248 /// Returns the height of this [PdfPage] in device-independent points.
249 /// One point is 1/72 inches, roughly 0.358 mm.
250 #[inline]
251 pub fn height(&self) -> PdfPoints {
252 PdfPoints::new(unsafe { self.bindings().FPDF_GetPageHeightF(self.page_handle) })
253 }
254
255 /// Returns the width and height of this [PdfPage] expressed as a [PdfRect].
256 #[inline]
257 pub fn page_size(&self) -> PdfRect {
258 PdfRect::new(
259 PdfPoints::ZERO,
260 PdfPoints::ZERO,
261 self.height(),
262 self.width(),
263 )
264 }
265
266 /// Returns [PdfPageOrientation::Landscape] if the width of this [PdfPage]
267 /// is greater than its height; otherwise returns [PdfPageOrientation::Portrait].
268 #[inline]
269 pub fn orientation(&self) -> PdfPageOrientation {
270 PdfPageOrientation::from_width_and_height(self.width(), self.height())
271 }
272
273 /// Returns `true` if this [PdfPage] has orientation [PdfPageOrientation::Portrait].
274 #[inline]
275 pub fn is_portrait(&self) -> bool {
276 self.orientation() == PdfPageOrientation::Portrait
277 }
278
279 /// Returns `true` if this [PdfPage] has orientation [PdfPageOrientation::Landscape].
280 #[inline]
281 pub fn is_landscape(&self) -> bool {
282 self.orientation() == PdfPageOrientation::Landscape
283 }
284
285 /// Returns any intrinsic rotation encoded into this document indicating a rotation
286 /// should be applied to this [PdfPage] during rendering.
287 #[inline]
288 pub fn rotation(&self) -> Result<PdfPageRenderRotation, PdfiumError> {
289 PdfPageRenderRotation::from_pdfium(unsafe {
290 self.bindings().FPDFPage_GetRotation(self.page_handle)
291 })
292 }
293
294 /// Sets the intrinsic rotation that should be applied to this [PdfPage] during rendering.
295 #[inline]
296 pub fn set_rotation(&mut self, rotation: PdfPageRenderRotation) {
297 unsafe {
298 self.bindings()
299 .FPDFPage_SetRotation(self.page_handle, rotation.as_pdfium());
300 }
301 }
302
303 /// Returns `true` if any object on the page contains transparency.
304 #[inline]
305 pub fn has_transparency(&self) -> bool {
306 unsafe {
307 self.bindings()
308 .is_true(self.bindings().FPDFPage_HasTransparency(self.page_handle))
309 }
310 }
311
312 /// Returns the paper size of this [PdfPage].
313 #[inline]
314 pub fn paper_size(&self) -> PdfPagePaperSize {
315 PdfPagePaperSize::from_points(self.width(), self.height())
316 }
317
318 /// Returns `true` if this [PdfPage] contains an embedded thumbnail.
319 ///
320 /// Embedded thumbnails can be generated as a courtesy by PDF generators to save PDF consumers
321 /// the burden of having to render their own thumbnails on the fly. If a thumbnail for this page
322 /// was not embedded at the time the document was created, one can easily be rendered using the
323 /// standard rendering functions:
324 ///
325 /// ```
326 /// let thumbnail_desired_pixel_size = 128;
327 ///
328 /// let thumbnail = page.render_with_config(
329 /// &PdfRenderConfig::thumbnail(thumbnail_desired_pixel_size)
330 /// )?; // Renders a 128 x 128 thumbnail of the page
331 /// ```
332 #[inline]
333 pub fn has_embedded_thumbnail(&self) -> bool {
334 // To determine whether the page includes a thumbnail, we ask Pdfium to return the
335 // size of the thumbnail data. A non-zero value indicates a thumbnail exists.
336
337 (unsafe {
338 self.bindings()
339 .FPDFPage_GetRawThumbnailData(self.page_handle, std::ptr::null_mut(), 0)
340 }) > 0
341 }
342
343 /// Returns the embedded thumbnail for this [PdfPage], if any.
344 ///
345 /// Embedded thumbnails can be generated as a courtesy by PDF generators to save PDF consumers
346 /// the burden of having to render their own thumbnails on the fly. If a thumbnail for this page
347 /// was not embedded at the time the document was created, one can easily be rendered using the
348 /// standard rendering functions:
349 ///
350 /// ```
351 /// let thumbnail_desired_pixel_size = 128;
352 ///
353 /// let thumbnail = page.render_with_config(
354 /// &PdfRenderConfig::thumbnail(thumbnail_desired_pixel_size)
355 /// )?; // Renders a 128 x 128 thumbnail of the page
356 /// ```
357 pub fn embedded_thumbnail(&self) -> Result<PdfBitmap<'_>, PdfiumError> {
358 let thumbnail_handle = unsafe {
359 self.bindings()
360 .FPDFPage_GetThumbnailAsBitmap(self.page_handle)
361 };
362
363 if thumbnail_handle.is_null() {
364 // No thumbnail is available for this page.
365
366 Err(PdfiumError::PageMissingEmbeddedThumbnail)
367 } else {
368 Ok(PdfBitmap::from_pdfium(thumbnail_handle))
369 }
370 }
371
372 /// Returns the collection of text boxes contained within this [PdfPage].
373 pub fn text(&self) -> Result<PdfPageText<'_>, PdfiumError> {
374 let text_handle = unsafe { self.bindings().FPDFText_LoadPage(self.page_handle) };
375
376 if text_handle.is_null() {
377 Err(PdfiumError::PdfiumLibraryInternalError(
378 PdfiumInternalError::Unknown,
379 ))
380 } else {
381 Ok(PdfPageText::from_pdfium(text_handle, self))
382 }
383 }
384
385 /// Returns an immutable collection of the annotations that have been added to this [PdfPage].
386 pub fn annotations(&self) -> &PdfPageAnnotations<'a> {
387 &self.annotations
388 }
389
390 /// Returns a mutable collection of the annotations that have been added to this [PdfPage].
391 pub fn annotations_mut(&mut self) -> &mut PdfPageAnnotations<'a> {
392 &mut self.annotations
393 }
394
395 /// Returns an immutable collection of the bounding boxes defining the extents of this [PdfPage].
396 #[inline]
397 pub fn boundaries(&self) -> &PdfPageBoundaries<'a> {
398 &self.boundaries
399 }
400
401 /// Returns a mutable collection of the bounding boxes defining the extents of this [PdfPage].
402 #[inline]
403 pub fn boundaries_mut(&mut self) -> &mut PdfPageBoundaries<'a> {
404 &mut self.boundaries
405 }
406
407 /// Returns an immutable collection of the links on this [PdfPage].
408 #[inline]
409 pub fn links(&self) -> &PdfPageLinks<'a> {
410 &self.links
411 }
412
413 /// Returns a mutable collection of the links on this [PdfPage].
414 #[inline]
415 pub fn links_mut(&mut self) -> &mut PdfPageLinks<'a> {
416 &mut self.links
417 }
418
419 /// Returns an immutable collection of all the page objects on this [PdfPage].
420 pub fn objects(&self) -> &PdfPageObjects<'a> {
421 &self.objects
422 }
423
424 /// Returns a mutable collection of all the page objects on this [PdfPage].
425 pub fn objects_mut(&mut self) -> &mut PdfPageObjects<'a> {
426 &mut self.objects
427 }
428
429 /// Returns a list of all the distinct [PdfFont] instances used by the page text objects
430 /// on this [PdfPage], if any.
431 pub fn fonts(&self) -> Vec<PdfFont<'_>> {
432 let mut distinct_font_handles = HashMap::new();
433
434 let mut result = Vec::new();
435
436 for object in self.objects().iter() {
437 if let Some(object) = object.as_text_object() {
438 let font = object.font();
439
440 if let Entry::Vacant(entry) = distinct_font_handles.entry(font.handle()) {
441 entry.insert(true);
442 result.push(font.handle());
443 }
444 }
445 }
446
447 result
448 .into_iter()
449 .map(|handle| PdfFont::from_pdfium(handle, None, false))
450 .collect()
451 }
452
453 /// Converts from a bitmap coordinate system, measured in [Pixels] and with constraints
454 /// and dimensions determined by the given [PdfRenderConfig] object, to the equivalent
455 /// position on this page, measured in [PdfPoints].
456 pub fn pixels_to_points(
457 &self,
458 x: Pixels,
459 y: Pixels,
460 config: &PdfRenderConfig,
461 ) -> Result<(PdfPoints, PdfPoints), PdfiumError> {
462 let mut page_x: c_double = 0.0;
463 let mut page_y: c_double = 0.0;
464
465 let settings = config.apply_to_page(self);
466
467 if self.bindings().is_true(unsafe {
468 self.bindings().FPDF_DeviceToPage(
469 self.page_handle,
470 settings.clipping.left as c_int,
471 settings.clipping.top as c_int,
472 (settings.clipping.right - settings.clipping.left) as c_int,
473 (settings.clipping.bottom - settings.clipping.top) as c_int,
474 settings.rotate,
475 x as c_int,
476 y as c_int,
477 &mut page_x,
478 &mut page_y,
479 )
480 }) {
481 Ok((PdfPoints::new(page_x as f32), PdfPoints::new(page_y as f32)))
482 } else {
483 Err(PdfiumError::CoordinateConversionFunctionIndicatedError)
484 }
485 }
486
487 /// Converts from the page coordinate system, measured in [PdfPoints], to the equivalent position
488 /// in a bitmap coordinate system measured in [Pixels] and with constraints and dimensions
489 /// defined by the given [PdfRenderConfig] object.
490 pub fn points_to_pixels(
491 &self,
492 x: PdfPoints,
493 y: PdfPoints,
494 config: &PdfRenderConfig,
495 ) -> Result<(Pixels, Pixels), PdfiumError> {
496 let mut device_x: c_int = 0;
497 let mut device_y: c_int = 0;
498
499 let settings = config.apply_to_page(self);
500
501 if self.bindings().is_true(unsafe {
502 self.bindings().FPDF_PageToDevice(
503 self.page_handle,
504 settings.clipping.left as c_int,
505 settings.clipping.top as c_int,
506 (settings.clipping.right - settings.clipping.left) as c_int,
507 (settings.clipping.bottom - settings.clipping.top) as c_int,
508 settings.rotate,
509 x.value.into(),
510 y.value.into(),
511 &mut device_x,
512 &mut device_y,
513 )
514 }) {
515 Ok((device_x as Pixels, device_y as Pixels))
516 } else {
517 Err(PdfiumError::CoordinateConversionFunctionIndicatedError)
518 }
519 }
520
521 /// Renders this [PdfPage] into a [PdfBitmap] with the given pixel dimensions and page rotation.
522 ///
523 /// It is the responsibility of the caller to ensure the given pixel width and height
524 /// correctly maintain the page's aspect ratio.
525 ///
526 /// See also [PdfPage::render_with_config()], which calculates the correct pixel dimensions,
527 /// rotation settings, and rendering options to apply from a [PdfRenderConfig] object.
528 ///
529 /// Each call to `PdfPage::render()` creates a new [PdfBitmap] object and allocates memory
530 /// for it. To avoid repeated allocations, create a single [PdfBitmap] object
531 /// using [PdfBitmap::empty()] and reuse it across multiple calls to [PdfPage::render_into_bitmap()].
532 pub fn render(
533 &self,
534 width: Pixels,
535 height: Pixels,
536 rotation: Option<PdfPageRenderRotation>,
537 ) -> Result<PdfBitmap<'_>, PdfiumError> {
538 let mut bitmap = PdfBitmap::empty(width, height, PdfBitmapFormat::default())?;
539
540 let mut config = PdfRenderConfig::new()
541 .set_target_width(width)
542 .set_target_height(height);
543
544 if let Some(rotation) = rotation {
545 config = config.rotate(rotation, true);
546 }
547
548 self.render_into_bitmap_with_config(&mut bitmap, &config)?;
549
550 Ok(bitmap)
551 }
552
553 /// Renders this [PdfPage] into a new [PdfBitmap] using pixel dimensions, page rotation settings,
554 /// and rendering options configured in the given [PdfRenderConfig].
555 ///
556 /// Each call to `PdfPage::render_with_config()` creates a new [PdfBitmap] object and
557 /// allocates memory for it. To avoid repeated allocations, create a single [PdfBitmap] object
558 /// using [PdfBitmap::empty()] and reuse it across multiple calls to
559 /// [PdfPage::render_into_bitmap_with_config()].
560 pub fn render_with_config(
561 &self,
562 config: &PdfRenderConfig,
563 ) -> Result<PdfBitmap<'_>, PdfiumError> {
564 let settings = config.apply_to_page(self);
565
566 let mut bitmap = PdfBitmap::empty(
567 settings.width as Pixels,
568 settings.height as Pixels,
569 PdfBitmapFormat::from_pdfium(settings.format as u32)
570 .unwrap_or_else(|_| PdfBitmapFormat::default()),
571 )?;
572
573 self.render_into_bitmap_with_settings(&mut bitmap, settings)?;
574
575 Ok(bitmap)
576 }
577
578 /// Renders this [PdfPage] into the given [PdfBitmap] using the given pixel dimensions
579 /// and page rotation.
580 ///
581 /// It is the responsibility of the caller to ensure the given pixel width and height
582 /// correctly maintain the page's aspect ratio. The size of the buffer backing the given bitmap
583 /// must be sufficiently large to hold the rendered image or an error will be returned.
584 ///
585 /// See also [PdfPage::render_into_bitmap_with_config()], which calculates the correct pixel dimensions,
586 /// rotation settings, and rendering options to apply from a [PdfRenderConfig] object.
587 pub fn render_into_bitmap(
588 &self,
589 bitmap: &mut PdfBitmap,
590 width: Pixels,
591 height: Pixels,
592 rotation: Option<PdfPageRenderRotation>,
593 ) -> Result<(), PdfiumError> {
594 let mut config = PdfRenderConfig::new()
595 .set_target_width(width)
596 .set_target_height(height);
597
598 if let Some(rotation) = rotation {
599 config = config.rotate(rotation, true);
600 }
601
602 self.render_into_bitmap_with_config(bitmap, &config)
603 }
604
605 /// Renders this [PdfPage] into the given [PdfBitmap] using pixel dimensions, page rotation settings,
606 /// and rendering options configured in the given [PdfRenderConfig].
607 ///
608 /// The size of the buffer backing the given bitmap must be sufficiently large to hold the
609 /// rendered image or an error will be returned.
610 #[inline]
611 pub fn render_into_bitmap_with_config(
612 &self,
613 bitmap: &mut PdfBitmap,
614 config: &PdfRenderConfig,
615 ) -> Result<(), PdfiumError> {
616 self.render_into_bitmap_with_settings(bitmap, config.apply_to_page(self))
617 }
618
619 /// Renders this [PdfPage] into the given [PdfBitmap] using the given [PdfRenderSettings].
620 /// The size of the buffer backing the given bitmap must be sufficiently large to hold
621 /// the rendered image or an error will be returned.
622 pub(crate) fn render_into_bitmap_with_settings(
623 &self,
624 bitmap: &mut PdfBitmap,
625 settings: PdfPageRenderSettings,
626 ) -> Result<(), PdfiumError> {
627 let bitmap_handle = bitmap.handle();
628
629 if settings.do_clear_bitmap_before_rendering {
630 // Clear the bitmap buffer by setting every pixel to a known color.
631
632 unsafe {
633 self.bindings().FPDFBitmap_FillRect(
634 bitmap_handle,
635 0,
636 0,
637 settings.width,
638 settings.height,
639 settings.clear_color,
640 );
641 }
642 }
643
644 if settings.do_render_form_data {
645 // Render the PDF page into the bitmap buffer, ignoring any custom transformation matrix.
646 // (Custom transforms cannot be applied to the rendering of form fields.)
647
648 unsafe {
649 self.bindings().FPDF_RenderPageBitmap(
650 bitmap_handle,
651 self.page_handle,
652 0,
653 0,
654 settings.width,
655 settings.height,
656 settings.rotate,
657 settings.render_flags,
658 );
659 }
660
661 if let Some(form_handle) = self.form_handle {
662 // Render user-supplied form data, if any, as an overlay on top of the page.
663
664 if let Some(form_field_highlight) = settings.form_field_highlight.as_ref() {
665 for (form_field_type, (color, alpha)) in form_field_highlight.iter() {
666 unsafe {
667 self.bindings().FPDF_SetFormFieldHighlightColor(
668 form_handle,
669 *form_field_type,
670 *color,
671 );
672
673 self.bindings()
674 .FPDF_SetFormFieldHighlightAlpha(form_handle, *alpha);
675 }
676 }
677 }
678
679 unsafe {
680 self.bindings().FPDF_FFLDraw(
681 form_handle,
682 bitmap_handle,
683 self.page_handle,
684 0,
685 0,
686 settings.width,
687 settings.height,
688 settings.rotate,
689 settings.render_flags,
690 );
691 }
692 }
693 } else {
694 // Render the PDF page into the bitmap buffer, applying any custom transformation matrix.
695
696 unsafe {
697 self.bindings().FPDF_RenderPageBitmapWithMatrix(
698 bitmap_handle,
699 self.page_handle,
700 &settings.matrix,
701 &settings.clipping,
702 settings.render_flags,
703 );
704 }
705 }
706
707 bitmap.set_byte_order_from_render_settings(&settings);
708
709 Ok(())
710 }
711
712 /// Applies the given transformation, expressed as six values representing the six configurable
713 /// elements of a nine-element 3x3 PDF transformation matrix, to the objects on this [PdfPage],
714 /// restricting the effects of the transformation to the given clipping rectangle.
715 ///
716 /// To move, scale, rotate, or skew the objects on this [PdfPage], consider using one or more of
717 /// the following functions. Internally they all use [PdfPage::transform()], but are
718 /// probably easier to use (and certainly clearer in their intent) in most situations.
719 ///
720 /// * [PdfPage::translate()]: changes the position of each object on this [PdfPage].
721 /// * [PdfPage::scale()]: changes the size of each object on this [PdfPage].
722 /// * [PdfPage::flip_horizontally()]: flips each object on this [PdfPage] horizontally around
723 /// the page origin point.
724 /// * [PdfPage::flip_vertically()]: flips each object on this [PdfPage] vertically around
725 /// the page origin point.
726 /// * [PdfPage::rotate_clockwise_degrees()], [PdfPage::rotate_counter_clockwise_degrees()],
727 /// [PdfPage::rotate_clockwise_radians()], [PdfPage::rotate_counter_clockwise_radians()]:
728 /// rotates each object on this [PdfPage] around its origin.
729 /// * [PdfPage::skew_degrees()], [PdfPage::skew_radians()]: skews each object
730 /// on this [PdfPage] relative to its axes.
731 ///
732 /// **The order in which transformations are applied is significant.**
733 /// For example, the result of rotating _then_ translating an object may be vastly different
734 /// from translating _then_ rotating the same object.
735 ///
736 /// An overview of PDF transformation matrices can be found in the PDF Reference Manual
737 /// version 1.7 on page 204; a detailed description can be found in section 4.2.3 on page 207.
738 #[inline]
739 #[allow(clippy::too_many_arguments)]
740 pub fn transform_with_clip(
741 &mut self,
742 a: PdfMatrixValue,
743 b: PdfMatrixValue,
744 c: PdfMatrixValue,
745 d: PdfMatrixValue,
746 e: PdfMatrixValue,
747 f: PdfMatrixValue,
748 clip: PdfRect,
749 ) -> Result<(), PdfiumError> {
750 self.apply_matrix_with_clip(PdfMatrix::new(a, b, c, d, e, f), clip)
751 }
752
753 /// Applies the given transformation, expressed as a [PdfMatrix], to this [PdfPage],
754 /// restricting the effects of the transformation matrix to the given clipping rectangle.
755 pub fn apply_matrix_with_clip(
756 &mut self,
757 matrix: PdfMatrix,
758 clip: PdfRect,
759 ) -> Result<(), PdfiumError> {
760 if self.bindings().is_true(unsafe {
761 self.bindings().FPDFPage_TransFormWithClip(
762 self.page_handle,
763 &matrix.as_pdfium(),
764 &clip.as_pdfium(),
765 )
766 }) {
767 // A probable bug in Pdfium means we must reload the page in order for the
768 // transformation to take effect. For more information, see:
769 // https://github.com/ajrcarey/pdfium-render/issues/93
770
771 self.reload_in_place();
772 Ok(())
773 } else {
774 Err(PdfiumError::PdfiumLibraryInternalError(
775 PdfiumInternalError::Unknown,
776 ))
777 }
778 }
779
780 create_transform_setters!(
781 &mut Self,
782 Result<(), PdfiumError>,
783 "each object on this [PdfPage]",
784 "each object on this [PdfPage].",
785 "each object on this [PdfPage],",
786 "",
787 pub(self)
788 ); // pub(self) visibility for the generated reset_matrix() function will effectively make it
789 // private. This is what we want, since Pdfium does not expose a function to directly set
790 // the transformation matrix of a page.
791
792 #[inline]
793 fn transform_impl(
794 &mut self,
795 a: PdfMatrixValue,
796 b: PdfMatrixValue,
797 c: PdfMatrixValue,
798 d: PdfMatrixValue,
799 e: PdfMatrixValue,
800 f: PdfMatrixValue,
801 ) -> Result<(), PdfiumError> {
802 self.transform_with_clip(a, b, c, d, e, f, PdfRect::MAX)
803 }
804
805 // The reset_matrix() function created by the create_transform_setters!() macro
806 // is not publicly visible, so this function should never be called.
807 #[allow(dead_code)]
808 fn reset_matrix_impl(&mut self, _: PdfMatrix) -> Result<(), PdfiumError> {
809 unreachable!();
810 }
811
812 /// Flattens all annotations and form fields on this [PdfPage] into the page contents.
813 #[cfg(feature = "flatten")]
814 // Use a custom-written flatten operation, rather than Pdfium's built-in flatten. See:
815 // https://github.com/ajrcarey/pdfium-render/issues/140
816 pub fn flatten(&mut self) -> Result<(), PdfiumError> {
817 flatten_page(self.handle())
818 }
819
820 /// Flattens all annotations and form fields on this [PdfPage] into the page contents.
821 #[cfg(not(feature = "flatten"))]
822 // Use Pdfium's built-in flatten. This has some problems; see:
823 // https://github.com/ajrcarey/pdfium-render/issues/140
824 pub fn flatten(&mut self) -> Result<(), PdfiumError> {
825 // TODO: AJRC - 28/5/22 - consider allowing the caller to set the FLAT_NORMALDISPLAY or FLAT_PRINT flag.
826 let flag = FLAT_PRINT;
827
828 match unsafe {
829 self.bindings()
830 .FPDFPage_Flatten(self.page_handle, flag as c_int)
831 } as u32
832 {
833 FLATTEN_SUCCESS => {
834 self.regenerate_content()?;
835
836 // As noted at https://bugs.chromium.org/p/pdfium/issues/detail?id=2055,
837 // FPDFPage_Flatten() updates the underlying dictionaries and content streams for
838 // the page, but does not update the FPDF_Page structure. We must reload the
839 // page for the effects of the flatten operation to be visible. For more information, see:
840 // https://github.com/ajrcarey/pdfium-render/issues/140
841
842 self.reload_in_place();
843 Ok(())
844 }
845 FLATTEN_NOTHINGTODO => Ok(()),
846 FLATTEN_FAIL => Err(PdfiumError::PageFlattenFailure),
847 _ => Err(PdfiumError::PageFlattenFailure),
848 }
849 }
850
851 /// Deletes this [PdfPage] from its containing `PdfPages` collection, consuming this [PdfPage].
852 pub fn delete(self) -> Result<(), PdfiumError> {
853 let index = PdfPageIndexCache::get_index_for_page(self.document_handle, self.page_handle)
854 .ok_or(PdfiumError::SourcePageIndexNotInCache)?;
855
856 unsafe {
857 self.bindings()
858 .FPDFPage_Delete(self.document_handle, index as c_int);
859 }
860
861 PdfPageIndexCache::delete_pages_at_index(self.document_handle, index, 1);
862
863 Ok(())
864 }
865
866 /// Returns the strategy used by `pdfium-render` to regenerate the content of a [PdfPage].
867 ///
868 /// Updates to a [PdfPage] are not committed to the underlying `PdfDocument` until the page's
869 /// content is regenerated. If a page is reloaded or closed without regenerating the page's
870 /// content, all uncommitted changes will be lost.
871 ///
872 /// By default, `pdfium-render` will trigger content regeneration on any change to a [PdfPage];
873 /// this removes the possibility of data loss, and ensures changes can be read back from other
874 /// data structures as soon as they are made. However, if many changes are made to a page at once,
875 /// then regenerating the content after every change is inefficient; it is faster to stage
876 /// all changes first, then regenerate the page's content just once. In this case,
877 /// changing the content regeneration strategy for a [PdfPage] can improve performance,
878 /// but you must be careful not to forget to commit your changes before closing
879 /// or reloading the page.
880 #[inline]
881 pub fn content_regeneration_strategy(&self) -> PdfPageContentRegenerationStrategy {
882 self.regeneration_strategy
883 }
884
885 /// Sets the strategy used by `pdfium-render` to regenerate the content of a [PdfPage].
886 ///
887 /// Updates to a [PdfPage] are not committed to the underlying `PdfDocument` until the page's
888 /// content is regenerated. If a page is reloaded or closed without regenerating the page's
889 /// content, all uncommitted changes will be lost.
890 ///
891 /// By default, `pdfium-render` will trigger content regeneration on any change to a [PdfPage];
892 /// this removes the possibility of data loss, and ensures changes can be read back from other
893 /// data structures as soon as they are made. However, if many changes are made to a page at once,
894 /// then regenerating the content after every change is inefficient; it is faster to stage
895 /// all changes first, then regenerate the page's content just once. In this case,
896 /// changing the content regeneration strategy for a [PdfPage] can improve performance,
897 /// but you must be careful not to forget to commit your changes before closing
898 /// or reloading the page.
899 #[inline]
900 pub fn set_content_regeneration_strategy(
901 &mut self,
902 strategy: PdfPageContentRegenerationStrategy,
903 ) {
904 self.regeneration_strategy = strategy;
905
906 if let Some(index) =
907 PdfPageIndexCache::get_index_for_page(self.document_handle(), self.page_handle())
908 {
909 PdfPageIndexCache::cache_props_for_page(
910 self.document_handle(),
911 self.page_handle(),
912 index,
913 strategy,
914 );
915 }
916 }
917
918 /// Commits any staged but unsaved changes to this [PdfPage] to the underlying [PdfDocument].
919 ///
920 /// Updates to a [PdfPage] are not committed to the underlying [PdfDocument] until the page's
921 /// content is regenerated. If a page is reloaded or closed without regenerating the page's
922 /// content, all uncommitted changes will be lost.
923 ///
924 /// By default, `pdfium-render` will trigger content regeneration on any change to a [PdfPage];
925 /// this removes the possibility of data loss, and ensures changes can be read back from other
926 /// data structures as soon as they are made. However, if many changes are made to a page at once,
927 /// then regenerating the content after every change is inefficient; it is faster to stage
928 /// all changes first, then regenerate the page's content just once. In this case,
929 /// changing the content regeneration strategy for a [PdfPage] can improve performance,
930 /// but you must be careful not to forget to commit your changes before closing
931 /// or reloading the page.
932 #[inline]
933 pub fn regenerate_content(&mut self) -> Result<(), PdfiumError> {
934 // This is a publicly-visible wrapper for the private regenerate_content_immut() function.
935 // It is only available to callers who hold a mutable reference to the page.
936
937 self.regenerate_content_immut()
938 }
939
940 /// Commits any staged but unsaved changes to this [PdfPage] to the underlying [PdfDocument].
941 #[inline]
942 pub(crate) fn regenerate_content_immut(&self) -> Result<(), PdfiumError> {
943 Self::regenerate_content_immut_for_handle(self.page_handle, self.bindings())
944 }
945
946 /// Commits any staged but unsaved changes to the page identified by the given internal
947 /// `FPDF_PAGE` handle to the underlying [PdfDocument] containing that page.
948 ///
949 /// This function always commits changes, irrespective of the page's currently set
950 /// content regeneration strategy.
951 pub(crate) fn regenerate_content_immut_for_handle(
952 page: FPDF_PAGE,
953 bindings: &dyn PdfiumLibraryBindings,
954 ) -> Result<(), PdfiumError> {
955 if bindings.is_true(unsafe { bindings.FPDFPage_GenerateContent(page) }) {
956 Ok(())
957 } else {
958 Err(PdfiumError::PdfiumLibraryInternalError(
959 PdfiumInternalError::Unknown,
960 ))
961 }
962 }
963
964 /// Reloads the page transparently to any caller, forcing a refresh of all page data structures.
965 /// This will replace this page's `FPDF_PAGE` handle. The page index cache will be updated.
966 fn reload_in_place(&mut self) {
967 if let Some(page_index) =
968 PdfPageIndexCache::get_index_for_page(self.document_handle, self.page_handle)
969 {
970 self.drop_impl();
971
972 self.page_handle = unsafe {
973 self.bindings()
974 .FPDF_LoadPage(self.document_handle, page_index as c_int)
975 };
976
977 PdfPageIndexCache::cache_props_for_page(
978 self.document_handle,
979 self.page_handle,
980 page_index,
981 self.content_regeneration_strategy(),
982 );
983 }
984 }
985
986 /// Drops the page by calling `FPDF_ClosePage()`, freeing held memory. This will invalidate
987 /// this page's `FPDF_PAGE` handle. The page index cache will be updated.
988 fn drop_impl(&mut self) {
989 if self.regeneration_strategy != PdfPageContentRegenerationStrategy::Manual
990 && self.is_content_regeneration_required
991 {
992 // Regenerate page content now if necessary, before the PdfPage moves out of scope.
993
994 let result = self.regenerate_content();
995
996 debug_assert!(result.is_ok());
997 }
998
999 unsafe {
1000 self.bindings().FPDF_ClosePage(self.page_handle);
1001 }
1002
1003 PdfPageIndexCache::remove_index_for_page(self.document_handle, self.page_handle);
1004 }
1005}
1006
1007impl<'a> Drop for PdfPage<'a> {
1008 /// Closes this [PdfPage], releasing held memory.
1009 #[inline]
1010 fn drop(&mut self) {
1011 self.drop_impl();
1012 }
1013}
1014
1015impl<'a> PdfiumLibraryBindingsAccessor<'a> for PdfPage<'a> {}
1016
1017#[cfg(feature = "thread_safe")]
1018unsafe impl<'a> Send for PdfPage<'a> {}
1019
1020#[cfg(feature = "thread_safe")]
1021unsafe impl<'a> Sync for PdfPage<'a> {}
1022
1023#[cfg(test)]
1024mod tests {
1025 use crate::prelude::*;
1026 use crate::utils::test::test_bind_to_pdfium;
1027 use image_025::{GenericImageView, ImageFormat};
1028
1029 #[test]
1030 fn test_page_rendering_reusing_bitmap() -> Result<(), PdfiumError> {
1031 // Renders each page in the given test PDF file to a separate JPEG file
1032 // by re-using the same bitmap buffer for each render.
1033
1034 let pdfium = test_bind_to_pdfium();
1035
1036 let document = pdfium.load_pdf_from_file("./test/export-test.pdf", None)?;
1037
1038 let render_config = PdfRenderConfig::new()
1039 .set_target_width(2000)
1040 .set_maximum_height(2000)
1041 .rotate_if_landscape(PdfPageRenderRotation::Degrees90, true);
1042
1043 let mut bitmap = PdfBitmap::empty(2500, 2500, PdfBitmapFormat::default())?;
1044
1045 for (index, page) in document.pages().iter().enumerate() {
1046 page.render_into_bitmap_with_config(&mut bitmap, &render_config)?; // Re-uses the same bitmap for rendering each page.
1047
1048 bitmap
1049 .as_image()?
1050 .into_rgb8()
1051 .save_with_format(format!("test-page-{}.jpg", index), ImageFormat::Jpeg)
1052 .map_err(|_| PdfiumError::ImageError)?;
1053 }
1054
1055 Ok(())
1056 }
1057
1058 #[test]
1059 fn test_rendered_image_dimension() -> Result<(), PdfiumError> {
1060 // Checks that downscaled dimensions are rounded correctly during page rendering.
1061 // See: https://github.com/ajrcarey/pdfium-render/pull/87
1062
1063 let pdfium = test_bind_to_pdfium();
1064
1065 let document = pdfium.load_pdf_from_file("./test/dimensions-test.pdf", None)?;
1066
1067 let render_config = PdfRenderConfig::new()
1068 .set_target_width(500)
1069 .set_maximum_height(500);
1070
1071 for (_index, page) in document.pages().iter().enumerate() {
1072 let rendered_page = page.render_with_config(&render_config)?.as_image()?;
1073
1074 let (width, _height) = rendered_page.dimensions();
1075
1076 assert_eq!(width, 500);
1077 }
1078
1079 Ok(())
1080 }
1081}