Skip to main content

firecrawl_pdfium/
page.rs

1//! Pages: metrics, rotation, and access to rendering/text.
2
3use crate::document::PdfDocument;
4use crate::error::{Error, Result};
5use crate::sys;
6
7/// Page dimensions in points (1/72 inch), after applying the page's
8/// `/Rotate` entry (PDFium's `FPDF_GetPageWidthF`/`HeightF` semantics: a
9/// portrait page with `/Rotate 90` reports landscape dimensions).
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct PageSize {
12    /// Width in points (1/72 inch).
13    pub width: f32,
14    /// Height in points (1/72 inch).
15    pub height: f32,
16}
17
18/// A rotation in 90° clockwise increments — used both for a page's own
19/// `/Rotate` entry and for extra rotation applied at render time.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum Rotation {
22    /// No rotation.
23    #[default]
24    None,
25    /// 90° clockwise.
26    Clockwise90,
27    /// 180°.
28    Rotate180,
29    /// 270° clockwise (90° counter-clockwise).
30    Clockwise270,
31}
32
33impl Rotation {
34    pub(crate) fn from_raw(raw: i32) -> Rotation {
35        match raw.rem_euclid(4) {
36            1 => Rotation::Clockwise90,
37            2 => Rotation::Rotate180,
38            3 => Rotation::Clockwise270,
39            _ => Rotation::None,
40        }
41    }
42
43    pub(crate) fn as_raw(self) -> i32 {
44        match self {
45            Rotation::None => 0,
46            Rotation::Clockwise90 => 1,
47            Rotation::Rotate180 => 2,
48            Rotation::Clockwise270 => 3,
49        }
50    }
51
52    /// Whether this rotation swaps width and height.
53    pub fn swaps_axes(self) -> bool {
54        matches!(self, Rotation::Clockwise90 | Rotation::Clockwise270)
55    }
56}
57
58/// An open page of a [`PdfDocument`].
59///
60/// Borrows its document, so the borrow checker statically prevents pages
61/// from outliving it. Dimension and rotation getters are cached at open
62/// time and take no lock.
63///
64/// `Send + Sync` like the document; all PDFium access is serialized.
65pub struct PdfPage<'doc> {
66    doc: &'doc PdfDocument,
67    handle: sys::FPDF_PAGE,
68    index: usize,
69    size: PageSize,
70    rotation: Rotation,
71    forms_active: bool,
72}
73
74// SAFETY: `handle` is only passed to PDFium under the process-wide FFI
75// lock; cached fields are immutable after construction. See PdfDocument.
76unsafe impl Send for PdfPage<'_> {}
77// SAFETY: all `&self` methods that touch PDFium acquire the FFI lock.
78unsafe impl Sync for PdfPage<'_> {}
79
80impl std::fmt::Debug for PdfPage<'_> {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("PdfPage")
83            .field("index", &self.index)
84            .field("size", &self.size)
85            .field("rotation", &self.rotation)
86            .finish_non_exhaustive()
87    }
88}
89
90impl<'doc> PdfPage<'doc> {
91    pub(crate) fn open(doc: &'doc PdfDocument, index: usize) -> Result<PdfPage<'doc>> {
92        let forms_active = doc.form_env().is_some();
93        let loaded = doc.pdfium().ffi(|b| {
94            // SAFETY: valid document handle; index bounds-checked by caller.
95            let handle = unsafe { b.FPDF_LoadPage(doc.handle(), index as i32) };
96            if handle.is_null() {
97                return None;
98            }
99            if forms_active {
100                if let Some(env) = doc.form_env() {
101                    // SAFETY: live page + form handles. Header: "Should be
102                    // invoked after user successfully loaded a PDF page, and
103                    // FPDFDOC_InitFormFillEnvironment() has been invoked."
104                    unsafe { b.FORM_OnAfterLoadPage(handle, env.handle()) };
105                }
106            }
107            // SAFETY: live page handle for all three calls.
108            let width = unsafe { b.FPDF_GetPageWidthF(handle) };
109            let height = unsafe { b.FPDF_GetPageHeightF(handle) };
110            let rotation = unsafe { b.FPDFPage_GetRotation(handle) };
111            Some((handle, width, height, rotation))
112        });
113
114        match loaded {
115            Some((handle, width, height, rotation)) => Ok(PdfPage {
116                doc,
117                handle,
118                index,
119                size: PageSize { width, height },
120                rotation: Rotation::from_raw(rotation),
121                forms_active,
122            }),
123            None => Err(Error::PageLoadFailed { index }),
124        }
125    }
126
127    /// This page's 0-based index in the document.
128    pub fn index(&self) -> usize {
129        self.index
130    }
131
132    /// Page size in points, post-`/Rotate` (cached; no FFI call).
133    pub fn size(&self) -> PageSize {
134        self.size
135    }
136
137    /// Width in points, post-`/Rotate` (cached).
138    pub fn width(&self) -> f32 {
139        self.size.width
140    }
141
142    /// Height in points, post-`/Rotate` (cached).
143    pub fn height(&self) -> f32 {
144        self.size.height
145    }
146
147    /// The page's own `/Rotate` entry (cached). Note that [`size`](Self::size)
148    /// already reflects this rotation.
149    pub fn rotation(&self) -> Rotation {
150        self.rotation
151    }
152
153    /// The page bounding box (intersection of media box and crop box) in
154    /// page space.
155    pub fn bounding_box(&self) -> Result<crate::PageRect> {
156        let mut rect = sys::FS_RECTF::default();
157        // SAFETY: live page handle, valid out-pointer.
158        let ok = self.ffi(|b| unsafe { b.FPDF_GetPageBoundingBox(self.handle, &mut rect) });
159        if ok != 0 {
160            Ok(crate::PageRect::new(
161                rect.left as f64,
162                rect.bottom as f64,
163                rect.right as f64,
164                rect.top as f64,
165            ))
166        } else {
167            Err(Error::PageLoadFailed { index: self.index })
168        }
169    }
170
171    /// Whether the page declares transparency.
172    pub fn has_transparency(&self) -> bool {
173        // SAFETY: live page handle.
174        self.ffi(|b| unsafe { b.FPDFPage_HasTransparency(self.handle) }) != 0
175    }
176
177    pub(crate) fn document(&self) -> &'doc PdfDocument {
178        self.doc
179    }
180
181    pub(crate) fn handle(&self) -> sys::FPDF_PAGE {
182        self.handle
183    }
184
185    pub(crate) fn ffi<R>(&self, f: impl FnOnce(&sys::Bindings) -> R) -> R {
186        self.doc.pdfium().ffi(f)
187    }
188}
189
190impl Drop for PdfPage<'_> {
191    fn drop(&mut self) {
192        self.ffi(|b| {
193            if self.forms_active {
194                if let Some(env) = self.doc.form_env() {
195                    // SAFETY: live handles; header requires this before
196                    // closing a page when a form environment is active.
197                    unsafe { b.FORM_OnBeforeClosePage(self.handle, env.handle()) };
198                }
199            }
200            // SAFETY: live page handle, closed exactly once (here).
201            unsafe { b.FPDF_ClosePage(self.handle) };
202        });
203    }
204}