1use crate::content::{TypedIter, UntypedIter};
4use crate::object::Array;
5use crate::object::Dict;
6use crate::object::Name;
7use crate::object::Rect;
8use crate::object::Stream;
9use crate::object::dict::keys::*;
10use crate::object::{Object, ObjectIdentifier};
11use crate::reader::ReaderContext;
12use crate::sync::OnceLock;
13use crate::util::FloatExt;
14use crate::xref::XRef;
15use alloc::collections::BTreeSet;
16use alloc::vec;
17use alloc::vec::Vec;
18use core::ops::Deref;
19use log::warn;
20
21#[derive(Debug, Clone)]
23struct PagesContext<'a> {
24 media_box: Option<Rect>,
25 crop_box: Option<Rect>,
26 rotate: Option<u32>,
27 resources: Dict<'a>,
28}
29
30impl PagesContext<'_> {
31 fn new() -> Self {
32 Self {
33 media_box: None,
34 crop_box: None,
35 rotate: None,
36 resources: Dict::empty(),
37 }
38 }
39}
40
41pub struct Pages<'a> {
43 pages: Vec<Page<'a>>,
44 xref: &'a XRef,
45}
46
47impl<'a> Pages<'a> {
48 pub(crate) fn new(
50 pages_dict: &Dict<'a>,
51 ctx: &ReaderContext<'a>,
52 xref: &'a XRef,
53 ) -> Option<Self> {
54 let mut pages = vec![];
55 let pages_ctx = PagesContext::new();
56 resolve_pages(pages_dict, &mut pages, pages_ctx, ctx)?;
57
58 Some(Self { pages, xref })
59 }
60
61 pub(crate) fn new_brute_force(ctx: &ReaderContext<'a>, xref: &'a XRef) -> Option<Self> {
66 let mut pages = vec![];
67
68 for object in xref.objects() {
69 if let Some(dict) = object.into_dict()
70 && let Some(page) = Page::new(&dict, &PagesContext::new(), ctx, true)
71 {
72 pages.push(page);
73 }
74 }
75
76 if pages.is_empty() {
77 return None;
78 }
79
80 Some(Self { pages, xref })
81 }
82
83 pub fn xref(&self) -> &'a XRef {
85 self.xref
86 }
87}
88
89impl<'a> Deref for Pages<'a> {
90 type Target = [Page<'a>];
91
92 fn deref(&self) -> &Self::Target {
93 &self.pages
94 }
95}
96
97const MAX_PAGE_TREE_DEPTH: usize = 256;
100
101const MAX_PAGE_COUNT: usize = 100_000;
105
106fn resolve_pages<'a>(
107 pages_dict: &Dict<'a>,
108 entries: &mut Vec<Page<'a>>,
109 ctx: PagesContext<'a>,
110 reader_ctx: &ReaderContext<'a>,
111) -> Option<()> {
112 let max_depth = reader_ctx
113 .load_limits()
114 .object_depth_limit()
115 .map(|d| d as usize)
116 .unwrap_or(MAX_PAGE_TREE_DEPTH);
117
118 let mut visited = BTreeSet::new();
119 resolve_pages_depth(
120 pages_dict,
121 entries,
122 ctx,
123 reader_ctx,
124 0,
125 max_depth,
126 &mut visited,
127 )
128}
129
130fn resolve_pages_depth<'a>(
131 pages_dict: &Dict<'a>,
132 entries: &mut Vec<Page<'a>>,
133 mut ctx: PagesContext<'a>,
134 reader_ctx: &ReaderContext<'a>,
135 depth: usize,
136 max_depth: usize,
137 visited: &mut BTreeSet<ObjectIdentifier>,
138) -> Option<()> {
139 if depth > max_depth {
140 log::warn!("Page tree depth exceeds {max_depth}, stopping traversal");
141 return None;
142 }
143
144 if let Some(node_id) = pages_dict.obj_id()
149 && !visited.insert(node_id)
150 {
151 log::warn!("Page tree cycle detected at {node_id:?}, stopping traversal");
152 return Some(());
153 }
154
155 if let Some(media_box) = pages_dict.get::<Rect>(MEDIA_BOX) {
156 ctx.media_box = Some(media_box);
157 }
158
159 if let Some(crop_box) = pages_dict.get::<Rect>(CROP_BOX) {
160 ctx.crop_box = Some(crop_box);
161 }
162
163 if let Some(rotate) = pages_dict.get::<i32>(ROTATE) {
166 ctx.rotate = Some(rotate.rem_euclid(360) as u32);
167 }
168
169 if let Some(resources) = pages_dict.get::<Dict<'a>>(RESOURCES) {
172 ctx.resources = resources;
173 }
174
175 let kids = pages_dict.get::<Array<'a>>(KIDS)?;
176
177 for dict in kids.iter::<Dict<'_>>() {
178 if entries.len() >= MAX_PAGE_COUNT {
179 log::warn!("Page count exceeds {MAX_PAGE_COUNT}, stopping page tree traversal");
180 return Some(());
181 }
182
183 match dict.get::<Name>(TYPE).as_deref() {
184 Some(PAGES) => {
185 resolve_pages_depth(
186 &dict,
187 entries,
188 ctx.clone(),
189 reader_ctx,
190 depth + 1,
191 max_depth,
192 visited,
193 );
194 }
195 _ => {
198 if let Some(page) = Page::new(&dict, &ctx, reader_ctx, false) {
199 entries.push(page);
200 }
201 }
202 }
203 }
204
205 Some(())
206}
207
208#[derive(Debug, Copy, Clone)]
210pub enum Rotation {
211 None,
213 Horizontal,
215 Flipped,
217 FlippedHorizontal,
219}
220
221pub struct Page<'a> {
223 inner: Dict<'a>,
224 media_box: Rect,
225 crop_box: Rect,
226 rotation: Rotation,
227 page_streams: OnceLock<Option<Vec<u8>>>,
228 resources: Resources<'a>,
229 ctx: ReaderContext<'a>,
230}
231
232impl<'a> Page<'a> {
233 fn new(
234 dict: &Dict<'a>,
235 ctx: &PagesContext<'a>,
236 reader_ctx: &ReaderContext<'a>,
237 brute_force: bool,
238 ) -> Option<Self> {
239 if brute_force && !dict.contains_key(CONTENTS) {
242 return None;
243 }
244
245 let media_box = dict
246 .get::<Rect>(MEDIA_BOX)
247 .or(ctx.media_box)
248 .unwrap_or(US_LETTER);
249
250 let crop_box = dict
251 .get::<Rect>(CROP_BOX)
252 .or(ctx.crop_box)
253 .unwrap_or(media_box);
254
255 let rotation = match dict
256 .get::<i32>(ROTATE)
257 .map(|r| r.rem_euclid(360) as u32)
258 .or(ctx.rotate)
259 .unwrap_or(0)
260 {
261 0 => Rotation::None,
262 90 => Rotation::Horizontal,
263 180 => Rotation::Flipped,
264 270 => Rotation::FlippedHorizontal,
265 _ => Rotation::None,
266 };
267
268 let resources = Resources::new(
269 dict.get::<Dict<'a>>(RESOURCES)
270 .unwrap_or_else(|| ctx.resources.clone()),
271 );
272
273 Some(Self {
274 inner: dict.clone(),
275 media_box,
276 crop_box,
277 rotation,
278 page_streams: OnceLock::new(),
279 resources,
280 ctx: reader_ctx.clone(),
281 })
282 }
283
284 fn operations_impl(&self) -> Option<UntypedIter<'_>> {
285 let stream = self.page_stream()?;
286 let iter = UntypedIter::new(stream);
287
288 Some(iter)
289 }
290
291 pub fn page_stream(&self) -> Option<&[u8]> {
293 let convert_single = |s: Stream<'_>| {
294 let data = s.decoded().ok()?;
295 Some(data.to_vec())
296 };
297
298 self.page_streams
299 .get_or_init(|| {
300 if let Some(stream) = self.inner.get::<Stream<'_>>(CONTENTS) {
301 convert_single(stream)
302 } else if let Some(array) = self.inner.get::<Array<'_>>(CONTENTS) {
303 let streams = array.iter::<Stream<'_>>().flat_map(convert_single);
304
305 let mut collected = vec![];
306
307 for stream in streams {
308 collected.extend(stream);
309 collected.push(b' ');
311 }
312
313 Some(collected)
314 } else {
315 warn!("contents entry of page was neither stream nor array of streams");
316
317 None
318 }
319 })
320 .as_ref()
321 .map(|d| d.as_slice())
322 }
323
324 pub fn resources(&self) -> &Resources<'a> {
326 &self.resources
327 }
328
329 pub fn media_box(&self) -> Rect {
331 self.media_box
332 }
333
334 pub fn rotation(&self) -> Rotation {
336 self.rotation
337 }
338
339 pub fn crop_box(&self) -> Rect {
341 self.crop_box
342 }
343
344 pub fn intersected_crop_box(&self) -> Rect {
346 self.crop_box().intersect(self.media_box())
347 }
348
349 pub fn base_dimensions(&self) -> (f32, f32) {
363 let crop_box = self.crop_box();
364 let media_box = self.media_box();
365
366 let effective = if crop_box.x0 >= media_box.x0 && crop_box.y0 >= media_box.y0 {
370 crop_box.intersect(media_box)
371 } else {
372 crop_box
373 };
374
375 if (effective.width() as f32).is_nearly_zero()
376 || (effective.height() as f32).is_nearly_zero()
377 {
378 (US_LETTER.width() as f32, US_LETTER.height() as f32)
379 } else {
380 (
381 effective.width().max(1.0) as f32,
382 effective.height().max(1.0) as f32,
383 )
384 }
385 }
386
387 pub fn render_dimensions(&self) -> (f32, f32) {
392 let (mut base_width, mut base_height) = self.base_dimensions();
393
394 if matches!(
395 self.rotation(),
396 Rotation::Horizontal | Rotation::FlippedHorizontal
397 ) {
398 core::mem::swap(&mut base_width, &mut base_height);
399 }
400
401 (base_width, base_height)
402 }
403
404 pub fn operations(&self) -> UntypedIter<'_> {
406 self.operations_impl().unwrap_or(UntypedIter::empty())
407 }
408
409 pub fn raw(&self) -> &Dict<'a> {
411 &self.inner
412 }
413
414 pub fn xref(&self) -> &'a XRef {
416 self.ctx.xref()
417 }
418
419 pub fn typed_operations(&self) -> TypedIter<'_> {
421 TypedIter::from_untyped(self.operations())
422 }
423
424 pub fn annots(&self) -> Vec<Dict<'a>> {
426 self.inner
427 .get::<Array<'_>>(crate::object::dict::keys::ANNOTS)
428 .map(|arr| arr.iter::<Dict<'_>>().collect())
429 .unwrap_or_default()
430 }
431}
432
433#[derive(Clone, Debug)]
435pub struct Resources<'a> {
436 pub ext_g_states: Dict<'a>,
438 pub fonts: Dict<'a>,
440 pub properties: Dict<'a>,
442 pub color_spaces: Dict<'a>,
444 pub x_objects: Dict<'a>,
446 pub patterns: Dict<'a>,
448 pub shadings: Dict<'a>,
450}
451
452impl<'a> Resources<'a> {
453 pub fn new(resources: Dict<'a>) -> Self {
458 let resources = resources.without_parent_chain();
466 let ext_g_states = resources.get::<Dict<'_>>(EXT_G_STATE).unwrap_or_default();
467 let fonts = resources.get::<Dict<'_>>(FONT).unwrap_or_default();
468 let color_spaces = resources.get::<Dict<'_>>(COLORSPACE).unwrap_or_default();
469 let x_objects = resources.get::<Dict<'_>>(XOBJECT).unwrap_or_default();
470 let patterns = resources.get::<Dict<'_>>(PATTERN).unwrap_or_default();
471 let shadings = resources.get::<Dict<'_>>(SHADING).unwrap_or_default();
472 let properties = resources.get::<Dict<'_>>(PROPERTIES).unwrap_or_default();
473
474 Self {
475 ext_g_states,
476 fonts,
477 color_spaces,
478 properties,
479 x_objects,
480 patterns,
481 shadings,
482 }
483 }
484
485 pub fn get_ext_g_state(&self, name: Name) -> Option<Dict<'a>> {
487 self.ext_g_states.get(name.deref())
488 }
489
490 pub fn get_color_space(&self, name: Name) -> Option<Object<'a>> {
492 self.color_spaces.get(name.deref())
493 }
494
495 pub fn get_font(&self, name: Name) -> Option<Dict<'a>> {
497 self.fonts.get(name.deref())
498 }
499
500 pub fn get_pattern(&self, name: Name) -> Option<Object<'a>> {
502 self.patterns.get(name.deref())
503 }
504
505 pub fn get_x_object(&self, name: Name) -> Option<Stream<'a>> {
507 self.x_objects.get(name.deref())
508 }
509
510 pub fn get_shading(&self, name: Name) -> Option<Object<'a>> {
512 self.shadings.get(name.deref())
513 }
514}
515
516const POINTS_PER_INCH: f64 = 72.0;
518const POINTS_PER_MM: f64 = 1.0 / (10.0 * 2.54) * POINTS_PER_INCH;
519
520pub const A4: Rect = Rect {
522 x0: 0.0,
523 y0: 0.0,
524 x1: 210.0 * POINTS_PER_MM,
525 y1: 297.0 * POINTS_PER_MM,
526};
527
528const US_LETTER: Rect = Rect {
536 x0: 0.0,
537 y0: 0.0,
538 x1: 8.5 * POINTS_PER_INCH,
539 y1: 11.0 * POINTS_PER_INCH,
540};
541
542pub(crate) mod cached {
543 use crate::page::Pages;
544 use crate::reader::ReaderContext;
545 use crate::xref::XRef;
546 use core::ops::Deref;
547
548 #[cfg(feature = "std")]
551 pub(crate) use std::sync::Arc;
552
553 #[cfg(not(feature = "std"))]
554 pub(crate) use alloc::rc::Rc as Arc;
555
556 pub(crate) struct CachedPages {
557 pages: Pages<'static>,
558 _xref: Arc<XRef>,
561 page_tree_rebuilt: bool,
564 }
565
566 impl CachedPages {
567 pub(crate) fn new(xref: Arc<XRef>) -> Option<Self> {
568 let xref_reference: &'static XRef = unsafe { core::mem::transmute(xref.deref()) };
576
577 let ctx = ReaderContext::new(xref_reference, false);
578 let normal = xref_reference
582 .get_with(xref.trailer_data().pages_ref, &ctx)
583 .and_then(|p| Pages::new(&p, &ctx, xref_reference));
584 let page_tree_rebuilt = normal.is_none();
585 let pages = normal.or_else(|| Pages::new_brute_force(&ctx, xref_reference))?;
586
587 Some(Self {
588 pages,
589 _xref: xref,
590 page_tree_rebuilt,
591 })
592 }
593
594 pub(crate) fn get(&self) -> &Pages<'_> {
595 &self.pages
596 }
597
598 pub(crate) fn page_tree_rebuilt(&self) -> bool {
600 self.page_tree_rebuilt
601 }
602 }
603}
604
605#[cfg(test)]
606mod cycle_tests {
607 use crate::pdf::Pdf;
608 use alloc::format;
609 use alloc::vec::Vec;
610
611 fn cyclic_pages_pdf() -> Vec<u8> {
616 let mut buf: Vec<u8> = Vec::new();
617 let mut offsets = [0usize; 4];
618 buf.extend_from_slice(b"%PDF-1.7\n");
619 offsets[1] = buf.len();
620 buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
621 offsets[2] = buf.len();
622 buf.extend_from_slice(
623 b"2 0 obj\n<< /Type /Pages /Kids [3 0 R 2 0 R] /Count 1 >>\nendobj\n",
624 );
625 offsets[3] = buf.len();
626 buf.extend_from_slice(
627 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
628 );
629 let xref_off = buf.len();
630 buf.extend_from_slice(b"xref\n0 4\n0000000000 65535 f \n");
631 for off in &offsets[1..4] {
632 buf.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
633 }
634 buf.extend_from_slice(
635 format!("trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF").as_bytes(),
636 );
637 buf
638 }
639
640 #[test]
641 fn cyclic_page_tree_yields_one_page_without_runaway() {
642 let pdf = Pdf::new(cyclic_pages_pdf()).expect("cyclic PDF should still load");
643 assert_eq!(
646 pdf.pages().len(),
647 1,
648 "page-tree cycle must not duplicate pages"
649 );
650 }
651}