pub mod annotation;
pub mod annotations;
pub mod boundaries;
pub mod field;
pub(crate) mod index_cache;
pub mod links;
pub mod object;
pub mod objects;
pub mod render_config;
pub mod size;
pub mod text;
#[cfg(feature = "paragraph")]
pub mod paragraph;
#[cfg(feature = "flatten")]
mod flatten;
use object::ownership::PdfPageObjectOwnership;
use crate::bindgen::{
FLATTEN_FAIL, FLATTEN_NOTHINGTODO, FLATTEN_SUCCESS, FLAT_PRINT, FPDF_DOCUMENT, FPDF_FORMHANDLE,
FPDF_PAGE,
};
use crate::bindings::PdfiumLibraryBindings;
use crate::create_transform_setters;
use crate::error::{PdfiumError, PdfiumInternalError};
use crate::pdf::bitmap::{PdfBitmap, PdfBitmapFormat, Pixels};
use crate::pdf::document::page::annotations::PdfPageAnnotations;
use crate::pdf::document::page::boundaries::PdfPageBoundaries;
use crate::pdf::document::page::index_cache::PdfPageIndexCache;
use crate::pdf::document::page::links::PdfPageLinks;
use crate::pdf::document::page::objects::common::PdfPageObjectsCommon;
use crate::pdf::document::page::objects::PdfPageObjects;
use crate::pdf::document::page::render_config::{PdfPageRenderSettings, PdfRenderConfig};
use crate::pdf::document::page::size::PdfPagePaperSize;
use crate::pdf::document::page::text::PdfPageText;
use crate::pdf::font::PdfFont;
use crate::pdf::matrix::{PdfMatrix, PdfMatrixValue};
use crate::pdf::points::PdfPoints;
use crate::pdf::rect::PdfRect;
use crate::pdfium::PdfiumLibraryBindingsAccessor;
use std::collections::{hash_map::Entry, HashMap};
use std::f32::consts::{FRAC_PI_2, PI};
use std::marker::PhantomData;
use std::os::raw::{c_double, c_int};
#[cfg(doc)]
use crate::pdf::document::{PdfDocument, PdfPages};
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PdfPageOrientation {
Portrait,
Landscape,
}
impl PdfPageOrientation {
#[inline]
pub(crate) fn from_width_and_height(width: PdfPoints, height: PdfPoints) -> Self {
if width.value > height.value {
PdfPageOrientation::Landscape
} else {
PdfPageOrientation::Portrait
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PdfPageRenderRotation {
None,
Degrees90,
Degrees180,
Degrees270,
}
impl PdfPageRenderRotation {
#[inline]
pub(crate) fn from_pdfium(value: i32) -> Result<Self, PdfiumError> {
match value {
0 => Ok(PdfPageRenderRotation::None),
1 => Ok(PdfPageRenderRotation::Degrees90),
2 => Ok(PdfPageRenderRotation::Degrees180),
3 => Ok(PdfPageRenderRotation::Degrees270),
_ => Err(PdfiumError::UnknownBitmapRotation),
}
}
#[inline]
pub(crate) fn as_pdfium(&self) -> i32 {
match self {
PdfPageRenderRotation::None => 0,
PdfPageRenderRotation::Degrees90 => 1,
PdfPageRenderRotation::Degrees180 => 2,
PdfPageRenderRotation::Degrees270 => 3,
}
}
#[inline]
pub const fn as_degrees(&self) -> f32 {
match self {
PdfPageRenderRotation::None => 0.0,
PdfPageRenderRotation::Degrees90 => 90.0,
PdfPageRenderRotation::Degrees180 => 180.0,
PdfPageRenderRotation::Degrees270 => 270.0,
}
}
pub(crate) const DEGREES_90_AS_RADIANS: f32 = FRAC_PI_2;
pub(crate) const DEGREES_180_AS_RADIANS: f32 = PI;
pub(crate) const DEGREES_270_AS_RADIANS: f32 = FRAC_PI_2 + PI;
#[inline]
pub const fn as_radians(&self) -> f32 {
match self {
PdfPageRenderRotation::None => 0.0,
PdfPageRenderRotation::Degrees90 => Self::DEGREES_90_AS_RADIANS,
PdfPageRenderRotation::Degrees180 => Self::DEGREES_180_AS_RADIANS,
PdfPageRenderRotation::Degrees270 => Self::DEGREES_270_AS_RADIANS,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PdfPageContentRegenerationStrategy {
AutomaticOnEveryChange,
AutomaticOnDrop,
Manual,
}
pub struct PdfPage<'a> {
document_handle: FPDF_DOCUMENT,
page_handle: FPDF_PAGE,
form_handle: Option<FPDF_FORMHANDLE>,
label: Option<String>,
regeneration_strategy: PdfPageContentRegenerationStrategy,
is_content_regeneration_required: bool,
annotations: PdfPageAnnotations<'a>,
boundaries: PdfPageBoundaries<'a>,
links: PdfPageLinks<'a>,
objects: PdfPageObjects<'a>,
lifetime: PhantomData<&'a FPDF_PAGE>,
}
impl<'a> PdfPage<'a> {
const DEFAULT_CONTENT_REGENERATION_STRATEGY: PdfPageContentRegenerationStrategy =
PdfPageContentRegenerationStrategy::AutomaticOnEveryChange;
#[inline]
pub(crate) fn from_pdfium(
document_handle: FPDF_DOCUMENT,
page_handle: FPDF_PAGE,
form_handle: Option<FPDF_FORMHANDLE>,
label: Option<String>,
) -> Self {
let mut result = PdfPage {
document_handle,
page_handle,
form_handle,
label,
regeneration_strategy: PdfPageContentRegenerationStrategy::Manual,
is_content_regeneration_required: false,
annotations: PdfPageAnnotations::from_pdfium(document_handle, page_handle, form_handle),
boundaries: PdfPageBoundaries::from_pdfium(page_handle),
links: PdfPageLinks::from_pdfium(page_handle, document_handle),
objects: PdfPageObjects::from_pdfium(document_handle, page_handle),
lifetime: PhantomData,
};
result.set_content_regeneration_strategy(Self::DEFAULT_CONTENT_REGENERATION_STRATEGY);
result
}
#[inline]
pub(crate) fn page_handle(&self) -> FPDF_PAGE {
self.page_handle
}
#[inline]
pub(crate) fn document_handle(&self) -> FPDF_DOCUMENT {
self.document_handle
}
#[inline]
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
#[inline]
pub fn width(&self) -> PdfPoints {
PdfPoints::new(unsafe { self.bindings().FPDF_GetPageWidthF(self.page_handle) })
}
#[inline]
pub fn height(&self) -> PdfPoints {
PdfPoints::new(unsafe { self.bindings().FPDF_GetPageHeightF(self.page_handle) })
}
#[inline]
pub fn page_size(&self) -> PdfRect {
PdfRect::new(
PdfPoints::ZERO,
PdfPoints::ZERO,
self.height(),
self.width(),
)
}
#[inline]
pub fn orientation(&self) -> PdfPageOrientation {
PdfPageOrientation::from_width_and_height(self.width(), self.height())
}
#[inline]
pub fn is_portrait(&self) -> bool {
self.orientation() == PdfPageOrientation::Portrait
}
#[inline]
pub fn is_landscape(&self) -> bool {
self.orientation() == PdfPageOrientation::Landscape
}
#[inline]
pub fn rotation(&self) -> Result<PdfPageRenderRotation, PdfiumError> {
PdfPageRenderRotation::from_pdfium(unsafe {
self.bindings().FPDFPage_GetRotation(self.page_handle)
})
}
#[inline]
pub fn set_rotation(&mut self, rotation: PdfPageRenderRotation) {
unsafe {
self.bindings()
.FPDFPage_SetRotation(self.page_handle, rotation.as_pdfium());
}
}
#[inline]
pub fn has_transparency(&self) -> bool {
unsafe {
self.bindings()
.is_true(self.bindings().FPDFPage_HasTransparency(self.page_handle))
}
}
#[inline]
pub fn paper_size(&self) -> PdfPagePaperSize {
PdfPagePaperSize::from_points(self.width(), self.height())
}
#[inline]
pub fn has_embedded_thumbnail(&self) -> bool {
(unsafe {
self.bindings()
.FPDFPage_GetRawThumbnailData(self.page_handle, std::ptr::null_mut(), 0)
}) > 0
}
pub fn embedded_thumbnail(&self) -> Result<PdfBitmap<'_>, PdfiumError> {
let thumbnail_handle = unsafe {
self.bindings()
.FPDFPage_GetThumbnailAsBitmap(self.page_handle)
};
if thumbnail_handle.is_null() {
Err(PdfiumError::PageMissingEmbeddedThumbnail)
} else {
Ok(PdfBitmap::from_pdfium(thumbnail_handle))
}
}
pub fn text(&self) -> Result<PdfPageText<'_>, PdfiumError> {
let text_handle = unsafe { self.bindings().FPDFText_LoadPage(self.page_handle) };
if text_handle.is_null() {
Err(PdfiumError::PdfiumLibraryInternalError(
PdfiumInternalError::Unknown,
))
} else {
Ok(PdfPageText::from_pdfium(text_handle, self))
}
}
pub fn annotations(&self) -> &PdfPageAnnotations<'a> {
&self.annotations
}
pub fn annotations_mut(&mut self) -> &mut PdfPageAnnotations<'a> {
&mut self.annotations
}
#[inline]
pub fn boundaries(&self) -> &PdfPageBoundaries<'a> {
&self.boundaries
}
#[inline]
pub fn boundaries_mut(&mut self) -> &mut PdfPageBoundaries<'a> {
&mut self.boundaries
}
#[inline]
pub fn links(&self) -> &PdfPageLinks<'a> {
&self.links
}
#[inline]
pub fn links_mut(&mut self) -> &mut PdfPageLinks<'a> {
&mut self.links
}
pub fn objects(&self) -> &PdfPageObjects<'a> {
&self.objects
}
pub fn objects_mut(&mut self) -> &mut PdfPageObjects<'a> {
&mut self.objects
}
pub fn fonts(&self) -> Vec<PdfFont<'_>> {
let mut distinct_font_handles = HashMap::new();
let mut result = Vec::new();
for object in self.objects().iter() {
if let Some(object) = object.as_text_object() {
let font = object.font();
if let Entry::Vacant(entry) = distinct_font_handles.entry(font.handle()) {
entry.insert(true);
result.push(font.handle());
}
}
}
result
.into_iter()
.map(|handle| PdfFont::from_pdfium(handle, None, false))
.collect()
}
pub fn pixels_to_points(
&self,
x: Pixels,
y: Pixels,
config: &PdfRenderConfig,
) -> Result<(PdfPoints, PdfPoints), PdfiumError> {
let mut page_x: c_double = 0.0;
let mut page_y: c_double = 0.0;
let settings = config.apply_to_page(self);
if self.bindings().is_true(unsafe {
self.bindings().FPDF_DeviceToPage(
self.page_handle,
settings.clipping.left as c_int,
settings.clipping.top as c_int,
(settings.clipping.right - settings.clipping.left) as c_int,
(settings.clipping.bottom - settings.clipping.top) as c_int,
settings.rotate,
x as c_int,
y as c_int,
&mut page_x,
&mut page_y,
)
}) {
Ok((PdfPoints::new(page_x as f32), PdfPoints::new(page_y as f32)))
} else {
Err(PdfiumError::CoordinateConversionFunctionIndicatedError)
}
}
pub fn points_to_pixels(
&self,
x: PdfPoints,
y: PdfPoints,
config: &PdfRenderConfig,
) -> Result<(Pixels, Pixels), PdfiumError> {
let mut device_x: c_int = 0;
let mut device_y: c_int = 0;
let settings = config.apply_to_page(self);
if self.bindings().is_true(unsafe {
self.bindings().FPDF_PageToDevice(
self.page_handle,
settings.clipping.left as c_int,
settings.clipping.top as c_int,
(settings.clipping.right - settings.clipping.left) as c_int,
(settings.clipping.bottom - settings.clipping.top) as c_int,
settings.rotate,
x.value.into(),
y.value.into(),
&mut device_x,
&mut device_y,
)
}) {
Ok((device_x as Pixels, device_y as Pixels))
} else {
Err(PdfiumError::CoordinateConversionFunctionIndicatedError)
}
}
pub fn render(
&self,
width: Pixels,
height: Pixels,
rotation: Option<PdfPageRenderRotation>,
) -> Result<PdfBitmap<'_>, PdfiumError> {
let mut bitmap =
PdfBitmap::empty(width, height, PdfBitmapFormat::default(), self.bindings())?;
let mut config = PdfRenderConfig::new()
.set_target_width(width)
.set_target_height(height);
if let Some(rotation) = rotation {
config = config.rotate(rotation, true);
}
self.render_into_bitmap_with_config(&mut bitmap, &config)?;
Ok(bitmap)
}
pub fn render_with_config(
&self,
config: &PdfRenderConfig,
) -> Result<PdfBitmap<'_>, PdfiumError> {
let settings = config.apply_to_page(self);
let mut bitmap = PdfBitmap::empty(
settings.width as Pixels,
settings.height as Pixels,
PdfBitmapFormat::from_pdfium(settings.format as u32)
.unwrap_or_else(|_| PdfBitmapFormat::default()),
self.bindings(),
)?;
self.render_into_bitmap_with_settings(&mut bitmap, settings)?;
Ok(bitmap)
}
pub fn render_into_bitmap(
&self,
bitmap: &mut PdfBitmap,
width: Pixels,
height: Pixels,
rotation: Option<PdfPageRenderRotation>,
) -> Result<(), PdfiumError> {
let mut config = PdfRenderConfig::new()
.set_target_width(width)
.set_target_height(height);
if let Some(rotation) = rotation {
config = config.rotate(rotation, true);
}
self.render_into_bitmap_with_config(bitmap, &config)
}
#[inline]
pub fn render_into_bitmap_with_config(
&self,
bitmap: &mut PdfBitmap,
config: &PdfRenderConfig,
) -> Result<(), PdfiumError> {
self.render_into_bitmap_with_settings(bitmap, config.apply_to_page(self))
}
pub(crate) fn render_into_bitmap_with_settings(
&self,
bitmap: &mut PdfBitmap,
settings: PdfPageRenderSettings,
) -> Result<(), PdfiumError> {
let bitmap_handle = bitmap.handle();
if settings.do_clear_bitmap_before_rendering {
unsafe {
self.bindings().FPDFBitmap_FillRect(
bitmap_handle,
0,
0,
settings.width,
settings.height,
settings.clear_color,
);
}
}
if settings.do_render_form_data {
unsafe {
self.bindings().FPDF_RenderPageBitmap(
bitmap_handle,
self.page_handle,
0,
0,
settings.width,
settings.height,
settings.rotate,
settings.render_flags,
);
}
if let Some(form_handle) = self.form_handle {
if let Some(form_field_highlight) = settings.form_field_highlight.as_ref() {
for (form_field_type, (color, alpha)) in form_field_highlight.iter() {
unsafe {
self.bindings().FPDF_SetFormFieldHighlightColor(
form_handle,
*form_field_type,
*color,
);
self.bindings()
.FPDF_SetFormFieldHighlightAlpha(form_handle, *alpha);
}
}
}
unsafe {
self.bindings().FPDF_FFLDraw(
form_handle,
bitmap_handle,
self.page_handle,
0,
0,
settings.width,
settings.height,
settings.rotate,
settings.render_flags,
);
}
}
} else {
unsafe {
self.bindings().FPDF_RenderPageBitmapWithMatrix(
bitmap_handle,
self.page_handle,
&settings.matrix,
&settings.clipping,
settings.render_flags,
);
}
}
bitmap.set_byte_order_from_render_settings(&settings);
Ok(())
}
#[inline]
#[allow(clippy::too_many_arguments)]
pub fn transform_with_clip(
&mut self,
a: PdfMatrixValue,
b: PdfMatrixValue,
c: PdfMatrixValue,
d: PdfMatrixValue,
e: PdfMatrixValue,
f: PdfMatrixValue,
clip: PdfRect,
) -> Result<(), PdfiumError> {
self.apply_matrix_with_clip(PdfMatrix::new(a, b, c, d, e, f), clip)
}
pub fn apply_matrix_with_clip(
&mut self,
matrix: PdfMatrix,
clip: PdfRect,
) -> Result<(), PdfiumError> {
if self.bindings().is_true(unsafe {
self.bindings().FPDFPage_TransFormWithClip(
self.page_handle,
&matrix.as_pdfium(),
&clip.as_pdfium(),
)
}) {
self.reload_in_place();
Ok(())
} else {
Err(PdfiumError::PdfiumLibraryInternalError(
PdfiumInternalError::Unknown,
))
}
}
create_transform_setters!(
&mut Self,
Result<(), PdfiumError>,
"each object on this [PdfPage]",
"each object on this [PdfPage].",
"each object on this [PdfPage],",
"",
pub(self)
);
#[inline]
fn transform_impl(
&mut self,
a: PdfMatrixValue,
b: PdfMatrixValue,
c: PdfMatrixValue,
d: PdfMatrixValue,
e: PdfMatrixValue,
f: PdfMatrixValue,
) -> Result<(), PdfiumError> {
self.transform_with_clip(a, b, c, d, e, f, PdfRect::MAX)
}
#[allow(dead_code)]
fn reset_matrix_impl(&mut self, _: PdfMatrix) -> Result<(), PdfiumError> {
unreachable!();
}
#[cfg(feature = "flatten")]
pub fn flatten(&mut self) -> Result<(), PdfiumError> {
flatten_page(self.handle())
}
#[cfg(not(feature = "flatten"))]
pub fn flatten(&mut self) -> Result<(), PdfiumError> {
let flag = FLAT_PRINT;
match unsafe {
self.bindings()
.FPDFPage_Flatten(self.page_handle, flag as c_int)
} as u32
{
FLATTEN_SUCCESS => {
self.regenerate_content()?;
self.reload_in_place();
Ok(())
}
FLATTEN_NOTHINGTODO => Ok(()),
FLATTEN_FAIL => Err(PdfiumError::PageFlattenFailure),
_ => Err(PdfiumError::PageFlattenFailure),
}
}
pub fn delete(self) -> Result<(), PdfiumError> {
let index = PdfPageIndexCache::get_index_for_page(self.document_handle, self.page_handle)
.ok_or(PdfiumError::SourcePageIndexNotInCache)?;
unsafe {
self.bindings()
.FPDFPage_Delete(self.document_handle, index as c_int);
}
PdfPageIndexCache::delete_pages_at_index(self.document_handle, index, 1);
Ok(())
}
#[inline]
pub fn content_regeneration_strategy(&self) -> PdfPageContentRegenerationStrategy {
self.regeneration_strategy
}
#[inline]
pub fn set_content_regeneration_strategy(
&mut self,
strategy: PdfPageContentRegenerationStrategy,
) {
self.regeneration_strategy = strategy;
if let Some(index) =
PdfPageIndexCache::get_index_for_page(self.document_handle(), self.page_handle())
{
PdfPageIndexCache::cache_props_for_page(
self.document_handle(),
self.page_handle(),
index,
strategy,
);
}
}
#[inline]
pub fn regenerate_content(&mut self) -> Result<(), PdfiumError> {
self.regenerate_content_immut()
}
#[inline]
pub(crate) fn regenerate_content_immut(&self) -> Result<(), PdfiumError> {
Self::regenerate_content_immut_for_handle(self.page_handle, self.bindings())
}
pub(crate) fn regenerate_content_immut_for_handle(
page: FPDF_PAGE,
bindings: &dyn PdfiumLibraryBindings,
) -> Result<(), PdfiumError> {
if bindings.is_true(unsafe { bindings.FPDFPage_GenerateContent(page) }) {
Ok(())
} else {
Err(PdfiumError::PdfiumLibraryInternalError(
PdfiumInternalError::Unknown,
))
}
}
fn reload_in_place(&mut self) {
if let Some(page_index) =
PdfPageIndexCache::get_index_for_page(self.document_handle, self.page_handle)
{
self.drop_impl();
self.page_handle = unsafe {
self.bindings()
.FPDF_LoadPage(self.document_handle, page_index as c_int)
};
PdfPageIndexCache::cache_props_for_page(
self.document_handle,
self.page_handle,
page_index,
self.content_regeneration_strategy(),
);
}
}
fn drop_impl(&mut self) {
if self.regeneration_strategy != PdfPageContentRegenerationStrategy::Manual
&& self.is_content_regeneration_required
{
let result = self.regenerate_content();
debug_assert!(result.is_ok());
}
unsafe {
self.bindings().FPDF_ClosePage(self.page_handle);
}
PdfPageIndexCache::remove_index_for_page(self.document_handle, self.page_handle);
}
}
impl<'a> Drop for PdfPage<'a> {
#[inline]
fn drop(&mut self) {
self.drop_impl();
}
}
impl<'a> PdfiumLibraryBindingsAccessor<'a> for PdfPage<'a> {}
#[cfg(feature = "thread_safe")]
unsafe impl<'a> Send for PdfPage<'a> {}
#[cfg(feature = "thread_safe")]
unsafe impl<'a> Sync for PdfPage<'a> {}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use crate::utils::test::test_bind_to_pdfium;
use image_025::{GenericImageView, ImageFormat};
#[test]
fn test_page_rendering_reusing_bitmap() -> Result<(), PdfiumError> {
let pdfium = test_bind_to_pdfium();
let document = pdfium.load_pdf_from_file("./test/export-test.pdf", None)?;
let render_config = PdfRenderConfig::new()
.set_target_width(2000)
.set_maximum_height(2000)
.rotate_if_landscape(PdfPageRenderRotation::Degrees90, true);
let mut bitmap =
PdfBitmap::empty(2500, 2500, PdfBitmapFormat::default(), pdfium.bindings())?;
for (index, page) in document.pages().iter().enumerate() {
page.render_into_bitmap_with_config(&mut bitmap, &render_config)?;
bitmap
.as_image()?
.into_rgb8()
.save_with_format(format!("test-page-{}.jpg", index), ImageFormat::Jpeg)
.map_err(|_| PdfiumError::ImageError)?;
}
Ok(())
}
#[test]
fn test_rendered_image_dimension() -> Result<(), PdfiumError> {
let pdfium = test_bind_to_pdfium();
let document = pdfium.load_pdf_from_file("./test/dimensions-test.pdf", None)?;
let render_config = PdfRenderConfig::new()
.set_target_width(500)
.set_maximum_height(500);
for (_index, page) in document.pages().iter().enumerate() {
let rendered_page = page.render_with_config(&render_config)?.as_image()?;
let (width, _height) = rendered_page.dimensions();
assert_eq!(width, 500);
}
Ok(())
}
}