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}