Skip to main content

pdfboss_core/
geom.rs

1//! Planar geometry shared by text extraction and rendering: points,
2//! axis-aligned rectangles, and 2-D affine transformation matrices using the
3//! PDF row-vector convention `[x y 1] · M`.
4
5/// A point in user or device space.
6#[derive(Debug, Clone, Copy, PartialEq, Default)]
7pub struct Point {
8    pub x: f32,
9    pub y: f32,
10}
11
12impl Point {
13    /// Creates a point from its coordinates.
14    pub const fn new(x: f32, y: f32) -> Point {
15        Point { x, y }
16    }
17}
18
19/// An axis-aligned rectangle described by two opposite corners
20/// `(x0, y0)` and `(x1, y1)`.
21///
22/// A rectangle is *normalized* when `x0 <= x1` and `y0 <= y1`. Operations
23/// that depend on orientation ([`Rect::union`], [`Rect::intersect`],
24/// [`Rect::contains`]) treat their inputs as if normalized.
25#[derive(Debug, Clone, Copy, PartialEq, Default)]
26pub struct Rect {
27    pub x0: f32,
28    pub y0: f32,
29    pub x1: f32,
30    pub y1: f32,
31}
32
33impl Rect {
34    /// Creates a rectangle from two opposite corners.
35    pub const fn new(x0: f32, y0: f32, x1: f32, y1: f32) -> Rect {
36        Rect { x0, y0, x1, y1 }
37    }
38
39    /// Signed width, `x1 - x0` (negative if not normalized).
40    pub fn width(self) -> f32 {
41        self.x1 - self.x0
42    }
43
44    /// Signed height, `y1 - y0` (negative if not normalized).
45    pub fn height(self) -> f32 {
46        self.y1 - self.y0
47    }
48
49    /// Returns the same area with `x0 <= x1` and `y0 <= y1`.
50    pub fn normalize(self) -> Rect {
51        Rect {
52            x0: self.x0.min(self.x1),
53            y0: self.y0.min(self.y1),
54            x1: self.x0.max(self.x1),
55            y1: self.y0.max(self.y1),
56        }
57    }
58
59    /// Smallest rectangle containing both `self` and `other`.
60    pub fn union(self, other: Rect) -> Rect {
61        let a = self.normalize();
62        let b = other.normalize();
63        Rect {
64            x0: a.x0.min(b.x0),
65            y0: a.y0.min(b.y0),
66            x1: a.x1.max(b.x1),
67            y1: a.y1.max(b.y1),
68        }
69    }
70
71    /// Overlapping area of `self` and `other`, or `None` if they are
72    /// disjoint. Rectangles that merely touch yield a degenerate
73    /// (zero-width or zero-height) rectangle.
74    pub fn intersect(self, other: Rect) -> Option<Rect> {
75        let a = self.normalize();
76        let b = other.normalize();
77        let r = Rect {
78            x0: a.x0.max(b.x0),
79            y0: a.y0.max(b.y0),
80            x1: a.x1.min(b.x1),
81            y1: a.y1.min(b.y1),
82        };
83        if r.x0 <= r.x1 && r.y0 <= r.y1 {
84            Some(r)
85        } else {
86            None
87        }
88    }
89
90    /// Whether `p` lies inside the (normalized) rectangle, borders included.
91    pub fn contains(self, p: Point) -> bool {
92        let r = self.normalize();
93        p.x >= r.x0 && p.x <= r.x1 && p.y >= r.y0 && p.y <= r.y1
94    }
95
96    /// Axis-aligned bounding box of the rectangle's four corners mapped
97    /// through `m`. The result is normalized.
98    pub fn transform(self, m: Matrix) -> Rect {
99        let corners = [
100            m.apply(Point::new(self.x0, self.y0)),
101            m.apply(Point::new(self.x1, self.y0)),
102            m.apply(Point::new(self.x1, self.y1)),
103            m.apply(Point::new(self.x0, self.y1)),
104        ];
105        let mut r = Rect::new(corners[0].x, corners[0].y, corners[0].x, corners[0].y);
106        for c in &corners[1..] {
107            r.x0 = r.x0.min(c.x);
108            r.y0 = r.y0.min(c.y);
109            r.x1 = r.x1.max(c.x);
110            r.y1 = r.y1.max(c.y);
111        }
112        r
113    }
114}
115
116/// A 2-D affine transformation `[a b c d e f]` in the PDF convention:
117///
118/// ```text
119/// x' = a·x + c·y + e
120/// y' = b·x + d·y + f
121/// ```
122#[derive(Debug, Clone, Copy, PartialEq)]
123pub struct Matrix {
124    pub a: f32,
125    pub b: f32,
126    pub c: f32,
127    pub d: f32,
128    pub e: f32,
129    pub f: f32,
130}
131
132impl Matrix {
133    /// The identity transformation.
134    pub const fn identity() -> Matrix {
135        Matrix {
136            a: 1.0,
137            b: 0.0,
138            c: 0.0,
139            d: 1.0,
140            e: 0.0,
141            f: 0.0,
142        }
143    }
144
145    /// Translation by `(tx, ty)`.
146    pub const fn translate(tx: f32, ty: f32) -> Matrix {
147        Matrix {
148            a: 1.0,
149            b: 0.0,
150            c: 0.0,
151            d: 1.0,
152            e: tx,
153            f: ty,
154        }
155    }
156
157    /// Scaling by `(sx, sy)` about the origin.
158    pub const fn scale(sx: f32, sy: f32) -> Matrix {
159        Matrix {
160            a: sx,
161            b: 0.0,
162            c: 0.0,
163            d: sy,
164            e: 0.0,
165            f: 0.0,
166        }
167    }
168
169    /// Counter-clockwise rotation by `deg` degrees about the origin.
170    pub fn rotate_deg(deg: f32) -> Matrix {
171        let (sin, cos) = deg.to_radians().sin_cos();
172        Matrix {
173            a: cos,
174            b: sin,
175            c: -sin,
176            d: cos,
177            e: 0.0,
178            f: 0.0,
179        }
180    }
181
182    /// Composition "apply `self` first, then `other`".
183    ///
184    /// With the row-vector convention this is the matrix product
185    /// `self × other`, so `p.apply(self.concat(other)) ==
186    /// other.apply(self.apply(p))`.
187    pub fn concat(self, other: Matrix) -> Matrix {
188        Matrix {
189            a: self.a * other.a + self.b * other.c,
190            b: self.a * other.b + self.b * other.d,
191            c: self.c * other.a + self.d * other.c,
192            d: self.c * other.b + self.d * other.d,
193            e: self.e * other.a + self.f * other.c + other.e,
194            f: self.e * other.b + self.f * other.d + other.f,
195        }
196    }
197
198    /// Transforms a point.
199    pub fn apply(self, p: Point) -> Point {
200        Point {
201            x: self.a * p.x + self.c * p.y + self.e,
202            y: self.b * p.x + self.d * p.y + self.f,
203        }
204    }
205
206    /// The inverse transformation, or `None` if `self` is singular
207    /// (determinant zero or non-finite).
208    pub fn invert(self) -> Option<Matrix> {
209        let det = self.a * self.d - self.b * self.c;
210        if det == 0.0 || !det.is_finite() {
211            return None;
212        }
213        let inv = 1.0 / det;
214        Some(Matrix {
215            a: self.d * inv,
216            b: -self.b * inv,
217            c: -self.c * inv,
218            d: self.a * inv,
219            e: (self.c * self.f - self.d * self.e) * inv,
220            f: (self.b * self.e - self.a * self.f) * inv,
221        })
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    const EPS: f32 = 1e-4;
230
231    fn assert_point_eq(got: Point, want: Point) {
232        assert!(
233            (got.x - want.x).abs() < EPS && (got.y - want.y).abs() < EPS,
234            "expected {want:?}, got {got:?}"
235        );
236    }
237
238    fn assert_matrix_eq(got: Matrix, want: Matrix) {
239        for (g, w) in [
240            (got.a, want.a),
241            (got.b, want.b),
242            (got.c, want.c),
243            (got.d, want.d),
244            (got.e, want.e),
245            (got.f, want.f),
246        ] {
247            assert!((g - w).abs() < EPS, "expected {want:?}, got {got:?}");
248        }
249    }
250
251    fn assert_rect_eq(got: Rect, want: Rect) {
252        for (g, w) in [
253            (got.x0, want.x0),
254            (got.y0, want.y0),
255            (got.x1, want.x1),
256            (got.y1, want.y1),
257        ] {
258            assert!((g - w).abs() < EPS, "expected {want:?}, got {got:?}");
259        }
260    }
261
262    #[test]
263    fn identity_is_neutral() {
264        let id = Matrix::identity();
265        let m = Matrix::translate(3.0, -4.0).concat(Matrix::scale(2.0, 0.5));
266        assert_matrix_eq(id.concat(m), m);
267        assert_matrix_eq(m.concat(id), m);
268        assert_point_eq(id.apply(Point::new(7.5, -2.0)), Point::new(7.5, -2.0));
269    }
270
271    #[test]
272    fn apply_translate_scale_rotate() {
273        let p = Point::new(1.0, 2.0);
274        assert_point_eq(
275            Matrix::translate(10.0, 20.0).apply(p),
276            Point::new(11.0, 22.0),
277        );
278        assert_point_eq(Matrix::scale(2.0, 3.0).apply(p), Point::new(2.0, 6.0));
279        assert_point_eq(
280            Matrix::rotate_deg(90.0).apply(Point::new(1.0, 0.0)),
281            Point::new(0.0, 1.0),
282        );
283        assert_point_eq(
284            Matrix::rotate_deg(180.0).apply(Point::new(1.0, 2.0)),
285            Point::new(-1.0, -2.0),
286        );
287    }
288
289    #[test]
290    fn concat_applies_self_then_other() {
291        // Translate by (10, 0), then scale by 2: (1, 2) -> (11, 2) -> (22, 4).
292        let m = Matrix::translate(10.0, 0.0).concat(Matrix::scale(2.0, 2.0));
293        assert_point_eq(m.apply(Point::new(1.0, 2.0)), Point::new(22.0, 4.0));
294        // Opposite order: scale first, then translate: (1, 2) -> (2, 4) -> (12, 4).
295        let m = Matrix::scale(2.0, 2.0).concat(Matrix::translate(10.0, 0.0));
296        assert_point_eq(m.apply(Point::new(1.0, 2.0)), Point::new(12.0, 4.0));
297    }
298
299    #[test]
300    fn concat_matches_sequential_application() {
301        let m1 = Matrix::rotate_deg(30.0).concat(Matrix::translate(5.0, -3.0));
302        let m2 = Matrix::scale(1.5, 0.25).concat(Matrix::rotate_deg(-45.0));
303        let p = Point::new(-2.0, 7.0);
304        assert_point_eq(m1.concat(m2).apply(p), m2.apply(m1.apply(p)));
305    }
306
307    #[test]
308    fn invert_round_trips() {
309        let m = Matrix::translate(4.0, -1.0)
310            .concat(Matrix::rotate_deg(37.0))
311            .concat(Matrix::scale(2.0, 5.0));
312        let inv = m.invert().expect("matrix should be invertible");
313        assert_matrix_eq(m.concat(inv), Matrix::identity());
314        assert_matrix_eq(inv.concat(m), Matrix::identity());
315
316        let p = Point::new(3.25, -9.5);
317        assert_point_eq(inv.apply(m.apply(p)), p);
318    }
319
320    #[test]
321    fn invert_translation() {
322        let inv = Matrix::translate(10.0, -2.0).invert().unwrap();
323        assert_matrix_eq(inv, Matrix::translate(-10.0, 2.0));
324    }
325
326    #[test]
327    fn invert_singular_is_none() {
328        assert!(Matrix::scale(0.0, 0.0).invert().is_none());
329        assert!(Matrix::scale(1.0, 0.0).invert().is_none());
330        // Collinear basis vectors: determinant zero.
331        let m = Matrix {
332            a: 1.0,
333            b: 2.0,
334            c: 2.0,
335            d: 4.0,
336            e: 5.0,
337            f: 6.0,
338        };
339        assert!(m.invert().is_none());
340    }
341
342    #[test]
343    fn rect_width_height_normalize() {
344        let r = Rect::new(10.0, 20.0, 4.0, 2.0);
345        assert!((r.width() - -6.0).abs() < EPS);
346        assert!((r.height() - -18.0).abs() < EPS);
347        let n = r.normalize();
348        assert_rect_eq(n, Rect::new(4.0, 2.0, 10.0, 20.0));
349        assert!((n.width() - 6.0).abs() < EPS);
350        assert!((n.height() - 18.0).abs() < EPS);
351    }
352
353    #[test]
354    fn rect_union() {
355        let a = Rect::new(0.0, 0.0, 2.0, 2.0);
356        let b = Rect::new(1.0, -1.0, 3.0, 1.0);
357        assert_rect_eq(a.union(b), Rect::new(0.0, -1.0, 3.0, 2.0));
358        // Union treats inputs as normalized.
359        let flipped = Rect::new(3.0, 1.0, 1.0, -1.0);
360        assert_rect_eq(a.union(flipped), Rect::new(0.0, -1.0, 3.0, 2.0));
361    }
362
363    #[test]
364    fn rect_intersect() {
365        let a = Rect::new(0.0, 0.0, 4.0, 4.0);
366        let b = Rect::new(2.0, 1.0, 6.0, 3.0);
367        assert_rect_eq(a.intersect(b).unwrap(), Rect::new(2.0, 1.0, 4.0, 3.0));
368        // Disjoint.
369        assert!(a.intersect(Rect::new(5.0, 5.0, 6.0, 6.0)).is_none());
370        // Touching edges produce a degenerate rectangle, not None.
371        let edge = a.intersect(Rect::new(4.0, 0.0, 8.0, 4.0)).unwrap();
372        assert!((edge.width() - 0.0).abs() < EPS);
373    }
374
375    #[test]
376    fn rect_contains() {
377        let r = Rect::new(0.0, 0.0, 10.0, 5.0);
378        assert!(r.contains(Point::new(5.0, 2.5)));
379        assert!(r.contains(Point::new(0.0, 0.0))); // corner inclusive
380        assert!(r.contains(Point::new(10.0, 5.0))); // corner inclusive
381        assert!(!r.contains(Point::new(10.1, 2.0)));
382        assert!(!r.contains(Point::new(5.0, -0.1)));
383        // Un-normalized rectangle behaves like its normalized form.
384        let f = Rect::new(10.0, 5.0, 0.0, 0.0);
385        assert!(f.contains(Point::new(5.0, 2.5)));
386    }
387
388    #[test]
389    fn rect_transform() {
390        let r = Rect::new(0.0, 0.0, 2.0, 1.0);
391        assert_rect_eq(
392            r.transform(Matrix::translate(5.0, 6.0)),
393            Rect::new(5.0, 6.0, 7.0, 7.0),
394        );
395        assert_rect_eq(
396            r.transform(Matrix::scale(2.0, 3.0)),
397            Rect::new(0.0, 0.0, 4.0, 3.0),
398        );
399        // Rotation by 90 degrees maps [0,2]x[0,1] onto [-1,0]x[0,2].
400        assert_rect_eq(
401            r.transform(Matrix::rotate_deg(90.0)),
402            Rect::new(-1.0, 0.0, 0.0, 2.0),
403        );
404    }
405}