use std::ffi::{c_ulong, CString};
use std::path::Path;
use std::sync::OnceLock;
use crate::error::{Error, Result};
use crate::forms::{FormEnv, FormType};
use crate::library::Pdfium;
use crate::page::{PageSize, PdfPage};
use crate::sys;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MetadataTag {
Title,
Author,
Subject,
Keywords,
Creator,
Producer,
CreationDate,
ModDate,
}
impl MetadataTag {
fn as_cstr(self) -> &'static std::ffi::CStr {
match self {
MetadataTag::Title => c"Title",
MetadataTag::Author => c"Author",
MetadataTag::Subject => c"Subject",
MetadataTag::Keywords => c"Keywords",
MetadataTag::Creator => c"Creator",
MetadataTag::Producer => c"Producer",
MetadataTag::CreationDate => c"CreationDate",
MetadataTag::ModDate => c"ModDate",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Permissions(pub u64);
impl Permissions {
fn bit(&self, n: u32) -> bool {
self.0 & (1 << (n - 1)) != 0
}
pub fn can_print(&self) -> bool {
self.bit(3)
}
pub fn can_modify(&self) -> bool {
self.bit(4)
}
pub fn can_copy(&self) -> bool {
self.bit(5)
}
pub fn can_annotate(&self) -> bool {
self.bit(6)
}
}
pub struct PdfDocument {
pdfium: Pdfium,
handle: sys::FPDF_DOCUMENT,
_bytes: Box<[u8]>,
forms: OnceLock<FormEnv>,
page_count: usize,
}
unsafe impl Send for PdfDocument {}
unsafe impl Sync for PdfDocument {}
impl std::fmt::Debug for PdfDocument {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PdfDocument")
.field("page_count", &self.page_count)
.field("forms_enabled", &self.forms.get().is_some())
.finish_non_exhaustive()
}
}
impl Pdfium {
pub fn load_document(
&self,
bytes: impl Into<Vec<u8>>,
password: Option<&str>,
) -> Result<PdfDocument> {
let bytes: Box<[u8]> = bytes.into().into_boxed_slice();
let c_password =
match password {
Some(p) => Some(CString::new(p).map_err(|_| {
Error::InvalidConfig("password must not contain NUL bytes".into())
})?),
None => None,
};
let (handle, last_error) = self.ffi(|b| {
let handle = unsafe {
b.FPDF_LoadMemDocument64(
bytes.as_ptr().cast(),
bytes.len(),
c_password.as_ref().map_or(std::ptr::null(), |p| p.as_ptr()),
)
};
let last_error = if handle.is_null() {
unsafe { b.FPDF_GetLastError() }
} else {
sys::FPDF_ERR_SUCCESS
};
(handle, last_error)
});
if handle.is_null() {
return Err(map_load_error(last_error, password.is_some()));
}
let raw_count = self.ffi(|b| unsafe { b.FPDF_GetPageCount(handle) });
let page_count = usize::try_from(raw_count).unwrap_or(0);
Ok(PdfDocument {
pdfium: *self,
handle,
_bytes: bytes,
forms: OnceLock::new(),
page_count,
})
}
pub fn load_document_from_file(
&self,
path: impl AsRef<Path>,
password: Option<&str>,
) -> Result<PdfDocument> {
let bytes = std::fs::read(path).map_err(Error::Io)?;
self.load_document(bytes, password)
}
}
fn map_load_error(code: c_ulong, password_supplied: bool) -> Error {
match code {
sys::FPDF_ERR_PASSWORD => {
if password_supplied {
Error::IncorrectPassword
} else {
Error::PasswordRequired
}
}
sys::FPDF_ERR_SECURITY => Error::UnsupportedSecurity,
sys::FPDF_ERR_FORMAT | sys::FPDF_ERR_FILE => Error::InvalidPdf,
#[allow(clippy::unnecessary_cast)]
code => Error::Pdfium { code: code as u64 },
}
}
impl PdfDocument {
pub fn page_count(&self) -> usize {
self.page_count
}
pub fn page(&self, index: usize) -> Result<PdfPage<'_>> {
if index >= self.page_count {
return Err(Error::PageIndexOutOfBounds {
index,
count: self.page_count,
});
}
PdfPage::open(self, index)
}
pub fn pages(&self) -> impl Iterator<Item = Result<PdfPage<'_>>> {
(0..self.page_count).map(move |i| self.page(i))
}
pub fn page_size(&self, index: usize) -> Result<PageSize> {
if index >= self.page_count {
return Err(Error::PageIndexOutOfBounds {
index,
count: self.page_count,
});
}
let mut size = sys::FS_SIZEF::default();
let ok = self
.ffi(|b| unsafe { b.FPDF_GetPageSizeByIndexF(self.handle, index as i32, &mut size) });
if ok != 0 {
Ok(PageSize {
width: size.width,
height: size.height,
})
} else {
Err(Error::PageLoadFailed { index })
}
}
pub fn form_type(&self) -> FormType {
FormType::from_raw(self.ffi(|b| unsafe { b.FPDF_GetFormType(self.handle) }))
}
pub fn enable_form_rendering(&self) -> Result<()> {
if self.forms.get().is_some() {
return Ok(());
}
let env = FormEnv::new(self.pdfium, self.handle)?;
if let Err(mut lost) = self.forms.set(env) {
self.pdfium.ffi(|b| lost.destroy(b));
}
Ok(())
}
pub fn forms_enabled(&self) -> bool {
self.forms.get().is_some()
}
pub fn permissions(&self) -> Permissions {
#[allow(clippy::unnecessary_cast)]
Permissions(self.ffi(|b| unsafe { b.FPDF_GetDocPermissions(self.handle) }) as u64)
}
pub fn security_handler_revision(&self) -> Option<i32> {
let rev = self.ffi(|b| unsafe { b.FPDF_GetSecurityHandlerRevision(self.handle) });
(rev != -1).then_some(rev)
}
pub fn pdf_version(&self) -> Option<i32> {
let mut version = 0;
let ok = self.ffi(|b| unsafe { b.FPDF_GetFileVersion(self.handle, &mut version) });
(ok != 0).then_some(version)
}
pub fn metadata(&self, tag: MetadataTag) -> Option<String> {
self.ffi(|b| {
read_utf16le_buffer(|buffer, buflen| {
unsafe { b.FPDF_GetMetaText(self.handle, tag.as_cstr().as_ptr(), buffer, buflen) }
})
})
}
pub fn page_label(&self, index: usize) -> Option<String> {
if index >= self.page_count {
return None;
}
self.ffi(|b| {
read_utf16le_buffer(|buffer, buflen| {
unsafe { b.FPDF_GetPageLabel(self.handle, index as i32, buffer, buflen) }
})
})
}
pub(crate) fn pdfium(&self) -> Pdfium {
self.pdfium
}
pub(crate) fn handle(&self) -> sys::FPDF_DOCUMENT {
self.handle
}
pub(crate) fn form_env(&self) -> Option<&FormEnv> {
self.forms.get()
}
fn ffi<R>(&self, f: impl FnOnce(&sys::Bindings) -> R) -> R {
self.pdfium.ffi(f)
}
}
impl Drop for PdfDocument {
fn drop(&mut self) {
self.pdfium.ffi(|b| {
if let Some(env) = self.forms.get_mut() {
env.destroy(b);
}
unsafe { b.FPDF_CloseDocument(self.handle) };
});
}
}
fn read_utf16le_buffer(
mut call: impl FnMut(*mut std::ffi::c_void, c_ulong) -> c_ulong,
) -> Option<String> {
let byte_len = call(std::ptr::null_mut(), 0);
if byte_len < 2 {
return None; }
let unit_len = (byte_len as usize) / 2;
let mut units = vec![0u16; unit_len];
let written = call(units.as_mut_ptr().cast(), (unit_len * 2) as c_ulong);
if written == 0 {
return None;
}
let written_units = (written as usize / 2).min(unit_len);
let text_units = &units[..written_units.saturating_sub(1)];
if text_units.is_empty() {
return None;
}
Some(String::from_utf16_lossy(text_units))
}