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::{Dict, ObjRef, Object, Stream};
18use crate::objstm;
19use crate::parser::{Parser, Resolve};
20use crate::source::{block_on, AsyncObjectSource, Immediate};
21use crate::tree::Entries;
22use crate::xref::{load_xref, Xref, XrefEntry};
23
24const MAX_TREE_DEPTH: usize = 256;
26
27pub struct Document {
29 data: Arc<Vec<u8>>,
30 version: (u8, u8),
31 xref: Arc<Xref>,
32 cache: RefCell<FastMap<(u32, u16), Rc<Object>>>,
34 loading: RefCell<FastSet<u32>>,
37 objstms: RefCell<FastMap<u32, Rc<objstm::ObjStm>>>,
41 decryptor: Option<Decryptor>,
45 pages: OnceCell<Arc<Vec<PageRec>>>,
50}
51
52pub struct DocumentSeed {
60 data: Arc<Vec<u8>>,
61 version: (u8, u8),
62 xref: Arc<Xref>,
63 decryptor: Option<Decryptor>,
64 pages: Arc<Vec<PageRec>>,
65}
66
67impl Clone for DocumentSeed {
68 fn clone(&self) -> DocumentSeed {
69 DocumentSeed {
70 data: Arc::clone(&self.data),
71 version: self.version,
72 xref: Arc::clone(&self.xref),
73 decryptor: self.decryptor.clone(),
74 pages: Arc::clone(&self.pages),
75 }
76 }
77}
78
79struct PageRec {
83 obj_ref: Option<ObjRef>,
84 media_box: Rect,
85 crop_box: Rect,
86 bleed_box: Rect,
87 trim_box: Rect,
88 art_box: Rect,
89 rotate: i32,
90 resources: Dict,
91 dict: Dict,
92}
93
94#[derive(Clone, Default)]
96struct Inherited {
97 resources: Option<Dict>,
98 media_box: Option<Rect>,
99 crop_box: Option<Rect>,
100 rotate: Option<i32>,
101}
102
103fn parse_version(data: &[u8]) -> (u8, u8) {
108 try_parse_version(data).unwrap_or((1, 4))
109}
110
111fn try_parse_version(data: &[u8]) -> Option<(u8, u8)> {
113 let window = &data[..data.len().min(1024)];
114 let pos = memchr::memmem::find(window, b"%PDF-")?;
115 let rest = &window[pos + 5..];
116 let (major, used) = read_version_component(rest)?;
117 if rest.get(used) != Some(&b'.') {
118 return None;
119 }
120 let (minor, _) = read_version_component(&rest[used + 1..])?;
121 Some((major, minor))
122}
123
124fn read_version_component(bytes: &[u8]) -> Option<(u8, usize)> {
127 let end = bytes
128 .iter()
129 .position(|b| !b.is_ascii_digit())
130 .unwrap_or(bytes.len());
131 if end == 0 || end > 3 {
132 return None;
133 }
134 let value = std::str::from_utf8(&bytes[..end]).ok()?.parse().ok()?;
135 Some((value, end))
136}
137
138fn normalize_rotation(deg: i32) -> i32 {
141 let r = deg.rem_euclid(360);
142 if r % 90 == 0 {
143 r
144 } else {
145 0
146 }
147}
148
149impl Document {
150 pub fn load(data: Vec<u8>) -> Result<Document> {
160 Document::load_with_password(data, "")
161 }
162
163 pub fn load_with_password(data: Vec<u8>, password: &str) -> Result<Document> {
171 let version = parse_version(&data);
172 let xref = load_xref(&data)?;
173 let mut doc = Document {
174 data: Arc::new(data),
175 version,
176 xref: Arc::new(xref),
177 cache: RefCell::new(FastMap::default()),
178 loading: RefCell::new(FastSet::default()),
179 objstms: RefCell::new(FastMap::default()),
180 decryptor: None,
181 pages: OnceCell::new(),
182 };
183 if doc
184 .xref
185 .trailer
186 .get("Encrypt")
187 .is_some_and(|o| !o.is_null())
188 {
189 doc.setup_decryption(password)?;
190 }
191 Ok(doc)
192 }
193
194 pub fn open_with_password(path: impl AsRef<Path>, password: &str) -> Result<Document> {
197 Document::load_with_password(std::fs::read(path)?, password)
198 }
199
200 fn setup_decryption(&mut self, password: &str) -> Result<()> {
208 let enc_obj = self
209 .xref
210 .trailer
211 .get("Encrypt")
212 .cloned()
213 .unwrap_or(Object::Null);
214 let enc = self.resolve(&enc_obj)?;
215 let enc_dict = enc.as_dict().ok_or(Error::Encrypted)?;
216 let enc_dict = direct_crypt_filters(enc_dict, |r| self.get(r).ok());
219 let id0: Vec<u8> = self
220 .xref
221 .trailer
222 .get("ID")
223 .and_then(Object::as_array)
224 .and_then(<[Object]>::first)
225 .and_then(Object::as_str_bytes)
226 .unwrap_or(&[])
227 .to_vec();
228 match Decryptor::from_standard_with_password_str(&enc_dict, &id0, password) {
229 Some(dec) => {
230 self.decryptor = Some(dec);
231 self.cache.borrow_mut().clear();
235 Ok(())
236 }
237 None => Err(Error::Encrypted),
238 }
239 }
240
241 pub fn open(path: impl AsRef<Path>) -> Result<Document> {
243 Document::load(std::fs::read(path)?)
244 }
245
246 pub fn version(&self) -> (u8, u8) {
248 self.version
249 }
250
251 pub fn bytes(&self) -> &[u8] {
253 &self.data
254 }
255
256 pub fn xref(&self) -> &Xref {
258 &self.xref
259 }
260
261 pub fn is_encrypted(&self) -> bool {
264 self.xref()
265 .trailer
266 .get("Encrypt")
267 .is_some_and(|o| !o.is_null())
268 }
269
270 pub fn is_locked(&self) -> bool {
275 self.is_encrypted() && self.decryptor.is_none()
276 }
277
278 pub fn get(&self, r: ObjRef) -> Result<Object> {
282 if let Some(cached) = self.cache.borrow().get(&(r.num, r.gen)) {
283 return Ok((**cached).clone());
284 }
285 if !self.loading.borrow_mut().insert(r.num) {
286 return Err(Error::CircularReference(r.num));
287 }
288 let result = self.load_object(r);
289 self.loading.borrow_mut().remove(&r.num);
290 let object = result?;
291 self.cache
292 .borrow_mut()
293 .insert((r.num, r.gen), Rc::new(object.clone()));
294 Ok(object)
295 }
296
297 fn load_object(&self, r: ObjRef) -> Result<Object> {
302 match self.xref.get(r.num) {
303 None | Some(XrefEntry::Free) => Err(Error::ObjectNotFound(r.num, r.gen)),
304 Some(XrefEntry::InFile { offset, .. }) => {
305 let offset = usize::try_from(offset)
306 .ok()
307 .filter(|&o| o < self.data.len())
308 .ok_or(Error::ObjectNotFound(r.num, r.gen))?;
309 self.object_at_spanned(offset).map(|parsed| parsed.1)
310 }
311 Some(XrefEntry::InStream { stream_num, index }) => {
312 self.load_from_object_stream(stream_num, index)
313 }
314 }
315 }
316
317 pub(crate) fn object_at_spanned(&self, offset: usize) -> Result<(ObjRef, Object, Span)> {
320 let mut parser = Parser::at(&self.data, offset);
321 let (r, mut object) = parser.parse_indirect(self)?;
322 if let Some(dec) = &self.decryptor {
326 dec.decrypt_object(&mut object, r.num, r.gen);
327 }
328 Ok((r, object, Span::new(offset as u64, parser.pos() as u64)))
329 }
330
331 pub(crate) fn objstm_handle(&self, stream_num: u32) -> Result<Rc<objstm::ObjStm>> {
334 if let Some(stm) = self.objstms.borrow().get(&stream_num) {
335 return Ok(Rc::clone(stm));
336 }
337 let container = self.get(ObjRef {
338 num: stream_num,
339 gen: 0,
340 })?;
341 let stream = container.as_stream().ok_or_else(|| Error::TypeMismatch {
342 expected: "stream",
343 found: type_name(&container),
344 })?;
345 let n = self
346 .resolve(stream.dict.get("N").unwrap_or(&Object::Null))?
347 .as_int()
348 .and_then(|v| usize::try_from(v).ok())
349 .ok_or(Error::MissingKey("N"))?;
350 let first = self
351 .resolve(stream.dict.get("First").unwrap_or(&Object::Null))?
352 .as_int()
353 .and_then(|v| usize::try_from(v).ok())
354 .ok_or(Error::MissingKey("First"))?;
355 let decoded = self.stream_data(stream)?;
356 let stm = Rc::new(objstm::ObjStm::parse(decoded, n, first)?);
357 self.objstms
358 .borrow_mut()
359 .insert(stream_num, Rc::clone(&stm));
360 Ok(stm)
361 }
362
363 fn load_from_object_stream(&self, stream_num: u32, index: u32) -> Result<Object> {
365 self.objstm_handle(stream_num)?.object(index)
366 }
367
368 pub fn resolve(&self, o: &Object) -> Result<Object> {
379 crate::source::resolve_sync_with(self, o)
380 }
381
382 pub fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
385 filters::decode_stream(s, self)
386 }
387
388 fn pages(&self) -> &[PageRec] {
390 self.pages.get_or_init(|| Arc::new(self.flatten_pages()))
391 }
392
393 pub fn seed(&self) -> DocumentSeed {
397 self.pages();
398 DocumentSeed {
399 data: Arc::clone(&self.data),
400 version: self.version,
401 xref: Arc::clone(&self.xref),
402 decryptor: self.decryptor.clone(),
403 pages: Arc::clone(self.pages.get().expect("pages() was just forced")),
404 }
405 }
406
407 pub fn from_seed(seed: DocumentSeed) -> Document {
421 Document {
422 data: seed.data,
423 version: seed.version,
424 xref: seed.xref,
425 cache: RefCell::new(FastMap::default()),
426 loading: RefCell::new(FastSet::default()),
427 objstms: RefCell::new(FastMap::default()),
428 decryptor: seed.decryptor,
429 pages: OnceCell::from(seed.pages),
430 }
431 }
432
433 pub fn fork(&self) -> Document {
436 Document::from_seed(self.seed())
437 }
438
439 pub fn page_count(&self) -> usize {
448 if let Some(pages) = self.pages.get() {
449 return pages.len();
450 }
451 if let Some(count) = self.declared_page_count() {
452 return count;
453 }
454 self.pages().len()
455 }
456
457 fn declared_page_count(&self) -> Option<usize> {
465 let root = self.xref.trailer.get("Root")?;
466 let catalog = self.resolve(root).ok()?;
467 let pages = self.resolve(catalog.as_dict()?.get("Pages")?).ok()?;
468 let count = usize::try_from(self.int_value(pages.as_dict()?, "Count")?).ok()?;
469 (count <= self.data.len()).then_some(count)
472 }
473
474 pub fn page(&self, index: usize) -> Result<Page> {
476 let pages = self.pages();
477 let rec = pages
478 .get(index)
479 .ok_or(Error::PageNotFound(index, pages.len()))?;
480 Ok(Page {
481 index,
482 media_box: rec.media_box,
483 crop_box: rec.crop_box,
484 bleed_box: rec.bleed_box,
485 trim_box: rec.trim_box,
486 art_box: rec.art_box,
487 rotate: rec.rotate,
488 resources: rec.resources.clone(),
489 dict: rec.dict.clone(),
490 obj_ref: rec.obj_ref,
491 })
492 }
493
494 pub fn metadata(&self) -> Metadata {
499 let meta = Metadata::default();
500 let Some(info) = self.xref.trailer.get("Info") else {
501 return meta;
502 };
503 let Ok(info) = self.resolve(info) else {
504 return meta;
505 };
506 let Some(dict) = info.as_dict() else {
507 return meta;
508 };
509 block_on(metadata_with(&Immediate(self), dict))
510 }
511
512 pub fn oc_state(&self) -> Option<crate::oc::OcState> {
519 block_on(crate::oc::OcState::load_with(
520 &Immediate(self),
521 &self.xref.trailer,
522 ))
523 }
524
525 pub fn structure_tree(&self) -> Option<crate::structure::StructureTree> {
529 block_on(crate::structure::StructureTree::load_with(
530 &Immediate(self),
531 &self.xref.trailer,
532 ))
533 }
534
535 pub fn named(&self, tree: crate::names::NameTree, key: &[u8]) -> Option<Object> {
540 block_on(crate::names::named_with(
541 &Immediate(self),
542 &self.xref.trailer,
543 tree,
544 key,
545 ))
546 }
547
548 pub fn names(&self, tree: crate::names::NameTree) -> Vec<(Vec<u8>, Object)> {
551 block_on(crate::names::names_with(
552 &Immediate(self),
553 &self.xref.trailer,
554 tree,
555 ))
556 }
557
558 pub fn named_destination(&self, name: &[u8]) -> Option<crate::destination::Destination> {
561 block_on(crate::destination::named_destination_with(
562 &Immediate(self),
563 &self.xref.trailer,
564 name,
565 ))
566 }
567
568 pub fn named_destinations(&self) -> Vec<(Vec<u8>, crate::destination::Destination)> {
570 block_on(crate::destination::named_destinations_with(
571 &Immediate(self),
572 &self.xref.trailer,
573 ))
574 }
575
576 pub fn destination(&self, value: &Object) -> Option<crate::destination::Destination> {
580 block_on(crate::destination::destination_value_with(
581 &Immediate(self),
582 &self.xref.trailer,
583 value,
584 ))
585 }
586
587 pub fn outline(&self) -> Vec<crate::outline::OutlineItem> {
590 block_on(crate::outline::outline_with(
591 &Immediate(self),
592 &self.xref.trailer,
593 ))
594 }
595
596 pub fn page_labels(&self) -> Option<Vec<crate::page_label::PageLabel>> {
599 block_on(crate::page_label::page_labels_with(
600 &Immediate(self),
601 &self.xref.trailer,
602 ))
603 }
604
605 pub fn language(&self) -> Option<String> {
608 block_on(crate::language::language_with(
609 &Immediate(self),
610 &self.xref.trailer,
611 ))
612 }
613
614 pub fn language_tag(&self) -> Option<crate::language::LanguageTag> {
618 crate::language::LanguageTag::parse(&self.language()?)
619 }
620
621 pub fn extensions(&self) -> Vec<crate::extension::DeveloperExtension> {
624 block_on(crate::extension::extensions_with(
625 &Immediate(self),
626 &self.xref.trailer,
627 ))
628 }
629
630 pub fn viewer_preferences(&self) -> Option<crate::viewer_preferences::ViewerPreferences> {
633 block_on(crate::viewer_preferences::viewer_preferences_with(
634 &Immediate(self),
635 &self.xref.trailer,
636 ))
637 }
638
639 pub fn linearization(&self) -> Option<crate::linearization::Linearization> {
643 crate::linearization::linearization_dictionary(&self.data)
644 }
645
646 pub fn is_linearized(&self) -> bool {
652 self.linearization()
653 .is_some_and(|record| record.is_current(self.data.len() as u64))
654 }
655
656 pub fn output_intents(&self) -> Vec<crate::output_intent::OutputIntent> {
659 block_on(crate::output_intent::output_intents_with(
660 &Immediate(self),
661 &self.xref.trailer,
662 ))
663 }
664
665 pub fn piece_info(&self) -> Vec<crate::piece_info::PagePiece> {
668 block_on(crate::piece_info::document_piece_info_with(
669 &Immediate(self),
670 &self.xref.trailer,
671 ))
672 }
673
674 pub fn page_piece_info(&self, page: &Page) -> Vec<crate::piece_info::PagePiece> {
677 block_on(crate::piece_info::piece_info_with(
678 &Immediate(self),
679 page.dict(),
680 ))
681 }
682
683 pub fn thumbnail(&self, page: &Page) -> Option<crate::thumbnail::Thumbnail> {
686 block_on(crate::thumbnail::thumbnail_with(&Immediate(self), page))
687 }
688
689 pub fn articles(&self) -> Vec<crate::article::ArticleThread> {
692 block_on(crate::article::articles_with(
693 &Immediate(self),
694 &self.xref.trailer,
695 ))
696 }
697
698 pub fn page_beads(&self, page: &Page) -> Vec<ObjRef> {
701 block_on(crate::article::page_beads_with(&Immediate(self), page))
702 }
703
704 pub fn presentation(&self, page: &Page) -> Option<crate::presentation::Presentation> {
707 block_on(crate::presentation::presentation_with(
708 &Immediate(self),
709 page,
710 ))
711 }
712
713 pub fn viewports(&self, page: &Page) -> Vec<crate::measure::Viewport> {
716 block_on(crate::measure::viewports_with(&Immediate(self), page))
717 }
718
719 pub fn separation_info(&self, page: &Page) -> Option<crate::separation_info::SeparationInfo> {
722 block_on(crate::separation_info::separation_info_with(
723 &Immediate(self),
724 page,
725 ))
726 }
727
728 pub fn annotations(&self, page: &Page) -> Vec<crate::annotation::Annotation> {
731 block_on(crate::annotation::annotations_with(
732 &Immediate(self),
733 &self.xref.trailer,
734 page,
735 ))
736 }
737
738 pub fn page_additional_actions(&self, page: &Page) -> Vec<crate::annotation::TriggeredAction> {
741 block_on(crate::annotation::page_additional_actions_with(
742 &Immediate(self),
743 &self.xref.trailer,
744 page,
745 ))
746 }
747
748 pub fn additional_actions(&self) -> Vec<crate::annotation::TriggeredAction> {
751 block_on(crate::annotation::document_additional_actions_with(
752 &Immediate(self),
753 &self.xref.trailer,
754 ))
755 }
756
757 pub fn action(&self, value: &Object) -> Option<crate::annotation::Action> {
761 block_on(crate::annotation::action_with(
762 &Immediate(self),
763 &self.xref.trailer,
764 value,
765 ))
766 }
767
768 pub fn permission_handlers(&self) -> Option<crate::permission::PermissionHandlers> {
771 block_on(crate::permission::permission_handlers_with(
772 &Immediate(self),
773 &self.xref.trailer,
774 ))
775 }
776
777 pub fn requirements(&self) -> Vec<crate::requirement::Requirement> {
780 block_on(crate::requirement::requirements_with(
781 &Immediate(self),
782 &self.xref.trailer,
783 ))
784 }
785
786 pub fn legal_attestation(&self) -> Option<crate::legal::LegalAttestation> {
789 block_on(crate::legal::legal_attestation_with(
790 &Immediate(self),
791 &self.xref.trailer,
792 ))
793 }
794
795 pub fn interactive_form(&self) -> Option<crate::form::InteractiveForm> {
798 block_on(crate::form::interactive_form_with(
799 &Immediate(self),
800 &self.xref.trailer,
801 ))
802 }
803
804 pub fn form_fields(&self) -> Vec<crate::form::FormField> {
807 block_on(crate::form::form_fields_with(
808 &Immediate(self),
809 &self.xref.trailer,
810 ))
811 }
812
813 pub fn page_label(&self, index: usize) -> Option<String> {
816 if index >= self.page_count() {
817 return None;
818 }
819 let ranges = self.page_labels()?;
820 Some(crate::page_label::page_label(&ranges, index))
821 }
822
823 pub fn embedded_files(&self) -> Vec<crate::embedded_file::EmbeddedFile> {
826 block_on(crate::embedded_file::embedded_files_with(
827 &Immediate(self),
828 &self.xref.trailer,
829 ))
830 }
831
832 pub fn embedded_file_data(&self, file: &crate::embedded_file::EmbeddedFile) -> Result<Vec<u8>> {
839 block_on(crate::embedded_file::embedded_file_data_with(
840 &Immediate(self),
841 file,
842 ))
843 }
844
845 pub fn file_spec_data(&self, spec: &crate::embedded_file::FileSpec) -> Result<Vec<u8>> {
853 block_on(crate::embedded_file::file_spec_data_with(
854 &Immediate(self),
855 spec,
856 ))
857 }
858
859 fn flatten_pages(&self) -> Vec<PageRec> {
866 let mut pages = Vec::new();
867 let Some(root) = self.xref.trailer.get("Root") else {
868 return pages;
869 };
870 let Ok(catalog) = self.resolve(root) else {
871 return pages;
872 };
873 let Some(tree_root) = catalog.as_dict().and_then(|d| d.get("Pages")) else {
874 return pages;
875 };
876 let mut visited: FastSet<ObjRef> = FastSet::default();
877 let mut stack: Vec<(Object, Inherited, usize)> =
878 vec![(tree_root.clone(), Inherited::default(), 0)];
879 while let Some((node, mut inherited, depth)) = stack.pop() {
880 if depth > MAX_TREE_DEPTH {
881 continue;
882 }
883 let node_ref = if let Object::Ref(r) = node {
884 Some(r)
885 } else {
886 None
887 };
888 if let Some(r) = node_ref {
889 if !visited.insert(r) {
890 continue; }
892 }
893 let Ok(resolved) = self.resolve(&node) else {
894 continue;
895 };
896 let Some(dict) = resolved.as_dict() else {
897 continue;
898 };
899 if let Some(res) = self.dict_value(dict, "Resources") {
900 inherited.resources = Some(res);
901 }
902 if let Some(mb) = self.rect_value(dict, "MediaBox") {
903 inherited.media_box = Some(mb);
904 }
905 if let Some(cb) = self.rect_value(dict, "CropBox") {
906 inherited.crop_box = Some(cb);
907 }
908 if let Some(rot) = self.int_value(dict, "Rotate") {
909 inherited.rotate = Some(rot);
910 }
911 let is_page = dict.get_name("Type").is_some_and(|n| n.0 == "Page");
912 let kids = if is_page {
913 None
914 } else {
915 self.array_value(dict, "Kids")
916 };
917 match kids {
918 Some(kids) => {
919 for kid in kids.iter().rev() {
921 stack.push((kid.clone(), inherited.clone(), depth + 1));
922 }
923 }
924 None => {
925 let bleed = self.rect_value(dict, "BleedBox");
929 let trim = self.rect_value(dict, "TrimBox");
930 let art = self.rect_value(dict, "ArtBox");
931 pages.push(make_page_rec(
932 node_ref,
933 dict.clone(),
934 &inherited,
935 bleed,
936 trim,
937 art,
938 ));
939 }
940 }
941 }
942 pages
943 }
944
945 fn dict_value(&self, dict: &Dict, key: &str) -> Option<Dict> {
947 self.resolve(dict.get(key)?).ok()?.as_dict().cloned()
948 }
949
950 fn array_value(&self, dict: &Dict, key: &str) -> Option<Vec<Object>> {
952 match self.resolve(dict.get(key)?).ok()? {
953 Object::Array(items) => Some(items),
954 _ => None,
955 }
956 }
957
958 fn int_value(&self, dict: &Dict, key: &str) -> Option<i32> {
960 let v = self.resolve(dict.get(key)?).ok()?.as_f64()?;
961 if v.is_finite() {
962 Some(v as i32)
963 } else {
964 None
965 }
966 }
967
968 fn rect_value(&self, dict: &Dict, key: &str) -> Option<Rect> {
973 let items = self.array_value(dict, key)?;
974 if items.len() != 4 {
975 return None;
976 }
977 let mut coords = [0.0f32; 4];
978 for (slot, item) in coords.iter_mut().zip(&items) {
979 let n = self.resolve(item).ok()?.as_f64()?;
980 if !n.is_finite() {
981 return None;
982 }
983 *slot = n as f32;
984 }
985 Some(Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
986 }
987}
988
989pub fn map_pages<T, F>(doc: &Document, work: F) -> Vec<Result<T>>
1008where
1009 T: Send + Sync,
1010 F: Fn(&Document, &Page) -> Result<T> + Send + Sync,
1011{
1012 let count = doc.pages().len();
1013 let workers = std::thread::available_parallelism()
1014 .map(std::num::NonZeroUsize::get)
1015 .unwrap_or(1)
1016 .min(count);
1017 if workers <= 1 {
1018 return (0..count)
1019 .map(|i| doc.page(i).and_then(|page| work(doc, &page)))
1020 .collect();
1021 }
1022
1023 let seed = doc.seed();
1024 let next = std::sync::atomic::AtomicUsize::new(0);
1025 let slots: Vec<std::sync::OnceLock<Result<T>>> =
1026 (0..count).map(|_| std::sync::OnceLock::new()).collect();
1027 std::thread::scope(|scope| {
1028 for _ in 0..workers {
1029 let seed = seed.clone();
1032 let next = &next;
1033 let slots = &slots;
1034 let work = &work;
1035 scope.spawn(move || {
1036 let worker = Document::from_seed(seed);
1037 loop {
1038 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1039 if i >= count {
1040 break;
1041 }
1042 let outcome = worker.page(i).and_then(|page| work(&worker, &page));
1043 if slots[i].set(outcome).is_err() {
1048 unreachable!("page index {i} was dispatched twice");
1049 }
1050 }
1051 });
1052 }
1053 });
1054 slots
1055 .into_iter()
1056 .map(|slot| {
1057 slot.into_inner()
1058 .expect("every page index below the count was dispatched")
1059 })
1060 .collect()
1061}
1062
1063fn make_page_rec(
1070 obj_ref: Option<ObjRef>,
1071 dict: Dict,
1072 inherited: &Inherited,
1073 bleed_box: Option<Rect>,
1074 trim_box: Option<Rect>,
1075 art_box: Option<Rect>,
1076) -> PageRec {
1077 let page = Page::from_tree_attrs(
1078 0,
1079 inherited.resources.clone(),
1080 inherited.media_box,
1081 inherited.crop_box,
1082 bleed_box,
1083 trim_box,
1084 art_box,
1085 inherited.rotate,
1086 dict,
1087 obj_ref,
1088 );
1089 PageRec {
1090 obj_ref: page.obj_ref,
1091 media_box: page.media_box,
1092 crop_box: page.crop_box,
1093 bleed_box: page.bleed_box,
1094 trim_box: page.trim_box,
1095 art_box: page.art_box,
1096 rotate: page.rotate,
1097 resources: page.resources,
1098 dict: page.dict,
1099 }
1100}
1101
1102pub(crate) fn type_name(o: &Object) -> &'static str {
1104 match o {
1105 Object::Null => "null",
1106 Object::Bool(_) => "boolean",
1107 Object::Int(_) => "integer",
1108 Object::Real(_) => "real",
1109 Object::String(_) => "string",
1110 Object::Name(_) => "name",
1111 Object::Array(_) => "array",
1112 Object::Dict(_) => "dictionary",
1113 Object::Stream(_) => "stream",
1114 Object::Ref(_) => "reference",
1115 }
1116}
1117
1118impl Resolve for Document {
1119 fn resolve_ref(&self, r: ObjRef) -> Option<Object> {
1120 self.get(r).ok()
1121 }
1122}
1123
1124impl crate::source::ObjectSource for Document {
1132 fn get(&self, r: ObjRef) -> Result<Object> {
1133 Document::get(self, r)
1134 }
1135
1136 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
1137 Document::stream_data(self, s)
1138 }
1139
1140 fn resolve(&self, o: &Object) -> Result<Object> {
1141 Document::resolve(self, o)
1142 }
1143}
1144
1145#[derive(Debug, Clone, Default, PartialEq, Eq)]
1150pub struct Metadata {
1151 pub title: Option<String>,
1152 pub author: Option<String>,
1153 pub subject: Option<String>,
1154 pub keywords: Option<String>,
1155 pub creator: Option<String>,
1156 pub producer: Option<String>,
1157 pub creation_date: Option<String>,
1158 pub mod_date: Option<String>,
1159}
1160
1161pub async fn metadata_with<S: AsyncObjectSource>(src: &S, dict: &Dict) -> Metadata {
1167 let entries = Entries { src, dict };
1168 Metadata {
1169 title: entries.text("Title").await,
1170 author: entries.text("Author").await,
1171 subject: entries.text("Subject").await,
1172 keywords: entries.text("Keywords").await,
1173 creator: entries.text("Creator").await,
1174 producer: entries.text("Producer").await,
1175 creation_date: entries.text("CreationDate").await,
1176 mod_date: entries.text("ModDate").await,
1177 }
1178}
1179
1180impl Metadata {
1181 pub fn creation_date_parsed(&self) -> Option<Date> {
1186 Date::parse_pdf(self.creation_date.as_deref()?)
1187 }
1188
1189 pub fn mod_date_parsed(&self) -> Option<Date> {
1193 Date::parse_pdf(self.mod_date.as_deref()?)
1194 }
1195}
1196
1197#[derive(Clone)]
1212pub struct Page {
1213 pub index: usize,
1215 pub media_box: Rect,
1216 pub crop_box: Rect,
1217 pub bleed_box: Rect,
1218 pub trim_box: Rect,
1219 pub art_box: Rect,
1220 pub rotate: i32,
1221 pub resources: Dict,
1223 dict: Dict,
1224 obj_ref: Option<ObjRef>,
1225}
1226
1227impl Page {
1228 pub fn from_parts(
1248 index: usize,
1249 media_box: Rect,
1250 crop_box: Rect,
1251 rotate: i32,
1252 resources: Dict,
1253 dict: Dict,
1254 obj_ref: Option<ObjRef>,
1255 ) -> Page {
1256 Page {
1257 index,
1258 media_box,
1259 crop_box,
1260 bleed_box: crop_box,
1261 trim_box: crop_box,
1262 art_box: crop_box,
1263 rotate: normalize_rotation(rotate),
1264 resources,
1265 dict,
1266 obj_ref,
1267 }
1268 }
1269
1270 pub const US_LETTER: Rect = Rect::new(0.0, 0.0, 612.0, 792.0);
1275
1276 #[allow(clippy::too_many_arguments)]
1291 pub fn from_tree_attrs(
1292 index: usize,
1293 resources: Option<Dict>,
1294 media_box: Option<Rect>,
1295 crop_box: Option<Rect>,
1296 bleed_box: Option<Rect>,
1297 trim_box: Option<Rect>,
1298 art_box: Option<Rect>,
1299 rotate: Option<i32>,
1300 dict: Dict,
1301 obj_ref: Option<ObjRef>,
1302 ) -> Page {
1303 let media_box = media_box
1304 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
1305 .unwrap_or(Page::US_LETTER);
1306 let crop_box = crop_box
1307 .and_then(|c| c.intersect(media_box))
1308 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
1309 .unwrap_or(media_box);
1310 let clip = |declared: Option<Rect>| {
1311 declared
1312 .and_then(|b| b.intersect(media_box))
1313 .filter(|r| r.width() > 0.0 && r.height() > 0.0)
1314 .unwrap_or(crop_box)
1315 };
1316 Page {
1317 index,
1318 media_box,
1319 crop_box,
1320 bleed_box: clip(bleed_box),
1321 trim_box: clip(trim_box),
1322 art_box: clip(art_box),
1323 rotate: normalize_rotation(rotate.unwrap_or(0)),
1324 resources: resources.unwrap_or_default(),
1325 dict,
1326 obj_ref,
1327 }
1328 }
1329
1330 pub fn object_ref(&self) -> Option<ObjRef> {
1334 self.obj_ref
1335 }
1336
1337 pub fn content(&self, doc: &Document) -> Result<Vec<u8>> {
1346 block_on(page_content_with(Immediate(doc), self))
1347 }
1348
1349 pub fn size(&self) -> (f32, f32) {
1358 let (w, h) = (self.crop_box.width(), self.crop_box.height());
1359 let turn = self.rotate.rem_euclid(360);
1360 if turn == 90 || turn == 270 {
1361 (h, w)
1362 } else {
1363 (w, h)
1364 }
1365 }
1366
1367 pub fn dict(&self) -> &Dict {
1369 &self.dict
1370 }
1371}
1372
1373pub async fn page_content_with<S: AsyncObjectSource>(src: S, page: &Page) -> Result<Vec<u8>> {
1406 let Some(contents) = page.dict.get("Contents") else {
1407 return Ok(Vec::new());
1408 };
1409 match src.resolve(contents).await? {
1410 Object::Stream(ref s) => content_stream_data_with(&src, s).await,
1411 Object::Array(items) => {
1412 let mut out = Vec::new();
1413 let mut first = true;
1414 for item in &items {
1415 let part = src.resolve(item).await?;
1416 let Some(stream) = part.as_stream() else {
1417 continue; };
1419 if !first {
1420 out.push(b'\n');
1421 }
1422 out.extend_from_slice(&content_stream_data_with(&src, stream).await?);
1423 first = false;
1424 }
1425 Ok(out)
1426 }
1427 _ => Ok(Vec::new()),
1428 }
1429}
1430
1431pub async fn decoded_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1464 if let Some(name) = filters::trailing_filter_checked_with(&src, &s.dict).await? {
1465 if filters::is_image_codec(&name.0) {
1466 return Err(Error::UnsupportedFilter(name.0));
1467 }
1468 }
1469 src.stream_data(s).await
1470}
1471
1472pub async fn content_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1482 decoded_stream_data_with(src, s).await
1483}
1484
1485#[cfg(test)]
1486mod tests {
1487 use super::*;
1488 use crate::object::Name;
1489 use crate::parser::{NoResolve, Parser};
1490 use crate::xref::XrefEntry;
1491 use pdfboss_testkit::{multi_page_doc, objstm_doc, objstm_payload, simple_doc, PdfBuilder};
1492
1493 #[test]
1497 fn async_page_content_matches_the_sync_accessor() {
1498 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("load");
1499 let mut saw_content = false;
1500 for index in 0..3 {
1501 let page = doc.page(index).expect("page");
1502 let direct = page.content(&doc).expect("sync content");
1503 let awaited =
1504 block_on(page_content_with(Immediate(&doc), &page)).expect("async content");
1505 assert_eq!(direct, awaited, "page {index}");
1506 saw_content |= !direct.is_empty();
1507 }
1508 assert!(
1510 saw_content,
1511 "fixture must give at least one page real content"
1512 );
1513 }
1514
1515 #[test]
1523 fn image_codec_page_contents_are_refused_not_returned() {
1524 for codec in ["JPXDecode", "DCTDecode"] {
1525 let mut b = PdfBuilder::new();
1526 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1527 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1528 b.object(
1529 3,
1530 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1531 );
1532 b.stream(
1533 4,
1534 &format!("/Filter /{codec}"),
1535 b"1 0 0 rg 0 0 100 100 re f",
1536 );
1537 let doc = Document::load(b.build(1)).expect("load");
1538 let page = doc.page(0).expect("page");
1539 match page.content(&doc) {
1540 Err(Error::UnsupportedFilter(n)) => assert_eq!(n, codec),
1541 other => panic!("{codec} content must be refused, got {other:?}"),
1542 }
1543 }
1544 }
1545
1546 #[test]
1553 fn an_unreadable_filter_refuses_the_content_not_returns_it_raw() {
1554 let mut b = PdfBuilder::new();
1555 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1556 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1557 b.object(
1558 3,
1559 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1560 );
1561 b.stream(4, "/Filter 6 0 R", b"1 0 0 rg 0 0 100 100 re f");
1562 b.object(6, "7 0 R");
1563 b.object(7, "6 0 R");
1564 let doc = Document::load(b.build(1)).expect("load");
1565 let page = doc.page(0).expect("page");
1566 assert!(
1567 page.content(&doc).is_err(),
1568 "an unreadable /Filter must refuse, not pass stored bytes"
1569 );
1570 }
1571
1572 #[test]
1577 fn a_shared_helper_is_reachable_from_a_caller_that_owns_its_source() {
1578 let doc = Document::load(simple_doc("Hello")).expect("load");
1579 let page = doc.page(0).expect("page");
1580
1581 let owner = Immediate(&doc);
1582 let through_reference = block_on(page_content_with(&owner, &page)).expect("async content");
1583
1584 assert_eq!(through_reference, page.content(&doc).expect("sync content"));
1585 assert!(!through_reference.is_empty());
1586 }
1587
1588 #[test]
1592 fn the_seed_is_shareable_across_threads() {
1593 fn assert_send_sync<T: Send + Sync>() {}
1594 assert_send_sync::<DocumentSeed>();
1595 }
1596
1597 #[test]
1601 fn a_fork_reads_exactly_what_its_parent_reads() {
1602 let doc = Document::load(pdfboss_testkit::encrypted_rc4_doc("forked secret")).unwrap();
1603 let fork = doc.fork();
1604 assert_eq!(fork.page_count(), doc.page_count());
1605 let (a, b) = (doc.page(0).unwrap(), fork.page(0).unwrap());
1606 assert_eq!(a.media_box, b.media_box);
1607 assert_eq!(
1608 a.content(&doc).expect("parent content"),
1609 b.content(&fork).expect("fork content"),
1610 "decrypted content agrees"
1611 );
1612 }
1613
1614 #[test]
1618 fn map_pages_preserves_page_order() {
1619 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1620 let contents = map_pages(&doc, |doc, page| page.content(doc));
1621 assert_eq!(contents.len(), 3);
1622 let texts: Vec<String> = contents
1623 .into_iter()
1624 .map(|c| String::from_utf8_lossy(&c.unwrap()).into_owned())
1625 .collect();
1626 assert!(texts[0].contains("(one)"), "{}", texts[0]);
1627 assert!(texts[1].contains("(two)"), "{}", texts[1]);
1628 assert!(texts[2].contains("(three)"), "{}", texts[2]);
1629 }
1630
1631 fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
1634 let pos = memchr::memmem::find(data, from).expect("pattern present in fixture");
1635 let mut out = Vec::with_capacity(data.len() - from.len() + to.len());
1636 out.extend_from_slice(&data[..pos]);
1637 out.extend_from_slice(to);
1638 out.extend_from_slice(&data[pos + from.len()..]);
1639 out
1640 }
1641
1642 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1643 memchr::memmem::find(haystack, needle).is_some()
1644 }
1645
1646 const FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
1647
1648 #[test]
1650 fn loads_simple_doc() {
1651 let doc = Document::load(simple_doc("Greetings, cosmos!")).unwrap();
1652 assert_eq!(doc.version(), (1, 7));
1653 assert_eq!(doc.page_count(), 1);
1654 let page = doc.page(0).unwrap();
1655 assert_eq!(page.index, 0);
1656 assert_eq!(page.media_box, Rect::new(0.0, 0.0, 612.0, 792.0));
1657 assert_eq!(page.crop_box, page.media_box);
1658 assert_eq!(page.rotate, 0);
1659 assert_eq!(page.size(), (612.0, 792.0));
1660 assert!(page.resources.get("Font").is_some());
1661 let content = page.content(&doc).unwrap();
1662 assert!(contains(&content, b"Greetings, cosmos!"));
1663 }
1664
1665 #[test]
1667 fn multi_page_ordering() {
1668 let doc = Document::load(multi_page_doc(&["alpha", "beta", "gamma"])).unwrap();
1669 assert_eq!(doc.page_count(), 3);
1670 for (i, text) in ["alpha", "beta", "gamma"].iter().enumerate() {
1671 let content = doc.page(i).unwrap().content(&doc).unwrap();
1672 assert!(
1673 contains(&content, text.as_bytes()),
1674 "page {i} should show {text}"
1675 );
1676 }
1677 }
1678
1679 #[test]
1680 fn page_index_out_of_bounds() {
1681 let doc = Document::load(simple_doc("x")).unwrap();
1682 assert!(matches!(doc.page(5), Err(Error::PageNotFound(5, 1))));
1683 }
1684
1685 #[test]
1686 fn open_reads_from_disk() {
1687 let dir = std::env::temp_dir();
1688 let path = dir.join(format!("pdfboss-doc-test-{}.pdf", std::process::id()));
1689 std::fs::write(&path, simple_doc("from disk")).unwrap();
1690 let doc = Document::open(&path).unwrap();
1691 std::fs::remove_file(&path).ok();
1692 assert_eq!(doc.page_count(), 1);
1693 let content = doc.page(0).unwrap().content(&doc).unwrap();
1694 assert!(contains(&content, b"from disk"));
1695 assert!(matches!(
1696 Document::open(dir.join("pdfboss-doc-test-missing.pdf")),
1697 Err(Error::Io(_))
1698 ));
1699 }
1700
1701 #[test]
1703 fn encrypt_in_trailer_is_rejected() {
1704 let data = replace_once(
1705 &simple_doc("secret"),
1706 b"trailer\n<< /Size",
1707 b"trailer\n<< /Encrypt 9 0 R /Size",
1708 );
1709 assert!(matches!(Document::load(data), Err(Error::Encrypted)));
1710 }
1711
1712 #[test]
1714 fn metadata_utf16be_round_trip() {
1715 let mut b = PdfBuilder::new();
1716 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1717 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1718 b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
1720 let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1721 let doc = Document::load(data).unwrap();
1722 let meta = doc.metadata();
1723 assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
1724 assert_eq!(meta.author.as_deref(), Some("plain author"));
1725 assert_eq!(meta.subject, None);
1726 assert_eq!(meta.keywords, None);
1727 assert_eq!(meta.creation_date, None);
1728 }
1729
1730 #[test]
1733 fn metadata_dates_parse_per_the_date_clause() {
1734 let mut b = PdfBuilder::new();
1735 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1736 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1737 b.object(
1739 6,
1740 "<< /CreationDate (D:199812231952-08'00) /ModDate (D:191020717014604) >>",
1741 );
1742 let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1743 let doc = Document::load(data).unwrap();
1744 let meta = doc.metadata();
1745 assert_eq!(meta.creation_date.as_deref(), Some("D:199812231952-08'00"));
1746 assert_eq!(
1747 meta.creation_date_parsed(),
1748 Some(Date {
1749 year: 1998,
1750 month: 12,
1751 day: 23,
1752 hour: 19,
1753 minute: 52,
1754 second: 0,
1755 utc_offset_minutes: -480,
1756 })
1757 );
1758 assert_eq!(meta.mod_date.as_deref(), Some("D:191020717014604"));
1759 assert_eq!(meta.mod_date_parsed(), None, "month 20 is not a date");
1760 }
1761
1762 #[test]
1763 fn metadata_without_info_is_all_none() {
1764 let doc = Document::load(simple_doc("x")).unwrap();
1765 assert_eq!(doc.metadata(), Metadata::default());
1766 }
1767
1768 #[test]
1770 fn missing_object_resolves_to_null() {
1771 let doc = Document::load(simple_doc("x")).unwrap();
1772 let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
1773 assert_eq!(doc.resolve(&missing).unwrap(), Object::Null);
1774 assert!(matches!(
1775 doc.get(ObjRef { num: 99, gen: 0 }),
1776 Err(Error::ObjectNotFound(99, 0))
1777 ));
1778 }
1779
1780 #[test]
1781 fn self_reference_is_circular() {
1782 let mut b = PdfBuilder::new();
1783 b.object(1, "<< /Type /Catalog >>");
1784 b.object(6, "6 0 R");
1785 let doc = Document::load(b.build(1)).unwrap();
1786 let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
1787 assert!(matches!(
1788 doc.resolve(&loops),
1789 Err(Error::CircularReference(6))
1790 ));
1791 }
1792
1793 #[test]
1795 fn generation_mismatch_is_tolerated() {
1796 let doc = Document::load(simple_doc("x")).unwrap();
1797 let catalog = doc.get(ObjRef { num: 1, gen: 7 }).unwrap();
1798 let dict = catalog.as_dict().unwrap();
1799 assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Catalog"));
1800 }
1801
1802 #[test]
1804 fn objects_in_object_streams_are_fetched() {
1805 let mut b = PdfBuilder::new();
1806 let (dict, payload) =
1807 objstm_payload(&[(1, "<< /Type /Catalog /Pages 2 0 R >>"), (5, FONT)]);
1808 b.stream(6, &dict, &payload);
1809 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1810 b.object(
1811 3,
1812 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1813 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
1814 );
1815 b.stream(4, "", b"BT /F1 12 Tf (compressed hello) Tj ET");
1816 let doc = Document::load(b.build_xref_stream(1)).unwrap();
1817 assert_eq!(doc.page_count(), 1);
1818 let page = doc.page(0).unwrap();
1819 assert!(contains(&page.content(&doc).unwrap(), b"compressed hello"));
1820 let font = doc.get(ObjRef { num: 5, gen: 0 }).unwrap();
1821 assert_eq!(
1822 font.as_dict()
1823 .and_then(|d| d.get_name("BaseFont"))
1824 .map(|n| n.0.as_str()),
1825 Some("Helvetica")
1826 );
1827 }
1828
1829 #[test]
1831 fn contents_array_is_joined_with_newlines() {
1832 let mut b = PdfBuilder::new();
1833 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1834 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1835 b.object(
1836 3,
1837 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1838 /Contents [4 0 R null 5 0 R] >>",
1839 );
1840 b.stream(4, "", b"q");
1841 b.stream(5, "", b"Q");
1842 let doc = Document::load(b.build(1)).unwrap();
1843 let content = doc.page(0).unwrap().content(&doc).unwrap();
1844 assert_eq!(content, b"q\nQ", "streams joined by \\n, null skipped");
1845 }
1846
1847 #[test]
1849 fn inheritance_from_pages_node_and_rotate_swap() {
1850 let mut b = PdfBuilder::new();
1851 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1852 b.object(
1853 2,
1854 "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 \
1855 /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 600] >>",
1856 );
1857 b.object(3, "<< /Type /Page /Parent 2 0 R >>");
1858 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate 270 >>");
1859 b.object(5, FONT);
1860 let doc = Document::load(b.build(1)).unwrap();
1861 assert_eq!(doc.page_count(), 2);
1862
1863 let first = doc.page(0).unwrap();
1864 assert_eq!(first.media_box, Rect::new(0.0, 0.0, 400.0, 600.0));
1865 assert_eq!(first.crop_box, first.media_box);
1866 assert!(first.resources.get("Font").is_some(), "inherited resources");
1867 assert_eq!(first.rotate, 0);
1868 assert!(
1869 first.content(&doc).unwrap().is_empty(),
1870 "no /Contents means empty content"
1871 );
1872 assert_eq!(first.size(), (400.0, 600.0));
1873
1874 let second = doc.page(1).unwrap();
1875 assert_eq!(second.rotate, 270);
1876 assert_eq!(second.size(), (600.0, 400.0), "rotate 270 swaps w/h");
1877 }
1878
1879 #[test]
1881 fn crop_box_intersected_and_rotate_normalized() {
1882 let mut b = PdfBuilder::new();
1883 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1884 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>");
1885 b.object(
1886 3,
1887 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1888 /CropBox [100 100 400 400] /Rotate 450 >>",
1889 );
1890 b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate -90 >>");
1891 b.object(
1892 5,
1893 "<< /Type /Page /Parent 2 0 R /Rotate 45 /MediaBox [0 0 0 0] >>",
1894 );
1895 let doc = Document::load(b.build(1)).unwrap();
1896
1897 let clipped = doc.page(0).unwrap();
1898 assert_eq!(clipped.crop_box, Rect::new(100.0, 100.0, 200.0, 200.0));
1899 assert_eq!(clipped.rotate, 90, "450 normalizes to 90");
1900 assert_eq!(clipped.size(), (100.0, 100.0));
1901
1902 assert_eq!(doc.page(1).unwrap().rotate, 270, "-90 normalizes to 270");
1903 let odd = doc.page(2).unwrap();
1904 assert_eq!(odd.rotate, 0, "non-multiple of 90 falls back to 0");
1905 assert_eq!(
1906 odd.media_box,
1907 Page::US_LETTER,
1908 "degenerate media box defaults"
1909 );
1910 }
1911
1912 #[test]
1914 fn page_boxes_declared_and_defaulted() {
1915 let mut b = PdfBuilder::new();
1916 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1917 b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>");
1918 b.object(
1919 3,
1920 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] \
1921 /CropBox [50 50 550 750] /BleedBox [40 40 560 760] \
1922 /TrimBox [5 0 R 6 0 R 7 0 R 8 0 R] >>",
1923 );
1924 b.object(4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] >>");
1925 b.object(5, "60");
1926 b.object(6, "70");
1927 b.object(7, "540");
1928 b.object(8, "730");
1929 let doc = Document::load(b.build(1)).unwrap();
1930
1931 let page = doc.page(0).unwrap();
1932 assert_eq!(page.bleed_box, Rect::new(40.0, 40.0, 560.0, 760.0));
1933 assert_eq!(
1934 page.trim_box,
1935 Rect::new(60.0, 70.0, 540.0, 730.0),
1936 "indirect array elements resolve"
1937 );
1938 assert_eq!(
1939 page.art_box, page.crop_box,
1940 "undeclared box is the crop box"
1941 );
1942
1943 let bare = doc.page(1).unwrap();
1944 assert_eq!(bare.bleed_box, bare.crop_box);
1945 assert_eq!(bare.trim_box, bare.crop_box);
1946 assert_eq!(bare.art_box, bare.crop_box);
1947 }
1948
1949 #[test]
1951 fn page_boxes_clip_to_media_and_fall_back() {
1952 let mut b = PdfBuilder::new();
1953 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1954 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1955 b.object(
1956 3,
1957 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1958 /CropBox [10 10 190 190] /TrimBox [100 100 400 400] \
1959 /BleedBox [0 0 0 0] /ArtBox [300 300 400 400] >>",
1960 );
1961 let doc = Document::load(b.build(1)).unwrap();
1962
1963 let page = doc.page(0).unwrap();
1964 assert_eq!(
1965 page.trim_box,
1966 Rect::new(100.0, 100.0, 200.0, 200.0),
1967 "a trim box past the media box clips to it"
1968 );
1969 assert_eq!(
1970 page.bleed_box, page.crop_box,
1971 "a degenerate bleed box falls back to the crop box"
1972 );
1973 assert_eq!(
1974 page.art_box, page.crop_box,
1975 "an art box disjoint from the media box falls back to the crop box"
1976 );
1977 }
1978
1979 #[test]
1984 fn page_boxes_are_not_inherited() {
1985 let mut b = PdfBuilder::new();
1986 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1987 b.object(
1988 2,
1989 "<< /Type /Pages /Kids [3 0 R] /Count 1 \
1990 /TrimBox [10 10 90 90] /BleedBox [5 5 95 95] /ArtBox [20 20 80 80] >>",
1991 );
1992 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1993 let doc = Document::load(b.build(1)).unwrap();
1994
1995 let page = doc.page(0).unwrap();
1996 assert_eq!(page.trim_box, page.crop_box);
1997 assert_eq!(page.bleed_box, page.crop_box);
1998 assert_eq!(page.art_box, page.crop_box);
1999 }
2000
2001 #[test]
2003 fn kids_cycle_truncates_without_hanging() {
2004 let mut b = PdfBuilder::new();
2005 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2006 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
2008 b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
2009 b.object(
2010 4,
2011 "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] /Contents 5 0 R >>",
2012 );
2013 b.stream(5, "", b"0 0 50 50 re f");
2014 let doc = Document::load(b.build(1)).unwrap();
2015 assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
2016 assert!(contains(
2017 &doc.page(0).unwrap().content(&doc).unwrap(),
2018 b"re f"
2019 ));
2020 }
2021
2022 #[test]
2024 fn tree_depth_is_capped() {
2025 let mut b = PdfBuilder::new();
2026 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2027 let last = 302u32;
2029 for num in 2..last {
2030 b.object(
2031 num,
2032 &format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", num + 1),
2033 );
2034 }
2035 b.object(last, "<< /Type /Page >>");
2036 let doc = Document::load(b.build(1)).unwrap();
2037 assert_eq!(doc.page_count(), 1, "declared /Count is reported cheaply");
2042 assert!(
2043 matches!(doc.page(0), Err(Error::PageNotFound(0, 0))),
2044 "leaf beyond the depth cap cannot be materialized"
2045 );
2046 }
2047
2048 #[test]
2050 fn page_count_reports_declared_count_cheaply() {
2051 let mut b = PdfBuilder::new();
2055 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2056 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
2057 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
2058 let doc = Document::load(b.build(1)).unwrap();
2059 assert_eq!(doc.page_count(), 5, "declared /Count reported verbatim");
2060 assert!(doc.page(0).is_ok(), "the one real page materializes");
2061 assert!(
2062 matches!(doc.page(1), Err(Error::PageNotFound(1, 1))),
2063 "access past the real pages fails with the true length"
2064 );
2065 }
2066
2067 #[test]
2069 fn page_count_falls_back_to_walk_when_count_absent() {
2070 let mut b = PdfBuilder::new();
2071 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2072 b.object(2, "<< /Type /Pages /Kids [3 0 R] >>"); b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
2074 let doc = Document::load(b.build(1)).unwrap();
2075 assert_eq!(
2076 doc.page_count(),
2077 1,
2078 "missing /Count is recovered by walking"
2079 );
2080 }
2081
2082 #[test]
2084 fn page_count_ignores_corrupt_oversized_count() {
2085 let mut b = PdfBuilder::new();
2088 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2089 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 999999999 >>");
2090 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
2091 let doc = Document::load(b.build(1)).unwrap();
2092 assert_eq!(doc.page_count(), 1, "implausible /Count is rejected");
2093 }
2094
2095 #[test]
2097 fn version_scan_and_default() {
2098 let mut b = PdfBuilder::new().version(2, 0);
2099 b.object(1, "<< /Type /Catalog >>");
2100 assert_eq!(Document::load(b.build(1)).unwrap().version(), (2, 0));
2101 let data = replace_once(&simple_doc("v"), b"%PDF-", b"%QQQ-");
2103 assert_eq!(Document::load(data).unwrap().version(), (1, 4));
2104 }
2105
2106 #[test]
2108 fn deeply_nested_root_object_does_not_overflow_the_stack() {
2109 let mut data = b"%PDF-1.7\n1 0 obj\n".to_vec();
2114 data.extend(std::iter::repeat_n(b'[', 50_000));
2115 data.extend(std::iter::repeat_n(b']', 50_000));
2116 data.extend_from_slice(b"\nendobj\ntrailer\n<</Root 1 0 R>>\n%%EOF\n");
2117 let outcome = std::thread::Builder::new()
2118 .stack_size(1024 * 1024)
2119 .spawn(move || Document::load(data).map(|doc| doc.page_count()))
2120 .expect("spawn test thread")
2121 .join()
2122 .expect("Document::load must not overflow the stack");
2123 assert!(matches!(outcome, Ok(0) | Err(_)));
2126 }
2127
2128 #[test]
2129 fn bytes_and_xref_accessors() {
2130 let data = simple_doc("accessors");
2131 let doc = Document::load(data.clone()).unwrap();
2132 assert_eq!(doc.bytes(), &data[..]);
2133 assert!(!doc.xref().is_empty());
2134 assert!(doc.xref().trailer.get("Root").is_some());
2135 }
2136
2137 #[test]
2138 fn object_at_spanned_reparses_identically() {
2139 let data = simple_doc("spanned");
2140 let doc = Document::load(data).unwrap();
2141 for (num, entry) in doc.xref().iter() {
2142 let XrefEntry::InFile { offset, gen } = entry else {
2143 continue;
2144 };
2145 let (r, object, span) = doc.object_at_spanned(offset as usize).unwrap();
2146 assert_eq!(r.num, num);
2147 assert_eq!(r.gen, gen);
2148 assert_eq!(span.start, offset);
2149 assert!(span.end as usize <= doc.bytes().len());
2150 let slice = &doc.bytes()[span.start as usize..span.end as usize];
2152 let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
2153 assert_eq!(r2, r);
2154 assert_eq!(object2, object);
2155 }
2156 }
2157
2158 #[test]
2159 fn page_object_ref_points_at_a_page_dict() {
2160 let doc = Document::load(multi_page_doc(&["one", "two"])).unwrap();
2161 for index in 0..doc.page_count() {
2162 let page = doc.page(index).unwrap();
2163 let r = page.object_ref().expect("builder pages are indirect");
2164 let resolved = doc.get(r).unwrap();
2165 assert_eq!(
2166 resolved
2167 .as_dict()
2168 .unwrap()
2169 .get_name("Type")
2170 .map(|n| n.0.as_str()),
2171 Some("Page")
2172 );
2173 }
2174 }
2175
2176 const RC4_FIXTURE_KEY_LEN: usize = 16; const RC4_FIXTURE_P: i32 = -44;
2186 const RC4_FIXTURE_ID0: &[u8] = b"0123456789abcdef";
2187 const RC4_FIXTURE_PAD: [u8; 32] = [
2188 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01,
2189 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53,
2190 0x69, 0x7A,
2191 ];
2192
2193 #[rustfmt::skip]
2194 const RC4_FIXTURE_MD5_S: [u32; 64] = [
2195 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
2196 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
2197 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
2198 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
2199 ];
2200 #[rustfmt::skip]
2201 const RC4_FIXTURE_MD5_K: [u32; 64] = [
2202 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
2203 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
2204 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
2205 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
2206 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
2207 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
2208 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
2209 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
2210 ];
2211
2212 fn rc4_fixture_md5(input: &[u8]) -> [u8; 16] {
2213 let (mut a0, mut b0, mut c0, mut d0) = (
2214 0x6745_2301u32,
2215 0xefcd_ab89u32,
2216 0x98ba_dcfeu32,
2217 0x1032_5476u32,
2218 );
2219 let mut msg = input.to_vec();
2220 let bitlen = (input.len() as u64).wrapping_mul(8);
2221 msg.push(0x80);
2222 while msg.len() % 64 != 56 {
2223 msg.push(0);
2224 }
2225 msg.extend_from_slice(&bitlen.to_le_bytes());
2226 for chunk in msg.as_chunks::<64>().0 {
2227 let mut m = [0u32; 16];
2228 for (word, bytes) in m.iter_mut().zip(chunk.as_chunks::<4>().0) {
2229 *word = u32::from_le_bytes(*bytes);
2230 }
2231 let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
2232 for i in 0..64 {
2233 let (f, g) = match i {
2234 0..=15 => ((b & c) | (!b & d), i),
2235 16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
2236 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
2237 _ => (c ^ (b | !d), (7 * i) % 16),
2238 };
2239 let f = f
2240 .wrapping_add(a)
2241 .wrapping_add(RC4_FIXTURE_MD5_K[i])
2242 .wrapping_add(m[g]);
2243 a = d;
2244 d = c;
2245 c = b;
2246 b = b.wrapping_add(f.rotate_left(RC4_FIXTURE_MD5_S[i]));
2247 }
2248 a0 = a0.wrapping_add(a);
2249 b0 = b0.wrapping_add(b);
2250 c0 = c0.wrapping_add(c);
2251 d0 = d0.wrapping_add(d);
2252 }
2253 let mut out = [0u8; 16];
2254 out[0..4].copy_from_slice(&a0.to_le_bytes());
2255 out[4..8].copy_from_slice(&b0.to_le_bytes());
2256 out[8..12].copy_from_slice(&c0.to_le_bytes());
2257 out[12..16].copy_from_slice(&d0.to_le_bytes());
2258 out
2259 }
2260
2261 fn rc4_fixture_rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
2262 let mut s: [u8; 256] = core::array::from_fn(|i| i as u8);
2263 let mut j = 0u8;
2264 for i in 0..256 {
2265 j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
2266 s.swap(i, j as usize);
2267 }
2268 let mut out = Vec::with_capacity(data.len());
2269 let (mut i, mut j) = (0u8, 0u8);
2270 for &byte in data {
2271 i = i.wrapping_add(1);
2272 j = j.wrapping_add(s[i as usize]);
2273 s.swap(i as usize, j as usize);
2274 let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
2275 out.push(byte ^ k);
2276 }
2277 out
2278 }
2279
2280 fn rc4_fixture_owner_entry() -> Vec<u8> {
2282 let mut d = rc4_fixture_md5(&RC4_FIXTURE_PAD);
2283 for _ in 0..50 {
2284 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
2285 }
2286 let rc4key = d[..RC4_FIXTURE_KEY_LEN].to_vec();
2287 let mut o = rc4_fixture_rc4(&rc4key, &RC4_FIXTURE_PAD);
2288 for i in 1u8..=19 {
2289 let k: Vec<u8> = rc4key.iter().map(|b| b ^ i).collect();
2290 o = rc4_fixture_rc4(&k, &o);
2291 }
2292 o
2293 }
2294
2295 fn rc4_fixture_file_key(o: &[u8]) -> Vec<u8> {
2297 let mut input = Vec::new();
2298 input.extend_from_slice(&RC4_FIXTURE_PAD);
2299 input.extend_from_slice(o);
2300 input.extend_from_slice(&(RC4_FIXTURE_P as u32).to_le_bytes());
2301 input.extend_from_slice(RC4_FIXTURE_ID0);
2302 let mut d = rc4_fixture_md5(&input);
2303 for _ in 0..50 {
2304 d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
2305 }
2306 d[..RC4_FIXTURE_KEY_LEN].to_vec()
2307 }
2308
2309 fn rc4_fixture_user_entry(key: &[u8]) -> Vec<u8> {
2311 let mut input = Vec::new();
2312 input.extend_from_slice(&RC4_FIXTURE_PAD);
2313 input.extend_from_slice(RC4_FIXTURE_ID0);
2314 let mut x = rc4_fixture_md5(&input).to_vec();
2315 x = rc4_fixture_rc4(key, &x);
2316 for i in 1u8..=19 {
2317 let k: Vec<u8> = key.iter().map(|b| b ^ i).collect();
2318 x = rc4_fixture_rc4(&k, &x);
2319 }
2320 x.resize(32, 0); x
2322 }
2323
2324 fn rc4_fixture_obj_key(key: &[u8], num: u32, gen: u16) -> Vec<u8> {
2325 let mut input = key.to_vec();
2326 input.extend_from_slice(&num.to_le_bytes()[..3]);
2327 input.extend_from_slice(&gen.to_le_bytes()[..2]);
2328 rc4_fixture_md5(&input)[..(key.len() + 5).min(16)].to_vec()
2329 }
2330
2331 fn rc4_fixture_hexstr(b: &[u8]) -> String {
2332 let mut s = String::from("<");
2333 for x in b {
2334 s.push_str(&format!("{x:02x}"));
2335 }
2336 s.push('>');
2337 s
2338 }
2339
2340 fn rc4_encrypted_fixture() -> Vec<u8> {
2344 let o = rc4_fixture_owner_entry();
2345 let key = rc4_fixture_file_key(&o);
2346 let u = rc4_fixture_user_entry(&key);
2347 let msg = rc4_fixture_rc4(&rc4_fixture_obj_key(&key, 3, 0), b"Top secret message");
2348
2349 let mut b = PdfBuilder::new().version(1, 4);
2350 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2351 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
2352 b.object(3, &format!("<< /Msg {} >>", rc4_fixture_hexstr(&msg)));
2353 b.object(
2354 9,
2355 &format!(
2356 "<< /Filter /Standard /V 2 /R 3 /Length 128 /P {} /O {} /U {} >>",
2357 RC4_FIXTURE_P,
2358 rc4_fixture_hexstr(&o),
2359 rc4_fixture_hexstr(&u)
2360 ),
2361 );
2362 let trailer = format!(
2363 "/Encrypt 9 0 R /ID [{}{}]",
2364 rc4_fixture_hexstr(RC4_FIXTURE_ID0),
2365 rc4_fixture_hexstr(RC4_FIXTURE_ID0)
2366 );
2367 b.trailer_extra(&trailer).build(1)
2368 }
2369
2370 #[test]
2372 fn encrypted_generation_mismatch_still_decrypts() {
2373 let doc = Document::load(rc4_encrypted_fixture()).expect("empty password opens the file");
2383 let obj3 = doc.get(ObjRef { num: 3, gen: 7 }).unwrap();
2384 let msg = obj3
2385 .as_dict()
2386 .unwrap()
2387 .get("Msg")
2388 .unwrap()
2389 .as_str_bytes()
2390 .unwrap();
2391 assert_eq!(
2392 msg, b"Top secret message",
2393 "decrypted using the file's real gen (0), not the mismatched request (7)"
2394 );
2395 }
2396
2397 #[test]
2399 fn objstm_doc_fixture_loads_and_resolves_members() {
2400 let data = objstm_doc(&[(7, "<< /Marker (inside) >>")]);
2401 let doc = Document::load(data).unwrap();
2402 assert_eq!(doc.page_count(), 1);
2403 let member = doc.get(ObjRef { num: 7, gen: 0 }).unwrap();
2404 let text = member.as_dict().unwrap().get("Marker").unwrap();
2405 assert_eq!(text.as_str_bytes(), Some(&b"inside"[..]));
2406 assert!(matches!(
2408 doc.xref().get(7),
2409 Some(XrefEntry::InStream { stream_num: 4, .. })
2410 ));
2411 }
2412
2413 #[test]
2416 fn page_from_parts_exposes_its_accessors() {
2417 let mut dict = Dict::default();
2418 dict.insert(Name("Type".into()), Object::Name(Name("Page".into())));
2419 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2420 let obj_ref = ObjRef { num: 7, gen: 0 };
2421
2422 let page = Page::from_parts(
2423 3,
2424 media,
2425 media,
2426 90,
2427 Dict::default(),
2428 dict.clone(),
2429 Some(obj_ref),
2430 );
2431
2432 assert_eq!(page.index, 3);
2433 assert_eq!(page.object_ref(), Some(obj_ref));
2434 assert_eq!(page.dict(), &dict);
2435 assert_eq!(page.size(), (400.0, 200.0));
2437 assert_eq!(page.bleed_box, page.crop_box);
2439 assert_eq!(page.trim_box, page.crop_box);
2440 assert_eq!(page.art_box, page.crop_box);
2441 }
2442
2443 #[test]
2447 fn page_from_parts_normalizes_rotation() {
2448 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2449 let build = |rotate: i32| {
2450 Page::from_parts(
2451 0,
2452 media,
2453 media,
2454 rotate,
2455 Dict::default(),
2456 Dict::default(),
2457 None,
2458 )
2459 };
2460
2461 for (given, canonical) in [(-90, 270), (450, 90), (540, 180), (720, 0), (-360, 0)] {
2462 let page = build(given);
2463 assert_eq!(
2464 page.rotate, canonical,
2465 "from_parts must normalize /Rotate {given} to {canonical}"
2466 );
2467 assert_eq!(
2468 page.size(),
2469 build(canonical).size(),
2470 "/Rotate {given} must report the same size as {canonical}"
2471 );
2472 }
2473 }
2474
2475 #[test]
2480 fn page_size_handles_unnormalized_rotation() {
2481 let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2482 let mut page = Page::from_parts(0, media, media, 0, Dict::default(), Dict::default(), None);
2483
2484 for rotate in [90, 270, -90, -270, 450, 630] {
2485 page.rotate = rotate;
2486 assert_eq!(
2487 page.size(),
2488 (400.0, 200.0),
2489 "/Rotate {rotate} is a quarter turn and must swap the size"
2490 );
2491 }
2492 for rotate in [0, 180, -180, 360, 540, 720] {
2493 page.rotate = rotate;
2494 assert_eq!(
2495 page.size(),
2496 (200.0, 400.0),
2497 "/Rotate {rotate} is a half turn and must leave the size alone"
2498 );
2499 }
2500 }
2501}