Skip to main content

firecrawl_pdfium/
document.rs

1//! Documents: loading, inspection, and page access.
2
3use std::ffi::{c_ulong, CString};
4use std::path::Path;
5use std::sync::OnceLock;
6
7use crate::error::{Error, Result};
8use crate::forms::{FormEnv, FormType};
9use crate::library::Pdfium;
10use crate::page::{PageSize, PdfPage};
11use crate::sys;
12
13/// Standard PDF metadata tags accepted by [`PdfDocument::metadata`].
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum MetadataTag {
17    /// Document title.
18    Title,
19    /// Author.
20    Author,
21    /// Subject.
22    Subject,
23    /// Keywords.
24    Keywords,
25    /// Application that created the original document.
26    Creator,
27    /// Application that produced the PDF.
28    Producer,
29    /// Creation date (PDF date string, e.g. `D:20260810...`).
30    CreationDate,
31    /// Last-modified date (PDF date string).
32    ModDate,
33}
34
35impl MetadataTag {
36    fn as_cstr(self) -> &'static std::ffi::CStr {
37        match self {
38            MetadataTag::Title => c"Title",
39            MetadataTag::Author => c"Author",
40            MetadataTag::Subject => c"Subject",
41            MetadataTag::Keywords => c"Keywords",
42            MetadataTag::Creator => c"Creator",
43            MetadataTag::Producer => c"Producer",
44            MetadataTag::CreationDate => c"CreationDate",
45            MetadataTag::ModDate => c"ModDate",
46        }
47    }
48}
49
50/// Document permission flags from the PDF's encryption dictionary
51/// (`FPDF_GetDocPermissions`). For unencrypted documents every permission
52/// is granted (`0xFFFF_FFFF`).
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Permissions(pub u64);
55
56impl Permissions {
57    fn bit(&self, n: u32) -> bool {
58        self.0 & (1 << (n - 1)) != 0
59    }
60
61    /// Bit 3: print the document.
62    pub fn can_print(&self) -> bool {
63        self.bit(3)
64    }
65
66    /// Bit 4: modify contents.
67    pub fn can_modify(&self) -> bool {
68        self.bit(4)
69    }
70
71    /// Bit 5: copy or extract text and graphics.
72    pub fn can_copy(&self) -> bool {
73        self.bit(5)
74    }
75
76    /// Bit 6: add or modify annotations / fill form fields.
77    pub fn can_annotate(&self) -> bool {
78        self.bit(6)
79    }
80}
81
82/// An open PDF document.
83///
84/// The document **owns the PDF bytes** for its whole lifetime (PDFium
85/// requires the backing buffer to stay valid while the document is open).
86/// Dropping the document releases the PDFium handle and, if forms were
87/// enabled, the form-fill environment first (in the order PDFium requires).
88///
89/// # Thread safety
90///
91/// `PdfDocument` is `Send + Sync`: every method serializes through the
92/// process-wide FFI lock. Sharing one document across threads is safe;
93/// calls will not run in parallel.
94pub struct PdfDocument {
95    pdfium: Pdfium,
96    handle: sys::FPDF_DOCUMENT,
97    /// Backing buffer for `handle`; must outlive it. Boxed slice so the
98    /// heap address is stable regardless of moves of `PdfDocument`.
99    _bytes: Box<[u8]>,
100    forms: OnceLock<FormEnv>,
101    page_count: usize,
102}
103
104// SAFETY: `handle` (and the handles inside `forms`) are only ever passed to
105// PDFium under the process-wide FFI lock; `_bytes` is never written after
106// construction. No method provides unsynchronized interior access.
107unsafe impl Send for PdfDocument {}
108// SAFETY: all `&self` methods acquire the FFI lock before touching PDFium
109// state, so concurrent `&self` access from multiple threads is serialized.
110unsafe impl Sync for PdfDocument {}
111
112impl std::fmt::Debug for PdfDocument {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.debug_struct("PdfDocument")
115            .field("page_count", &self.page_count)
116            .field("forms_enabled", &self.forms.get().is_some())
117            .finish_non_exhaustive()
118    }
119}
120
121impl Pdfium {
122    /// Opens a PDF from bytes, taking ownership of them.
123    ///
124    /// `password` unlocks encrypted documents (PDFium tries UTF-8 then
125    /// Latin-1 encodings of it). Pass `None` for unencrypted documents;
126    /// a `Some` password is ignored by unencrypted documents.
127    ///
128    /// # Errors
129    ///
130    /// - [`Error::PasswordRequired`] — encrypted, no password given.
131    /// - [`Error::IncorrectPassword`] — encrypted, wrong password.
132    /// - [`Error::UnsupportedSecurity`] — unsupported encryption scheme.
133    /// - [`Error::InvalidPdf`] — not a PDF / unrecoverably corrupt.
134    pub fn load_document(
135        &self,
136        bytes: impl Into<Vec<u8>>,
137        password: Option<&str>,
138    ) -> Result<PdfDocument> {
139        let bytes: Box<[u8]> = bytes.into().into_boxed_slice();
140        let c_password =
141            match password {
142                Some(p) => Some(CString::new(p).map_err(|_| {
143                    Error::InvalidConfig("password must not contain NUL bytes".into())
144                })?),
145                None => None,
146            };
147
148        let (handle, last_error) = self.ffi(|b| {
149            // SAFETY: `bytes` is a live allocation of the given length and
150            // outlives the handle (owned by the PdfDocument built below;
151            // on error the handle is never created). Password pointer is a
152            // valid NUL-terminated string or null.
153            let handle = unsafe {
154                b.FPDF_LoadMemDocument64(
155                    bytes.as_ptr().cast(),
156                    bytes.len(),
157                    c_password.as_ref().map_or(std::ptr::null(), |p| p.as_ptr()),
158                )
159            };
160            // FPDF_GetLastError is documented as meaningful only right
161            // after a failed load; fetch it in the same critical section
162            // so another thread's call cannot clobber it.
163            let last_error = if handle.is_null() {
164                // SAFETY: no preconditions beyond initialization.
165                unsafe { b.FPDF_GetLastError() }
166            } else {
167                sys::FPDF_ERR_SUCCESS
168            };
169            (handle, last_error)
170        });
171
172        if handle.is_null() {
173            return Err(map_load_error(last_error, password.is_some()));
174        }
175
176        // SAFETY: valid document handle.
177        let raw_count = self.ffi(|b| unsafe { b.FPDF_GetPageCount(handle) });
178        let page_count = usize::try_from(raw_count).unwrap_or(0);
179
180        Ok(PdfDocument {
181            pdfium: *self,
182            handle,
183            _bytes: bytes,
184            forms: OnceLock::new(),
185            page_count,
186        })
187    }
188
189    /// Opens a PDF file from disk (reads it fully into memory first —
190    /// PDFium is fastest and simplest with in-memory documents).
191    pub fn load_document_from_file(
192        &self,
193        path: impl AsRef<Path>,
194        password: Option<&str>,
195    ) -> Result<PdfDocument> {
196        let bytes = std::fs::read(path).map_err(Error::Io)?;
197        self.load_document(bytes, password)
198    }
199}
200
201fn map_load_error(code: c_ulong, password_supplied: bool) -> Error {
202    match code {
203        sys::FPDF_ERR_PASSWORD => {
204            if password_supplied {
205                Error::IncorrectPassword
206            } else {
207                Error::PasswordRequired
208            }
209        }
210        sys::FPDF_ERR_SECURITY => Error::UnsupportedSecurity,
211        sys::FPDF_ERR_FORMAT | sys::FPDF_ERR_FILE => Error::InvalidPdf,
212        // FPDF_ERR_SUCCESS with a null handle should not happen; treat any
213        // unexpected code (incl. UNKNOWN and PAGE) as an opaque PDFium error.
214        // `c_ulong` is 32-bit on Windows; the widening cast is real there.
215        #[allow(clippy::unnecessary_cast)]
216        code => Error::Pdfium { code: code as u64 },
217    }
218}
219
220impl PdfDocument {
221    /// Number of pages.
222    pub fn page_count(&self) -> usize {
223        self.page_count
224    }
225
226    /// Opens page `index` (0-based).
227    pub fn page(&self, index: usize) -> Result<PdfPage<'_>> {
228        if index >= self.page_count {
229            return Err(Error::PageIndexOutOfBounds {
230                index,
231                count: self.page_count,
232            });
233        }
234        PdfPage::open(self, index)
235    }
236
237    /// Iterates over all pages, opening each lazily.
238    pub fn pages(&self) -> impl Iterator<Item = Result<PdfPage<'_>>> {
239        (0..self.page_count).map(move |i| self.page(i))
240    }
241
242    /// Page size in points **without loading the page** — cheap for
243    /// dimension surveys of large documents.
244    pub fn page_size(&self, index: usize) -> Result<PageSize> {
245        if index >= self.page_count {
246            return Err(Error::PageIndexOutOfBounds {
247                index,
248                count: self.page_count,
249            });
250        }
251        let mut size = sys::FS_SIZEF::default();
252        // SAFETY: valid handle, in-bounds index, valid out-pointer.
253        let ok = self
254            .ffi(|b| unsafe { b.FPDF_GetPageSizeByIndexF(self.handle, index as i32, &mut size) });
255        if ok != 0 {
256            Ok(PageSize {
257                width: size.width,
258                height: size.height,
259            })
260        } else {
261            Err(Error::PageLoadFailed { index })
262        }
263    }
264
265    /// The document's interactive form type (cheap; does not initialize
266    /// form rendering).
267    pub fn form_type(&self) -> FormType {
268        // SAFETY: valid handle.
269        FormType::from_raw(self.ffi(|b| unsafe { b.FPDF_GetFormType(self.handle) }))
270    }
271
272    /// Initializes PDFium's form-fill environment so
273    /// [`RenderConfig::form_fields`](crate::RenderConfig::form_fields)
274    /// can draw AcroForm field appearances.
275    ///
276    /// Idempotent. Pages opened **after** this call participate in form
277    /// rendering; enable forms before opening pages you intend to render.
278    pub fn enable_form_rendering(&self) -> Result<()> {
279        if self.forms.get().is_some() {
280            return Ok(());
281        }
282        let env = FormEnv::new(self.pdfium, self.handle)?;
283        // A racing second init would leak a FormEnv teardown; set() failing
284        // means another thread won — destroy ours cleanly.
285        if let Err(mut lost) = self.forms.set(env) {
286            self.pdfium.ffi(|b| lost.destroy(b));
287        }
288        Ok(())
289    }
290
291    /// Whether [`enable_form_rendering`](Self::enable_form_rendering) has
292    /// been called successfully.
293    pub fn forms_enabled(&self) -> bool {
294        self.forms.get().is_some()
295    }
296
297    /// Document permissions from the encryption dictionary. Unencrypted
298    /// documents report all permissions granted.
299    pub fn permissions(&self) -> Permissions {
300        // SAFETY: valid handle.
301        // `c_ulong` is 32-bit on Windows; the widening cast is real there.
302        #[allow(clippy::unnecessary_cast)]
303        Permissions(self.ffi(|b| unsafe { b.FPDF_GetDocPermissions(self.handle) }) as u64)
304    }
305
306    /// Security handler revision (2/3/4/5/6), or `None` for unencrypted
307    /// documents.
308    pub fn security_handler_revision(&self) -> Option<i32> {
309        // SAFETY: valid handle.
310        let rev = self.ffi(|b| unsafe { b.FPDF_GetSecurityHandlerRevision(self.handle) });
311        (rev != -1).then_some(rev)
312    }
313
314    /// PDF file version as reported by the header, times ten
315    /// (14 = PDF 1.4, 17 = PDF 1.7, 20 = PDF 2.0). `None` if unavailable.
316    pub fn pdf_version(&self) -> Option<i32> {
317        let mut version = 0;
318        // SAFETY: valid handle and out-pointer.
319        let ok = self.ffi(|b| unsafe { b.FPDF_GetFileVersion(self.handle, &mut version) });
320        (ok != 0).then_some(version)
321    }
322
323    /// A standard metadata field, or `None` when absent/empty.
324    pub fn metadata(&self, tag: MetadataTag) -> Option<String> {
325        self.ffi(|b| {
326            read_utf16le_buffer(|buffer, buflen| {
327                // SAFETY: valid handle; tag is a NUL-terminated static;
328                // buffer/buflen follow the two-call length protocol.
329                unsafe { b.FPDF_GetMetaText(self.handle, tag.as_cstr().as_ptr(), buffer, buflen) }
330            })
331        })
332    }
333
334    /// The page label for `index` (e.g. "iv", "A-2"), or `None` when the
335    /// document defines no label for it.
336    pub fn page_label(&self, index: usize) -> Option<String> {
337        if index >= self.page_count {
338            return None;
339        }
340        self.ffi(|b| {
341            read_utf16le_buffer(|buffer, buflen| {
342                // SAFETY: valid handle, in-bounds index, two-call protocol.
343                unsafe { b.FPDF_GetPageLabel(self.handle, index as i32, buffer, buflen) }
344            })
345        })
346    }
347
348    pub(crate) fn pdfium(&self) -> Pdfium {
349        self.pdfium
350    }
351
352    pub(crate) fn handle(&self) -> sys::FPDF_DOCUMENT {
353        self.handle
354    }
355
356    pub(crate) fn form_env(&self) -> Option<&FormEnv> {
357        self.forms.get()
358    }
359
360    fn ffi<R>(&self, f: impl FnOnce(&sys::Bindings) -> R) -> R {
361        self.pdfium.ffi(f)
362    }
363}
364
365impl Drop for PdfDocument {
366    fn drop(&mut self) {
367        self.pdfium.ffi(|b| {
368            // Teardown order required by PDFium: exit the form-fill
369            // environment before closing the document it wraps.
370            if let Some(env) = self.forms.get_mut() {
371                env.destroy(b);
372            }
373            // SAFETY: handle is live (created in load_document, closed
374            // only here); pages hold `&PdfDocument`, so the borrow checker
375            // guarantees none outlive this drop.
376            unsafe { b.FPDF_CloseDocument(self.handle) };
377        });
378    }
379}
380
381/// Runs PDFium's two-call UTF-16LE string protocol: first call with a null
382/// buffer returns the byte length including the NUL terminator; `0` means
383/// "no such value".
384fn read_utf16le_buffer(
385    mut call: impl FnMut(*mut std::ffi::c_void, c_ulong) -> c_ulong,
386) -> Option<String> {
387    let byte_len = call(std::ptr::null_mut(), 0);
388    if byte_len < 2 {
389        return None; // absent, or empty (just the terminator)
390    }
391    let unit_len = (byte_len as usize) / 2;
392    let mut units = vec![0u16; unit_len];
393    // Pass the actual allocated size, not the echoed length: if PDFium ever
394    // reported an odd byte_len, echoing it back would overstate the buffer
395    // by one byte.
396    let written = call(units.as_mut_ptr().cast(), (unit_len * 2) as c_ulong);
397    if written == 0 {
398        return None;
399    }
400    let written_units = (written as usize / 2).min(unit_len);
401    // Strip the trailing NUL terminator.
402    let text_units = &units[..written_units.saturating_sub(1)];
403    if text_units.is_empty() {
404        return None;
405    }
406    Some(String::from_utf16_lossy(text_units))
407}