Skip to main content

pdfplumber_core/
geometry.rs

1/// A 2D point.
2#[derive(Debug, Clone, Copy, PartialEq)]
3#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
4pub struct Point {
5    /// X coordinate.
6    pub x: f64,
7    /// Y coordinate.
8    pub y: f64,
9}
10
11impl Point {
12    /// Create a new point at `(x, y)`.
13    pub fn new(x: f64, y: f64) -> Self {
14        Self { x, y }
15    }
16}
17
18/// Current Transformation Matrix (CTM) — affine transform.
19///
20/// Represented as six values `[a, b, c, d, e, f]` corresponding to:
21/// ```text
22/// | a  b  0 |
23/// | c  d  0 |
24/// | e  f  1 |
25/// ```
26/// Point transformation: `(x', y') = (a*x + c*y + e, b*x + d*y + f)`
27#[derive(Debug, Clone, Copy, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct Ctm {
30    /// Scale X (horizontal scaling).
31    pub a: f64,
32    /// Shear Y.
33    pub b: f64,
34    /// Shear X.
35    pub c: f64,
36    /// Scale Y (vertical scaling).
37    pub d: f64,
38    /// Translate X.
39    pub e: f64,
40    /// Translate Y.
41    pub f: f64,
42}
43
44impl Default for Ctm {
45    fn default() -> Self {
46        Self::identity()
47    }
48}
49
50impl Ctm {
51    /// Create a new CTM with the given values.
52    pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
53        Self { a, b, c, d, e, f }
54    }
55
56    /// Identity matrix (no transformation).
57    pub fn identity() -> Self {
58        Self {
59            a: 1.0,
60            b: 0.0,
61            c: 0.0,
62            d: 1.0,
63            e: 0.0,
64            f: 0.0,
65        }
66    }
67
68    /// Transform a point through this CTM.
69    pub fn transform_point(&self, p: Point) -> Point {
70        Point {
71            x: self.a * p.x + self.c * p.y + self.e,
72            y: self.b * p.x + self.d * p.y + self.f,
73        }
74    }
75
76    /// Concatenate this CTM with another: `self × other`.
77    pub fn concat(&self, other: &Ctm) -> Ctm {
78        Ctm {
79            a: self.a * other.a + self.b * other.c,
80            b: self.a * other.b + self.b * other.d,
81            c: self.c * other.a + self.d * other.c,
82            d: self.c * other.b + self.d * other.d,
83            e: self.e * other.a + self.f * other.c + other.e,
84            f: self.e * other.b + self.f * other.d + other.f,
85        }
86    }
87}
88
89/// Orientation of a geometric element.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub enum Orientation {
93    /// Horizontal (left-to-right or right-to-left).
94    Horizontal,
95    /// Vertical (top-to-bottom or bottom-to-top).
96    Vertical,
97    /// Diagonal (neither purely horizontal nor vertical).
98    Diagonal,
99}
100
101/// Bounding box with top-left origin coordinate system.
102///
103/// Coordinates follow pdfplumber convention:
104/// - `x0`: left edge
105/// - `top`: top edge (distance from top of page)
106/// - `x1`: right edge
107/// - `bottom`: bottom edge (distance from top of page)
108#[derive(Debug, Clone, Copy, PartialEq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct BBox {
111    /// Left edge x coordinate.
112    pub x0: f64,
113    /// Top edge y coordinate (distance from top of page).
114    pub top: f64,
115    /// Right edge x coordinate.
116    pub x1: f64,
117    /// Bottom edge y coordinate (distance from top of page).
118    pub bottom: f64,
119}
120
121impl BBox {
122    /// Create a new bounding box from `(x0, top)` to `(x1, bottom)`.
123    pub fn new(x0: f64, top: f64, x1: f64, bottom: f64) -> Self {
124        Self {
125            x0,
126            top,
127            x1,
128            bottom,
129        }
130    }
131
132    /// Width of the bounding box.
133    pub fn width(&self) -> f64 {
134        self.x1 - self.x0
135    }
136
137    /// Height of the bounding box.
138    pub fn height(&self) -> f64 {
139        self.bottom - self.top
140    }
141
142    /// Compute the union of two bounding boxes.
143    pub fn union(&self, other: &BBox) -> BBox {
144        BBox {
145            x0: self.x0.min(other.x0),
146            top: self.top.min(other.top),
147            x1: self.x1.max(other.x1),
148            bottom: self.bottom.max(other.bottom),
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    fn assert_point_approx(p: Point, x: f64, y: f64) {
158        assert!((p.x - x).abs() < 1e-10, "x: expected {x}, got {}", p.x);
159        assert!((p.y - y).abs() < 1e-10, "y: expected {y}, got {}", p.y);
160    }
161
162    // --- Point tests ---
163
164    #[test]
165    fn test_point_new() {
166        let p = Point::new(3.0, 4.0);
167        assert_eq!(p.x, 3.0);
168        assert_eq!(p.y, 4.0);
169    }
170
171    // --- Ctm tests ---
172
173    #[test]
174    fn test_ctm_identity() {
175        let ctm = Ctm::identity();
176        assert_eq!(ctm.a, 1.0);
177        assert_eq!(ctm.b, 0.0);
178        assert_eq!(ctm.c, 0.0);
179        assert_eq!(ctm.d, 1.0);
180        assert_eq!(ctm.e, 0.0);
181        assert_eq!(ctm.f, 0.0);
182    }
183
184    #[test]
185    fn test_ctm_default_is_identity() {
186        assert_eq!(Ctm::default(), Ctm::identity());
187    }
188
189    #[test]
190    fn test_ctm_transform_identity() {
191        let ctm = Ctm::identity();
192        let p = ctm.transform_point(Point::new(5.0, 10.0));
193        assert_point_approx(p, 5.0, 10.0);
194    }
195
196    #[test]
197    fn test_ctm_transform_translation() {
198        // Translation by (100, 200)
199        let ctm = Ctm::new(1.0, 0.0, 0.0, 1.0, 100.0, 200.0);
200        let p = ctm.transform_point(Point::new(5.0, 10.0));
201        assert_point_approx(p, 105.0, 210.0);
202    }
203
204    #[test]
205    fn test_ctm_transform_scaling() {
206        // Scale by 2x horizontal, 3x vertical
207        let ctm = Ctm::new(2.0, 0.0, 0.0, 3.0, 0.0, 0.0);
208        let p = ctm.transform_point(Point::new(5.0, 10.0));
209        assert_point_approx(p, 10.0, 30.0);
210    }
211
212    #[test]
213    fn test_ctm_transform_scale_and_translate() {
214        // Scale by 2x then translate by (10, 20)
215        let ctm = Ctm::new(2.0, 0.0, 0.0, 2.0, 10.0, 20.0);
216        let p = ctm.transform_point(Point::new(5.0, 10.0));
217        assert_point_approx(p, 20.0, 40.0);
218    }
219
220    #[test]
221    fn test_ctm_concat_identity() {
222        let a = Ctm::new(2.0, 0.0, 0.0, 3.0, 10.0, 20.0);
223        let id = Ctm::identity();
224        assert_eq!(a.concat(&id), a);
225    }
226
227    #[test]
228    fn test_ctm_concat_two_translations() {
229        let a = Ctm::new(1.0, 0.0, 0.0, 1.0, 10.0, 20.0);
230        let b = Ctm::new(1.0, 0.0, 0.0, 1.0, 5.0, 7.0);
231        let c = a.concat(&b);
232        let p = c.transform_point(Point::new(0.0, 0.0));
233        assert_point_approx(p, 15.0, 27.0);
234    }
235
236    #[test]
237    fn test_ctm_concat_scale_then_translate() {
238        // Scale 2x, then translate by (10, 20)
239        let scale = Ctm::new(2.0, 0.0, 0.0, 2.0, 0.0, 0.0);
240        let translate = Ctm::new(1.0, 0.0, 0.0, 1.0, 10.0, 20.0);
241        let combined = scale.concat(&translate);
242        let p = combined.transform_point(Point::new(3.0, 4.0));
243        // scale first: (6, 8), then translate: (16, 28)
244        assert_point_approx(p, 16.0, 28.0);
245    }
246
247    // --- BBox tests ---
248
249    #[test]
250    fn test_bbox_new() {
251        let bbox = BBox::new(10.0, 20.0, 30.0, 40.0);
252        assert_eq!(bbox.x0, 10.0);
253        assert_eq!(bbox.top, 20.0);
254        assert_eq!(bbox.x1, 30.0);
255        assert_eq!(bbox.bottom, 40.0);
256    }
257
258    #[test]
259    fn test_bbox_dimensions() {
260        let bbox = BBox::new(10.0, 20.0, 50.0, 60.0);
261        assert_eq!(bbox.width(), 40.0);
262        assert_eq!(bbox.height(), 40.0);
263    }
264
265    #[test]
266    fn test_bbox_zero_size() {
267        let bbox = BBox::new(10.0, 20.0, 10.0, 20.0);
268        assert_eq!(bbox.width(), 0.0);
269        assert_eq!(bbox.height(), 0.0);
270    }
271
272    // --- Orientation tests ---
273
274    #[test]
275    fn test_orientation_variants() {
276        let h = Orientation::Horizontal;
277        let v = Orientation::Vertical;
278        let d = Orientation::Diagonal;
279        assert_ne!(h, v);
280        assert_ne!(v, d);
281        assert_ne!(h, d);
282    }
283
284    #[test]
285    fn test_orientation_clone_copy() {
286        let o = Orientation::Horizontal;
287        let o2 = o; // Copy
288        let o3 = o.clone(); // Clone
289        assert_eq!(o, o2);
290        assert_eq!(o, o3);
291    }
292
293    #[test]
294    fn test_bbox_union() {
295        let a = BBox::new(10.0, 20.0, 30.0, 40.0);
296        let b = BBox::new(5.0, 25.0, 35.0, 45.0);
297        let u = a.union(&b);
298        assert_eq!(u.x0, 5.0);
299        assert_eq!(u.top, 20.0);
300        assert_eq!(u.x1, 35.0);
301        assert_eq!(u.bottom, 45.0);
302    }
303}