Skip to main content

docling_pdf/render/
geom.rs

1//! Affine geometry for the renderer: PDF's `[a b c d e f]` matrices in `f64`,
2//! applied as `x' = a·x + c·y + e`, `y' = b·x + d·y + f` (ISO 32000-1, 8.3.4).
3
4use tiny_skia::Transform;
5
6/// A PDF transformation matrix.
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct Mat {
9    pub a: f64,
10    pub b: f64,
11    pub c: f64,
12    pub d: f64,
13    pub e: f64,
14    pub f: f64,
15}
16
17impl Mat {
18    pub const IDENTITY: Mat = Mat {
19        a: 1.0,
20        b: 0.0,
21        c: 0.0,
22        d: 1.0,
23        e: 0.0,
24        f: 0.0,
25    };
26
27    pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Mat {
28        Mat { a, b, c, d, e, f }
29    }
30
31    pub fn scale(sx: f64, sy: f64) -> Mat {
32        Mat::new(sx, 0.0, 0.0, sy, 0.0, 0.0)
33    }
34
35    pub fn translate(tx: f64, ty: f64) -> Mat {
36        Mat::new(1.0, 0.0, 0.0, 1.0, tx, ty)
37    }
38
39    /// From a six-element array, `None` unless every entry is finite.
40    pub fn from_slice(v: &[f64]) -> Option<Mat> {
41        if v.len() != 6 || v.iter().any(|x| !x.is_finite()) {
42            return None;
43        }
44        Some(Mat::new(v[0], v[1], v[2], v[3], v[4], v[5]))
45    }
46
47    /// `self × r`: apply `self` first, then `r` (PDF's `cm` prepends the new
48    /// matrix to the CTM: `new_ctm = m × ctm`).
49    pub fn then(self, r: Mat) -> Mat {
50        Mat {
51            a: self.a * r.a + self.b * r.c,
52            b: self.a * r.b + self.b * r.d,
53            c: self.c * r.a + self.d * r.c,
54            d: self.c * r.b + self.d * r.d,
55            e: self.e * r.a + self.f * r.c + r.e,
56            f: self.e * r.b + self.f * r.d + r.f,
57        }
58    }
59
60    pub fn apply(self, x: f64, y: f64) -> (f64, f64) {
61        (
62            self.a * x + self.c * y + self.e,
63            self.b * x + self.d * y + self.f,
64        )
65    }
66
67    /// The linear part applied to a vector (no translation).
68    pub fn apply_vec(self, x: f64, y: f64) -> (f64, f64) {
69        (self.a * x + self.c * y, self.b * x + self.d * y)
70    }
71
72    pub fn det(self) -> f64 {
73        self.a * self.d - self.b * self.c
74    }
75
76    /// `sqrt(|det|)`: the geometric mean scale — what docling-parse scales a
77    /// line width by (`pdf_state<SHAPE>::trafo_scale`).
78    pub fn mean_scale(self) -> f64 {
79        self.det().abs().sqrt()
80    }
81
82    /// The larger singular value: the most a unit length can stretch.
83    pub fn max_scale(self) -> f64 {
84        let sx = self.a.hypot(self.b);
85        let sy = self.c.hypot(self.d);
86        sx.max(sy)
87    }
88
89    pub fn invert(self) -> Option<Mat> {
90        let det = self.det();
91        if det.abs() < 1e-12 || !det.is_finite() {
92            return None;
93        }
94        let ia = self.d / det;
95        let ib = -self.b / det;
96        let ic = -self.c / det;
97        let id = self.a / det;
98        Some(Mat {
99            a: ia,
100            b: ib,
101            c: ic,
102            d: id,
103            e: -(self.e * ia + self.f * ic),
104            f: -(self.e * ib + self.f * id),
105        })
106    }
107
108    pub fn is_finite(self) -> bool {
109        [self.a, self.b, self.c, self.d, self.e, self.f]
110            .iter()
111            .all(|v| v.is_finite())
112    }
113
114    /// The same transform for tiny-skia (`f32`; `from_row(sx, ky, kx, sy, tx, ty)`
115    /// takes the matrix in the same `a b c d e f` order).
116    pub fn to_ts(self) -> Transform {
117        Transform::from_row(
118            self.a as f32,
119            self.b as f32,
120            self.c as f32,
121            self.d as f32,
122            self.e as f32,
123            self.f as f32,
124        )
125    }
126
127    /// The axis-aligned bounding box of the unit square under this matrix,
128    /// `(x_min, y_min, x_max, y_max)`.
129    pub fn unit_bbox(self) -> (f64, f64, f64, f64) {
130        let pts = [
131            self.apply(0.0, 0.0),
132            self.apply(1.0, 0.0),
133            self.apply(0.0, 1.0),
134            self.apply(1.0, 1.0),
135        ];
136        let mut b = (pts[0].0, pts[0].1, pts[0].0, pts[0].1);
137        for &(x, y) in &pts[1..] {
138            b.0 = b.0.min(x);
139            b.1 = b.1.min(y);
140            b.2 = b.2.max(x);
141            b.3 = b.3.max(y);
142        }
143        b
144    }
145}
146
147/// An axis-aligned rectangle in device space, `x0 <= x1`, `y0 <= y1`.
148#[derive(Clone, Copy, Debug, PartialEq)]
149pub struct Box2 {
150    pub x0: f64,
151    pub y0: f64,
152    pub x1: f64,
153    pub y1: f64,
154}
155
156impl Box2 {
157    pub fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Box2 {
158        Box2 {
159            x0: x0.min(x1),
160            y0: y0.min(y1),
161            x1: x0.max(x1),
162            y1: y0.max(y1),
163        }
164    }
165
166    pub fn intersect(self, o: Box2) -> Box2 {
167        Box2 {
168            x0: self.x0.max(o.x0),
169            y0: self.y0.max(o.y0),
170            x1: self.x1.min(o.x1),
171            y1: self.y1.min(o.y1),
172        }
173    }
174
175    pub fn is_empty(self) -> bool {
176        !(self.x1 > self.x0 && self.y1 > self.y0)
177    }
178
179    pub fn width(self) -> f64 {
180        (self.x1 - self.x0).max(0.0)
181    }
182
183    pub fn height(self) -> f64 {
184        (self.y1 - self.y0).max(0.0)
185    }
186
187    /// The four corners of `[x0 y0 x1 y1]` under `m`, as a bounding box.
188    pub fn transformed(self, m: Mat) -> Box2 {
189        let pts = [
190            m.apply(self.x0, self.y0),
191            m.apply(self.x1, self.y0),
192            m.apply(self.x0, self.y1),
193            m.apply(self.x1, self.y1),
194        ];
195        let mut b = Box2::new(pts[0].0, pts[0].1, pts[0].0, pts[0].1);
196        for &(x, y) in &pts[1..] {
197            b.x0 = b.x0.min(x);
198            b.y0 = b.y0.min(y);
199            b.x1 = b.x1.max(x);
200            b.y1 = b.y1.max(y);
201        }
202        b
203    }
204}