1use justpdf_core::page::{Rect, collect_pages};
5use justpdf_core::PdfDocument;
6use justpdf_core::content::{ContentOp, Operand, parse_content_stream};
7use justpdf_core::object::PdfObject;
8use justpdf_core::page::PageInfo;
9
10use crate::error::{RenderError, Result};
11use crate::graphics_state::Matrix;
12
13pub struct BBoxDevice {
15 min_x: f64,
16 min_y: f64,
17 max_x: f64,
18 max_y: f64,
19 has_content: bool,
20 #[allow(dead_code)]
22 page_transform: Matrix,
23 ctm: Matrix,
25 ctm_stack: Vec<Matrix>,
26}
27
28impl BBoxDevice {
29 pub fn new(page_transform: Matrix) -> Self {
30 Self {
31 min_x: f64::MAX,
32 min_y: f64::MAX,
33 max_x: f64::MIN,
34 max_y: f64::MIN,
35 has_content: false,
36 page_transform,
37 ctm: Matrix::identity(),
38 ctm_stack: Vec::new(),
39 }
40 }
41
42 pub fn bbox(&self) -> Option<Rect> {
44 if !self.has_content {
45 return None;
46 }
47 Some(Rect {
48 llx: self.min_x,
49 lly: self.min_y,
50 urx: self.max_x,
51 ury: self.max_y,
52 })
53 }
54
55 fn extend_point(&mut self, x: f64, y: f64) {
57 let (tx, ty) = self.ctm.transform_point(x, y);
59 self.min_x = self.min_x.min(tx);
60 self.min_y = self.min_y.min(ty);
61 self.max_x = self.max_x.max(tx);
62 self.max_y = self.max_y.max(ty);
63 self.has_content = true;
64 }
65
66 pub fn extend_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
68 self.extend_point(x, y);
69 self.extend_point(x + w, y);
70 self.extend_point(x + w, y + h);
71 self.extend_point(x, y + h);
72 }
73
74 pub fn process_ops(&mut self, ops: &[ContentOp]) {
76 let mut path_points: Vec<(f64, f64)> = Vec::new();
77 let mut text_matrix = Matrix::identity();
78 let mut text_line_matrix = Matrix::identity();
79 let mut font_size = 12.0_f64;
80 let mut text_rise = 0.0_f64;
81
82 for op in ops {
83 let operator = op.operator_str();
84 let operands = &op.operands;
85
86 match operator {
87 "q" => self.ctm_stack.push(self.ctm),
88 "Q" => {
89 if let Some(m) = self.ctm_stack.pop() {
90 self.ctm = m;
91 }
92 }
93 "cm" => {
94 if operands.len() >= 6 {
95 let m = Matrix {
96 a: f(operands, 0),
97 b: f(operands, 1),
98 c: f(operands, 2),
99 d: f(operands, 3),
100 e: f(operands, 4),
101 f: f(operands, 5),
102 };
103 self.ctm = m.concat(&self.ctm);
104 }
105 }
106
107 "m" | "l" => {
109 if operands.len() >= 2 {
110 path_points.push((f(operands, 0), f(operands, 1)));
111 }
112 }
113 "c" => {
114 if operands.len() >= 6 {
115 path_points.push((f(operands, 0), f(operands, 1)));
116 path_points.push((f(operands, 2), f(operands, 3)));
117 path_points.push((f(operands, 4), f(operands, 5)));
118 }
119 }
120 "v" => {
121 if operands.len() >= 4 {
122 path_points.push((f(operands, 0), f(operands, 1)));
123 path_points.push((f(operands, 2), f(operands, 3)));
124 }
125 }
126 "y" => {
127 if operands.len() >= 4 {
128 path_points.push((f(operands, 0), f(operands, 1)));
129 path_points.push((f(operands, 2), f(operands, 3)));
130 }
131 }
132 "re" => {
133 if operands.len() >= 4 {
134 let x = f(operands, 0);
135 let y = f(operands, 1);
136 let w = f(operands, 2);
137 let h = f(operands, 3);
138 path_points.push((x, y));
139 path_points.push((x + w, y));
140 path_points.push((x + w, y + h));
141 path_points.push((x, y + h));
142 }
143 }
144
145 "S" | "s" | "f" | "F" | "f*" | "B" | "B*" | "b" | "b*" => {
147 for &(x, y) in &path_points {
148 self.extend_point(x, y);
149 }
150 path_points.clear();
151 }
152 "n" => {
153 path_points.clear();
154 }
155
156 "BT" => {
158 text_matrix = Matrix::identity();
159 text_line_matrix = Matrix::identity();
160 }
161 "Tf" => {
162 if operands.len() > 1 {
163 font_size = f(operands, 1);
164 }
165 }
166 "Ts" => {
167 text_rise = f(operands, 0);
168 }
169 "Td" | "TD" => {
170 let tx = f(operands, 0);
171 let ty = f(operands, 1);
172 let t = Matrix::translate(tx, ty);
173 text_line_matrix = t.concat(&text_line_matrix);
174 text_matrix = text_line_matrix;
175 }
176 "Tm" => {
177 if operands.len() >= 6 {
178 let m = Matrix {
179 a: f(operands, 0),
180 b: f(operands, 1),
181 c: f(operands, 2),
182 d: f(operands, 3),
183 e: f(operands, 4),
184 f: f(operands, 5),
185 };
186 text_matrix = m;
187 text_line_matrix = m;
188 }
189 }
190 "Tj" | "'" => {
191 let trm = text_matrix.concat(&self.ctm);
193 let (tx, ty) = (trm.e, trm.f + text_rise);
194 self.extend_point_raw(tx, ty);
195 self.extend_point_raw(tx, ty + font_size);
196 }
197 "TJ" => {
198 let trm = text_matrix.concat(&self.ctm);
199 let (tx, ty) = (trm.e, trm.f + text_rise);
200 self.extend_point_raw(tx, ty);
201 self.extend_point_raw(tx, ty + font_size);
202 }
203
204 "Do" => {
206 self.extend_point(0.0, 0.0);
208 self.extend_point(1.0, 0.0);
209 self.extend_point(1.0, 1.0);
210 self.extend_point(0.0, 1.0);
211 }
212
213 _ => {}
214 }
215 }
216 }
217
218 fn extend_point_raw(&mut self, x: f64, y: f64) {
220 self.min_x = self.min_x.min(x);
221 self.min_y = self.min_y.min(y);
222 self.max_x = self.max_x.max(x);
223 self.max_y = self.max_y.max(y);
224 self.has_content = true;
225 }
226}
227
228pub fn compute_page_bbox(doc: &PdfDocument, page_index: usize) -> Result<Option<Rect>> {
230 let pages = collect_pages(doc)?;
231 let page = pages
232 .get(page_index)
233 .ok_or_else(|| RenderError::InvalidDimensions {
234 detail: format!("page index {page_index} out of range"),
235 })?
236 .clone();
237
238 let media_box = page.crop_box.unwrap_or(page.media_box);
239 let page_transform = crate::render::compute_page_transform(&media_box, 1.0, page.rotate);
240
241 let mut bbox = BBoxDevice::new(page_transform);
242
243 let content_data = get_page_content(doc, &page)?;
245 if content_data.is_empty() {
246 return Ok(None);
247 }
248
249 let ops = parse_content_stream(&content_data).map_err(RenderError::Core)?;
250 bbox.process_ops(&ops);
251
252 Ok(bbox.bbox())
253}
254
255fn get_page_content(doc: &PdfDocument, page: &PageInfo) -> Result<Vec<u8>> {
257 let contents = match &page.contents_ref {
258 Some(c) => c.clone(),
259 None => return Ok(Vec::new()),
260 };
261
262 match &contents {
263 PdfObject::Reference(r) => {
264 let r = r.clone();
265 let obj = doc.resolve(&r)?;
266 match obj {
267 PdfObject::Stream { dict, data } => {
268 Ok(doc.decode_stream(&dict, &data).unwrap_or_default())
269 }
270 PdfObject::Array(arr) => concat_streams(doc, &arr),
271 _ => Ok(Vec::new()),
272 }
273 }
274 PdfObject::Stream { dict, data } => {
275 Ok(doc.decode_stream(dict, data).unwrap_or_default())
276 }
277 PdfObject::Array(arr) => {
278 let arr = arr.clone();
279 concat_streams(doc, &arr)
280 }
281 _ => Ok(Vec::new()),
282 }
283}
284
285fn concat_streams(doc: &PdfDocument, arr: &[PdfObject]) -> Result<Vec<u8>> {
286 let mut combined = Vec::new();
287 for item in arr {
288 let obj = match item {
289 PdfObject::Reference(r) => {
290 let r = r.clone();
291 doc.resolve(&r)?
292 }
293 other => other.clone(),
294 };
295 if let PdfObject::Stream { dict, data } = obj
296 && let Ok(decoded) = doc.decode_stream(&dict, &data)
297 {
298 combined.extend_from_slice(&decoded);
299 combined.push(b' ');
300 }
301 }
302 Ok(combined)
303}
304
305fn f(operands: &[Operand], idx: usize) -> f64 {
306 operands.get(idx).and_then(|o| o.as_f64()).unwrap_or(0.0)
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn test_bbox_no_content() {
315 let bbox = BBoxDevice::new(Matrix::identity());
316 assert!(bbox.bbox().is_none());
317 }
318
319 #[test]
320 fn test_bbox_single_point() {
321 let mut bbox = BBoxDevice::new(Matrix::identity());
322 bbox.extend_point(10.0, 20.0);
323 let r = bbox.bbox().unwrap();
324 assert!((r.llx - 10.0).abs() < 0.001);
325 assert!((r.lly - 20.0).abs() < 0.001);
326 }
327
328 #[test]
329 fn test_bbox_rectangle() {
330 let mut bbox = BBoxDevice::new(Matrix::identity());
331 bbox.extend_rect(10.0, 20.0, 100.0, 50.0);
332 let r = bbox.bbox().unwrap();
333 assert!((r.llx - 10.0).abs() < 0.001);
334 assert!((r.lly - 20.0).abs() < 0.001);
335 assert!((r.urx - 110.0).abs() < 0.001);
336 assert!((r.ury - 70.0).abs() < 0.001);
337 }
338}