1use crate::hash::{FastMap, FastSet};
6use std::cell::{OnceCell, RefCell};
7use std::path::Path;
8use std::rc::Rc;
9use std::sync::Arc;
10
11use crate::crypt::{direct_crypt_filters, Decryptor};
12use crate::date::Date;
13use crate::elements::Span;
14use crate::error::{Error, Result};
15use crate::filters;
16use crate::geom::Rect;
17use crate::object::{decode_text_string, Dict, ObjRef, Object, Stream};
18use crate::objstm;
19use crate::parser::{Parser, Resolve};
20use crate::source::{block_on, AsyncObjectSource, Immediate};
21use crate::xref::{load_xref, Xref, XrefEntry};
22
23const MAX_TREE_DEPTH: usize = 256;
25
26pub struct Document {
28 data: Arc<Vec<u8>>,
29 version: (u8, u8),
30 xref: Arc<Xref>,
31 cache: RefCell<FastMap<(u32, u16), Rc<Object>>>,
33 loading: RefCell<FastSet<u32>>,
36 objstms: RefCell<FastMap<u32, Rc<objstm::ObjStm>>>,
40 decryptor: Option<Decryptor>,
44 pages: OnceCell<Arc<Vec<PageRec>>>,
49}
50
51pub struct DocumentSeed {
59 data: Arc<Vec<u8>>,
60 version: (u8, u8),
61 xref: Arc<Xref>,
62 decryptor: Option<Decryptor>,
63 pages: Arc<Vec<PageRec>>,
64}
65
66impl Clone for DocumentSeed {
67 fn clone(&self) -> DocumentSeed {
68 DocumentSeed {
69 data: Arc::clone(&self.data),
70 version: self.version,
71 xref: Arc::clone(&self.xref),
72 decryptor: self.decryptor.clone(),
73 pages: Arc::clone(&self.pages),
74 }
75 }
76}
77
78struct PageRec {
82 obj_ref: Option<ObjRef>,
83 media_box: Rect,
84 crop_box: Rect,
85 bleed_box: Rect,
86 trim_box: Rect,
87 art_box: Rect,
88 rotate: i32,
89 resources: Dict,
90 dict: Dict,
91}
92
93#[derive(Clone, Default)]
95struct Inherited {
96 resources: Option<Dict>,
97 media_box: Option<Rect>,
98 crop_box: Option<Rect>,
99 rotate: Option<i32>,
100}
101
102fn parse_version(data: &[u8]) -> (u8, u8) {
107 try_parse_version(data).unwrap_or((1, 4))
108}
109
110fn try_parse_version(data: &[u8]) -> Option<(u8, u8)> {
112 let window = &data[..data.len().min(1024)];
113 let pos = memchr::memmem::find(window, b"%PDF-")?;
114 let rest = &window[pos + 5..];
115 let (major, used) = read_version_component(rest)?;
116 if rest.get(used) != Some(&b'.') {
117 return None;
118 }
119 let (minor, _) = read_version_component(&rest[used + 1..])?;
120 Some((major, minor))
121}
122
123fn read_version_component(bytes: &[u8]) -> Option<(u8, usize)> {
126 let end = bytes
127 .iter()
128 .position(|b| !b.is_ascii_digit())
129 .unwrap_or(bytes.len());
130 if end == 0 || end > 3 {
131 return None;
132 }
133 let value = std::str::from_utf8(&bytes[..end]).ok()?.parse().ok()?;
134 Some((value, end))
135}
136
137fn normalize_rotation(deg: i32) -> i32 {
140 let r = deg.rem_euclid(360);
141 if r % 90 == 0 {
142 r
143 } else {
144 0
145 }
146}
147
148impl Document {
149 pub fn load(data: Vec<u8>) -> Result<Document> {
159 Document::load_with_password(data, "")
160 }
161
162 pub fn load_with_password(data: Vec<u8>, password: &str) -> Result<Document> {
170 let version = parse_version(&data);
171 let xref = load_xref(&data)?;
172 let mut doc = Document {
173 data: Arc::new(data),
174 version,
175 xref: Arc::new(xref),
176 cache: RefCell::new(FastMap::default()),
177 loading: RefCell::new(FastSet::default()),
178 objstms: RefCell::new(FastMap::default()),
179 decryptor: None,
180 pages: OnceCell::new(),
181 };
182 if doc
183 .xref
184 .trailer
185 .get("Encrypt")
186 .is_some_and(|o| !o.is_null())
187 {
188 doc.setup_decryption(password)?;
189 }
190 Ok(doc)
191 }
192
193 pub fn open_with_password(path: impl AsRef<Path>, password: &str) -> Result<Document> {
196 Document::load_with_password(std::fs::read(path)?, password)
197 }
198
199 fn setup_decryption(&mut self, password: &str) -> Result<()> {
207 let enc_obj = self
208 .xref
209 .trailer
210 .get("Encrypt")
211 .cloned()
212 .unwrap_or(Object::Null);
213 let enc = self.resolve(&enc_obj)?;
214 let enc_dict = enc.as_dict().ok_or(Error::Encrypted)?;
215 let enc_dict = direct_crypt_filters(enc_dict, |r| self.get(r).ok());
218 let id0: Vec<u8> = self
219 .xref
220 .trailer
221 .get("ID")
222 .and_then(Object::as_array)
223 .and_then(<[Object]>::first)
224 .and_then(Object::as_str_bytes)
225 .unwrap_or(&[])
226 .to_vec();
227 match Decryptor::from_standard_with_password_str(&enc_dict, &id0, password) {
228 Some(dec) => {
229 self.decryptor = Some(dec);
230 self.cache.borrow_mut().clear();
234 Ok(())
235 }
236 None => Err(Error::Encrypted),
237 }
238 }
239
240 pub fn open(path: impl AsRef<Path>) -> Result<Document> {
242 Document::load(std::fs::read(path)?)
243 }
244
245 pub fn version(&self) -> (u8, u8) {
247 self.version
248 }
249
250 pub fn bytes(&self) -> &[u8] {
252 &self.data
253 }
254
255 pub fn xref(&self) -> &Xref {
257 &self.xref
258 }
259
260 pub fn is_encrypted(&self) -> bool {
263 self.xref()
264 .trailer
265 .get("Encrypt")
266 .is_some_and(|o| !o.is_null())
267 }
268
269 pub fn is_locked(&self) -> bool {
274 self.is_encrypted() && self.decryptor.is_none()
275 }
276
277 pub fn get(&self, r: ObjRef) -> Result<Object> {
281 if let Some(cached) = self.cache.borrow().get(&(r.num, r.gen)) {
282 return Ok((**cached).clone());
283 }
284 if !self.loading.borrow_mut().insert(r.num) {
285 return Err(Error::CircularReference(r.num));
286 }
287 let result = self.load_object(r);
288 self.loading.borrow_mut().remove(&r.num);
289 let object = result?;
290 self.cache
291 .borrow_mut()
292 .insert((r.num, r.gen), Rc::new(object.clone()));
293 Ok(object)
294 }
295
296 fn load_object(&self, r: ObjRef) -> Result<Object> {
301 match self.xref.get(r.num) {
302 None | Some(XrefEntry::Free) => Err(Error::ObjectNotFound(r.num, r.gen)),
303 Some(XrefEntry::InFile { offset, .. }) => {
304 let offset = usize::try_from(offset)
305 .ok()
306 .filter(|&o| o < self.data.len())
307 .ok_or(Error::ObjectNotFound(r.num, r.gen))?;
308 self.object_at_spanned(offset).map(|parsed| parsed.1)
309 }
310 Some(XrefEntry::InStream { stream_num, index }) => {
311 self.load_from_object_stream(stream_num, index)
312 }
313 }
314 }
315
316 pub(crate) fn object_at_spanned(&self, offset: usize) -> Result<(ObjRef, Object, Span)> {
319 let mut parser = Parser::at(&self.data, offset);
320 let (r, mut object) = parser.parse_indirect(self)?;
321 if let Some(dec) = &self.decryptor {
325 dec.decrypt_object(&mut object, r.num, r.gen);
326 }
327 Ok((r, object, Span::new(offset as u64, parser.pos() as u64)))
328 }
329
330 pub(crate) fn objstm_handle(&self, stream_num: u32) -> Result<Rc<objstm::ObjStm>> {
333 if let Some(stm) = self.objstms.borrow().get(&stream_num) {
334 return Ok(Rc::clone(stm));
335 }
336 let container = self.get(ObjRef {
337 num: stream_num,
338 gen: 0,
339 })?;
340 let stream = container.as_stream().ok_or_else(|| Error::TypeMismatch {
341 expected: "stream",
342 found: type_name(&container),
343 })?;
344 let n = self
345 .resolve(stream.dict.get("N").unwrap_or(&Object::Null))?
346 .as_int()
347 .and_then(|v| usize::try_from(v).ok())
348 .ok_or(Error::MissingKey("N"))?;
349 let first = self
350 .resolve(stream.dict.get("First").unwrap_or(&Object::Null))?
351 .as_int()
352 .and_then(|v| usize::try_from(v).ok())
353 .ok_or(Error::MissingKey("First"))?;
354 let decoded = self.stream_data(stream)?;
355 let stm = Rc::new(objstm::ObjStm::parse(decoded, n, first)?);
356 self.objstms
357 .borrow_mut()
358 .insert(stream_num, Rc::clone(&stm));
359 Ok(stm)
360 }
361
362 fn load_from_object_stream(&self, stream_num: u32, index: u32) -> Result<Object> {
364 self.objstm_handle(stream_num)?.object(index)
365 }
366
367 pub fn resolve(&self, o: &Object) -> Result<Object> {
378 crate::source::resolve_sync_with(self, o)
379 }
380
381 pub fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
384 filters::decode_stream(s, self)
385 }
386
387 fn pages(&self) -> &[PageRec] {
389 self.pages.get_or_init(|| Arc::new(self.flatten_pages()))
390 }
391
392 pub fn seed(&self) -> DocumentSeed {
396 self.pages();
397 DocumentSeed {
398 data: Arc::clone(&self.data),
399 version: self.version,
400 xref: Arc::clone(&self.xref),
401 decryptor: self.decryptor.clone(),
402 pages: Arc::clone(self.pages.get().expect("pages() was just forced")),
403 }
404 }
405
406 pub fn from_seed(seed: DocumentSeed) -> Document {
420 Document {
421 data: seed.data,
422 version: seed.version,
423 xref: seed.xref,
424 cache: RefCell::new(FastMap::default()),
425 loading: RefCell::new(FastSet::default()),
426 objstms: RefCell::new(FastMap::default()),
427 decryptor: seed.decryptor,
428 pages: OnceCell::from(seed.pages),
429 }
430 }
431
432 pub fn fork(&self) -> Document {
435 Document::from_seed(self.seed())
436 }
437
438 pub fn page_count(&self) -> usize {
447 if let Some(pages) = self.pages.get() {
448 return pages.len();
449 }
450 if let Some(count) = self.declared_page_count() {
451 return count;
452 }
453 self.pages().len()
454 }
455
456 fn declared_page_count(&self) -> Option<usize> {
464 let root = self.xref.trailer.get("Root")?;
465 let catalog = self.resolve(root).ok()?;
466 let pages = self.resolve(catalog.as_dict()?.get("Pages")?).ok()?;
467 let count = usize::try_from(self.int_value(pages.as_dict()?, "Count")?).ok()?;
468 (count <= self.data.len()).then_some(count)
471 }
472
473 pub fn page(&self, index: usize) -> Result<Page> {
475 let pages = self.pages();
476 let rec = pages
477 .get(index)
478 .ok_or(Error::PageNotFound(index, pages.len()))?;
479 Ok(Page {
480 index,
481 media_box: rec.media_box,
482 crop_box: rec.crop_box,
483 bleed_box: rec.bleed_box,
484 trim_box: rec.trim_box,
485 art_box: rec.art_box,
486 rotate: rec.rotate,
487 resources: rec.resources.clone(),
488 dict: rec.dict.clone(),
489 obj_ref: rec.obj_ref,
490 })
491 }
492
493 pub fn metadata(&self) -> Metadata {
498 let mut meta = Metadata::default();
499 let Some(info) = self.xref.trailer.get("Info") else {
500 return meta;
501 };
502 let Ok(info) = self.resolve(info) else {
503 return meta;
504 };
505 let Some(dict) = info.as_dict() else {
506 return meta;
507 };
508 meta.title = self.meta_string(dict, "Title");
509 meta.author = self.meta_string(dict, "Author");
510 meta.subject = self.meta_string(dict, "Subject");
511 meta.keywords = self.meta_string(dict, "Keywords");
512 meta.creator = self.meta_string(dict, "Creator");
513 meta.producer = self.meta_string(dict, "Producer");
514 meta.creation_date = self.meta_string(dict, "CreationDate");
515 meta.mod_date = self.meta_string(dict, "ModDate");
516 meta
517 }
518
519 pub fn oc_state(&self) -> Option<crate::oc::OcState> {
526 block_on(crate::oc::OcState::load_with(
527 &Immediate(self),
528 &self.xref.trailer,
529 ))
530 }
531
532 pub fn structure_tree(&self) -> Option<crate::structure::StructureTree> {
536 block_on(crate::structure::StructureTree::load_with(
537 &Immediate(self),
538 &self.xref.trailer,
539 ))
540 }
541
542 pub fn named(&self, tree: crate::names::NameTree, key: &[u8]) -> Option<Object> {
547 block_on(crate::names::named_with(
548 &Immediate(self),
549 &self.xref.trailer,
550 tree,
551 key,
552 ))
553 }
554
555 pub fn names(&self, tree: crate::names::NameTree) -> Vec<(Vec<u8>, Object)> {
558 block_on(crate::names::names_with(
559 &Immediate(self),
560 &self.xref.trailer,
561 tree,
562 ))
563 }
564
565 pub fn named_destination(&self, name: &[u8]) -> Option<crate::destination::Destination> {
568 block_on(crate::destination::named_destination_with(
569 &Immediate(self),
570 &self.xref.trailer,
571 name,
572 ))
573 }
574
575 pub fn named_destinations(&self) -> Vec<(Vec<u8>, crate::destination::Destination)> {
577 block_on(crate::destination::named_destinations_with(
578 &Immediate(self),
579 &self.xref.trailer,
580 ))
581 }
582
583 pub fn destination(&self, value: &Object) -> Option<crate::destination::Destination> {
587 block_on(crate::destination::destination_value_with(
588 &Immediate(self),
589 &self.xref.trailer,
590 value,
591 ))
592 }
593
594 pub fn outline(&self) -> Vec<crate::outline::OutlineItem> {
597 block_on(crate::outline::outline_with(
598 &Immediate(self),
599 &self.xref.trailer,
600 ))
601 }
602
603 pub fn page_labels(&self) -> Option<Vec<crate::page_label::PageLabel>> {
606 block_on(crate::page_label::page_labels_with(
607 &Immediate(self),
608 &self.xref.trailer,
609 ))
610 }
611
612 pub fn language(&self) -> Option<String> {
615 block_on(crate::language::language_with(
616 &Immediate(self),
617 &self.xref.trailer,
618 ))
619 }
620
621 pub fn language_tag(&self) -> Option<crate::language::LanguageTag> {
625 crate::language::LanguageTag::parse(&self.language()?)
626 }
627
628 pub fn extensions(&self) -> Vec<crate::extension::DeveloperExtension> {
631 block_on(crate::extension::extensions_with(
632 &Immediate(self),
633 &self.xref.trailer,
634 ))
635 }
636
637 pub fn viewer_preferences(&self) -> Option<crate::viewer_preferences::ViewerPreferences> {
640 block_on(crate::viewer_preferences::viewer_preferences_with(
641 &Immediate(self),
642 &self.xref.trailer,
643 ))
644 }
645
646 pub fn page_label(&self, index: usize) -> Option<String> {
649 if index >= self.page_count() {
650 return None;
651 }
652 let ranges = self.page_labels()?;
653 Some(crate::page_label::page_label(&ranges, index))
654 }
655
656 pub fn embedded_files(&self) -> Vec<crate::embedded_file::EmbeddedFile> {
659 block_on(crate::embedded_file::embedded_files_with(
660 &Immediate(self),
661 &self.xref.trailer,
662 ))
663 }
664
665 pub fn embedded_file_data(&self, file: &crate::embedded_file::EmbeddedFile) -> Result<Vec<u8>> {
672 block_on(crate::embedded_file::embedded_file_data_with(
673 &Immediate(self),
674 file,
675 ))
676 }
677
678 fn meta_string(&self, dict: &Dict, key: &str) -> Option<String> {
682 let value = self.resolve(dict.get(key)?).ok()?;
683 Some(decode_text_string(value.as_str_bytes()?))
684 }
685
686 fn flatten_pages(&self) -> Vec<PageRec> {
693 let mut pages = Vec::new();
694 let Some(root) = self.xref.trailer.get("Root") else {
695 return pages;
696 };
697 let Ok(catalog) = self.resolve(root) else {
698 return pages;
699 };
700 let Some(tree_root) = catalog.as_dict().and_then(|d| d.get("Pages")) else {
701 return pages;
702 };
703 let mut visited: FastSet<ObjRef> = FastSet::default();
704 let mut stack: Vec<(Object, Inherited, usize)> =
705 vec![(tree_root.clone(), Inherited::default(), 0)];
706 while let Some((node, mut inherited, depth)) = stack.pop() {
707 if depth > MAX_TREE_DEPTH {
708 continue;
709 }
710 let node_ref = if let Object::Ref(r) = node {
711 Some(r)
712 } else {
713 None
714 };
715 if let Some(r) = node_ref {
716 if !visited.insert(r) {
717 continue; }
719 }
720 let Ok(resolved) = self.resolve(&node) else {
721 continue;
722 };
723 let Some(dict) = resolved.as_dict() else {
724 continue;
725 };
726 if let Some(res) = self.dict_value(dict, "Resources") {
727 inherited.resources = Some(res);
728 }
729 if let Some(mb) = self.rect_value(dict, "MediaBox") {
730 inherited.media_box = Some(mb);
731 }
732 if let Some(cb) = self.rect_value(dict, "CropBox") {
733 inherited.crop_box = Some(cb);
734 }
735 if let Some(rot) = self.int_value(dict, "Rotate") {
736 inherited.rotate = Some(rot);
737 }
738 let is_page = dict.get_name("Type").is_some_and(|n| n.0 == "Page");
739 let kids = if is_page {
740 None
741 } else {
742 self.array_value(dict, "Kids")
743 };
744 match kids {
745 Some(kids) => {
746 for kid in kids.iter().rev() {
748 stack.push((kid.clone(), inherited.clone(), depth + 1));
749 }
750 }
751 None => {
752 let bleed = self.rect_value(dict, "BleedBox");
756 let trim = self.rect_value(dict, "TrimBox");
757 let art = self.rect_value(dict, "ArtBox");
758 pages.push(make_page_rec(
759 node_ref,
760 dict.clone(),
761 &inherited,
762 bleed,
763 trim,
764 art,
765 ));
766 }
767 }
768 }
769 pages
770 }
771
772 fn dict_value(&self, dict: &Dict, key: &str) -> Option<Dict> {
774 self.resolve(dict.get(key)?).ok()?.as_dict().cloned()
775 }
776
777 fn array_value(&self, dict: &Dict, key: &str) -> Option<Vec<Object>> {
779 match self.resolve(dict.get(key)?).ok()? {
780 Object::Array(items) => Some(items),
781 _ => None,
782 }
783 }
784
785 fn int_value(&self, dict: &Dict, key: &str) -> Option<i32> {
787 let v = self.resolve(dict.get(key)?).ok()?.as_f64()?;
788 if v.is_finite() {
789 Some(v as i32)
790 } else {
791 None
792 }
793 }
794
795 fn rect_value(&self, dict: &Dict, key: &str) -> Option<Rect> {
800 let items = self.array_value(dict, key)?;
801 if items.len() != 4 {
802 return None;
803 }
804 let mut coords = [0.0f32; 4];
805 for (slot, item) in coords.iter_mut().zip(&items) {
806 let n = self.resolve(item).ok()?.as_f64()?;
807 if !n.is_finite() {
808 return None;
809 }
810 *slot = n as f32;
811 }
812 Some(Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
813 }
814}
815
816pub fn map_pages<T, F>(doc: &Document, work: F) -> Vec<Result<T>>
835where
836 T: Send + Sync,
837 F: Fn(&Document, &Page) -> Result<T> + Send + Sync,
838{
839 let count = doc.pages().len();
840 let workers = std::thread::available_parallelism()
841 .map(std::num::NonZeroUsize::get)
842 .unwrap_or(1)
843 .min(count);
844 if workers <= 1 {
845 return (0..count)
846 .map(|i| doc.page(i).and_then(|page| work(doc, &page)))
847 .collect();
848 }
849
850 let seed = doc.seed();
851 let next = std::sync::atomic::AtomicUsize::new(0);
852 let slots: Vec<std::sync::OnceLock<Result<T>>> =
853 (0..count).map(|_| std::sync::OnceLock::new()).collect();
854 std::thread::scope(|scope| {
855 for _ in 0..workers {
856 let seed = seed.clone();
859 let next = &next;
860 let slots = &slots;
861 let work = &work;
862 scope.spawn(move || {
863 let worker = Document::from_seed(seed);
864 loop {
865 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
866 if i >= count {
867 break;
868 }
869 let outcome = worker.page(i).and_then(|page| work(&worker, &page));
870 if slots[i].set(outcome).is_err() {
875 unreachable!("page index {i} was dispatched twice");
876 }
877 }
878 });
879 }
880 });
881 slots
882 .into_iter()
883 .map(|slot| {
884 slot.into_inner()
885 .expect("every page index below the count was dispatched")
886 })
887 .collect()
888}
889
890fn make_page_rec(
897 obj_ref: Option<ObjRef>,
898 dict: Dict,
899 inherited: &Inherited,
900 bleed_box: Option<Rect>,
901 trim_box: Option<Rect>,
902 art_box: Option<Rect>,
903) -> PageRec {
904 let page = Page::from_tree_attrs(
905 0,
906 inherited.resources.clone(),
907 inherited.media_box,
908 inherited.crop_box,
909 bleed_box,
910 trim_box,
911 art_box,
912 inherited.rotate,
913 dict,
914 obj_ref,
915 );
916 PageRec {
917 obj_ref: page.obj_ref,
918 media_box: page.media_box,
919 crop_box: page.crop_box,
920 bleed_box: page.bleed_box,
921 trim_box: page.trim_box,
922 art_box: page.art_box,
923 rotate: page.rotate,
924 resources: page.resources,
925 dict: page.dict,
926 }
927}
928
929pub(crate) fn type_name(o: &Object) -> &'static str {
931 match o {
932 Object::Null => "null",
933 Object::Bool(_) => "boolean",
934 Object::Int(_) => "integer",
935 Object::Real(_) => "real",
936 Object::String(_) => "string",
937 Object::Name(_) => "name",
938 Object::Array(_) => "array",
939 Object::Dict(_) => "dictionary",
940 Object::Stream(_) => "stream",
941 Object::Ref(_) => "reference",
942 }
943}
944
945impl Resolve for Document {
946 fn resolve_ref(&self, r: ObjRef) -> Option<Object> {
947 self.get(r).ok()
948 }
949}
950
951impl crate::source::ObjectSource for Document {
959 fn get(&self, r: ObjRef) -> Result<Object> {
960 Document::get(self, r)
961 }
962
963 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
964 Document::stream_data(self, s)
965 }
966
967 fn resolve(&self, o: &Object) -> Result<Object> {
968 Document::resolve(self, o)
969 }
970}
971
972#[derive(Debug, Clone, Default, PartialEq, Eq)]
977pub struct Metadata {
978 pub title: Option<String>,
979 pub author: Option<String>,
980 pub subject: Option<String>,
981 pub keywords: Option<String>,
982 pub creator: Option<String>,
983 pub producer: Option<String>,
984 pub creation_date: Option<String>,
985 pub mod_date: Option<String>,
986}
987
988impl Metadata {
989 pub fn creation_date_parsed(&self) -> Option<Date> {
994 Date::parse_pdf(self.creation_date.as_deref()?)
995 }
996
997 pub fn mod_date_parsed(&self) -> Option<Date> {
1001 Date::parse_pdf(self.mod_date.as_deref()?)
1002 }
1003}
1004
1005#[derive(Clone)]
1020pub struct Page {
1021 pub index: usize,
1023 pub media_box: Rect,
1024 pub crop_box: Rect,
1025 pub bleed_box: Rect,
1026 pub trim_box: Rect,
1027 pub art_box: Rect,
1028 pub rotate: i32,
1029 pub resources: Dict,
1031 dict: Dict,
1032 obj_ref: Option<ObjRef>,
1033}
1034
1035impl Page {
1036 pub fn from_parts(
1056 index: usize,
1057 media_box: Rect,
1058 crop_box: Rect,
1059 rotate: i32,
1060 resources: Dict,
1061 dict: Dict,
1062 obj_ref: Option<ObjRef>,
1063 ) -> Page {
1064 Page {
1065 index,
1066 media_box,
1067 crop_box,
1068 bleed_box: crop_box,
1069 trim_box: crop_box,
1070 art_box: crop_box,
1071 rotate: normalize_rotation(rotate),
1072 resources,
1073 dict,
1074 obj_ref,
1075 }
1076 }
1077
1078 pub const US_LETTER: Rect = Rect::new(0.0, 0.0, 612.0, 792.0);
1083
1084 #[allow(clippy::too_many_arguments)]
1099 pub fn from_tree_attrs(
1100 index: usize,
1101 resources: Option<Dict>,
1102 media_box: Option<Rect>,
1103 crop_box: Option<Rect>,
1104 bleed_box: Option<Rect>,
1105 trim_box: Option<Rect>,
1106 art_box: Option<Rect>,
1107 rotate: Option<i32>,
1108 dict: Dict,
1109 obj_ref: Option<ObjRef>,
1110 ) -> Page {
1111 let media_box = media_box
1112 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
1113 .unwrap_or(Page::US_LETTER);
1114 let crop_box = crop_box
1115 .and_then(|c| c.intersect(media_box))
1116 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
1117 .unwrap_or(media_box);
1118 let clip = |declared: Option<Rect>| {
1119 declared
1120 .and_then(|b| b.intersect(media_box))
1121 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
1122 .unwrap_or(crop_box)
1123 };
1124 Page {
1125 index,
1126 media_box,
1127 crop_box,
1128 bleed_box: clip(bleed_box),
1129 trim_box: clip(trim_box),
1130 art_box: clip(art_box),
1131 rotate: normalize_rotation(rotate.unwrap_or(0)),
1132 resources: resources.unwrap_or_default(),
1133 dict,
1134 obj_ref,
1135 }
1136 }
1137
1138 pub fn object_ref(&self) -> Option<ObjRef> {
1142 self.obj_ref
1143 }
1144
1145 pub fn content(&self, doc: &Document) -> Result<Vec<u8>> {
1154 block_on(page_content_with(Immediate(doc), self))
1155 }
1156
1157 pub fn size(&self) -> (f32, f32) {
1166 let (w, h) = (self.crop_box.width(), self.crop_box.height());
1167 let turn = self.rotate.rem_euclid(360);
1168 if turn == 90 || turn == 270 {
1169 (h, w)
1170 } else {
1171 (w, h)
1172 }
1173 }
1174
1175 pub fn dict(&self) -> &Dict {
1177 &self.dict
1178 }
1179}
1180
1181pub async fn page_content_with<S: AsyncObjectSource>(src: S, page: &Page) -> Result<Vec<u8>> {
1214 let Some(contents) = page.dict.get("Contents") else {
1215 return Ok(Vec::new());
1216 };
1217 match src.resolve(contents).await? {
1218 Object::Stream(ref s) => content_stream_data_with(&src, s).await,
1219 Object::Array(items) => {
1220 let mut out = Vec::new();
1221 let mut first = true;
1222 for item in &items {
1223 let part = src.resolve(item).await?;
1224 let Some(stream) = part.as_stream() else {
1225 continue; };
1227 if !first {
1228 out.push(b'\n');
1229 }
1230 out.extend_from_slice(&content_stream_data_with(&src, stream).await?);
1231 first = false;
1232 }
1233 Ok(out)
1234 }
1235 _ => Ok(Vec::new()),
1236 }
1237}
1238
1239pub async fn decoded_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1272 if let Some(name) = filters::trailing_filter_checked_with(&src, &s.dict).await? {
1273 if filters::is_image_codec(&name.0) {
1274 return Err(Error::UnsupportedFilter(name.0));
1275 }
1276 }
1277 src.stream_data(s).await
1278}
1279
1280pub async fn content_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1290 decoded_stream_data_with(src, s).await
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295 use super::*;
1296 use crate::object::Name;
1297 use crate::parser::{NoResolve, Parser};
1298 use crate::xref::XrefEntry;
1299 use pdfboss_testkit::{multi_page_doc, objstm_doc, objstm_payload, simple_doc, PdfBuilder};
1300
1301 #[test]
1305 fn async_page_content_matches_the_sync_accessor() {
1306 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("load");
1307 let mut saw_content = false;
1308 for index in 0..3 {
1309 let page = doc.page(index).expect("page");
1310 let direct = page.content(&doc).expect("sync content");
1311 let awaited =
1312 block_on(page_content_with(Immediate(&doc), &page)).expect("async content");
1313 assert_eq!(direct, awaited, "page {index}");
1314 saw_content |= !direct.is_empty();
1315 }
1316 assert!(
1318 saw_content,
1319 "fixture must give at least one page real content"
1320 );
1321 }
1322
1323 #[test]
1331 fn image_codec_page_contents_are_refused_not_returned() {
1332 for codec in ["JPXDecode", "DCTDecode"] {
1333 let mut b = PdfBuilder::new();
1334 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1335 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1336 b.object(
1337 3,
1338 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1339 );
1340 b.stream(
1341 4,
1342 &format!("/Filter /{codec}"),
1343 b"1 0 0 rg 0 0 100 100 re f",
1344 );
1345 let doc = Document::load(b.build(1)).expect("load");
1346 let page = doc.page(0).expect("page");
1347 match page.content(&doc) {
1348 Err(Error::UnsupportedFilter(n)) => assert_eq!(n, codec),
1349 other => panic!("{codec} content must be refused, got {other:?}"),
1350 }
1351 }
1352 }
1353
1354 #[test]
1361 fn an_unreadable_filter_refuses_the_content_not_returns_it_raw() {
1362 let mut b = PdfBuilder::new();
1363 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1364 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1365 b.object(
1366 3,
1367 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1368 );
1369 b.stream(4, "/Filter 6 0 R", b"1 0 0 rg 0 0 100 100 re f");
1370 b.object(6, "7 0 R");
1371 b.object(7, "6 0 R");
1372 let doc = Document::load(b.build(1)).expect("load");
1373 let page = doc.page(0).expect("page");
1374 assert!(
1375 page.content(&doc).is_err(),
1376 "an unreadable /Filter must refuse, not pass stored bytes"
1377 );
1378 }
1379
1380 #[test]
1385 fn a_shared_helper_is_reachable_from_a_caller_that_owns_its_source() {
1386 let doc = Document::load(simple_doc("Hello")).expect("load");
1387 let page = doc.page(0).expect("page");
1388
1389 let owner = Immediate(&doc);
1390 let through_reference = block_on(page_content_with(&owner, &page)).expect("async content");
1391
1392 assert_eq!(through_reference, page.content(&doc).expect("sync content"));
1393 assert!(!through_reference.is_empty());
1394 }
1395
1396 #[test]
1400 fn the_seed_is_shareable_across_threads() {
1401 fn assert_send_sync<T: Send + Sync>() {}
1402 assert_send_sync::<DocumentSeed>();
1403 }
1404
1405 #[test]
1409 fn a_fork_reads_exactly_what_its_parent_reads() {
1410 let doc = Document::load(pdfboss_testkit::encrypted_rc4_doc("forked secret")).unwrap();
1411 let fork = doc.fork();
1412 assert_eq!(fork.page_count(), doc.page_count());
1413 let (a, b) = (doc.page(0).unwrap(), fork.page(0).unwrap());
1414 assert_eq!(a.media_box, b.media_box);
1415 assert_eq!(
1416 a.content(&doc).expect("parent content"),
1417 b.content(&fork).expect("fork content"),
1418 "decrypted content agrees"
1419 );
1420 }
1421
1422 #[test]
1426 fn map_pages_preserves_page_order() {
1427 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1428 let contents = map_pages(&doc, |doc, page| page.content(doc));
1429 assert_eq!(contents.len(), 3);
1430 let texts: Vec<String> = contents
1431 .into_iter()
1432 .map(|c| String::from_utf8_lossy(&c.unwrap()).into_owned())
1433 .collect();
1434 assert!(texts[0].contains("(one)"), "{}", texts[0]);
1435 assert!(texts[1].contains("(two)"), "{}", texts[1]);
1436 assert!(texts[2].contains("(three)"), "{}", texts[2]);
1437 }
1438
1439 fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
1442 let pos = memchr::memmem::find(data, from).expect("pattern present in fixture");
1443 let mut out = Vec::with_capacity(data.len() - from.len() + to.len());
1444 out.extend_from_slice(&data[..pos]);
1445 out.extend_from_slice(to);
1446 out.extend_from_slice(&data[pos + from.len()..]);
1447 out
1448 }
1449
1450 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1451 memchr::memmem::find(haystack, needle).is_some()
1452 }
1453
1454 const FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
1455
1456 #[test]
1458 fn loads_simple_doc() {
1459 let doc = Document::load(simple_doc("Greetings, cosmos!")).unwrap();
1460 assert_eq!(doc.version(), (1, 7));
1461 assert_eq!(doc.page_count(), 1);
1462 let page = doc.page(0).unwrap();
1463 assert_eq!(page.index, 0);
1464 assert_eq!(page.media_box, Rect::new(0.0, 0.0, 612.0, 792.0));
1465 assert_eq!(page.crop_box, page.media_box);
1466 assert_eq!(page.rotate, 0);
1467 assert_eq!(page.size(), (612.0, 792.0));
1468 assert!(page.resources.get("Font").is_some());
1469 let content = page.content(&doc).unwrap();
1470 assert!(contains(&content, b"Greetings, cosmos!"));
1471 }
1472
1473 #[test]
1475 fn multi_page_ordering() {
1476 let doc = Document::load(multi_page_doc(&["alpha", "beta", "gamma"])).unwrap();
1477 assert_eq!(doc.page_count(), 3);
1478 for (i, text) in ["alpha", "beta", "gamma"].iter().enumerate() {
1479 let content = doc.page(i).unwrap().content(&doc).unwrap();
1480 assert!(
1481 contains(&content, text.as_bytes()),
1482 "page {i} should show {text}"
1483 );
1484 }
1485 }
1486
1487 #[test]
1488 fn page_index_out_of_bounds() {
1489 let doc = Document::load(simple_doc("x")).unwrap();
1490 assert!(matches!(doc.page(5), Err(Error::PageNotFound(5, 1))));
1491 }
1492
1493 #[test]
1494 fn open_reads_from_disk() {
1495 let dir = std::env::temp_dir();
1496 let path = dir.join(format!("pdfboss-doc-test-{}.pdf", std::process::id()));
1497 std::fs::write(&path, simple_doc("from disk")).unwrap();
1498 let doc = Document::open(&path).unwrap();
1499 std::fs::remove_file(&path).ok();
1500 assert_eq!(doc.page_count(), 1);
1501 let content = doc.page(0).unwrap().content(&doc).unwrap();
1502 assert!(contains(&content, b"from disk"));
1503 assert!(matches!(
1504 Document::open(dir.join("pdfboss-doc-test-missing.pdf")),
1505 Err(Error::Io(_))
1506 ));
1507 }
1508
1509 #[test]
1511 fn encrypt_in_trailer_is_rejected() {
1512 let data = replace_once(
1513 &simple_doc("secret"),
1514 b"trailer\n<< /Size",
1515 b"trailer\n<< /Encrypt 9 0 R /Size",
1516 );
1517 assert!(matches!(Document::load(data), Err(Error::Encrypted)));
1518 }
1519
1520 #[test]
1522 fn metadata_utf16be_round_trip() {
1523 let mut b = PdfBuilder::new();
1524 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1525 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1526 b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
1528 let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1529 let doc = Document::load(data).unwrap();
1530 let meta = doc.metadata();
1531 assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
1532 assert_eq!(meta.author.as_deref(), Some("plain author"));
1533 assert_eq!(meta.subject, None);
1534 assert_eq!(meta.keywords, None);
1535 assert_eq!(meta.creation_date, None);
1536 }
1537
1538 #[test]
1541 fn metadata_dates_parse_per_the_date_clause() {
1542 let mut b = PdfBuilder::new();
1543 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1544 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1545 b.object(
1547 6,
1548 "<< /CreationDate (D:199812231952-08'00) /ModDate (D:191020717014604) >>",
1549 );
1550 let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1551 let doc = Document::load(data).unwrap();
1552 let meta = doc.metadata();
1553 assert_eq!(meta.creation_date.as_deref(), Some("D:199812231952-08'00"));
1554 assert_eq!(
1555 meta.creation_date_parsed(),
1556 Some(Date {
1557 year: 1998,
1558 month: 12,
1559 day: 23,
1560 hour: 19,
1561 minute: 52,
1562 second: 0,
1563 utc_offset_minutes: -480,
1564 })
1565 );
1566 assert_eq!(meta.mod_date.as_deref(), Some("D:191020717014604"));
1567 assert_eq!(meta.mod_date_parsed(), None, "month 20 is not a date");
1568 }
1569
1570 #[test]
1571 fn metadata_without_info_is_all_none() {
1572 let doc = Document::load(simple_doc("x")).unwrap();
1573 assert_eq!(doc.metadata(), Metadata::default());
1574 }
1575
1576 #[test]
1578 fn missing_object_resolves_to_null() {
1579 let doc = Document::load(simple_doc("x")).unwrap();
1580 let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
1581 assert_eq!(doc.resolve(&missing).unwrap(), Object::Null);
1582 assert!(matches!(
1583 doc.get(ObjRef { num: 99, gen: 0 }),
1584 Err(Error::ObjectNotFound(99, 0))
1585 ));
1586 }
1587
1588 #[test]
1589 fn self_reference_is_circular() {
1590 let mut b = PdfBuilder::new();
1591 b.object(1, "<< /Type /Catalog >>");
1592 b.object(6, "6 0 R");
1593 let doc = Document::load(b.build(1)).unwrap();
1594 let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
1595 assert!(matches!(
1596 doc.resolve(&loops),
1597 Err(Error::CircularReference(6))
1598 ));
1599 }
1600
1601 #[test]
1603 fn generation_mismatch_is_tolerated() {
1604 let doc = Document::load(simple_doc("x")).unwrap();
1605 let catalog = doc.get(ObjRef { num: 1, gen: 7 }).unwrap();
1606 let dict = catalog.as_dict().unwrap();
1607 assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Catalog"));
1608 }
1609
1610 #[test]
1612 fn objects_in_object_streams_are_fetched() {
1613 let mut b = PdfBuilder::new();
1614 let (dict, payload) =
1615 objstm_payload(&[(1, "<< /Type /Catalog /Pages 2 0 R >>"), (5, FONT)]);
1616 b.stream(6, &dict, &payload);
1617 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1618 b.object(
1619 3,
1620 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1621 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
1622 );
1623 b.stream(4, "", b"BT /F1 12 Tf (compressed hello) Tj ET");
1624 let doc = Document::load(b.build_xref_stream(1)).unwrap();
1625 assert_eq!(doc.page_count(), 1);
1626 let page = doc.page(0).unwrap();
1627 assert!(contains(&page.content(&doc).unwrap(), b"compressed hello"));
1628 let font = doc.get(ObjRef { num: 5, gen: 0 }).unwrap();
1629 assert_eq!(
1630 font.as_dict()
1631 .and_then(|d| d.get_name("BaseFont"))
1632 .map(|n| n.0.as_str()),
1633 Some("Helvetica")
1634 );
1635 }
1636
1637 #[test]
1639 fn contents_array_is_joined_with_newlines() {
1640 let mut b = PdfBuilder::new();
1641 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1642 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1643 b.object(
1644 3,
1645 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1646 /Contents [4 0 R null 5 0 R] >>",
1647 );
1648 b.stream(4, "", b"q");
1649 b.stream(5, "", b"Q");
1650 let doc = Document::load(b.build(1)).unwrap();
1651 let content = doc.page(0).unwrap().content(&doc).unwrap();
1652 assert_eq!(content, b"q\nQ", "streams joined by \\n, null skipped");
1653 }
1654
1655 #[test]
1657 fn inheritance_from_pages_node_and_rotate_swap() {
1658 let mut b = PdfBuilder::new();
1659 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1660 b.object(
1661 2,
1662 "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 \
1663 /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 600] >>",
1664 );
1665 b.object(3, "<< /Type /Page /Parent 2 0 R >>");
1666 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate 270 >>");
1667 b.object(5, FONT);
1668 let doc = Document::load(b.build(1)).unwrap();
1669 assert_eq!(doc.page_count(), 2);
1670
1671 let first = doc.page(0).unwrap();
1672 assert_eq!(first.media_box, Rect::new(0.0, 0.0, 400.0, 600.0));
1673 assert_eq!(first.crop_box, first.media_box);
1674 assert!(first.resources.get("Font").is_some(), "inherited resources");
1675 assert_eq!(first.rotate, 0);
1676 assert!(
1677 first.content(&doc).unwrap().is_empty(),
1678 "no /Contents means empty content"
1679 );
1680 assert_eq!(first.size(), (400.0, 600.0));
1681
1682 let second = doc.page(1).unwrap();
1683 assert_eq!(second.rotate, 270);
1684 assert_eq!(second.size(), (600.0, 400.0), "rotate 270 swaps w/h");
1685 }
1686
1687 #[test]
1689 fn crop_box_intersected_and_rotate_normalized() {
1690 let mut b = PdfBuilder::new();
1691 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1692 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>");
1693 b.object(
1694 3,
1695 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1696 /CropBox [100 100 400 400] /Rotate 450 >>",
1697 );
1698 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate -90 >>");
1699 b.object(
1700 5,
1701 "<< /Type /Page /Parent 2 0 R /Rotate 45 /MediaBox [0 0 0 0] >>",
1702 );
1703 let doc = Document::load(b.build(1)).unwrap();
1704
1705 let clipped = doc.page(0).unwrap();
1706 assert_eq!(clipped.crop_box, Rect::new(100.0, 100.0, 200.0, 200.0));
1707 assert_eq!(clipped.rotate, 90, "450 normalizes to 90");
1708 assert_eq!(clipped.size(), (100.0, 100.0));
1709
1710 assert_eq!(doc.page(1).unwrap().rotate, 270, "-90 normalizes to 270");
1711 let odd = doc.page(2).unwrap();
1712 assert_eq!(odd.rotate, 0, "non-multiple of 90 falls back to 0");
1713 assert_eq!(
1714 odd.media_box,
1715 Page::US_LETTER,
1716 "degenerate media box defaults"
1717 );
1718 }
1719
1720 #[test]
1722 fn page_boxes_declared_and_defaulted() {
1723 let mut b = PdfBuilder::new();
1724 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1725 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>");
1726 b.object(
1727 3,
1728 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] \
1729 /CropBox [50 50 550 750] /BleedBox [40 40 560 760] \
1730 /TrimBox [5 0 R 6 0 R 7 0 R 8 0 R] >>",
1731 );
1732 b.object(4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] >>");
1733 b.object(5, "60");
1734 b.object(6, "70");
1735 b.object(7, "540");
1736 b.object(8, "730");
1737 let doc = Document::load(b.build(1)).unwrap();
1738
1739 let page = doc.page(0).unwrap();
1740 assert_eq!(page.bleed_box, Rect::new(40.0, 40.0, 560.0, 760.0));
1741 assert_eq!(
1742 page.trim_box,
1743 Rect::new(60.0, 70.0, 540.0, 730.0),
1744 "indirect array elements resolve"
1745 );
1746 assert_eq!(
1747 page.art_box, page.crop_box,
1748 "undeclared box is the crop box"
1749 );
1750
1751 let bare = doc.page(1).unwrap();
1752 assert_eq!(bare.bleed_box, bare.crop_box);
1753 assert_eq!(bare.trim_box, bare.crop_box);
1754 assert_eq!(bare.art_box, bare.crop_box);
1755 }
1756
1757 #[test]
1759 fn page_boxes_clip_to_media_and_fall_back() {
1760 let mut b = PdfBuilder::new();
1761 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1762 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1763 b.object(
1764 3,
1765 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1766 /CropBox [10 10 190 190] /TrimBox [100 100 400 400] \
1767 /BleedBox [0 0 0 0] /ArtBox [300 300 400 400] >>",
1768 );
1769 let doc = Document::load(b.build(1)).unwrap();
1770
1771 let page = doc.page(0).unwrap();
1772 assert_eq!(
1773 page.trim_box,
1774 Rect::new(100.0, 100.0, 200.0, 200.0),
1775 "a trim box past the media box clips to it"
1776 );
1777 assert_eq!(
1778 page.bleed_box, page.crop_box,
1779 "a degenerate bleed box falls back to the crop box"
1780 );
1781 assert_eq!(
1782 page.art_box, page.crop_box,
1783 "an art box disjoint from the media box falls back to the crop box"
1784 );
1785 }
1786
1787 #[test]
1792 fn page_boxes_are_not_inherited() {
1793 let mut b = PdfBuilder::new();
1794 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1795 b.object(
1796 2,
1797 "<< /Type /Pages /Kids [3 0 R] /Count 1 \
1798 /TrimBox [10 10 90 90] /BleedBox [5 5 95 95] /ArtBox [20 20 80 80] >>",
1799 );
1800 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1801 let doc = Document::load(b.build(1)).unwrap();
1802
1803 let page = doc.page(0).unwrap();
1804 assert_eq!(page.trim_box, page.crop_box);
1805 assert_eq!(page.bleed_box, page.crop_box);
1806 assert_eq!(page.art_box, page.crop_box);
1807 }
1808
1809 #[test]
1811 fn kids_cycle_truncates_without_hanging() {
1812 let mut b = PdfBuilder::new();
1813 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1814 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1816 b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
1817 b.object(
1818 4,
1819 "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] /Contents 5 0 R >>",
1820 );
1821 b.stream(5, "", b"0 0 50 50 re f");
1822 let doc = Document::load(b.build(1)).unwrap();
1823 assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
1824 assert!(contains(
1825 &doc.page(0).unwrap().content(&doc).unwrap(),
1826 b"re f"
1827 ));
1828 }
1829
1830 #[test]
1832 fn tree_depth_is_capped() {
1833 let mut b = PdfBuilder::new();
1834 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1835 let last = 302u32;
1837 for num in 2..last {
1838 b.object(
1839 num,
1840 &format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", num + 1),
1841 );
1842 }
1843 b.object(last, "<< /Type /Page >>");
1844 let doc = Document::load(b.build(1)).unwrap();
1845 assert_eq!(doc.page_count(), 1, "declared /Count is reported cheaply");
1850 assert!(
1851 matches!(doc.page(0), Err(Error::PageNotFound(0, 0))),
1852 "leaf beyond the depth cap cannot be materialized"
1853 );
1854 }
1855
1856 #[test]
1858 fn page_count_reports_declared_count_cheaply() {
1859 let mut b = PdfBuilder::new();
1863 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1864 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
1865 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1866 let doc = Document::load(b.build(1)).unwrap();
1867 assert_eq!(doc.page_count(), 5, "declared /Count reported verbatim");
1868 assert!(doc.page(0).is_ok(), "the one real page materializes");
1869 assert!(
1870 matches!(doc.page(1), Err(Error::PageNotFound(1, 1))),
1871 "access past the real pages fails with the true length"
1872 );
1873 }
1874
1875 #[test]
1877 fn page_count_falls_back_to_walk_when_count_absent() {
1878 let mut b = PdfBuilder::new();
1879 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1880 b.object(2, "<< /Type /Pages /Kids [3 0 R] >>"); b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1882 let doc = Document::load(b.build(1)).unwrap();
1883 assert_eq!(
1884 doc.page_count(),
1885 1,
1886 "missing /Count is recovered by walking"
1887 );
1888 }
1889
1890 #[test]
1892 fn page_count_ignores_corrupt_oversized_count() {
1893 let mut b = PdfBuilder::new();
1896 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1897 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 999999999 >>");
1898 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1899 let doc = Document::load(b.build(1)).unwrap();
1900 assert_eq!(doc.page_count(), 1, "implausible /Count is rejected");
1901 }
1902
1903 #[test]
1905 fn version_scan_and_default() {
1906 let mut b = PdfBuilder::new().version(2, 0);
1907 b.object(1, "<< /Type /Catalog >>");
1908 assert_eq!(Document::load(b.build(1)).unwrap().version(), (2, 0));
1909 let data = replace_once(&simple_doc("v"), b"%PDF-", b"%QQQ-");
1911 assert_eq!(Document::load(data).unwrap().version(), (1, 4));
1912 }
1913
1914 #[test]
1916 fn deeply_nested_root_object_does_not_overflow_the_stack() {
1917 let mut data = b"%PDF-1.7\n1 0 obj\n".to_vec();
1922 data.extend(std::iter::repeat_n(b'[', 50_000));
1923 data.extend(std::iter::repeat_n(b']', 50_000));
1924 data.extend_from_slice(b"\nendobj\ntrailer\n<</Root 1 0 R>>\n%%EOF\n");
1925 let outcome = std::thread::Builder::new()
1926 .stack_size(1024 * 1024)
1927 .spawn(move || Document::load(data).map(|doc| doc.page_count()))
1928 .expect("spawn test thread")
1929 .join()
1930 .expect("Document::load must not overflow the stack");
1931 assert!(matches!(outcome, Ok(0) | Err(_)));
1934 }
1935
1936 #[test]
1937 fn bytes_and_xref_accessors() {
1938 let data = simple_doc("accessors");
1939 let doc = Document::load(data.clone()).unwrap();
1940 assert_eq!(doc.bytes(), &data[..]);
1941 assert!(!doc.xref().is_empty());
1942 assert!(doc.xref().trailer.get("Root").is_some());
1943 }
1944
1945 #[test]
1946 fn object_at_spanned_reparses_identically() {
1947 let data = simple_doc("spanned");
1948 let doc = Document::load(data).unwrap();
1949 for (num, entry) in doc.xref().iter() {
1950 let XrefEntry::InFile { offset, gen } = entry else {
1951 continue;
1952 };
1953 let (r, object, span) = doc.object_at_spanned(offset as usize).unwrap();
1954 assert_eq!(r.num, num);
1955 assert_eq!(r.gen, gen);
1956 assert_eq!(span.start, offset);
1957 assert!(span.end as usize <= doc.bytes().len());
1958 let slice = &doc.bytes()[span.start as usize..span.end as usize];
1960 let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
1961 assert_eq!(r2, r);
1962 assert_eq!(object2, object);
1963 }
1964 }
1965
1966 #[test]
1967 fn page_object_ref_points_at_a_page_dict() {
1968 let doc = Document::load(multi_page_doc(&["one", "two"])).unwrap();
1969 for index in 0..doc.page_count() {
1970 let page = doc.page(index).unwrap();
1971 let r = page.object_ref().expect("builder pages are indirect");
1972 let resolved = doc.get(r).unwrap();
1973 assert_eq!(
1974 resolved
1975 .as_dict()
1976 .unwrap()
1977 .get_name("Type")
1978 .map(|n| n.0.as_str()),
1979 Some("Page")
1980 );
1981 }
1982 }
1983
1984 const RC4_FIXTURE_KEY_LEN: usize = 16; const RC4_FIXTURE_P: i32 = -44;
1994 const RC4_FIXTURE_ID0: &[u8] = b"0123456789abcdef";
1995 const RC4_FIXTURE_PAD: [u8; 32] = [
1996 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01,
1997 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53,
1998 0x69, 0x7A,
1999 ];
2000
2001 #[rustfmt::skip]
2002 const RC4_FIXTURE_MD5_S: [u32; 64] = [
2003 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
2004 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
2005 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
2006 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
2007 ];
2008 #[rustfmt::skip]
2009 const RC4_FIXTURE_MD5_K: [u32; 64] = [
2010 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
2011 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
2012 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
2013 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
2014 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
2015 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
2016 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
2017 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
2018 ];
2019
2020 fn rc4_fixture_md5(input: &[u8]) -> [u8; 16] {
2021 let (mut a0, mut b0, mut c0, mut d0) = (
2022 0x6745_2301u32,
2023 0xefcd_ab89u32,
2024 0x98ba_dcfeu32,
2025 0x1032_5476u32,
2026 );
2027 let mut msg = input.to_vec();
2028 let bitlen = (input.len() as u64).wrapping_mul(8);
2029 msg.push(0x80);
2030 while msg.len() % 64 != 56 {
2031 msg.push(0);
2032 }
2033 msg.extend_from_slice(&bitlen.to_le_bytes());
2034 for chunk in msg.as_chunks::<64>().0 {
2035 let mut m = [0u32; 16];
2036 for (word, bytes) in m.iter_mut().zip(chunk.as_chunks::<4>().0) {
2037 *word = u32::from_le_bytes(*bytes);
2038 }
2039 let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
2040 for i in 0..64 {
2041 let (f, g) = match i {
2042 0..=15 => ((b & c) | (!b & d), i),
2043 16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
2044 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
2045 _ => (c ^ (b | !d), (7 * i) % 16),
2046 };
2047 let f = f
2048 .wrapping_add(a)
2049 .wrapping_add(RC4_FIXTURE_MD5_K[i])
2050 .wrapping_add(m[g]);
2051 a = d;
2052 d = c;
2053 c = b;
2054 b = b.wrapping_add(f.rotate_left(RC4_FIXTURE_MD5_S[i]));
2055 }
2056 a0 = a0.wrapping_add(a);
2057 b0 = b0.wrapping_add(b);
2058 c0 = c0.wrapping_add(c);
2059 d0 = d0.wrapping_add(d);
2060 }
2061 let mut out = [0u8; 16];
2062 out[0..4].copy_from_slice(&a0.to_le_bytes());
2063 out[4..8].copy_from_slice(&b0.to_le_bytes());
2064 out[8..12].copy_from_slice(&c0.to_le_bytes());
2065 out[12..16].copy_from_slice(&d0.to_le_bytes());
2066 out
2067 }
2068
2069 fn rc4_fixture_rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
2070 let mut s: [u8; 256] = core::array::from_fn(|i| i as u8);
2071 let mut j = 0u8;
2072 for i in 0..256 {
2073 j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
2074 s.swap(i, j as usize);
2075 }
2076 let mut out = Vec::with_capacity(data.len());
2077 let (mut i, mut j) = (0u8, 0u8);
2078 for &byte in data {
2079 i = i.wrapping_add(1);
2080 j = j.wrapping_add(s[i as usize]);
2081 s.swap(i as usize, j as usize);
2082 let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
2083 out.push(byte ^ k);
2084 }
2085 out
2086 }
2087
2088 fn rc4_fixture_owner_entry() -> Vec<u8> {
2090 let mut d = rc4_fixture_md5(&RC4_FIXTURE_PAD);
2091 for _ in 0..50 {
2092 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
2093 }
2094 let rc4key = d[..RC4_FIXTURE_KEY_LEN].to_vec();
2095 let mut o = rc4_fixture_rc4(&rc4key, &RC4_FIXTURE_PAD);
2096 for i in 1u8..=19 {
2097 let k: Vec<u8> = rc4key.iter().map(|b| b ^ i).collect();
2098 o = rc4_fixture_rc4(&k, &o);
2099 }
2100 o
2101 }
2102
2103 fn rc4_fixture_file_key(o: &[u8]) -> Vec<u8> {
2105 let mut input = Vec::new();
2106 input.extend_from_slice(&RC4_FIXTURE_PAD);
2107 input.extend_from_slice(o);
2108 input.extend_from_slice(&(RC4_FIXTURE_P as u32).to_le_bytes());
2109 input.extend_from_slice(RC4_FIXTURE_ID0);
2110 let mut d = rc4_fixture_md5(&input);
2111 for _ in 0..50 {
2112 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
2113 }
2114 d[..RC4_FIXTURE_KEY_LEN].to_vec()
2115 }
2116
2117 fn rc4_fixture_user_entry(key: &[u8]) -> Vec<u8> {
2119 let mut input = Vec::new();
2120 input.extend_from_slice(&RC4_FIXTURE_PAD);
2121 input.extend_from_slice(RC4_FIXTURE_ID0);
2122 let mut x = rc4_fixture_md5(&input).to_vec();
2123 x = rc4_fixture_rc4(key, &x);
2124 for i in 1u8..=19 {
2125 let k: Vec<u8> = key.iter().map(|b| b ^ i).collect();
2126 x = rc4_fixture_rc4(&k, &x);
2127 }
2128 x.resize(32, 0); x
2130 }
2131
2132 fn rc4_fixture_obj_key(key: &[u8], num: u32, gen: u16) -> Vec<u8> {
2133 let mut input = key.to_vec();
2134 input.extend_from_slice(&num.to_le_bytes()[..3]);
2135 input.extend_from_slice(&gen.to_le_bytes()[..2]);
2136 rc4_fixture_md5(&input)[..(key.len() + 5).min(16)].to_vec()
2137 }
2138
2139 fn rc4_fixture_hexstr(b: &[u8]) -> String {
2140 let mut s = String::from("<");
2141 for x in b {
2142 s.push_str(&format!("{x:02x}"));
2143 }
2144 s.push('>');
2145 s
2146 }
2147
2148 fn rc4_encrypted_fixture() -> Vec<u8> {
2152 let o = rc4_fixture_owner_entry();
2153 let key = rc4_fixture_file_key(&o);
2154 let u = rc4_fixture_user_entry(&key);
2155 let msg = rc4_fixture_rc4(&rc4_fixture_obj_key(&key, 3, 0), b"Top secret message");
2156
2157 let mut b = PdfBuilder::new().version(1, 4);
2158 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2159 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
2160 b.object(3, &format!("<< /Msg {} >>", rc4_fixture_hexstr(&msg)));
2161 b.object(
2162 9,
2163 &format!(
2164 "<< /Filter /Standard /V 2 /R 3 /Length 128 /P {} /O {} /U {} >>",
2165 RC4_FIXTURE_P,
2166 rc4_fixture_hexstr(&o),
2167 rc4_fixture_hexstr(&u)
2168 ),
2169 );
2170 let trailer = format!(
2171 "/Encrypt 9 0 R /ID [{}{}]",
2172 rc4_fixture_hexstr(RC4_FIXTURE_ID0),
2173 rc4_fixture_hexstr(RC4_FIXTURE_ID0)
2174 );
2175 b.trailer_extra(&trailer).build(1)
2176 }
2177
2178 #[test]
2180 fn encrypted_generation_mismatch_still_decrypts() {
2181 let doc = Document::load(rc4_encrypted_fixture()).expect("empty password opens the file");
2191 let obj3 = doc.get(ObjRef { num: 3, gen: 7 }).unwrap();
2192 let msg = obj3
2193 .as_dict()
2194 .unwrap()
2195 .get("Msg")
2196 .unwrap()
2197 .as_str_bytes()
2198 .unwrap();
2199 assert_eq!(
2200 msg, b"Top secret message",
2201 "decrypted using the file's real gen (0), not the mismatched request (7)"
2202 );
2203 }
2204
2205 #[test]
2207 fn objstm_doc_fixture_loads_and_resolves_members() {
2208 let data = objstm_doc(&[(7, "<< /Marker (inside) >>")]);
2209 let doc = Document::load(data).unwrap();
2210 assert_eq!(doc.page_count(), 1);
2211 let member = doc.get(ObjRef { num: 7, gen: 0 }).unwrap();
2212 let text = member.as_dict().unwrap().get("Marker").unwrap();
2213 assert_eq!(text.as_str_bytes(), Some(&b"inside"[..]));
2214 assert!(matches!(
2216 doc.xref().get(7),
2217 Some(XrefEntry::InStream { stream_num: 4, .. })
2218 ));
2219 }
2220
2221 #[test]
2224 fn page_from_parts_exposes_its_accessors() {
2225 let mut dict = Dict::default();
2226 dict.insert(Name("Type".into()), Object::Name(Name("Page".into())));
2227 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2228 let obj_ref = ObjRef { num: 7, gen: 0 };
2229
2230 let page = Page::from_parts(
2231 3,
2232 media,
2233 media,
2234 90,
2235 Dict::default(),
2236 dict.clone(),
2237 Some(obj_ref),
2238 );
2239
2240 assert_eq!(page.index, 3);
2241 assert_eq!(page.object_ref(), Some(obj_ref));
2242 assert_eq!(page.dict(), &dict);
2243 assert_eq!(page.size(), (400.0, 200.0));
2245 assert_eq!(page.bleed_box, page.crop_box);
2247 assert_eq!(page.trim_box, page.crop_box);
2248 assert_eq!(page.art_box, page.crop_box);
2249 }
2250
2251 #[test]
2255 fn page_from_parts_normalizes_rotation() {
2256 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2257 let build = |rotate: i32| {
2258 Page::from_parts(
2259 0,
2260 media,
2261 media,
2262 rotate,
2263 Dict::default(),
2264 Dict::default(),
2265 None,
2266 )
2267 };
2268
2269 for (given, canonical) in [(-90, 270), (450, 90), (540, 180), (720, 0), (-360, 0)] {
2270 let page = build(given);
2271 assert_eq!(
2272 page.rotate, canonical,
2273 "from_parts must normalize /Rotate {given} to {canonical}"
2274 );
2275 assert_eq!(
2276 page.size(),
2277 build(canonical).size(),
2278 "/Rotate {given} must report the same size as {canonical}"
2279 );
2280 }
2281 }
2282
2283 #[test]
2288 fn page_size_handles_unnormalized_rotation() {
2289 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2290 let mut page = Page::from_parts(0, media, media, 0, Dict::default(), Dict::default(), None);
2291
2292 for rotate in [90, 270, -90, -270, 450, 630] {
2293 page.rotate = rotate;
2294 assert_eq!(
2295 page.size(),
2296 (400.0, 200.0),
2297 "/Rotate {rotate} is a quarter turn and must swap the size"
2298 );
2299 }
2300 for rotate in [0, 180, -180, 360, 540, 720] {
2301 page.rotate = rotate;
2302 assert_eq!(
2303 page.size(),
2304 (200.0, 400.0),
2305 "/Rotate {rotate} is a half turn and must leave the size alone"
2306 );
2307 }
2308 }
2309}