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