use crate::error::PdfiumError;
use crate::ffi;
use crate::library::Library;
use crate::page::Page;
pub struct Document<'lib> {
pub(crate) handle: pdfium_sys::FPDF_DOCUMENT,
pub(crate) _lib: std::marker::PhantomData<&'lib Library>,
}
pub struct FormEnvironment<'doc, 'lib: 'doc> {
pub(crate) handle: pdfium_sys::FPDF_FORMHANDLE,
_callbacks: Box<pdfium_sys::FPDF_FORMFILLINFO>,
_doc: std::marker::PhantomData<&'doc Document<'lib>>,
}
#[derive(Debug, Clone)]
pub struct OutlineEntry {
pub level: u8,
pub title: String,
pub page_index: Option<i32>,
pub y: Option<f32>,
}
#[derive(Debug, Clone)]
pub struct XfaPacket {
pub index: i32,
pub name: Option<String>,
pub content: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SignatureSummary {
pub count: Option<u32>,
pub byte_range_reaches_eof: Option<bool>,
}
struct SignatureApi {
count: unsafe extern "C" fn(pdfium_sys::FPDF_DOCUMENT) -> std::os::raw::c_int,
object: unsafe extern "C" fn(
pdfium_sys::FPDF_DOCUMENT,
std::os::raw::c_int,
) -> pdfium_sys::FPDF_SIGNATURE,
byte_range: unsafe extern "C" fn(
pdfium_sys::FPDF_SIGNATURE,
*mut std::os::raw::c_int,
std::os::raw::c_ulong,
) -> std::os::raw::c_ulong,
}
impl SignatureApi {
#[cfg(not(target_arch = "wasm32"))]
fn load() -> Option<Self> {
let bindings = pdfium_sys::dynamic::pdfium();
Some(Self {
count: bindings.FPDF_GetSignatureCount?,
object: bindings.FPDF_GetSignatureObject?,
byte_range: bindings.FPDFSignatureObj_GetByteRange?,
})
}
#[cfg(target_arch = "wasm32")]
fn load() -> Option<Self> {
Some(Self {
count: pdfium_sys::FPDF_GetSignatureCount,
object: pdfium_sys::FPDF_GetSignatureObject,
byte_range: pdfium_sys::FPDFSignatureObj_GetByteRange,
})
}
}
impl<'lib> Document<'lib> {
pub fn page_count(&self) -> i32 {
unsafe { ffi!(FPDF_GetPageCount(self.handle)) }
}
pub fn form_type(&self) -> i32 {
unsafe { ffi!(FPDF_GetFormType(self.handle)) }
}
pub fn form_environment(&self) -> Option<FormEnvironment<'_, 'lib>> {
if self.form_type() == 0 {
return None;
}
let mut callbacks = Box::new(pdfium_sys::FPDF_FORMFILLINFO::default());
callbacks.version = 1;
let handle = unsafe {
ffi!(FPDFDOC_InitFormFillEnvironment(
self.handle,
&mut *callbacks
))
};
(!handle.is_null()).then_some(FormEnvironment {
handle,
_callbacks: callbacks,
_doc: std::marker::PhantomData,
})
}
pub fn page(&self, index: i32) -> Result<Page<'_, 'lib>, PdfiumError> {
let handle = unsafe { ffi!(FPDF_LoadPage(self.handle, index)) };
if handle.is_null() {
return Err(PdfiumError::PageNotFound);
}
Ok(Page {
handle,
doc_handle: self.handle,
_doc: std::marker::PhantomData,
})
}
pub fn meta_text(&self, tag: &str) -> Option<String> {
let tag_c = std::ffi::CString::new(tag).ok()?;
let needed = unsafe {
ffi!(FPDF_GetMetaText(
self.handle,
tag_c.as_ptr(),
std::ptr::null_mut(),
0
))
} as usize;
if needed < 2 {
return None;
}
let mut buf: Vec<u16> = vec![0; needed / 2];
let written = unsafe {
ffi!(FPDF_GetMetaText(
self.handle,
tag_c.as_ptr(),
buf.as_mut_ptr() as *mut std::os::raw::c_void,
needed as std::os::raw::c_ulong,
))
} as usize;
if written < 2 {
return None;
}
let chars = written / 2;
let end = if buf.get(chars - 1) == Some(&0) {
chars - 1
} else {
chars
};
if end == 0 {
return None;
}
Some(String::from_utf16_lossy(&buf[..end]))
}
pub fn file_version(&self) -> Option<i32> {
let mut version = 0;
let ok = unsafe { ffi!(FPDF_GetFileVersion(self.handle, &mut version)) };
(ok != 0).then_some(version)
}
pub fn security_handler_revision(&self) -> i32 {
unsafe { ffi!(FPDF_GetSecurityHandlerRevision(self.handle)) }
}
pub fn permissions(&self) -> u64 {
unsafe { ffi!(FPDF_GetDocPermissions(self.handle)) as u64 }
}
pub fn signature_summary(&self, file_size: Option<u64>) -> SignatureSummary {
const MAX_BYTE_RANGE_VALUES: usize = 8;
let Some(api) = SignatureApi::load() else {
return SignatureSummary::default();
};
let count = unsafe { (api.count)(self.handle) }.max(0) as u32;
let Some(file_size) = file_size.filter(|_| count > 0) else {
return SignatureSummary {
count: Some(count),
byte_range_reaches_eof: None,
};
};
let mut known = false;
let mut reaches_eof = true;
for index in 0..count {
let signature = unsafe { (api.object)(self.handle, index as i32) };
if signature.is_null() {
continue;
}
let mut ranges = [0i32; MAX_BYTE_RANGE_VALUES];
let len = unsafe {
(api.byte_range)(
signature,
ranges.as_mut_ptr(),
ranges.len() as std::os::raw::c_ulong,
)
} as usize;
if !(2..=MAX_BYTE_RANGE_VALUES).contains(&len) {
continue;
}
known = true;
let start = i64::from(ranges[len - 2]);
let length = i64::from(ranges[len - 1]);
if start < 0
|| length < 0
|| u64::try_from(start + length)
.ok()
.is_some_and(|range_end| range_end < file_size)
{
reaches_eof = false;
}
}
SignatureSummary {
count: Some(count),
byte_range_reaches_eof: known.then_some(reaches_eof),
}
}
pub fn xfa_packet_count(&self) -> i32 {
unsafe { ffi!(FPDF_GetXFAPacketCount(self.handle)) }
}
pub fn xfa_packets(&self) -> Vec<XfaPacket> {
let count = self.xfa_packet_count();
if count <= 0 {
return Vec::new();
}
let mut out = Vec::with_capacity(count as usize);
for index in 0..count {
let name_len = unsafe {
ffi!(FPDF_GetXFAPacketName(
self.handle,
index,
std::ptr::null_mut(),
0
))
} as usize;
let name = (name_len > 0)
.then(|| {
let mut buf = vec![0u8; name_len];
let written = unsafe {
ffi!(FPDF_GetXFAPacketName(
self.handle,
index,
buf.as_mut_ptr() as *mut std::os::raw::c_void,
name_len as std::os::raw::c_ulong,
))
} as usize;
if written == 0 {
return None;
}
buf.truncate(written.min(name_len));
while buf.last() == Some(&0) {
buf.pop();
}
Some(String::from_utf8_lossy(&buf).into_owned())
})
.flatten();
let mut content_len: std::os::raw::c_ulong = 0;
let sized = unsafe {
ffi!(FPDF_GetXFAPacketContent(
self.handle,
index,
std::ptr::null_mut(),
0,
&mut content_len,
))
};
let content = (sized != 0 && content_len > 0)
.then(|| {
let mut buf = vec![0u8; content_len as usize];
let mut written: std::os::raw::c_ulong = 0;
let ok = unsafe {
ffi!(FPDF_GetXFAPacketContent(
self.handle,
index,
buf.as_mut_ptr() as *mut std::os::raw::c_void,
content_len,
&mut written,
))
};
if ok == 0 {
return None;
}
buf.truncate((written as usize).min(buf.len()));
Some(buf)
})
.flatten();
out.push(XfaPacket {
index,
name,
content,
});
}
out
}
pub fn outline(&self) -> Vec<OutlineEntry> {
let mut out = Vec::new();
let root = unsafe {
ffi!(FPDFBookmark_GetFirstChild(
self.handle,
std::ptr::null_mut()
))
};
if !root.is_null() {
self.walk_bookmark(root, 1, &mut out);
}
out
}
fn walk_bookmark(
&self,
bookmark: pdfium_sys::FPDF_BOOKMARK,
level: u8,
out: &mut Vec<OutlineEntry>,
) {
let mut cur = bookmark;
while !cur.is_null() {
let title = read_bookmark_title(cur);
let (page_index, y) = resolve_dest(self.handle, cur);
out.push(OutlineEntry {
level,
title,
page_index,
y,
});
let child = unsafe { ffi!(FPDFBookmark_GetFirstChild(self.handle, cur)) };
if !child.is_null() {
self.walk_bookmark(child, level.saturating_add(1), out);
}
cur = unsafe { ffi!(FPDFBookmark_GetNextSibling(self.handle, cur)) };
}
}
}
impl FormEnvironment<'_, '_> {
pub fn run_document_actions(&self) {
unsafe { ffi!(FORM_DoDocumentJSAction(self.handle)) };
unsafe { ffi!(FORM_DoDocumentOpenAction(self.handle)) };
}
}
impl Drop for FormEnvironment<'_, '_> {
fn drop(&mut self) {
unsafe { ffi!(FPDFDOC_ExitFormFillEnvironment(self.handle)) };
}
}
fn read_bookmark_title(bookmark: pdfium_sys::FPDF_BOOKMARK) -> String {
let needed = unsafe { ffi!(FPDFBookmark_GetTitle(bookmark, std::ptr::null_mut(), 0)) } as usize;
if needed < 2 {
return String::new();
}
let mut buf: Vec<u16> = vec![0; needed / 2];
let written = unsafe {
ffi!(FPDFBookmark_GetTitle(
bookmark,
buf.as_mut_ptr() as *mut std::os::raw::c_void,
needed as std::os::raw::c_ulong,
))
} as usize;
if written < 2 {
return String::new();
}
let chars = written / 2;
let end = if buf.get(chars - 1) == Some(&0) {
chars - 1
} else {
chars
};
String::from_utf16_lossy(&buf[..end])
}
fn resolve_dest(
doc: pdfium_sys::FPDF_DOCUMENT,
bookmark: pdfium_sys::FPDF_BOOKMARK,
) -> (Option<i32>, Option<f32>) {
let mut dest = unsafe { ffi!(FPDFBookmark_GetDest(doc, bookmark)) };
if dest.is_null() {
let action = unsafe { ffi!(FPDFBookmark_GetAction(bookmark)) };
if !action.is_null() {
dest = unsafe { ffi!(FPDFAction_GetDest(doc, action)) };
}
}
if dest.is_null() {
return (None, None);
}
let page_index = unsafe { ffi!(FPDFDest_GetDestPageIndex(doc, dest)) };
let page_index = if page_index >= 0 {
Some(page_index)
} else {
None
};
let mut has_x: pdfium_sys::FPDF_BOOL = 0;
let mut has_y: pdfium_sys::FPDF_BOOL = 0;
let mut has_z: pdfium_sys::FPDF_BOOL = 0;
let mut x: f32 = 0.0;
let mut y: f32 = 0.0;
let mut z: f32 = 0.0;
let ok = unsafe {
ffi!(FPDFDest_GetLocationInPage(
dest, &mut has_x, &mut has_y, &mut has_z, &mut x, &mut y, &mut z
))
};
let y_out = if ok != 0 && has_y != 0 { Some(y) } else { None };
(page_index, y_out)
}
impl Drop for Document<'_> {
fn drop(&mut self) {
unsafe { ffi!(FPDF_CloseDocument(self.handle)) };
}
}