Skip to main content

firecrawl_pdfium/
coords.rs

1//! Coordinate spaces and the pixel↔page transform.
2//!
3//! Two coordinate spaces appear throughout this crate:
4//!
5//! - **Page space** — PDF user space: units of points (1/72 inch), origin at
6//!   the *bottom-left* of the page, y increasing *upward*. Text character
7//!   boxes and PDF content live here.
8//! - **Pixel space** — a rendered bitmap: units of pixels, origin at the
9//!   *top-left*, y increasing *downward*.
10//!
11//! [`PageTransform`] converts between them for one specific render
12//! geometry. It is derived from PDFium's own `FPDF_DeviceToPage` mapping at
13//! render time (so `/Rotate` entries and extra render rotations follow
14//! PDFium's exact semantics) and is plain data afterwards: it stays valid
15//! after the page and document are closed.
16
17/// A point in page space (points, origin bottom-left, y-up).
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct PagePoint {
20    /// Horizontal position in points, from the left page edge.
21    pub x: f64,
22    /// Vertical position in points, from the *bottom* page edge (y-up).
23    pub y: f64,
24}
25
26impl PagePoint {
27    /// Creates a page-space point.
28    pub fn new(x: f64, y: f64) -> Self {
29        Self { x, y }
30    }
31}
32
33/// A point in pixel space (pixels, origin top-left, y-down).
34///
35/// Coordinates are `f64` so sub-pixel positions survive round trips; pixel
36/// *indices* map to the pixel's top-left corner (the center of pixel
37/// `(3, 7)` is `(3.5, 7.5)`).
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct PixelPoint {
40    /// Horizontal position in pixels, from the left bitmap edge.
41    pub x: f64,
42    /// Vertical position in pixels, from the *top* bitmap edge (y-down).
43    pub y: f64,
44}
45
46impl PixelPoint {
47    /// Creates a pixel-space point.
48    pub fn new(x: f64, y: f64) -> Self {
49        Self { x, y }
50    }
51}
52
53/// An axis-aligned rectangle in page space.
54///
55/// Follows PDF conventions: `bottom <= top` and `left <= right` when
56/// normalized (y-up).
57#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct PageRect {
59    /// Smallest x edge, in points.
60    pub left: f64,
61    /// Smallest y edge, in points (page space is y-up).
62    pub bottom: f64,
63    /// Largest x edge, in points.
64    pub right: f64,
65    /// Largest y edge, in points.
66    pub top: f64,
67}
68
69impl PageRect {
70    /// Creates a page-space rectangle from its four edges.
71    pub fn new(left: f64, bottom: f64, right: f64, top: f64) -> Self {
72        Self {
73            left,
74            bottom,
75            right,
76            top,
77        }
78    }
79
80    /// Horizontal extent (`right - left`).
81    pub fn width(&self) -> f64 {
82        self.right - self.left
83    }
84
85    /// Vertical extent (`top - bottom`).
86    pub fn height(&self) -> f64 {
87        self.top - self.bottom
88    }
89
90    /// Returns the same rectangle with `left <= right` and `bottom <= top`.
91    pub fn normalized(&self) -> PageRect {
92        PageRect {
93            left: self.left.min(self.right),
94            right: self.left.max(self.right),
95            bottom: self.bottom.min(self.top),
96            top: self.bottom.max(self.top),
97        }
98    }
99}
100
101/// An axis-aligned rectangle in pixel space: top-left corner plus size.
102#[derive(Debug, Clone, Copy, PartialEq)]
103pub struct PixelRect {
104    /// Left edge in pixels.
105    pub x: f64,
106    /// Top edge in pixels.
107    pub y: f64,
108    /// Width in pixels.
109    pub width: f64,
110    /// Height in pixels.
111    pub height: f64,
112}
113
114impl PixelRect {
115    /// Creates a pixel-space rectangle from top-left corner and size.
116    pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
117        Self {
118            x,
119            y,
120            width,
121            height,
122        }
123    }
124}
125
126/// Affine transform between pixel space of one rendered bitmap and page
127/// space of the page it was rendered from.
128///
129/// Obtained from [`RenderedPage::transform`] or
130/// [`PdfPage::transform_for`]. Plain data: freely `Clone`/`Send`/`Sync`,
131/// and independent of any PDFium resource.
132///
133/// [`RenderedPage::transform`]: crate::RenderedPage::transform
134/// [`PdfPage::transform_for`]: crate::PdfPage::transform_for
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub struct PageTransform {
137    /// Pixel→page coefficients: `page = (a*x + b*y + e, c*x + d*y + f)`.
138    fwd: [f64; 6],
139    /// Page→pixel coefficients, same layout.
140    inv: [f64; 6],
141    pixel_width: u32,
142    pixel_height: u32,
143}
144
145impl PageTransform {
146    /// Builds a transform from PDFium's page-space images of the three
147    /// device corners `(0,0)`, `(w,0)`, `(0,h)`.
148    pub(crate) fn from_corners(
149        pixel_width: u32,
150        pixel_height: u32,
151        origin: (f64, f64), // page coords of device (0, 0)
152        x_axis: (f64, f64), // page coords of device (w, 0)
153        y_axis: (f64, f64), // page coords of device (0, h)
154    ) -> Option<PageTransform> {
155        let w = f64::from(pixel_width);
156        let h = f64::from(pixel_height);
157        let a = (x_axis.0 - origin.0) / w;
158        let c = (x_axis.1 - origin.1) / w;
159        let b = (y_axis.0 - origin.0) / h;
160        let d = (y_axis.1 - origin.1) / h;
161        let (e, f) = origin;
162
163        let det = a * d - b * c;
164        if det == 0.0 || !det.is_finite() {
165            return None;
166        }
167        let ia = d / det;
168        let ib = -b / det;
169        let ic = -c / det;
170        let id = a / det;
171        let ie = -(ia * e + ib * f);
172        let if_ = -(ic * e + id * f);
173
174        Some(PageTransform {
175            fwd: [a, b, c, d, e, f],
176            inv: [ia, ib, ic, id, ie, if_],
177            pixel_width,
178            pixel_height,
179        })
180    }
181
182    /// Width in pixels of the bitmap this transform describes.
183    pub fn pixel_width(&self) -> u32 {
184        self.pixel_width
185    }
186
187    /// Height in pixels of the bitmap this transform describes.
188    pub fn pixel_height(&self) -> u32 {
189        self.pixel_height
190    }
191
192    /// Maps a pixel-space point to page space.
193    pub fn pixel_to_page(&self, p: PixelPoint) -> PagePoint {
194        let [a, b, c, d, e, f] = self.fwd;
195        PagePoint::new(a * p.x + b * p.y + e, c * p.x + d * p.y + f)
196    }
197
198    /// Maps a page-space point to pixel space.
199    pub fn page_to_pixel(&self, p: PagePoint) -> PixelPoint {
200        let [a, b, c, d, e, f] = self.inv;
201        PixelPoint::new(a * p.x + b * p.y + e, c * p.x + d * p.y + f)
202    }
203
204    /// Maps a pixel-space rectangle to a normalized page-space rectangle.
205    pub fn pixel_rect_to_page(&self, r: PixelRect) -> PageRect {
206        let p1 = self.pixel_to_page(PixelPoint::new(r.x, r.y));
207        let p2 = self.pixel_to_page(PixelPoint::new(r.x + r.width, r.y + r.height));
208        PageRect::new(p1.x, p1.y, p2.x, p2.y).normalized()
209    }
210
211    /// Maps a page-space rectangle to a pixel-space rectangle
212    /// (top-left + size, with non-negative size).
213    pub fn page_rect_to_pixel(&self, r: PageRect) -> PixelRect {
214        let p1 = self.page_to_pixel(PagePoint::new(r.left, r.top));
215        let p2 = self.page_to_pixel(PagePoint::new(r.right, r.bottom));
216        let x = p1.x.min(p2.x);
217        let y = p1.y.min(p2.y);
218        PixelRect::new(x, y, (p2.x - p1.x).abs(), (p2.y - p1.y).abs())
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn identity_like_roundtrip() {
228        // A 200x100pt page rendered at 2x: device (0,0) -> page (0, 100),
229        // device (400,0) -> page (200, 100), device (0,200) -> page (0, 0).
230        let t = PageTransform::from_corners(400, 200, (0.0, 100.0), (200.0, 100.0), (0.0, 0.0))
231            .unwrap();
232        let p = t.pixel_to_page(PixelPoint::new(100.0, 50.0));
233        assert!((p.x - 50.0).abs() < 1e-9);
234        assert!((p.y - 75.0).abs() < 1e-9);
235        let d = t.page_to_pixel(p);
236        assert!((d.x - 100.0).abs() < 1e-9);
237        assert!((d.y - 50.0).abs() < 1e-9);
238    }
239
240    #[test]
241    fn degenerate_corners_rejected() {
242        assert!(
243            PageTransform::from_corners(100, 100, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)).is_none()
244        );
245    }
246}