Skip to main content

firecrawl_pdfium/
forms.rs

1//! Form-fill environment: form type detection and the no-op callback stubs
2//! required to render AcroForm field appearances.
3
4use std::ffi::c_int;
5
6use crate::error::{Error, Result};
7use crate::library::Pdfium;
8use crate::sys;
9
10/// The kind of interactive form a document contains, per
11/// `FPDF_GetFormType`.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum FormType {
15    /// No interactive form.
16    None,
17    /// AcroForm (the widely supported kind; renderable by this crate).
18    AcroForm,
19    /// Full XFA form. PDFium default builds cannot render XFA content;
20    /// AcroForm fallback pages, if present, may still render.
21    XfaFull,
22    /// XFA foreground (XFAF) subset.
23    XfaForeground,
24    /// A value this crate does not recognize (newer PDFium).
25    Unknown(i32),
26}
27
28impl FormType {
29    pub(crate) fn from_raw(raw: c_int) -> FormType {
30        match raw {
31            sys::FORMTYPE_NONE => FormType::None,
32            sys::FORMTYPE_ACRO_FORM => FormType::AcroForm,
33            sys::FORMTYPE_XFA_FULL => FormType::XfaFull,
34            sys::FORMTYPE_XFA_FOREGROUND => FormType::XfaForeground,
35            other => FormType::Unknown(other),
36        }
37    }
38
39    /// True if this crate can render the form's field appearances
40    /// (AcroForm only).
41    pub fn is_renderable(&self) -> bool {
42        matches!(self, FormType::AcroForm)
43    }
44}
45
46/// An initialized PDFium form-fill environment, owned by a `PdfDocument`.
47///
48/// Holds the `FPDF_FORMFILLINFO` at a stable heap address for the whole
49/// life of the form handle — PDFium requires: "The FPDF_FORMFILLINFO passed
50/// in via |formInfo| must remain valid until the returned FPDF_FORMHANDLE
51/// is closed."
52pub(crate) struct FormEnv {
53    /// Boxed so the address PDFium captured stays stable.
54    info: Box<sys::FPDF_FORMFILLINFO>,
55    handle: sys::FPDF_FORMHANDLE,
56}
57
58// SAFETY: the raw handle and the boxed info struct are only dereferenced by
59// PDFium during FFI calls, and every FFI call in this crate is serialized
60// behind the process-wide lock. Moving/sharing the owning struct across
61// threads does not touch them.
62unsafe impl Send for FormEnv {}
63unsafe impl Sync for FormEnv {}
64
65impl FormEnv {
66    /// Initializes the form-fill environment for `document`.
67    ///
68    /// Caller must guarantee `document` is a live handle owned by the same
69    /// `PdfDocument` that will own the returned env (enforced by the only
70    /// call site).
71    pub(crate) fn new(pdfium: Pdfium, document: sys::FPDF_DOCUMENT) -> Result<FormEnv> {
72        let mut info = Box::new(new_formfillinfo());
73        let handle = pdfium.ffi(|b|
74            // SAFETY: `document` is live; `info` is a fully initialized
75            // version-1 FPDF_FORMFILLINFO whose heap address stays stable
76            // for the life of the returned handle (boxed, dropped only in
77            // FormEnv::drop after ExitFormFillEnvironment).
78            unsafe { b.FPDFDOC_InitFormFillEnvironment(document, info.as_mut()) });
79        if handle.is_null() {
80            return Err(Error::FormInitFailed);
81        }
82        Ok(FormEnv { info, handle })
83    }
84
85    pub(crate) fn handle(&self) -> sys::FPDF_FORMHANDLE {
86        self.handle
87    }
88
89    /// Tears down the environment. Must be called (under the FFI lock,
90    /// with the owning document still open) before the document closes;
91    /// `PdfDocument::drop` does this.
92    pub(crate) fn destroy(&mut self, bindings: &sys::Bindings) {
93        if !self.handle.is_null() {
94            // SAFETY: handle is live and owned by us; PDFium docs:
95            // "This function is a no-op when |hHandle| is null", and takes
96            // ownership of the handle. `info` stays alive (boxed in self)
97            // until after this call, satisfying the lifetime rule.
98            unsafe { bindings.FPDFDOC_ExitFormFillEnvironment(self.handle) };
99            self.handle = std::ptr::null_mut();
100        }
101        let _ = &self.info; // keep the box alive through teardown
102    }
103}
104
105/// Builds the version-1 `FPDF_FORMFILLINFO` this crate submits.
106///
107/// The header marks these version-1 members "Implementation Required: yes":
108/// `FFI_Invalidate`, `FFI_SetCursor`, `FFI_SetTimer`, `FFI_KillTimer`,
109/// `FFI_GetLocalTime`, `FFI_GetPage`, `FFI_GetRotation`,
110/// `FFI_ExecuteNamedAction` (plus `FFI_GetCurrentPage` "when V8 support is
111/// present"). All of them get no-op stubs below; optional members are
112/// `None`. Version-2/XFA members are ignored for a version-1 client but the
113/// struct layout includes them (see `sys::FPDF_FORMFILLINFO`).
114///
115/// **Stub contract: no stub may call back into PDFium** — stubs run while
116/// the process-wide FFI lock is held by the frame that entered PDFium.
117fn new_formfillinfo() -> sys::FPDF_FORMFILLINFO {
118    sys::FPDF_FORMFILLINFO {
119        version: 1,
120        Release: None,
121        FFI_Invalidate: Some(ffi_invalidate),
122        FFI_OutputSelectedRect: None,
123        FFI_SetCursor: Some(ffi_set_cursor),
124        FFI_SetTimer: Some(ffi_set_timer),
125        FFI_KillTimer: Some(ffi_kill_timer),
126        FFI_GetLocalTime: Some(ffi_get_local_time),
127        FFI_OnChange: None,
128        FFI_GetPage: Some(ffi_get_page),
129        FFI_GetCurrentPage: Some(ffi_get_current_page),
130        FFI_GetRotation: Some(ffi_get_rotation),
131        FFI_ExecuteNamedAction: Some(ffi_execute_named_action),
132        FFI_SetTextFieldFocus: None,
133        FFI_DoURIAction: None,
134        FFI_DoGoToAction: None,
135        m_pJsPlatform: std::ptr::null_mut(),
136        xfa_disabled: 0,
137        FFI_DisplayCaret: None,
138        FFI_GetCurrentPageIndex: None,
139        FFI_SetCurrentPage: None,
140        FFI_GotoURL: None,
141        FFI_GetPageViewRect: None,
142        FFI_PageEvent: None,
143        FFI_PopupMenu: None,
144        FFI_OpenFile: None,
145        FFI_EmailTo: None,
146        FFI_UploadTo: None,
147        FFI_GetPlatform: None,
148        FFI_GetLanguage: None,
149        FFI_DownloadFromURL: None,
150        FFI_PostRequestURL: None,
151        FFI_PutRequestURL: None,
152        FFI_OnFocusChange: None,
153        FFI_DoURIActionWithKeyboardModifier: None,
154    }
155}
156
157unsafe extern "C" fn ffi_invalidate(
158    _this: *mut sys::FPDF_FORMFILLINFO,
159    _page: sys::FPDF_PAGE,
160    _left: f64,
161    _top: f64,
162    _right: f64,
163    _bottom: f64,
164) {
165    // We render on demand; there is no incremental screen to invalidate.
166}
167
168unsafe extern "C" fn ffi_set_cursor(_this: *mut sys::FPDF_FORMFILLINFO, _cursor: c_int) {}
169
170unsafe extern "C" fn ffi_set_timer(
171    _this: *mut sys::FPDF_FORMFILLINFO,
172    _elapse: c_int,
173    _timer_func: sys::TimerCallback,
174) -> c_int {
175    // 0 = "could not install a timer". Without JavaScript there is nothing
176    // that needs one.
177    0
178}
179
180unsafe extern "C" fn ffi_kill_timer(_this: *mut sys::FPDF_FORMFILLINFO, _timer_id: c_int) {}
181
182unsafe extern "C" fn ffi_get_local_time(
183    _this: *mut sys::FPDF_FORMFILLINFO,
184) -> sys::FPDF_SYSTEMTIME {
185    sys::FPDF_SYSTEMTIME::default()
186}
187
188unsafe extern "C" fn ffi_get_page(
189    _this: *mut sys::FPDF_FORMFILLINFO,
190    _document: sys::FPDF_DOCUMENT,
191    _page_index: c_int,
192) -> sys::FPDF_PAGE {
193    // Null is a documented valid answer ("page not yet loaded").
194    std::ptr::null_mut()
195}
196
197unsafe extern "C" fn ffi_get_current_page(
198    _this: *mut sys::FPDF_FORMFILLINFO,
199    _document: sys::FPDF_DOCUMENT,
200) -> sys::FPDF_PAGE {
201    std::ptr::null_mut()
202}
203
204unsafe extern "C" fn ffi_get_rotation(
205    _this: *mut sys::FPDF_FORMFILLINFO,
206    _page: sys::FPDF_PAGE,
207) -> c_int {
208    0
209}
210
211unsafe extern "C" fn ffi_execute_named_action(
212    _this: *mut sys::FPDF_FORMFILLINFO,
213    _named_action: sys::FPDF_BYTESTRING,
214) {
215}