1use folio_core::{FolioError, Rect, Result};
4use folio_cos::{CosDoc, ObjectId, PdfObject};
5use indexmap::IndexMap;
6
7use crate::info::DocInfo;
8use crate::page::Page;
9
10pub struct PdfDoc {
15 cos: CosDoc,
16}
17
18impl PdfDoc {
19 pub fn new() -> Result<Self> {
21 let mut cos = CosDoc::new();
22
23 let mut pages_dict = IndexMap::new();
25 pages_dict.insert(b"Type".to_vec(), PdfObject::Name(b"Pages".to_vec()));
26 pages_dict.insert(b"Kids".to_vec(), PdfObject::Array(vec![]));
27 pages_dict.insert(b"Count".to_vec(), PdfObject::Integer(0));
28 let pages_id = cos.create_indirect(PdfObject::Dict(pages_dict));
29
30 let mut catalog_dict = IndexMap::new();
32 catalog_dict.insert(b"Type".to_vec(), PdfObject::Name(b"Catalog".to_vec()));
33 catalog_dict.insert(b"Pages".to_vec(), PdfObject::Reference(pages_id));
34 let catalog_id = cos.create_indirect(PdfObject::Dict(catalog_dict));
35
36 cos.trailer_mut()
38 .insert(b"Root".to_vec(), PdfObject::Reference(catalog_id));
39
40 Ok(Self { cos })
41 }
42
43 pub fn open(path: &str) -> Result<Self> {
45 let cos = CosDoc::open_file(path)?;
46 Ok(Self { cos })
47 }
48
49 pub fn open_from_bytes(data: Vec<u8>) -> Result<Self> {
51 let cos = CosDoc::open(data)?;
52 Ok(Self { cos })
53 }
54
55 pub fn cos(&self) -> &CosDoc {
57 &self.cos
58 }
59
60 pub fn cos_mut(&mut self) -> &mut CosDoc {
62 &mut self.cos
63 }
64
65 fn catalog_ref(&self) -> Result<ObjectId> {
67 self.cos
68 .trailer()
69 .get(b"Root".as_slice())
70 .and_then(|o| o.as_reference())
71 .ok_or_else(|| FolioError::InvalidObject("Missing /Root in trailer".into()))
72 }
73
74 fn pages_ref(&mut self) -> Result<ObjectId> {
76 let catalog_ref = self.catalog_ref()?;
77 let catalog = self
78 .cos
79 .get_object(catalog_ref.num)?
80 .ok_or_else(|| FolioError::InvalidObject("Catalog not found".into()))?
81 .clone();
82
83 catalog
84 .dict_get(b"Pages")
85 .and_then(|o| o.as_reference())
86 .ok_or_else(|| FolioError::InvalidObject("Missing /Pages in catalog".into()))
87 }
88
89 pub fn page_count(&mut self) -> Result<u32> {
91 let pages_ref = self.pages_ref()?;
92 let pages = self
93 .cos
94 .get_object(pages_ref.num)?
95 .ok_or_else(|| FolioError::InvalidObject("Pages dict not found".into()))?
96 .clone();
97
98 pages
99 .dict_get_i64(b"Count")
100 .map(|c| c as u32)
101 .ok_or_else(|| FolioError::InvalidObject("Missing /Count in Pages".into()))
102 }
103
104 pub fn get_page(&mut self, page_num: u32) -> Result<Page> {
106 if page_num == 0 {
107 return Err(FolioError::InvalidArgument(
108 "Page numbers are 1-based".into(),
109 ));
110 }
111
112 let page_refs = self.collect_page_refs()?;
113 let index = (page_num - 1) as usize;
114
115 if index >= page_refs.len() {
116 return Err(FolioError::InvalidArgument(format!(
117 "Page {} out of range (document has {} pages)",
118 page_num,
119 page_refs.len()
120 )));
121 }
122
123 let page_ref = page_refs[index];
124 let page_obj = self
125 .cos
126 .get_object(page_ref.num)?
127 .ok_or_else(|| {
128 FolioError::InvalidObject(format!("Page object {} not found", page_ref.num))
129 })?
130 .clone();
131
132 let page_obj = self.resolve_inherited_attrs(page_obj)?;
136
137 Ok(Page::new(page_ref, page_obj, page_num))
138 }
139
140 fn resolve_inherited_attrs(&mut self, page_obj: PdfObject) -> Result<PdfObject> {
146 const INHERITABLE: &[&[u8]] = &[b"MediaBox", b"CropBox", b"Rotate", b"Resources"];
147
148 let mut dict = match page_obj.as_dict() {
149 Some(d) => d.clone(),
150 None => return Ok(page_obj),
151 };
152
153 let missing: Vec<&[u8]> = INHERITABLE
155 .iter()
156 .filter(|&&key| !dict.contains_key(key))
157 .copied()
158 .collect();
159
160 if missing.is_empty() {
161 return Ok(page_obj);
162 }
163
164 let mut parent_ref = dict
166 .get(b"Parent".as_slice())
167 .and_then(|o| o.as_reference());
168 let mut visited = std::collections::HashSet::new();
169
170 while let Some(pref) = parent_ref {
171 if visited.contains(&pref.num) {
172 break; }
174 visited.insert(pref.num);
175
176 let parent = match self.cos.get_object(pref.num)? {
177 Some(obj) => obj.clone(),
178 None => break,
179 };
180
181 let parent_dict = match parent.as_dict() {
182 Some(d) => d,
183 None => break,
184 };
185
186 for &key in &missing {
188 if !dict.contains_key(key) {
189 if let Some(value) = parent_dict.get(key) {
190 dict.insert(key.to_vec(), value.clone());
191 }
192 }
193 }
194
195 if INHERITABLE.iter().all(|&key| dict.contains_key(key)) {
197 break;
198 }
199
200 parent_ref = parent_dict
202 .get(b"Parent".as_slice())
203 .and_then(|o| o.as_reference());
204 }
205
206 Ok(PdfObject::Dict(dict))
207 }
208
209 fn collect_page_refs(&mut self) -> Result<Vec<ObjectId>> {
211 let pages_ref = self.pages_ref()?;
212 let mut result = Vec::new();
213 self.collect_pages_recursive(pages_ref, &mut result)?;
214 Ok(result)
215 }
216
217 fn collect_pages_recursive(
218 &mut self,
219 node_ref: ObjectId,
220 result: &mut Vec<ObjectId>,
221 ) -> Result<()> {
222 let node = self
223 .cos
224 .get_object(node_ref.num)?
225 .ok_or_else(|| {
226 FolioError::InvalidObject(format!("Page tree node {} not found", node_ref.num))
227 })?
228 .clone();
229
230 let type_name = node.dict_get_name(b"Type").unwrap_or(b"");
231
232 match type_name {
233 b"Pages" => {
234 if let Some(kids) = node.dict_get(b"Kids").and_then(|o| o.as_array()) {
236 for kid in kids {
237 if let Some(kid_ref) = kid.as_reference() {
238 self.collect_pages_recursive(kid_ref, result)?;
239 }
240 }
241 }
242 }
243 b"Page" | _ => {
244 result.push(node_ref);
246 }
247 }
248
249 Ok(())
250 }
251
252 pub fn create_page(&mut self, media_box: Rect) -> Result<u32> {
254 let pages_ref = self.pages_ref()?;
255
256 let mut page_dict = IndexMap::new();
258 page_dict.insert(b"Type".to_vec(), PdfObject::Name(b"Page".to_vec()));
259 page_dict.insert(b"Parent".to_vec(), PdfObject::Reference(pages_ref));
260 page_dict.insert(
261 b"MediaBox".to_vec(),
262 PdfObject::Array(vec![
263 PdfObject::Real(media_box.x1),
264 PdfObject::Real(media_box.y1),
265 PdfObject::Real(media_box.x2),
266 PdfObject::Real(media_box.y2),
267 ]),
268 );
269 let page_id = self.cos.create_indirect(PdfObject::Dict(page_dict));
270
271 let pages = self
273 .cos
274 .get_object(pages_ref.num)?
275 .ok_or_else(|| FolioError::InvalidObject("Pages not found".into()))?
276 .clone();
277
278 let mut pages_dict = pages.as_dict().cloned().unwrap_or_default();
279
280 let mut kids = pages_dict
282 .get(b"Kids".as_slice())
283 .and_then(|o| o.as_array())
284 .map(|a| a.to_vec())
285 .unwrap_or_default();
286 kids.push(PdfObject::Reference(page_id));
287 pages_dict.insert(b"Kids".to_vec(), PdfObject::Array(kids.clone()));
288
289 pages_dict.insert(b"Count".to_vec(), PdfObject::Integer(kids.len() as i64));
291
292 self.cos
293 .update_object(pages_ref.num, PdfObject::Dict(pages_dict));
294
295 Ok(kids.len() as u32)
296 }
297
298 pub fn doc_info(&mut self) -> Result<DocInfo> {
300 let info_ref = self
301 .cos
302 .trailer()
303 .get(b"Info".as_slice())
304 .and_then(|o| o.as_reference());
305
306 match info_ref {
307 Some(id) => {
308 let obj = self
309 .cos
310 .get_object(id.num)?
311 .cloned()
312 .unwrap_or(PdfObject::Null);
313 Ok(DocInfo::from_dict(
314 obj.as_dict().cloned().unwrap_or_default(),
315 ))
316 }
317 None => Ok(DocInfo::default()),
318 }
319 }
320
321 pub fn is_modified(&self) -> bool {
323 self.cos.is_modified()
324 }
325
326 pub fn save_to_bytes(&mut self) -> Result<Vec<u8>> {
328 self.cos.save_to_bytes()
329 }
330
331 pub fn save(&mut self, path: &str) -> Result<()> {
333 self.cos.save_to_file(path)
334 }
335}