Skip to main content

zpdf_core/
geometry.rs

1#[derive(Debug, Clone, Copy, PartialEq)]
2pub struct Point {
3    pub x: f64,
4    pub y: f64,
5}
6
7impl Point {
8    pub fn new(x: f64, y: f64) -> Self {
9        Self { x, y }
10    }
11
12    pub fn zero() -> Self {
13        Self { x: 0.0, y: 0.0 }
14    }
15
16    pub fn transform(self, m: &Matrix) -> Self {
17        Self {
18            x: m.a * self.x + m.c * self.y + m.e,
19            y: m.b * self.x + m.d * self.y + m.f,
20        }
21    }
22}
23
24/// PDF rectangle: [x0, y0, x1, y1] (lower-left, upper-right).
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Rect {
27    pub x0: f64,
28    pub y0: f64,
29    pub x1: f64,
30    pub y1: f64,
31}
32
33impl Rect {
34    pub fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Self {
35        Self { x0, y0, x1, y1 }
36    }
37
38    pub fn width(&self) -> f64 {
39        (self.x1 - self.x0).abs()
40    }
41
42    pub fn height(&self) -> f64 {
43        (self.y1 - self.y0).abs()
44    }
45
46    pub fn normalize(&self) -> Self {
47        Self {
48            x0: self.x0.min(self.x1),
49            y0: self.y0.min(self.y1),
50            x1: self.x0.max(self.x1),
51            y1: self.y0.max(self.y1),
52        }
53    }
54}
55
56/// 3x2 affine transformation matrix.
57///
58/// PDF uses the representation `[a b c d e f]` which maps:
59///   x' = a*x + c*y + e
60///   y' = b*x + d*y + f
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct Matrix {
63    pub a: f64,
64    pub b: f64,
65    pub c: f64,
66    pub d: f64,
67    pub e: f64,
68    pub f: f64,
69}
70
71impl Matrix {
72    pub fn identity() -> Self {
73        Self {
74            a: 1.0,
75            b: 0.0,
76            c: 0.0,
77            d: 1.0,
78            e: 0.0,
79            f: 0.0,
80        }
81    }
82
83    pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
84        Self { a, b, c, d, e, f }
85    }
86
87    pub fn translate(tx: f64, ty: f64) -> Self {
88        Self {
89            a: 1.0,
90            b: 0.0,
91            c: 0.0,
92            d: 1.0,
93            e: tx,
94            f: ty,
95        }
96    }
97
98    pub fn scale(sx: f64, sy: f64) -> Self {
99        Self {
100            a: sx,
101            b: 0.0,
102            c: 0.0,
103            d: sy,
104            e: 0.0,
105            f: 0.0,
106        }
107    }
108
109    /// Matrix multiply: self * other.
110    ///
111    /// Represents applying `other` first, then `self`.
112    /// PDF `cm` semantics: new_CTM = old_CTM.concat(cm_matrix).
113    pub fn concat(&self, other: &Matrix) -> Self {
114        // Layout: [a c e; b d f; 0 0 1]
115        Self {
116            a: self.a * other.a + self.c * other.b,
117            b: self.b * other.a + self.d * other.b,
118            c: self.a * other.c + self.c * other.d,
119            d: self.b * other.c + self.d * other.d,
120            e: self.a * other.e + self.c * other.f + self.e,
121            f: self.b * other.e + self.d * other.f + self.f,
122        }
123    }
124
125    pub fn determinant(&self) -> f64 {
126        self.a * self.d - self.b * self.c
127    }
128
129    pub fn inverse(&self) -> Option<Self> {
130        let det = self.determinant();
131        if det.abs() < 1e-12 {
132            return None;
133        }
134        let inv_det = 1.0 / det;
135        Some(Self {
136            a: self.d * inv_det,
137            b: -self.b * inv_det,
138            c: -self.c * inv_det,
139            d: self.a * inv_det,
140            e: (self.c * self.f - self.e * self.d) * inv_det,
141            f: (self.e * self.b - self.a * self.f) * inv_det,
142        })
143    }
144}
145
146impl Default for Matrix {
147    fn default() -> Self {
148        Self::identity()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn identity_concat() {
158        let id = Matrix::identity();
159        let m = Matrix::new(2.0, 0.0, 0.0, 3.0, 10.0, 20.0);
160        assert_eq!(id.concat(&m), m);
161        assert_eq!(m.concat(&id), m);
162    }
163
164    #[test]
165    fn inverse_roundtrip() {
166        let m = Matrix::new(2.0, 1.0, -1.0, 3.0, 5.0, 7.0);
167        let inv = m.inverse().unwrap();
168        let result = m.concat(&inv);
169        let id = Matrix::identity();
170        assert!((result.a - id.a).abs() < 1e-10);
171        assert!((result.d - id.d).abs() < 1e-10);
172        assert!((result.e - id.e).abs() < 1e-10);
173    }
174
175    #[test]
176    fn point_transform() {
177        let m = Matrix::translate(10.0, 20.0);
178        let p = Point::new(1.0, 2.0);
179        let t = p.transform(&m);
180        assert!((t.x - 11.0).abs() < 1e-10);
181        assert!((t.y - 22.0).abs() < 1e-10);
182    }
183
184    #[test]
185    fn rect_normalize() {
186        let r = Rect::new(10.0, 20.0, 5.0, 8.0);
187        let n = r.normalize();
188        assert_eq!(n, Rect::new(5.0, 8.0, 10.0, 20.0));
189    }
190}