1use std::borrow::Cow;
2use std::collections::{HashMap, HashSet};
3
4use tracing::warn;
5use zpdf_core::{Error, ObjectId, PdfObject, Result};
6use zpdf_parser::PdfFile;
7
8use crate::page::{PdfPage, MAX_PAGE_COUNT, MAX_PAGE_TREE_DEPTH};
9
10pub struct Catalog {
11 pub pages_ref: ObjectId,
12 pub page_count: usize,
13 page_refs: Vec<ObjectId>,
14 page_index: HashMap<ObjectId, usize>,
17}
18
19impl Catalog {
20 pub fn from_trailer(file: &PdfFile) -> Result<Self> {
21 let pages_ref = Self::resolve_pages_ref(file);
24 let mut page_refs = Vec::new();
25 let mut visited = HashSet::new();
26 if let Some(pages_ref) = pages_ref {
27 Self::collect_page_refs(file, pages_ref, &mut page_refs, &mut visited, 0)?;
28 }
29
30 if page_refs.is_empty() {
36 warn!("page tree unreachable via /Pages; scanning all objects for /Type /Page");
37 page_refs = file.find_objects_by_type("Page");
38 }
39
40 if page_refs.is_empty() {
47 warn!("no /Type /Page objects; scanning for page-shaped dicts");
48 page_refs = Self::scan_page_like(file);
49 }
50
51 if page_refs.is_empty() {
52 return Err(Error::InvalidObject(
53 0,
54 "page tree contains no usable pages".into(),
55 ));
56 }
57
58 let mut page_index = HashMap::with_capacity(page_refs.len());
61 for (i, &id) in page_refs.iter().enumerate() {
62 page_index.entry(id).or_insert(i);
63 }
64
65 Ok(Self {
66 pages_ref: pages_ref.unwrap_or(ObjectId(0, 0)),
67 page_count: page_refs.len(),
68 page_refs,
69 page_index,
70 })
71 }
72
73 pub fn page_index_of(&self, id: ObjectId) -> Option<usize> {
77 self.page_index.get(&id).copied()
78 }
79
80 fn scan_page_like(file: &PdfFile) -> Vec<ObjectId> {
85 file.all_object_ids()
86 .into_iter()
87 .filter(|&id| {
88 let Ok(obj) = file.resolve(id) else {
89 return false;
90 };
91 let Ok(dict) = obj.as_dict() else {
92 return false;
93 };
94 dict.get("Kids").is_none()
95 && dict.get("Pages").is_none()
96 && (dict.get("MediaBox").is_some() || dict.get("Contents").is_some())
97 })
98 .collect()
99 }
100
101 fn resolve_pages_ref(file: &PdfFile) -> Option<ObjectId> {
105 let root_ref = file.trailer.get_ref("Root").ok()?;
106 let root = file.resolve(root_ref).ok()?;
107 root.as_dict().ok()?.get_ref("Pages").ok()
108 }
109
110 fn collect_page_refs(
111 file: &PdfFile,
112 node_id: ObjectId,
113 refs: &mut Vec<ObjectId>,
114 visited: &mut HashSet<ObjectId>,
115 depth: usize,
116 ) -> Result<()> {
117 if depth > MAX_PAGE_TREE_DEPTH {
118 warn!("page tree deeper than {MAX_PAGE_TREE_DEPTH} at {node_id}; pruning subtree");
119 return Ok(());
120 }
121 if !visited.insert(node_id) {
122 warn!("page tree cycle: node {node_id} already visited; pruning");
123 return Ok(());
124 }
125
126 let node = match file.resolve(node_id) {
127 Ok(PdfObject::Null) => {
128 warn!("page tree node {node_id} resolves to null; skipping");
129 return Ok(());
130 }
131 Ok(obj) => obj,
132 Err(e) => {
133 warn!("failed to resolve page tree node {node_id}: {e}; skipping");
134 return Ok(());
135 }
136 };
137 let Ok(dict) = node.as_dict() else {
138 warn!(
139 "page tree node {node_id} is {}, expected Dict; skipping",
140 node.type_name()
141 );
142 return Ok(());
143 };
144
145 let is_pages = match dict.get_name("Type") {
148 Ok("Pages") => true,
149 Ok("Page") => false,
150 _ => dict.get("Kids").is_some(),
151 };
152
153 if is_pages {
154 let kids: Cow<'_, [PdfObject]> = match dict.get("Kids") {
156 Some(PdfObject::Array(a)) => Cow::Borrowed(a.as_slice()),
157 Some(PdfObject::Ref(r)) => match file.resolve(*r) {
158 Ok(PdfObject::Array(a)) => Cow::Owned(a),
159 _ => {
160 warn!("pages node {node_id}: /Kids ref {r} is not an array; skipping");
161 return Ok(());
162 }
163 },
164 _ => {
165 warn!("pages node {node_id} has no /Kids array; skipping");
166 return Ok(());
167 }
168 };
169 for kid in kids.iter() {
170 match kid {
171 PdfObject::Ref(r) => {
172 Self::collect_page_refs(file, *r, refs, visited, depth + 1)?;
173 }
174 PdfObject::Null => {
175 warn!("pages node {node_id}: null kid; skipping");
176 }
177 other => {
178 warn!(
179 "pages node {node_id}: kid is {}, expected Ref; skipping",
180 other.type_name()
181 );
182 }
183 }
184 }
185 } else {
186 if refs.len() >= MAX_PAGE_COUNT {
188 warn!("page tree exceeds {MAX_PAGE_COUNT} pages; stopping collection");
189 return Ok(());
190 }
191 refs.push(node_id);
192 }
193 Ok(())
194 }
195
196 pub fn get_page(&self, file: &PdfFile, index: usize) -> Result<PdfPage> {
197 let page_ref =
198 self.page_refs.get(index).copied().ok_or_else(|| {
199 Error::InvalidObject(0, format!("page index {index} out of range"))
200 })?;
201
202 PdfPage::from_object(file, page_ref)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use crate::page::MAX_PAGE_TREE_DEPTH;
209 use crate::test_util::build_pdf;
210 use crate::PdfDocument;
211
212 #[test]
213 fn kids_cycle_is_pruned() {
214 let doc = PdfDocument::open(build_pdf(&[
216 "<< /Type /Catalog /Pages 2 0 R >>",
217 "<< /Type /Pages /Kids [3 0 R 2 0 R] /Count 1 >>",
218 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>",
219 ]))
220 .expect("open");
221 assert_eq!(doc.page_count(), 1);
222 }
223
224 #[test]
225 fn dangling_and_null_kids_are_skipped() {
226 let doc = PdfDocument::open(build_pdf(&[
229 "<< /Type /Catalog /Pages 2 0 R >>",
230 "<< /Type /Pages /Kids [99 0 R 3 0 R null] /Count 3 >>",
231 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>",
232 ]))
233 .expect("open");
234 assert_eq!(doc.page_count(), 1);
235 assert!(doc.page(0).is_ok());
236 }
237
238 #[test]
239 fn missing_type_nodes_tolerated() {
240 let doc = PdfDocument::open(build_pdf(&[
243 "<< /Type /Catalog /Pages 2 0 R >>",
244 "<< /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>",
245 "<< /Parent 2 0 R >>",
246 ]))
247 .expect("open");
248 assert_eq!(doc.page_count(), 1);
249 let page = doc.page(0).expect("page");
250 assert_eq!(page.media_box.width(), 200.0);
251 }
252
253 #[test]
254 fn empty_page_tree_is_an_error() {
255 assert!(PdfDocument::open(build_pdf(&[
256 "<< /Type /Catalog /Pages 2 0 R >>",
257 "<< /Type /Pages /Kids [] /Count 0 >>",
258 ]))
259 .is_err());
260 }
261
262 #[test]
263 fn null_root_is_a_hard_error() {
264 assert!(PdfDocument::open(build_pdf(&["null"])).is_err());
266 }
267
268 #[test]
269 fn overly_deep_page_tree_is_pruned() {
270 let mut objects: Vec<String> = vec!["<< /Type /Catalog /Pages 2 0 R >>".into()];
275 let chain = MAX_PAGE_TREE_DEPTH + 10;
276 for i in 0..chain {
277 objects.push(format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", i + 3));
278 }
279 objects.push("<< /Type /Page /MediaBox [0 0 10 10] >>".into());
280 let refs: Vec<&str> = objects.iter().map(|s| s.as_str()).collect();
281 let doc = PdfDocument::open(build_pdf(&refs)).expect("fallback recovers the pruned leaf");
282 assert_eq!(doc.page_count(), 1);
283 }
284}