1use std::collections::HashSet;
4use std::sync::{Arc, OnceLock};
5
6use otf_pdf_syntax::{
7 Diagnostic, Dict, Error, File, LimitKind, ObjId, Object, OpenOptions, Result, SharedSource,
8 Source, Version,
9};
10
11use crate::pages::{Page, collect_pages};
12use crate::store::Store;
13use crate::text::decode_text_string;
14
15#[derive(Debug, Clone, Default, PartialEq)]
17pub struct Info {
18 pub title: Option<String>,
20 pub author: Option<String>,
22 pub subject: Option<String>,
24 pub keywords: Option<String>,
26 pub creator: Option<String>,
28 pub producer: Option<String>,
30 pub creation_date: Option<String>,
32 pub mod_date: Option<String>,
34 pub trapped: Option<String>,
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct OutlineItem {
41 pub id: Option<ObjId>,
43 pub title: String,
45 pub dest: Option<Object>,
47 pub action: Option<Dict>,
49 pub open: bool,
51 pub children: Vec<OutlineItem>,
53 pub extra: Dict,
56}
57
58#[derive(Debug, Clone, PartialEq)]
60pub struct Attachment {
61 pub name: String,
63 pub filename: Option<String>,
65 pub description: Option<String>,
67 pub mime_type: Option<String>,
69 pub size: Option<u64>,
71 pub stream: Option<ObjId>,
73}
74
75#[derive(Debug)]
77pub struct Document {
78 pub(crate) store: Store,
79 pages: OnceLock<std::result::Result<Arc<Vec<Page>>, Error>>,
80}
81
82impl Document {
83 pub fn open(source: impl Source + Send + Sync + 'static, options: OpenOptions) -> Result<Self> {
85 Self::open_shared(Arc::new(source), options)
86 }
87
88 pub fn open_shared(source: SharedSource, options: OpenOptions) -> Result<Self> {
90 let file = File::open(source, options)?;
91 let root = file.trailer().get(b"Root").cloned().unwrap_or_default();
92 file.resolve_dict(&root).ok_or(Error::NoRoot)?;
93 Ok(Self {
94 store: Store::new(file),
95 pages: OnceLock::new(),
96 })
97 }
98
99 pub fn file(&self) -> &File {
101 self.store.file()
102 }
103
104 pub fn store(&self) -> &Store {
106 &self.store
107 }
108
109 pub fn store_mut(&mut self) -> &mut Store {
112 self.invalidate();
113 &mut self.store
114 }
115
116 pub(crate) fn invalidate(&mut self) {
118 self.pages = OnceLock::new();
119 }
120
121 pub fn catalog_id(&self) -> Option<ObjId> {
124 self.store.trailer().get_ref(b"Root")
125 }
126
127 pub fn catalog(&self) -> Dict {
129 self.store
130 .trailer()
131 .get(b"Root")
132 .and_then(|r| self.store.resolve_dict(r))
133 .unwrap_or_default()
134 }
135
136 pub fn version(&self) -> Option<Version> {
139 let header = self.file().header().map(|h| h.version);
140 let catalog = self
141 .catalog()
142 .get_name(b"Version")
143 .and_then(|v| otf_pdf_syntax::find_header(&[b"%PDF-", v].concat()[..]).ok())
144 .map(|h| h.version);
145 match (header, catalog) {
146 (Some(h), Some(c)) => Some(h.max(c)),
147 (h, c) => h.or(c),
148 }
149 }
150
151 pub fn diagnostics(&self) -> Vec<Diagnostic> {
153 self.file().diagnostics()
154 }
155
156 pub fn is_encrypted(&self) -> bool {
158 self.file().security().is_some()
159 }
160
161 pub fn pages(&self) -> Result<Arc<Vec<Page>>> {
163 self.pages
164 .get_or_init(|| collect_pages(&self.store, &self.catalog()).map(Arc::new))
165 .clone()
166 }
167
168 pub fn page_count(&self) -> Result<usize> {
170 self.pages().map(|p| p.len())
171 }
172
173 pub fn info(&self) -> Info {
175 let Some(dict) = self
176 .store
177 .trailer()
178 .get(b"Info")
179 .and_then(|i| self.store.resolve_dict(i))
180 else {
181 return Info::default();
182 };
183 let text = |key: &[u8]| match self.store.resolve(dict.get(key)?) {
184 Object::String(s) => Some(decode_text_string(&s.bytes)),
185 Object::Name(n) => Some(String::from_utf8_lossy(n.as_bytes()).into_owned()),
186 _ => None,
187 };
188 Info {
189 title: text(b"Title"),
190 author: text(b"Author"),
191 subject: text(b"Subject"),
192 keywords: text(b"Keywords"),
193 creator: text(b"Creator"),
194 producer: text(b"Producer"),
195 creation_date: text(b"CreationDate"),
196 mod_date: text(b"ModDate"),
197 trapped: text(b"Trapped"),
198 }
199 }
200
201 pub fn xmp_metadata(&self) -> Result<Option<Vec<u8>>> {
203 let Some(meta) = self.catalog().get(b"Metadata").cloned() else {
204 return Ok(None);
205 };
206 let Object::Stream(stream) = self.store.resolve(&meta) else {
207 return Ok(None);
208 };
209 Ok(Some(self.store.decode_stream(&stream)?.data))
210 }
211
212 pub fn outlines(&self) -> Result<Vec<OutlineItem>> {
214 let Some(root) = self
215 .catalog()
216 .get(b"Outlines")
217 .and_then(|o| self.store.resolve_dict(o))
218 else {
219 return Ok(Vec::new());
220 };
221 let mut seen = HashSet::new();
222 let mut budget = self.store.limits().max_objects as usize;
223 self.outline_children(&root, 0, &mut seen, &mut budget)
224 }
225
226 fn outline_children(
227 &self,
228 parent: &Dict,
229 depth: u32,
230 seen: &mut HashSet<ObjId>,
231 budget: &mut usize,
232 ) -> Result<Vec<OutlineItem>> {
233 if depth > self.store.limits().max_depth {
234 return Err(Error::Limit(LimitKind::Depth));
235 }
236 let mut items = Vec::new();
237 let mut next = parent.get(b"First").cloned();
238 while let Some(node) = next.take() {
239 let id = node.as_ref();
240 if id.is_some_and(|id| !seen.insert(id)) {
242 break;
243 }
244 *budget = budget
245 .checked_sub(1)
246 .ok_or(Error::Limit(LimitKind::Objects))?;
247 self.store.limits().charge(1)?;
248 let Some(dict) = self.store.resolve_dict(&node) else {
249 break;
250 };
251 let title = match self
252 .store
253 .resolve(dict.get(b"Title").unwrap_or(&Object::Null))
254 {
255 Object::String(s) => decode_text_string(&s.bytes),
256 _ => String::new(),
257 };
258 let children = self.outline_children(&dict, depth + 1, seen, budget)?;
259 let mut extra = dict.clone();
260 for key in [
261 &b"Title"[..],
262 b"Parent",
263 b"Prev",
264 b"Next",
265 b"First",
266 b"Last",
267 b"Count",
268 b"Dest",
269 b"A",
270 ] {
271 extra.remove(key);
272 }
273 items.push(OutlineItem {
274 id,
275 extra,
276 title,
277 dest: dict.get(b"Dest").map(|d| self.store.resolve(d)),
278 action: dict.get(b"A").and_then(|a| self.store.resolve_dict(a)),
279 open: dict.get_int(b"Count").is_some_and(|c| c > 0),
280 children,
281 });
282 next = dict.get(b"Next").cloned();
283 }
284 Ok(items)
285 }
286
287 pub fn attachments(&self) -> Result<Vec<Attachment>> {
289 let Some(tree) = self
290 .catalog()
291 .get(b"Names")
292 .and_then(|n| self.store.resolve_dict(n))
293 .and_then(|names| names.get(b"EmbeddedFiles").cloned())
294 else {
295 return Ok(Vec::new());
296 };
297 let entries = name_tree(&self.store, &tree)?;
298 Ok(entries
299 .into_iter()
300 .filter_map(|(key, value)| {
301 let spec = self.store.resolve_dict(&value)?;
302 let text = |k: &[u8]| match self.store.resolve(spec.get(k)?) {
303 Object::String(s) => Some(decode_text_string(&s.bytes)),
304 _ => None,
305 };
306 let ef = spec.get(b"EF").and_then(|e| self.store.resolve_dict(e));
307 let stream_ref = ef
308 .as_ref()
309 .and_then(|ef| ef.get(b"UF").or_else(|| ef.get(b"F")))
310 .and_then(Object::as_ref);
311 let stream_dict = stream_ref.and_then(|r| self.store.resolve_dict(&Object::Ref(r)));
312 let params = stream_dict
313 .as_ref()
314 .and_then(|d| d.get(b"Params"))
315 .and_then(|p| self.store.resolve_dict(p));
316 Some(Attachment {
317 name: decode_text_string(&key),
318 filename: text(b"UF").or_else(|| text(b"F")),
319 description: text(b"Desc"),
320 mime_type: stream_dict
321 .as_ref()
322 .and_then(|d| d.get_name(b"Subtype"))
323 .map(|n| String::from_utf8_lossy(n).into_owned()),
324 size: params
325 .and_then(|p| p.get_int(b"Size"))
326 .and_then(|s| u64::try_from(s).ok()),
327 stream: stream_ref,
328 })
329 })
330 .collect())
331 }
332
333 pub fn attachment_data(&self, attachment: &Attachment) -> Result<Vec<u8>> {
335 let id = attachment.stream.ok_or(Error::WrongType {
336 expected: "embedded file stream",
337 found: "null",
338 })?;
339 if let Some(sec) = self.file().security()
340 && !sec.is_authenticated()
341 {
342 return Err(Error::InvalidPassword);
343 }
344 let obj = self.store.get(id)?;
345 let stream = obj.as_stream().ok_or(Error::WrongType {
346 expected: "stream",
347 found: obj.type_name(),
348 })?;
349 Ok(self.store.decode_stream(stream)?.data)
350 }
351}
352
353pub fn name_tree(file: &Store, root: &Object) -> Result<Vec<(Vec<u8>, Object)>> {
355 let mut out = Vec::new();
356 let mut seen = HashSet::new();
357 let mut stack = vec![(root.clone(), 0u32)];
358 while let Some((node, depth)) = stack.pop() {
359 if depth > file.limits().max_depth {
360 return Err(Error::Limit(LimitKind::Depth));
361 }
362 if node.as_ref().is_some_and(|id| !seen.insert(id)) {
363 continue;
364 }
365 file.limits().charge(1)?;
366 let Some(dict) = file.resolve_dict(&node) else {
367 continue;
368 };
369 if let Some(names) = dict.get(b"Names").map(|n| file.resolve(n)) {
370 for pair in names.as_array().unwrap_or_default().chunks(2) {
371 if let [key, value] = pair
372 && let Object::String(k) = file.resolve(key)
373 {
374 out.push((k.bytes, value.clone()));
375 }
376 }
377 }
378 if let Some(kids) = dict.get(b"Kids").map(|k| file.resolve(k)) {
379 for kid in kids.as_array().unwrap_or_default().iter().rev() {
380 stack.push((kid.clone(), depth + 1));
381 }
382 }
383 }
384 Ok(out)
385}