1use std::path::{Path, PathBuf};
23
24use objc2::AnyThread;
25use objc2::runtime::AnyObject;
26use objc2::rc::{Retained, autoreleasepool};
27use objc2_core_foundation::{CGPoint, CGRect, CGSize};
28use objc2_core_graphics::{CGBitmapContextCreate, CGColorSpace, CGContext, CGImageAlphaInfo, CGImageByteOrderInfo, CGInterpolationQuality, CGPDFDocument};
29use objc2_foundation::{NSArray, NSData, NSDate, NSPoint, NSString, NSStringCompareOptions, NSURL};
30use objc2_pdf_kit::{
31 PDFActionGoTo, PDFActionURL, PDFDestination, PDFDisplayBox, PDFDocument, PDFDocumentAttribute, PDFDocumentAuthorAttribute,
32 PDFDocumentCreationDateAttribute, PDFDocumentCreatorAttribute, PDFDocumentKeywordsAttribute, PDFDocumentModificationDateAttribute,
33 PDFDocumentProducerAttribute, PDFDocumentSubjectAttribute, PDFDocumentTitleAttribute, PDFOutline, PDFPage, PDFSelection,
34};
35
36use crate::geom::{self, PageGeom};
37use crate::ocg::Patcher;
38use crate::{Attachment, Bitmap, Error, Hit, Info, Layer, LayerNode, Link, LinkTarget, NEEDS_PASSWORD, OutlineEntry, Rect, Result, SearchOptions, Selection, WRONG_PASSWORD, render_size};
39
40const MAX_HITS: usize = 10_000;
42
43fn err(msg: impl Into<String>) -> Error {
44 Error(msg.into())
45}
46
47fn catalog_of<T>(doc: &PDFDocument, f: impl FnOnce(catalog::Dict, &CGPDFDocument) -> T) -> Option<T> {
50 let doc = unsafe { doc.documentRef() }?;
54 let cat = CGPDFDocument::catalog(Some(&doc));
55 (!cat.is_null()).then(|| f(cat, &doc))
56}
57
58pub struct Document {
60 doc: Retained<PDFDocument>,
61 count: usize,
62 path: PathBuf,
63 password: Option<String>,
64 switched: Option<Retained<PDFDocument>>,
66 patcher: Option<Patcher>,
68}
69
70impl Document {
71 pub fn open(path: &Path) -> Result<Document> {
76 Self::open_with_password(path, None)
77 }
78
79 pub fn open_with_password(path: &Path, password: Option<&str>) -> Result<Document> {
83 if !path.is_file() {
84 return Err(err(format!("no file at {}", path.display())));
85 }
86 let path_text = path.to_str().ok_or_else(|| err("the path is not valid UTF-8"))?;
87 autoreleasepool(|_| {
88 let url = NSURL::fileURLWithPath(&NSString::from_str(path_text));
89 let doc = unsafe { PDFDocument::initWithURL(PDFDocument::alloc(), &url) }.ok_or_else(|| err("not a PDF this system can read"))?;
91 if unsafe { doc.isLocked() } {
93 let Some(pw) = password else { return Err(err(NEEDS_PASSWORD)) };
94 if !unsafe { doc.unlockWithPassword(&NSString::from_str(pw)) } {
96 return Err(err(WRONG_PASSWORD));
97 }
98 }
99 let count = unsafe { doc.pageCount() };
101 Ok(Document { doc, count, path: path.to_path_buf(), password: password.map(str::to_string), switched: None, patcher: None })
102 })
103 }
104
105 pub fn page_count(&self) -> usize {
106 self.count
107 }
108
109 pub fn title(&self) -> Option<String> {
111 autoreleasepool(|_| {
112 let attrs = unsafe { self.doc.documentAttributes() }?;
115 let key = unsafe { PDFDocumentTitleAttribute };
116 let value = attrs.objectForKey(key)?;
117 let s = value.downcast::<NSString>().ok()?.to_string();
118 let s = s.trim().to_string();
119 (!s.is_empty()).then_some(s)
120 })
121 }
122
123 pub fn info(&self) -> Info {
125 autoreleasepool(|_| {
126 let attrs = unsafe { self.doc.documentAttributes() };
128 let get = |key: &PDFDocumentAttribute| attrs.as_ref().and_then(|a| a.objectForKey(key));
129 let text = |key: &PDFDocumentAttribute| get(key).and_then(|v| text_of(&v));
131 let date = |key: &PDFDocumentAttribute| get(key).and_then(|v| v.downcast::<NSDate>().ok()).map(|d| d.timeIntervalSince1970());
132 unsafe {
133 Info {
134 title: text(PDFDocumentTitleAttribute),
135 author: text(PDFDocumentAuthorAttribute),
136 subject: text(PDFDocumentSubjectAttribute),
137 keywords: text(PDFDocumentKeywordsAttribute),
138 creator: text(PDFDocumentCreatorAttribute),
139 producer: text(PDFDocumentProducerAttribute),
140 created: date(PDFDocumentCreationDateAttribute),
141 modified: date(PDFDocumentModificationDateAttribute),
142 version: (self.doc.majorVersion() as i64, self.doc.minorVersion() as i64),
143 pages: self.count,
144 encrypted: self.doc.isEncrypted(),
145 }
146 }
147 })
148 }
149
150 fn page(&self, i: usize) -> Result<Retained<PDFPage>> {
151 if i >= self.count {
152 return Err(err(format!("no page {}", i + 1)));
153 }
154 unsafe { self.doc.pageAtIndex(i) }.ok_or_else(|| err(format!("page {} can't be read", i + 1)))
156 }
157
158 fn drawn_page(&self, i: usize) -> Result<Retained<PDFPage>> {
160 let Some(doc) = &self.switched else { return self.page(i) };
161 if i >= self.count {
162 return Err(err(format!("no page {}", i + 1)));
163 }
164 unsafe { doc.pageAtIndex(i) }.ok_or_else(|| err(format!("page {} can't be read", i + 1)))
167 }
168
169 fn geom(page: &PDFPage) -> PageGeom {
170 let (b, rot) = unsafe { (page.boundsForBox(PDFDisplayBox::CropBox), page.rotation()) };
172 PageGeom { x0: b.origin.x, y0: b.origin.y, w: b.size.width.abs(), h: b.size.height.abs(), turns: geom::turns(rot as i64) }
173 }
174
175 pub fn page_size(&self, i: usize) -> Option<(f64, f64)> {
177 autoreleasepool(|_| self.page(i).ok().map(|p| Self::geom(&p).size()))
178 }
179
180 pub fn page_sizes(&self) -> Vec<(f64, f64)> {
182 (0..self.count).map(|i| self.page_size(i).unwrap_or((612.0, 792.0))).collect()
183 }
184
185 pub fn render(&self, i: usize, scale: f64, max_px: usize) -> Result<Bitmap> {
188 let size = self.page_size(i).ok_or_else(|| err(format!("no page {}", i + 1)))?;
189 let (w, h) = render_size(size, scale, max_px.max(1));
190 self.render_part(i, Rect { x: 0.0, y: 0.0, w: size.0, h: size.1 }, w, h)
191 }
192
193 pub fn render_part(&self, i: usize, part: Rect, w: usize, h: usize) -> Result<Bitmap> {
198 autoreleasepool(|_| {
199 let page = self.drawn_page(i)?;
200 let size = Self::geom(&page).size();
201 let (w, h) = (w.max(1), h.max(1));
202 let mut rgba = vec![0u8; w * h * 4];
203 let space = CGColorSpace::new_device_rgb().ok_or_else(|| err("no RGB colour space"))?;
204 let info = CGImageAlphaInfo::PremultipliedLast.0 | CGImageByteOrderInfo::Order32Big.0;
205 let ctx = unsafe { CGBitmapContextCreate(rgba.as_mut_ptr().cast(), w, h, 8, w * 4, Some(&space), info) }.ok_or_else(|| err("no bitmap context"))?;
209 let c = Some(&*ctx);
210 CGContext::set_rgb_fill_color(c, 1.0, 1.0, 1.0, 1.0);
211 CGContext::fill_rect(c, CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(w as f64, h as f64)));
212 CGContext::set_interpolation_quality(c, CGInterpolationQuality::High);
213 if part.w > 0.0 && part.h > 0.0 {
214 CGContext::scale_ctm(c, w as f64 / part.w, h as f64 / part.h);
215 CGContext::translate_ctm(c, -part.x, part.y + part.h - size.1);
218 unsafe { page.drawWithBox_toContext(PDFDisplayBox::CropBox, &ctx) };
224 }
225 drop(ctx);
226 for px in rgba.as_chunks_mut::<4>().0 {
228 px[3] = 255;
229 }
230 Ok(Bitmap { width: w, height: h, rgba })
231 })
232 }
233
234 pub fn thumbnail(&self, i: usize, max_px: usize) -> Result<Bitmap> {
236 let size = self.page_size(i).ok_or_else(|| err(format!("no page {}", i + 1)))?;
237 let longest = size.0.max(size.1).max(1.0);
238 self.render(i, max_px as f64 / longest, max_px)
239 }
240
241 pub fn text(&self, i: usize) -> String {
243 autoreleasepool(|_| {
244 let Ok(page) = self.page(i) else { return String::new() };
245 unsafe { page.string() }.map(|s| s.to_string()).unwrap_or_default()
247 })
248 }
249
250 pub fn search(&self, text: &str, options: SearchOptions) -> Vec<Hit> {
253 if text.is_empty() {
254 return Vec::new();
255 }
256 autoreleasepool(|_| {
257 let mut flags = NSStringCompareOptions::empty();
258 if !options.match_diacritics {
259 flags |= NSStringCompareOptions::DiacriticInsensitiveSearch;
260 }
261 if !options.match_case {
262 flags |= NSStringCompareOptions::CaseInsensitiveSearch;
263 }
264 let mut texts: std::collections::HashMap<usize, Vec<u16>> = std::collections::HashMap::new();
266 let found = unsafe { self.doc.findString_withOptions(&NSString::from_str(text), flags) };
268 let mut hits = Vec::new();
269 for sel in found.iter().take(MAX_HITS) {
270 let pages = unsafe { sel.pages() };
272 for page in pages.iter() {
273 let index = unsafe { self.doc.indexForPage(&page) };
275 if index >= self.count {
276 continue;
277 }
278 if options.whole_words {
279 let range = unsafe { sel.rangeAtIndex_onPage(0, &page) };
281 let text = texts.entry(index).or_insert_with(|| unsafe { page.string() }.map(|s| s.to_string().encode_utf16().collect()).unwrap_or_default());
282 if !crate::is_whole_word(text, range.location, range.length) {
283 continue;
284 }
285 }
286 hits.push(Hit { page: index, rects: line_rects(&sel, &page) });
287 }
288 }
289 hits
290 })
291 }
292
293 pub fn selection(&self, page: usize, from: (f64, f64), to: (f64, f64)) -> Selection {
296 autoreleasepool(|_| {
297 let Ok(p) = self.page(page) else { return Selection::default() };
298 let g = Self::geom(&p);
299 let (a, b) = (g.to_page(from.0, from.1), g.to_page(to.0, to.1));
300 let Some(sel) = (unsafe { p.selectionFromPoint_toPoint(NSPoint::new(a.0, a.1), NSPoint::new(b.0, b.1)) }) else {
302 return Selection::default();
303 };
304 let text = unsafe { sel.string() }.map(|s| s.to_string()).unwrap_or_default();
306 let rects = if text.is_empty() { Vec::new() } else { line_rects(&sel, &p) };
307 Selection { text, rects }
308 })
309 }
310
311 pub fn outline(&self) -> Vec<OutlineEntry> {
313 autoreleasepool(|_| {
314 let mut out = Vec::new();
315 if let Some(root) = unsafe { self.doc.outlineRoot() } {
317 self.walk(&root, 0, &mut out);
318 }
319 out
320 })
321 }
322
323 pub fn attachments(&self) -> Vec<Attachment> {
328 autoreleasepool(|_| self.with_catalog(|cat, doc| catalog::embedded(cat, doc).into_iter().map(|(a, _)| a).collect()).unwrap_or_default())
329 }
330
331 pub fn attachment_data(&self, i: usize) -> Result<Vec<u8>> {
333 autoreleasepool(|_| {
334 self.with_catalog(|cat, doc| catalog::embedded(cat, doc).into_iter().nth(i).and_then(|(_, stream)| catalog::stream_bytes(stream)))
335 .flatten()
336 .ok_or_else(|| err("the attachment can't be read"))
337 })
338 }
339
340 pub fn layers(&self) -> Vec<Layer> {
343 autoreleasepool(|_| self.with_catalog(|cat, _| catalog::layers(cat)).unwrap_or_default())
344 }
345
346 pub fn layer_tree(&self) -> Option<Vec<LayerNode>> {
350 autoreleasepool(|_| self.with_catalog(|cat, _| catalog::layer_tree(cat)).flatten())
351 }
352
353 pub fn set_layers(&mut self, visible: &[bool]) -> Result<()> {
359 let layers = self.layers();
360 if visible.len() != layers.len() {
361 return Err(err("the list of layers doesn't match the document"));
362 }
363 if layers.iter().map(|l| l.visible).eq(visible.iter().copied()) {
364 self.switched = None;
365 return Ok(());
366 }
367 let mut bytes = std::fs::read(&self.path).map_err(|e| err(e.to_string()))?;
368 if self.patcher.is_none() {
369 let patcher = Patcher::new(&bytes, self.password.as_deref())?;
370 if patcher.groups() != layers.len() {
371 return Err(err("the PDF's layers can't be switched"));
372 }
373 self.patcher = Some(patcher);
374 }
375 let tail = self.patcher.as_ref().map(|p| p.update(visible, bytes.len())).unwrap_or_default();
376 bytes.extend_from_slice(&tail);
377 autoreleasepool(|_| {
378 let data = NSData::with_bytes(&bytes);
379 let copy = unsafe { PDFDocument::initWithData(PDFDocument::alloc(), &data) }.ok_or_else(|| err("the PDF's layers can't be switched"))?;
381 if unsafe { copy.isLocked() } && !self.password.as_deref().is_some_and(|pw| unsafe { copy.unlockWithPassword(&NSString::from_str(pw)) }) {
383 return Err(err("the PDF's layers can't be switched"));
384 }
385 let same_pages = unsafe { copy.pageCount() } == self.count;
387 let took = catalog_of(©, |cat, _| catalog::layers(cat)).is_some_and(|l| l.iter().map(|l| l.visible).eq(visible.iter().copied()));
389 if !same_pages || !took {
390 return Err(err("the PDF's layers can't be switched"));
391 }
392 self.switched = Some(copy);
393 Ok(())
394 })
395 }
396
397 fn with_catalog<T>(&self, f: impl FnOnce(catalog::Dict, &CGPDFDocument) -> T) -> Option<T> {
400 catalog_of(&self.doc, f)
401 }
402
403 pub fn links(&self, i: usize) -> Vec<Link> {
406 autoreleasepool(|_| {
407 let Ok(page) = self.page(i) else { return Vec::new() };
408 let g = Self::geom(&page);
409 let mut out = Vec::new();
410 let annotations = unsafe { page.annotations() };
412 for a in annotations.iter() {
413 let (kind, b) = unsafe { (a.r#type(), a.bounds()) };
415 if kind.is_none_or(|k| k.to_string() != "Link") || b.size.width <= 0.0 || b.size.height <= 0.0 {
416 continue;
417 }
418 let (url, dest, action) = unsafe { (a.URL(), a.destination(), a.action()) };
421 let url = url.or_else(|| action.clone().and_then(|act| act.downcast::<PDFActionURL>().ok()).and_then(|u| unsafe { u.URL() }));
422 let dest = dest.or_else(|| action.and_then(|act| act.downcast::<PDFActionGoTo>().ok()).map(|go| unsafe { go.destination() }));
424 let target = if let Some(text) = url.and_then(|u| u.absoluteString()).map(|s| s.to_string()) {
425 Some(LinkTarget::Url(text))
426 } else {
427 dest.and_then(|d| self.destination(&d))
428 };
429 if let Some(target) = target {
430 out.push(Link { rect: g.rect_to_display(b.origin.x, b.origin.y, b.size.width, b.size.height), target });
431 }
432 }
433 out
434 })
435 }
436
437 fn destination(&self, d: &PDFDestination) -> Option<LinkTarget> {
439 let (page, point) = unsafe { (d.page()?, d.point()) };
441 let index = unsafe { self.doc.indexForPage(&page) };
442 if index >= self.count {
443 return None;
444 }
445 let known = |v: f64| v.is_finite() && v.abs() < 1e7;
447 let point = (known(point.x) && known(point.y)).then(|| Self::geom(&page).to_display(point.x, point.y));
448 Some(LinkTarget::Page { page: index, point })
449 }
450
451 fn walk(&self, node: &PDFOutline, depth: usize, out: &mut Vec<OutlineEntry>) {
452 if depth > 16 || out.len() > 5000 {
454 return;
455 }
456 unsafe {
458 for n in 0..node.numberOfChildren() {
459 let Some(child) = node.childAtIndex(n) else { continue };
460 let title = child.label().map(|s| s.to_string()).unwrap_or_default();
461 let page = child.destination().and_then(|d| d.page()).map(|p| self.doc.indexForPage(&p)).filter(|&i| i < self.count);
462 out.push(OutlineEntry { title, depth, page });
463 self.walk(&child, depth + 1, out);
464 }
465 }
466 }
467}
468
469pub fn open_for_print(path: &Path, password: Option<&str>, layers: Option<&[bool]>) -> Result<Document> {
473 let mut doc = Document::open_with_password(path, password)?;
474 if let Some(visible) = layers {
475 doc.set_layers(visible)?;
476 }
477 Ok(doc)
478}
479
480pub fn print(path: &Path, password: Option<&str>, title: &str, layers: Option<&[bool]>) -> Result<bool> {
490 let mtm = objc2::MainThreadMarker::new().ok_or_else(|| err("printing needs the main thread"))?;
491 let doc = open_for_print(path, password, layers)?;
492 autoreleasepool(|_| {
493 if !unsafe { doc.doc.allowsPrinting() } {
495 return Err(err("this PDF does not allow printing"));
496 }
497 let info = objc2_app_kit::NSPrintInfo::sharedPrintInfo();
498 let op = unsafe { doc.switched.as_ref().unwrap_or(&doc.doc).printOperationForPrintInfo_scalingMode_autoRotate(Some(&info), objc2_pdf_kit::PDFPrintScalingMode::PageScaleDownToFit, true, mtm) }
500 .ok_or_else(|| err("PDFKit made no print job"))?;
501 op.setJobTitle(Some(&NSString::from_str(title)));
502 op.setShowsPrintPanel(true);
503 op.setShowsProgressPanel(true);
504 Ok(op.runOperation())
505 })
506}
507
508fn text_of(v: &AnyObject) -> Option<String> {
511 let s = if let Some(s) = v.downcast_ref::<NSString>() {
512 s.to_string()
513 } else {
514 let list = v.downcast_ref::<NSArray>()?;
515 list.iter().filter_map(|x| x.downcast_ref::<NSString>().map(|s| s.to_string())).collect::<Vec<_>>().join(", ")
516 };
517 let s = s.trim().to_string();
518 (!s.is_empty()).then_some(s)
519}
520
521fn line_rects(sel: &PDFSelection, page: &PDFPage) -> Vec<Rect> {
523 let g = Document::geom(page);
524 let lines = unsafe { sel.selectionsByLine() };
526 let mut rects = Vec::new();
527 for line in lines.iter() {
528 let b = unsafe { line.boundsForPage(page) };
530 if b.size.width > 0.0 && b.size.height > 0.0 {
531 rects.push(g.rect_to_display(b.origin.x, b.origin.y, b.size.width, b.size.height));
532 }
533 }
534 rects
535}
536
537#[cfg(test)]
538mod tests {
539 use objc2_foundation::NSDictionary;
540 use objc2_pdf_kit::{PDFDocumentOwnerPasswordOption, PDFDocumentUserPasswordOption};
541
542 use super::*;
543
544 fn plain_pdf(path: &Path) {
546 let objs = [
547 "<< /Type /Catalog /Pages 2 0 R >>",
548 "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
549 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>",
550 "<< /Title (Locked Book) /Author (Falcon) /Keywords (one two) >>",
551 ];
552 let mut out = b"%PDF-1.4\n".to_vec();
553 let mut offsets = Vec::new();
554 for (k, body) in objs.iter().enumerate() {
555 offsets.push(out.len());
556 out.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", k + 1).as_bytes());
557 }
558 let xref = out.len();
559 out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
560 for o in offsets {
561 out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
562 }
563 out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R /Info 4 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
564 std::fs::write(path, out).unwrap();
565 }
566
567 #[test]
568 fn passwords_unlock_and_metadata_reads() {
569 let dir = tempfile::tempdir().unwrap();
570 let (plain, locked) = (dir.path().join("plain.pdf"), dir.path().join("locked.pdf"));
571 plain_pdf(&plain);
572 let info = Document::open(&plain).unwrap().info();
573 assert_eq!(info.title.as_deref(), Some("Locked Book"));
574 assert_eq!(info.author.as_deref(), Some("Falcon"));
575 assert_eq!((info.pages, info.encrypted), (1, false));
576 assert!(info.version.0 >= 1);
577 autoreleasepool(|_| {
579 let url = NSURL::fileURLWithPath(&NSString::from_str(plain.to_str().unwrap()));
580 unsafe {
583 let doc = PDFDocument::initWithURL(PDFDocument::alloc(), &url).unwrap();
584 let (user, owner) = (NSString::from_str("secret"), NSString::from_str("owner"));
585 let keys = [PDFDocumentUserPasswordOption, PDFDocumentOwnerPasswordOption];
586 let options = NSDictionary::from_slices(&keys, &[user.as_ref(), owner.as_ref()]);
587 let out = NSURL::fileURLWithPath(&NSString::from_str(locked.to_str().unwrap()));
588 assert!(doc.writeToURL_withOptions(&out, Some(&options)));
589 }
590 });
591 let e = Document::open(&locked).err().unwrap();
592 assert_eq!(e.0, NEEDS_PASSWORD);
593 assert!(e.needs_password());
594 let e = Document::open_with_password(&locked, Some("nope")).err().unwrap();
595 assert_eq!(e.0, WRONG_PASSWORD);
596 assert!(e.needs_password());
597 let doc = Document::open_with_password(&locked, Some("secret")).unwrap();
598 assert_eq!(doc.page_count(), 1);
599 assert!(doc.info().encrypted);
600 assert!(!Error("no file".into()).needs_password());
601 }
602
603 #[test]
604 fn printing_needs_the_main_thread() {
605 let dir = tempfile::tempdir().unwrap();
607 let path = dir.path().join("plain.pdf");
608 plain_pdf(&path);
609 let e = print(&path, None, "x", None).err().unwrap();
610 assert!(e.0.contains("main thread"), "{e}");
611 }
612}
613
614mod catalog {
623 use std::ffi::CStr;
624 use std::ptr::{self, NonNull};
625
626 use objc2_core_graphics::{
627 CGPDFArray, CGPDFArrayRef, CGPDFDataFormat, CGPDFDictionary, CGPDFDictionaryRef, CGPDFDocument, CGPDFInteger, CGPDFPage, CGPDFStream,
628 CGPDFStreamRef, CGPDFString, CGPDFStringRef,
629 };
630
631 use crate::{Attachment, Layer, LayerNode, OrderItem, layer_tree as build_tree};
632
633 pub type Dict = CGPDFDictionaryRef;
634
635 const MAX_DEPTH: usize = 32;
636 const MAX_ITEMS: usize = 10_000;
637
638 fn key(k: &CStr) -> NonNull<std::ffi::c_char> {
639 NonNull::new(k.as_ptr().cast_mut()).expect("a C string literal is never null")
640 }
641
642 fn dict(d: Dict, k: &CStr) -> Option<Dict> {
643 let mut out: Dict = ptr::null_mut();
644 (unsafe { CGPDFDictionary::dictionary(d, key(k), &mut out) } && !out.is_null()).then_some(out)
647 }
648
649 fn array(d: Dict, k: &CStr) -> Option<CGPDFArrayRef> {
650 let mut out: CGPDFArrayRef = ptr::null_mut();
651 (unsafe { CGPDFDictionary::array(d, key(k), &mut out) } && !out.is_null()).then_some(out)
653 }
654
655 fn stream(d: Dict, k: &CStr) -> Option<CGPDFStreamRef> {
656 let mut out: CGPDFStreamRef = ptr::null_mut();
657 (unsafe { CGPDFDictionary::stream(d, key(k), &mut out) } && !out.is_null()).then_some(out)
659 }
660
661 fn integer(d: Dict, k: &CStr) -> Option<i64> {
662 let mut out: CGPDFInteger = 0;
663 unsafe { CGPDFDictionary::integer(d, key(k), &mut out) }.then_some(out as i64)
665 }
666
667 fn name(d: Dict, k: &CStr) -> Option<String> {
668 let mut out: *const std::ffi::c_char = ptr::null();
669 (unsafe { CGPDFDictionary::name(d, key(k), &mut out) } && !out.is_null()).then(|| unsafe { CStr::from_ptr(out) }.to_string_lossy().into_owned())
672 }
673
674 fn text(s: CGPDFStringRef) -> Option<String> {
675 (!s.is_null()).then(|| unsafe { CGPDFString::text_string(s) }).flatten().map(|cf| cf.to_string())
677 }
678
679 fn string(d: Dict, k: &CStr) -> Option<String> {
680 let mut out: CGPDFStringRef = ptr::null_mut();
681 if unsafe { CGPDFDictionary::string(d, key(k), &mut out) } { text(out) } else { None }
683 }
684
685 fn len(a: CGPDFArrayRef) -> usize {
686 unsafe { CGPDFArray::count(a) }.min(MAX_ITEMS)
688 }
689
690 fn array_dict(a: CGPDFArrayRef, i: usize) -> Option<Dict> {
691 let mut out: Dict = ptr::null_mut();
692 (unsafe { CGPDFArray::dictionary(a, i, &mut out) } && !out.is_null()).then_some(out)
694 }
695
696 fn array_string(a: CGPDFArrayRef, i: usize) -> Option<String> {
697 let mut out: CGPDFStringRef = ptr::null_mut();
698 if unsafe { CGPDFArray::string(a, i, &mut out) } { text(out) } else { None }
700 }
701
702 fn file_spec(spec: Dict, fallback: Option<String>) -> Option<(Attachment, CGPDFStreamRef)> {
705 let ef = dict(spec, c"EF")?;
706 let data = stream(ef, c"UF").or_else(|| stream(ef, c"F"))?;
707 let name = string(spec, c"UF").or_else(|| string(spec, c"F")).or(fallback).unwrap_or_else(|| "attachment".into());
708 let params = dict(unsafe { CGPDFStream::dictionary(data) }, c"Params");
710 let size = params.and_then(|p| integer(p, c"Size")).and_then(|n| u64::try_from(n).ok());
711 Some((Attachment { name, size, description: string(spec, c"Desc") }, data))
712 }
713
714 fn name_tree(node: Dict, depth: usize, out: &mut Vec<(Attachment, CGPDFStreamRef)>) {
716 if depth > MAX_DEPTH || out.len() >= MAX_ITEMS {
717 return;
718 }
719 if let Some(names) = array(node, c"Names") {
720 for i in (0..len(names)).step_by(2) {
721 if let Some(spec) = array_dict(names, i + 1)
722 && let Some(a) = file_spec(spec, array_string(names, i))
723 {
724 out.push(a);
725 }
726 }
727 }
728 if let Some(kids) = array(node, c"Kids") {
729 for i in 0..len(kids) {
730 if let Some(kid) = array_dict(kids, i) {
731 name_tree(kid, depth + 1, out);
732 }
733 }
734 }
735 }
736
737 pub fn embedded(cat: Dict, doc: &CGPDFDocument) -> Vec<(Attachment, CGPDFStreamRef)> {
740 let mut out = Vec::new();
741 if let Some(tree) = dict(cat, c"Names").and_then(|n| dict(n, c"EmbeddedFiles")) {
742 name_tree(tree, 0, &mut out);
743 }
744 for n in 1..=CGPDFDocument::number_of_pages(Some(doc)) {
745 let Some(page) = CGPDFDocument::page(Some(doc), n) else { continue };
746 let pd = CGPDFPage::dictionary(Some(&page));
747 if pd.is_null() {
748 continue;
749 }
750 let Some(annots) = array(pd, c"Annots") else { continue };
751 for i in 0..len(annots) {
752 if out.len() >= MAX_ITEMS {
753 return out;
754 }
755 if let Some(a) = array_dict(annots, i)
756 && name(a, c"Subtype").as_deref() == Some("FileAttachment")
757 && let Some(spec) = dict(a, c"FS")
758 && let Some(f) = file_spec(spec, string(a, c"Contents"))
759 {
760 out.push(f);
761 }
762 }
763 }
764 out
765 }
766
767 pub fn stream_bytes(s: CGPDFStreamRef) -> Option<Vec<u8>> {
769 let mut format = CGPDFDataFormat::Raw;
770 unsafe { CGPDFStream::data(s, &mut format) }.map(|d| d.to_vec())
772 }
773
774 fn array_array(a: CGPDFArrayRef, i: usize) -> Option<CGPDFArrayRef> {
775 let mut out: CGPDFArrayRef = ptr::null_mut();
776 (unsafe { CGPDFArray::array(a, i, &mut out) } && !out.is_null()).then_some(out)
778 }
779
780 fn order_items(a: CGPDFArrayRef, groups: &[Dict], depth: usize, budget: &mut usize) -> Vec<OrderItem> {
783 let mut out = Vec::new();
784 if depth > MAX_DEPTH {
785 return out;
786 }
787 for i in 0..len(a) {
788 if *budget == 0 {
789 break;
790 }
791 *budget -= 1;
792 out.push(if let Some(g) = array_dict(a, i) {
793 groups.iter().position(|&x| x == g).map_or(OrderItem::Other, OrderItem::Group)
794 } else if let Some(inner) = array_array(a, i) {
795 OrderItem::Array(order_items(inner, groups, depth + 1, budget))
796 } else if let Some(t) = array_string(a, i) {
797 OrderItem::Text(t)
798 } else {
799 OrderItem::Other
800 });
801 }
802 out
803 }
804
805 pub fn layer_tree(cat: Dict) -> Option<Vec<LayerNode>> {
810 let props = dict(cat, c"OCProperties")?;
811 let groups_array = array(props, c"OCGs")?;
812 let groups: Vec<Dict> = (0..len(groups_array)).filter_map(|i| array_dict(groups_array, i)).collect();
813 let order = array(dict(props, c"D")?, c"Order")?;
814 let mut budget = MAX_ITEMS;
815 Some(build_tree(&order_items(order, &groups, 0, &mut budget), groups.len()))
816 }
817
818 pub fn layers(cat: Dict) -> Vec<Layer> {
823 let Some(props) = dict(cat, c"OCProperties") else { return Vec::new() };
824 let Some(groups) = array(props, c"OCGs") else { return Vec::new() };
825 let config = dict(props, c"D");
826 let base_on = config.and_then(|d| name(d, c"BaseState")).is_none_or(|s| s != "OFF");
827 let listed = |k: &CStr| -> Vec<Dict> { config.and_then(|d| array(d, k)).map(|a| (0..len(a)).filter_map(|i| array_dict(a, i)).collect()).unwrap_or_default() };
828 let (on, off) = (listed(c"ON"), listed(c"OFF"));
829 (0..len(groups))
830 .filter_map(|i| array_dict(groups, i))
831 .map(|g| {
832 let visible = if base_on { !off.contains(&g) } else { on.contains(&g) };
833 Layer { name: string(g, c"Name").unwrap_or_default(), visible }
834 })
835 .collect()
836 }
837}
838