1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
25
26use oxideav_core::vector::{
27 FillRule, Group, Node, Paint, Path, PathCommand, PathNode, Point, Rgba, Transform2D,
28 VectorFrame,
29};
30use oxideav_core::TimeBase;
31use oxideav_scene::{Metadata, Page, Scene};
32
33use crate::decrypt::{open_with_password, StandardHandler};
34use crate::error::PdfError;
35use crate::objects::{Dict, Object, ObjectId, Stream};
36use crate::pubsec::{
37 open_with_certificate, open_with_certificate_and_trust_store, PubSecCredential, TrustStore,
38};
39use crate::reader::content::{
40 parse_content_stream_full_with_tiling, parse_content_stream_full_with_type3, TilingPattern,
41 Type3Font,
42};
43use crate::reader::parse::Parser;
44use crate::reader::xref::{parse_xref, XrefEntry, XrefTable};
45
46pub struct DocumentReader<'a> {
56 input: &'a [u8],
57 xref: XrefTable,
58 cache: HashMap<ObjectId, Object>,
59 crypt: Option<StandardHandler>,
60 objstm_cache: HashMap<u32, ObjStmDecoded>,
70}
71
72struct ObjStmDecoded {
79 payload: Vec<u8>,
80 slots: Vec<(u32, usize)>,
83}
84
85impl<'a> DocumentReader<'a> {
86 pub fn open(input: &'a [u8]) -> Result<Self, PdfError> {
90 Self::open_with_password(input, b"")
91 }
92
93 pub fn open_with_password(input: &'a [u8], password: &[u8]) -> Result<Self, PdfError> {
102 let xref = parse_xref(input)?;
103 let crypt = build_crypt(&xref, input, password)?;
104 Ok(Self {
105 input,
106 xref,
107 cache: HashMap::new(),
108 objstm_cache: HashMap::new(),
109 crypt,
110 })
111 }
112
113 pub fn open_with_certificate(
120 input: &'a [u8],
121 credential: &PubSecCredential,
122 ) -> Result<Self, PdfError> {
123 let xref = parse_xref(input)?;
124 let crypt = build_crypt_pubsec(&xref, input, credential, None)?;
125 Ok(Self {
126 input,
127 xref,
128 cache: HashMap::new(),
129 objstm_cache: HashMap::new(),
130 crypt,
131 })
132 }
133
134 pub fn open_with_certificate_and_trust_store(
139 input: &'a [u8],
140 credential: &PubSecCredential,
141 trust_store: &TrustStore,
142 ) -> Result<Self, PdfError> {
143 let xref = parse_xref(input)?;
144 let crypt = build_crypt_pubsec(&xref, input, credential, Some(trust_store))?;
145 Ok(Self {
146 input,
147 xref,
148 cache: HashMap::new(),
149 objstm_cache: HashMap::new(),
150 crypt,
151 })
152 }
153
154 pub fn xref(&self) -> &XrefTable {
156 &self.xref
157 }
158
159 pub fn is_encrypted(&self) -> bool {
162 self.crypt.is_some()
163 }
164
165 pub fn signatures(&mut self) -> Result<Vec<crate::reader::sig::PdfSignature>, PdfError> {
185 crate::reader::sig::signatures(self)
186 }
187
188 pub fn doc_timestamps(&mut self) -> Result<Vec<crate::reader::sig::PdfDocTimestamp>, PdfError> {
192 crate::reader::sig::doc_timestamps(self)
193 }
194
195 pub fn annotations(
214 &mut self,
215 ) -> Result<Vec<crate::reader::annotation::PdfAnnotation>, PdfError> {
216 crate::reader::annotation::annotations(self)
217 }
218
219 pub fn actions(&mut self) -> Result<Vec<crate::reader::actions::PdfAction>, PdfError> {
227 crate::reader::actions::actions(self)
228 }
229
230 pub fn optional_content(
238 &mut self,
239 ) -> Result<Option<crate::reader::ocg::OptionalContent>, PdfError> {
240 crate::reader::ocg::optional_content(self)
241 }
242
243 pub fn linearization(
254 &self,
255 ) -> Result<Option<crate::reader::linearize::LinearizationParams>, PdfError> {
256 crate::reader::linearize::LinearizationParams::parse(self.input)
257 }
258
259 pub fn verify_hierarchy(
265 &mut self,
266 ) -> Result<crate::reader::hierarchy::HierarchyReport, PdfError> {
267 crate::reader::hierarchy::verify_hierarchy(self)
268 }
269
270 pub fn pdfa_signals(&mut self) -> Result<crate::reader::pdfa::PdfACatalogSignals, PdfError> {
278 crate::reader::pdfa::pdfa_signals(self)
279 }
280
281 pub fn pdfa_conformance(&mut self) -> Result<crate::reader::pdfa::PdfAConformance, PdfError> {
290 let signals = self.pdfa_signals()?;
291 let xmp = self.xmp_packet()?;
292 Ok(crate::reader::pdfa::PdfAConformance::from_signals_and_xmp(
293 &signals,
294 xmp.as_ref(),
295 ))
296 }
297
298 pub fn xmp_packet(&mut self) -> Result<Option<crate::reader::xmp::XmpPacket>, PdfError> {
305 Ok(self
306 .xmp_metadata()?
307 .as_deref()
308 .map(crate::reader::xmp::XmpPacket::parse))
309 }
310
311 pub fn xmp_metadata(&mut self) -> Result<Option<Vec<u8>>, PdfError> {
312 let root_id = self.xref.root()?;
313 let catalog = self.resolve(root_id)?;
314 let Object::Dict(catalog) = catalog else {
315 return Err(PdfError::other(format!(
316 "PDF reader: /Root must be a dictionary (got {catalog:?})"
317 )));
318 };
319 let metadata_obj = catalog
320 .entries()
321 .iter()
322 .find(|(k, _)| k == "Metadata")
323 .map(|(_, v)| v.clone());
324 let Some(metadata_obj) = metadata_obj else {
325 return Ok(None);
326 };
327 let stream_obj = match metadata_obj {
331 Object::Reference(id) => self.resolve(id)?,
332 other => other,
333 };
334 let Object::Stream(s) = stream_obj else {
335 return Err(PdfError::other(format!(
336 "PDF reader: catalog /Metadata must resolve to a Stream (got {stream_obj:?})"
337 )));
338 };
339 Ok(Some(decode_stream(&s)?))
340 }
341
342 pub fn resolve(&mut self, id: ObjectId) -> Result<Object, PdfError> {
353 if let Some(o) = self.cache.get(&id) {
354 return Ok(o.clone());
355 }
356 if let Some(XrefEntry::Compressed {
363 obj_stream_id,
364 index_within_stream,
365 }) = self.xref.entries.get(&id.number).copied()
366 {
367 let body = self.resolve_compressed(id, obj_stream_id, index_within_stream)?;
368 self.cache.insert(id, body.clone());
369 return Ok(body);
370 }
371 let off = self
372 .xref
373 .offset_of(id)
374 .ok_or_else(|| PdfError::other(format!("PDF reader: object {id:?} not in xref")))?;
375 let mut p = Parser::new(self.input);
376 p.lexer_mut().seek(off as usize);
377 let input = self.input;
384 let xref_snapshot = &self.xref;
385 let id_being_parsed = id;
386 let mut resolver = move |length_ref_id: ObjectId| -> Result<i64, PdfError> {
387 resolve_indirect_length(input, xref_snapshot, length_ref_id, id_being_parsed)
388 };
389 let (parsed_id, mut body) = p.parse_indirect_with_length_resolver(&mut resolver)?;
390 if parsed_id != id {
391 return Err(PdfError::other(format!(
392 "PDF reader: xref points to wrong object — wanted {id:?}, got {parsed_id:?}"
393 )));
394 }
395 if let Some(crypt) = &self.crypt {
396 decrypt_object_in_place(&mut body, id, crypt)?;
397 }
398 self.cache.insert(id, body.clone());
399 Ok(body)
400 }
401
402 fn resolve_compressed(
416 &mut self,
417 wanted: ObjectId,
418 obj_stream_num: u32,
419 index_within_stream: u32,
420 ) -> Result<Object, PdfError> {
421 if let Some(decoded) = self.objstm_cache.get(&obj_stream_num) {
423 return Self::slot_from_decoded(decoded, wanted, index_within_stream);
424 }
425 let decoded = self.decode_objstm_container(wanted, obj_stream_num)?;
426 let body = Self::slot_from_decoded(&decoded, wanted, index_within_stream)?;
427 self.objstm_cache.insert(obj_stream_num, decoded);
428 Ok(body)
429 }
430
431 fn decode_objstm_container(
436 &mut self,
437 wanted: ObjectId,
438 obj_stream_num: u32,
439 ) -> Result<ObjStmDecoded, PdfError> {
440 let container_id = ObjectId::new(obj_stream_num);
441 if let Some(XrefEntry::Compressed { .. }) = self.xref.entries.get(&container_id.number) {
454 return Err(PdfError::other(format!(
455 "PDF reader: ObjStm container {container_id:?} (for compressed object \
456 {wanted:?}) is itself declared as a Type-2 compressed entry in the xref \
457 — forbidden by ISO 32000-1 §7.5.7"
458 )));
459 }
460 let container = self.resolve(container_id)?;
461 let Object::Stream(s) = container else {
462 return Err(PdfError::other(format!(
463 "PDF reader: object {wanted:?} expected its container {container_id:?} to be a Stream"
464 )));
465 };
466 let dict = &s.dict;
468 let lookup = |k: &str| {
469 dict.entries()
470 .iter()
471 .find(|(kk, _)| kk == k)
472 .map(|(_, v)| v.clone())
473 };
474 if !matches!(lookup("Type"), Some(Object::Name(ref n)) if n == "ObjStm") {
475 return Err(PdfError::other(format!(
476 "PDF reader: container {container_id:?} is not /Type /ObjStm"
477 )));
478 }
479 let n = match lookup("N") {
480 Some(Object::Integer(v)) if v >= 0 => v as u32,
481 other => {
482 return Err(PdfError::other(format!(
483 "PDF reader: ObjStm /N must be a non-negative integer (got {other:?})"
484 )))
485 }
486 };
487 let first = match lookup("First") {
488 Some(Object::Integer(v)) if v >= 0 => v as usize,
489 other => {
490 return Err(PdfError::other(format!(
491 "PDF reader: ObjStm /First must be a non-negative integer (got {other:?})"
492 )))
493 }
494 };
495 let payload = decode_stream(&s)?;
496 if first > payload.len() {
497 return Err(PdfError::other(format!(
498 "PDF reader: ObjStm /First {first} exceeds payload length {}",
499 payload.len()
500 )));
501 }
502 let header_bytes = &payload[..first];
503 let mut hp = Parser::new(header_bytes);
507 let mut slots: Vec<(u32, usize)> = Vec::with_capacity(n as usize);
508 for i in 0..n {
509 let on = hp.parse_object()?.ok_or_else(|| {
510 PdfError::other(format!(
511 "PDF reader: ObjStm header truncated at pair {i} (obj_num)"
512 ))
513 })?;
514 let off = hp.parse_object()?.ok_or_else(|| {
515 PdfError::other(format!(
516 "PDF reader: ObjStm header truncated at pair {i} (offset)"
517 ))
518 })?;
519 let (Object::Integer(num), Object::Integer(o)) = (on, off) else {
520 return Err(PdfError::other(format!(
521 "PDF reader: ObjStm header pair {i} must be two integers"
522 )));
523 };
524 if num < 1 || o < 0 {
525 return Err(PdfError::other(format!(
526 "PDF reader: ObjStm header pair {i} out of range ({num}, {o})"
527 )));
528 }
529 let abs_off = first
530 .checked_add(o as usize)
531 .ok_or_else(|| PdfError::other("PDF reader: ObjStm offset overflow"))?;
532 if abs_off > payload.len() {
533 return Err(PdfError::other(format!(
534 "PDF reader: ObjStm body offset {abs_off} past payload length {}",
535 payload.len()
536 )));
537 }
538 slots.push((num as u32, abs_off));
539 }
540 Ok(ObjStmDecoded { payload, slots })
541 }
542
543 fn slot_from_decoded(
549 decoded: &ObjStmDecoded,
550 wanted: ObjectId,
551 index_within_stream: u32,
552 ) -> Result<Object, PdfError> {
553 let n = decoded.slots.len() as u32;
554 if index_within_stream >= n {
555 return Err(PdfError::other(format!(
556 "PDF reader: ObjStm index {index_within_stream} out of range (N={n})"
557 )));
558 }
559 let (header_num, abs_off) = decoded.slots[index_within_stream as usize];
560 if header_num != wanted.number {
561 return Err(PdfError::other(format!(
562 "PDF reader: ObjStm slot {index_within_stream} declares object {header_num},\
563 but xref expected {}",
564 wanted.number
565 )));
566 }
567 let mut bp = Parser::new(&decoded.payload[abs_off..]);
571 let body = bp
572 .parse_object()?
573 .ok_or_else(|| PdfError::other("PDF reader: ObjStm body parse returned EOF"))?;
574 Ok(body)
575 }
576
577 pub fn deref(&mut self, obj: Object) -> Result<Object, PdfError> {
580 let mut cur = obj;
581 let mut hops = 0;
582 while let Object::Reference(id) = cur {
583 cur = self.resolve(id)?;
584 hops += 1;
585 if hops > 16 {
586 return Err(PdfError::other(
587 "PDF reader: indirect-reference chain too deep (>16 hops)",
588 ));
589 }
590 }
591 Ok(cur)
592 }
593}
594
595fn resolve_indirect_length(
614 input: &[u8],
615 xref: &XrefTable,
616 length_ref: ObjectId,
617 containing_stream: ObjectId,
618) -> Result<i64, PdfError> {
619 if length_ref == containing_stream {
620 return Err(PdfError::other(format!(
621 "PDF reader: stream {containing_stream:?} /Length refers to itself"
622 )));
623 }
624 if let Some(XrefEntry::Compressed { .. }) = xref.entries.get(&length_ref.number) {
632 return Err(PdfError::other(format!(
633 "PDF reader: indirect /Length {length_ref:?} lives in an object stream \
634 — not supported (ISO 32000-1 §7.5.7); the length object should be a \
635 direct uncompressed integer"
636 )));
637 }
638 let off = xref.offset_of(length_ref).ok_or_else(|| {
639 PdfError::other(format!(
640 "PDF reader: indirect /Length {length_ref:?} (for stream \
641 {containing_stream:?}) is not in the xref table"
642 ))
643 })?;
644 let mut p = Parser::new(input);
645 p.lexer_mut().seek(off as usize);
646 let (parsed_id, body) = p.parse_indirect()?;
651 if parsed_id != length_ref {
652 return Err(PdfError::other(format!(
653 "PDF reader: xref points to wrong object for indirect /Length — \
654 wanted {length_ref:?}, got {parsed_id:?}"
655 )));
656 }
657 match body {
658 Object::Integer(n) => Ok(n),
659 other => Err(PdfError::other(format!(
660 "PDF reader: indirect /Length {length_ref:?} target must be an \
661 integer (got {other:?})"
662 ))),
663 }
664}
665
666fn build_crypt(
671 xref: &XrefTable,
672 input: &[u8],
673 password: &[u8],
674) -> Result<Option<StandardHandler>, PdfError> {
675 let encrypt_ref = xref.trailer.entries().iter().find(|(k, _)| k == "Encrypt");
676 let Some((_, encrypt_obj)) = encrypt_ref else {
677 return Ok(None);
678 };
679
680 let encrypt_dict = match encrypt_obj {
682 Object::Dict(d) => d.clone(),
683 Object::Reference(id) => {
684 let off = xref
685 .offset_of(*id)
686 .ok_or_else(|| PdfError::other("PDF reader: /Encrypt refers to missing object"))?;
687 let mut p = Parser::new(input);
688 p.lexer_mut().seek(off as usize);
689 let (_, body) = p.parse_indirect()?;
690 match body {
691 Object::Dict(d) => d,
692 other => {
693 return Err(PdfError::other(format!(
694 "PDF reader: /Encrypt must resolve to a dict (got {other:?})"
695 )))
696 }
697 }
698 }
699 other => {
700 return Err(PdfError::other(format!(
701 "PDF reader: /Encrypt must be a dict or reference (got {other:?})"
702 )))
703 }
704 };
705
706 let id_obj = xref
709 .trailer
710 .entries()
711 .iter()
712 .find(|(k, _)| k == "ID")
713 .map(|(_, v)| v.clone())
714 .ok_or_else(|| PdfError::other("PDF reader: encrypted PDF missing /ID in trailer"))?;
715 let Object::Array(id_items) = id_obj else {
716 return Err(PdfError::other("PDF reader: /ID must be an array"));
717 };
718 if id_items.is_empty() {
719 return Err(PdfError::other("PDF reader: trailer /ID array is empty"));
720 }
721 let file_id = match &id_items[0] {
722 Object::LiteralString(s) | Object::HexString(s) => s.clone(),
723 other => {
724 return Err(PdfError::other(format!(
725 "PDF reader: /ID[0] must be a string (got {other:?})"
726 )))
727 }
728 };
729
730 let handler = open_with_password(&encrypt_dict, &file_id, password)?;
731 handler
732 .ok_or_else(|| {
733 PdfError::other("PDF reader: wrong password (or PDF requires owner password)")
734 })
735 .map(Some)
736}
737
738fn build_crypt_pubsec(
745 xref: &XrefTable,
746 input: &[u8],
747 credential: &PubSecCredential,
748 trust_store: Option<&TrustStore>,
749) -> Result<Option<StandardHandler>, PdfError> {
750 let encrypt_ref = xref.trailer.entries().iter().find(|(k, _)| k == "Encrypt");
751 let Some((_, encrypt_obj)) = encrypt_ref else {
752 return Ok(None);
753 };
754 let encrypt_dict = match encrypt_obj {
755 Object::Dict(d) => d.clone(),
756 Object::Reference(id) => {
757 let off = xref
758 .offset_of(*id)
759 .ok_or_else(|| PdfError::other("PDF reader: /Encrypt refers to missing object"))?;
760 let mut p = Parser::new(input);
761 p.lexer_mut().seek(off as usize);
762 let (_, body) = p.parse_indirect()?;
763 match body {
764 Object::Dict(d) => d,
765 other => {
766 return Err(PdfError::other(format!(
767 "PDF reader: /Encrypt must resolve to a dict (got {other:?})"
768 )))
769 }
770 }
771 }
772 other => {
773 return Err(PdfError::other(format!(
774 "PDF reader: /Encrypt must be a dict or reference (got {other:?})"
775 )))
776 }
777 };
778 let handler = match trust_store {
779 Some(store) => open_with_certificate_and_trust_store(&encrypt_dict, credential, store)?,
780 None => open_with_certificate(&encrypt_dict, credential)?,
781 };
782 handler
783 .ok_or_else(|| {
784 PdfError::other(
785 "PDF reader: certificate did not match any recipient in /Recipients (round-10)",
786 )
787 })
788 .map(Some)
789}
790
791fn decrypt_object_in_place(
805 obj: &mut Object,
806 id: ObjectId,
807 crypt: &StandardHandler,
808) -> Result<(), PdfError> {
809 match obj {
810 Object::LiteralString(s) | Object::HexString(s) => {
811 *s = crypt.decrypt_object(id, s)?;
812 }
813 Object::Array(items) => {
814 for item in items {
815 decrypt_object_in_place(item, id, crypt)?;
816 }
817 }
818 Object::Dict(d) => {
819 decrypt_dict_in_place(d, id, crypt)?;
820 }
821 Object::Stream(s) => {
822 decrypt_dict_in_place(&mut s.dict, id, crypt)?;
827 if has_identity_crypt_filter(&s.dict) {
829 return Ok(());
830 }
831 s.data = crypt.decrypt_object(id, &s.data)?;
835 }
836 _ => {}
838 }
839 Ok(())
840}
841
842fn has_identity_crypt_filter(dict: &Dict) -> bool {
850 let filter = dict
851 .entries()
852 .iter()
853 .find(|(k, _)| k == "Filter")
854 .map(|(_, v)| v);
855 let parms = dict
856 .entries()
857 .iter()
858 .find(|(k, _)| k == "DecodeParms")
859 .map(|(_, v)| v);
860
861 let crypt_pos: Option<usize> = match filter {
862 Some(Object::Name(s)) if s == "Crypt" => Some(0),
863 Some(Object::Array(items)) => items
864 .iter()
865 .position(|f| matches!(f, Object::Name(n) if n == "Crypt")),
866 _ => None,
867 };
868 let Some(idx) = crypt_pos else {
869 return false;
870 };
871
872 let parms_dict = match parms {
874 Some(Object::Dict(d)) if idx == 0 => Some(d.clone()),
875 Some(Object::Array(items)) => match items.get(idx) {
876 Some(Object::Dict(d)) => Some(d.clone()),
877 _ => None,
878 },
879 _ => None,
880 };
881 let Some(d) = parms_dict else {
882 return true;
885 };
886 match d
887 .entries()
888 .iter()
889 .find(|(k, _)| k == "Name")
890 .map(|(_, v)| v)
891 {
892 Some(Object::Name(s)) => s == "Identity",
893 None => true,
895 _ => false,
896 }
897}
898
899fn decrypt_dict_in_place(
900 d: &mut Dict,
901 id: ObjectId,
902 crypt: &StandardHandler,
903) -> Result<(), PdfError> {
904 let mut new_entries: Vec<(String, Object)> = Vec::with_capacity(d.entries().len());
907 for (k, v) in d.entries() {
908 let mut v = v.clone();
909 decrypt_object_in_place(&mut v, id, crypt)?;
910 new_entries.push((k.clone(), v));
911 }
912 *d = Dict::default();
913 for (k, v) in new_entries {
914 d.set(&k, v);
915 }
916 Ok(())
917}
918
919pub fn read_pdf_to_scene(input: &[u8]) -> Result<Scene, PdfError> {
931 read_pdf_to_scene_with_password(input, b"")
932}
933
934pub fn read_pdf_to_scene_with_password(input: &[u8], password: &[u8]) -> Result<Scene, PdfError> {
942 let reader = DocumentReader::open_with_password(input, password)?;
943 decode_to_scene(reader)
944}
945
946pub fn read_pdf_to_scene_with_certificate(
956 input: &[u8],
957 credential: &PubSecCredential,
958) -> Result<Scene, PdfError> {
959 let reader = DocumentReader::open_with_certificate(input, credential)?;
960 decode_to_scene(reader)
961}
962
963pub fn read_pdf_to_scene_with_certificate_and_trust_store(
972 input: &[u8],
973 credential: &PubSecCredential,
974 trust_store: &TrustStore,
975) -> Result<Scene, PdfError> {
976 let reader =
977 DocumentReader::open_with_certificate_and_trust_store(input, credential, trust_store)?;
978 decode_to_scene(reader)
979}
980
981fn decode_to_scene(mut reader: DocumentReader<'_>) -> Result<Scene, PdfError> {
982 let root_id = reader.xref.root()?;
984 let catalog = reader.resolve(root_id)?;
985 let Object::Dict(catalog) = catalog else {
986 return Err(PdfError::other(format!(
987 "PDF reader: /Root must be a dictionary (got {catalog:?})"
988 )));
989 };
990 let pages_ref = catalog
991 .entries()
992 .iter()
993 .find(|(k, _)| k == "Pages")
994 .map(|(_, v)| v.clone())
995 .ok_or_else(|| PdfError::other("PDF reader: catalog missing /Pages"))?;
996 let Object::Reference(pages_root_id) = pages_ref else {
997 return Err(PdfError::other(format!(
998 "PDF reader: catalog /Pages must be an indirect reference (got {pages_ref:?})"
999 )));
1000 };
1001
1002 let mut leaves = Vec::new();
1005 walk_pages_tree(&mut reader, pages_root_id, &mut leaves)?;
1006 if leaves.is_empty() {
1007 return Err(PdfError::other(
1008 "PDF reader: /Pages tree contained no Page leaves",
1009 ));
1010 }
1011
1012 let optional_content = crate::reader::ocg::optional_content(&mut reader).unwrap_or(None);
1017
1018 let mut scene_pages = Vec::with_capacity(leaves.len());
1020 for leaf_id in leaves {
1021 scene_pages.push(decode_page(
1022 &mut reader,
1023 leaf_id,
1024 optional_content.as_ref(),
1025 )?);
1026 }
1027
1028 let metadata = if let Some(info_id) = reader.xref.info() {
1030 let info = reader.resolve(info_id)?;
1031 decode_metadata(info)?
1032 } else {
1033 Metadata::default()
1034 };
1035
1036 Ok(Scene {
1037 pages: Some(scene_pages),
1038 metadata,
1039 ..Scene::default()
1040 })
1041}
1042
1043const MAX_PAGES_TREE_DEPTH: u32 = 256;
1051
1052fn walk_pages_tree(
1053 reader: &mut DocumentReader<'_>,
1054 node_id: ObjectId,
1055 out: &mut Vec<ObjectId>,
1056) -> Result<(), PdfError> {
1057 let mut visited = HashSet::new();
1058 walk_pages_tree_inner(reader, node_id, out, &mut visited, 0)
1059}
1060
1061fn walk_pages_tree_inner(
1062 reader: &mut DocumentReader<'_>,
1063 node_id: ObjectId,
1064 out: &mut Vec<ObjectId>,
1065 visited: &mut HashSet<ObjectId>,
1066 depth: u32,
1067) -> Result<(), PdfError> {
1068 if depth > MAX_PAGES_TREE_DEPTH {
1069 return Err(PdfError::other(format!(
1070 "PDF reader: /Pages tree exceeds maximum depth ({MAX_PAGES_TREE_DEPTH})"
1071 )));
1072 }
1073 if !visited.insert(node_id) {
1078 return Err(PdfError::other(format!(
1079 "PDF reader: /Pages tree contains a cycle at {node_id:?}"
1080 )));
1081 }
1082 let node = reader.resolve(node_id)?;
1083 let Object::Dict(d) = node else {
1084 return Err(PdfError::other(format!(
1085 "PDF reader: pages-tree node {node_id:?} is not a dict"
1086 )));
1087 };
1088 let kind = d
1089 .entries()
1090 .iter()
1091 .find(|(k, _)| k == "Type")
1092 .and_then(|(_, v)| match v {
1093 Object::Name(s) => Some(s.as_str()),
1094 _ => None,
1095 });
1096 match kind {
1097 Some("Page") => {
1098 out.push(node_id);
1099 Ok(())
1100 }
1101 Some("Pages") => {
1102 let kids = d
1103 .entries()
1104 .iter()
1105 .find(|(k, _)| k == "Kids")
1106 .map(|(_, v)| v.clone())
1107 .ok_or_else(|| {
1108 PdfError::other(format!("PDF reader: /Pages node {node_id:?} missing /Kids"))
1109 })?;
1110 let Object::Array(items) = kids else {
1111 return Err(PdfError::other(format!(
1112 "PDF reader: /Kids must be an array on {node_id:?}"
1113 )));
1114 };
1115 for item in items {
1116 if let Object::Reference(id) = item {
1117 walk_pages_tree_inner(reader, id, out, visited, depth + 1)?;
1118 }
1119 }
1120 Ok(())
1121 }
1122 _ => Err(PdfError::other(format!(
1123 "PDF reader: pages-tree node {node_id:?} has unknown /Type {kind:?}"
1124 ))),
1125 }
1126}
1127
1128const MAX_PAGE_TREE_DEPTH: usize = 64;
1134
1135fn resolve_inheritable_attr(
1150 reader: &mut DocumentReader<'_>,
1151 page_dict: &Dict,
1152 key: &str,
1153) -> Result<Option<Object>, PdfError> {
1154 if let Some((_, v)) = page_dict.entries().iter().find(|(k, _)| k == key) {
1155 return Ok(Some(v.clone()));
1156 }
1157 let mut parent = page_dict
1160 .entries()
1161 .iter()
1162 .find(|(k, _)| k == "Parent")
1163 .map(|(_, v)| v.clone());
1164 let mut visited: HashSet<ObjectId> = HashSet::new();
1165 let mut depth = 0;
1166 while let Some(Object::Reference(parent_id)) = parent {
1167 if depth >= MAX_PAGE_TREE_DEPTH || !visited.insert(parent_id) {
1168 break;
1169 }
1170 depth += 1;
1171 let Object::Dict(node) = reader.resolve(parent_id)? else {
1172 break;
1173 };
1174 if let Some((_, v)) = node.entries().iter().find(|(k, _)| k == key) {
1175 return Ok(Some(v.clone()));
1176 }
1177 parent = node
1178 .entries()
1179 .iter()
1180 .find(|(k, _)| k == "Parent")
1181 .map(|(_, v)| v.clone());
1182 }
1183 Ok(None)
1184}
1185
1186fn decode_page(
1187 reader: &mut DocumentReader<'_>,
1188 page_id: ObjectId,
1189 optional_content: Option<&crate::reader::ocg::OptionalContent>,
1190) -> Result<Page, PdfError> {
1191 let page_obj = reader.resolve(page_id)?;
1192 let Object::Dict(page_dict) = page_obj else {
1193 return Err(PdfError::other(format!(
1194 "PDF reader: page {page_id:?} is not a dict"
1195 )));
1196 };
1197
1198 let media_box = resolve_inheritable_attr(reader, &page_dict, "MediaBox")?;
1203 let (width, height) = match media_box {
1204 Some(Object::Array(items)) if items.len() == 4 => {
1205 let llx = number_to_f32(&items[0])?;
1206 let lly = number_to_f32(&items[1])?;
1207 let urx = number_to_f32(&items[2])?;
1208 let ury = number_to_f32(&items[3])?;
1209 ((urx - llx).abs(), (ury - lly).abs())
1210 }
1211 Some(other) => {
1212 return Err(PdfError::other(format!(
1213 "PDF reader: /MediaBox must be a 4-array (got {other:?})"
1214 )));
1215 }
1216 None => {
1217 (595.0, 842.0)
1220 }
1221 };
1222
1223 let contents_obj = page_dict
1225 .entries()
1226 .iter()
1227 .find(|(k, _)| k == "Contents")
1228 .map(|(_, v)| v.clone());
1229 let content_bytes = match contents_obj {
1230 Some(Object::Reference(id)) => extract_stream_data(reader, id)?,
1231 Some(Object::Array(items)) => {
1232 let mut all = Vec::new();
1233 for item in items {
1234 if let Object::Reference(id) = item {
1235 all.extend_from_slice(&extract_stream_data(reader, id)?);
1236 all.push(b'\n');
1237 }
1238 }
1239 all
1240 }
1241 Some(other) => {
1242 return Err(PdfError::other(format!(
1243 "PDF reader: /Contents must be a Stream or array (got {other:?})"
1244 )));
1245 }
1246 None => Vec::new(),
1247 };
1248
1249 let resources_obj = resolve_inheritable_attr(reader, &page_dict, "Resources")?;
1256 let resources_dict = match resources_obj {
1257 Some(Object::Reference(id)) => match reader.resolve(id)? {
1258 Object::Dict(d) => Some(d),
1259 _ => None,
1260 },
1261 Some(Object::Dict(d)) => Some(d),
1262 _ => None,
1263 };
1264 let ext_gstate_dict = if let Some(rdict) = resources_dict.as_ref() {
1265 resolve_ext_gstate(reader, rdict)?
1266 } else {
1267 None
1268 };
1269 let fonts_dict = if let Some(rdict) = resources_dict.as_ref() {
1270 resolve_font_resources(reader, rdict)?
1271 } else {
1272 None
1273 };
1274 let shading_dict = if let Some(rdict) = resources_dict.as_ref() {
1275 resolve_shading_resources(reader, rdict)?
1276 } else {
1277 None
1278 };
1279 let color_space_dict = if let Some(rdict) = resources_dict.as_ref() {
1280 resolve_color_space_resources(reader, rdict)?
1281 } else {
1282 None
1283 };
1284 let properties_dict = if let Some(rdict) = resources_dict.as_ref() {
1285 resolve_properties_resources(reader, rdict)?
1286 } else {
1287 None
1288 };
1289 let xobject_forms = if let Some(rdict) = resources_dict.as_ref() {
1290 let mut seen = HashSet::new();
1291 resolve_xobject_forms(reader, rdict, 0, &mut seen)?
1292 } else {
1293 None
1294 };
1295 let pattern_dict = if let Some(rdict) = resources_dict.as_ref() {
1296 resolve_pattern_resources(reader, rdict)?
1297 } else {
1298 None
1299 };
1300 let tiling_patterns = if let Some(rdict) = resources_dict.as_ref() {
1301 resolve_tiling_patterns(reader, rdict, 0)?
1302 } else {
1303 None
1304 };
1305 let type3_fonts = if let Some(rdict) = resources_dict.as_ref() {
1306 resolve_type3_fonts(reader, rdict, 0)?
1307 } else {
1308 None
1309 };
1310
1311 let parsed = parse_content_stream_full_with_type3(
1312 &content_bytes,
1313 ext_gstate_dict.as_ref(),
1314 fonts_dict.as_ref(),
1315 shading_dict.as_ref(),
1316 color_space_dict.as_ref(),
1317 properties_dict.as_ref(),
1318 xobject_forms.as_ref(),
1319 pattern_dict.as_ref(),
1320 tiling_patterns.as_ref(),
1321 type3_fonts.as_ref(),
1322 )?;
1323 let mut root = parsed.root;
1324
1325 let annot_groups = resolve_annotation_appearances(reader, &page_dict, optional_content)?;
1330 if !annot_groups.is_empty() {
1331 if root.transform != Transform2D::identity() || root.clip.is_some() || root.opacity != 1.0 {
1336 root = Group {
1337 children: vec![Node::Group(root)],
1338 ..Group::default()
1339 };
1340 }
1341 root.children
1342 .extend(annot_groups.into_iter().map(Node::Group));
1343 }
1344
1345 let mut page = Page::new(width, height);
1346 if let Some(Object::Integer(deg)) = resolve_inheritable_attr(reader, &page_dict, "Rotate")? {
1352 if deg % 90 == 0 {
1353 page.orientation = (deg.rem_euclid(360)) as u16;
1354 }
1355 }
1356 page.content = VectorFrame {
1357 width,
1358 height,
1359 view_box: None,
1360 root,
1361 pts: None,
1362 time_base: TimeBase::new(1, 1),
1363 };
1364 Ok(page)
1365}
1366
1367fn resolve_ext_gstate(
1379 reader: &mut DocumentReader<'_>,
1380 resources: &Dict,
1381) -> Result<Option<Dict>, PdfError> {
1382 let ext_obj = resources
1383 .entries()
1384 .iter()
1385 .find(|(k, _)| k == "ExtGState")
1386 .map(|(_, v)| v.clone());
1387 let ext_obj = match ext_obj {
1388 Some(Object::Reference(id)) => reader.resolve(id)?,
1389 Some(other) => other,
1390 None => return Ok(None),
1391 };
1392 let Object::Dict(ext_dict) = ext_obj else {
1393 return Ok(None);
1394 };
1395 let mut out = Dict::new();
1399 for (name, value) in ext_dict.entries() {
1400 let resolved = match value {
1401 Object::Reference(id) => reader.resolve(*id)?,
1402 other => other.clone(),
1403 };
1404 if let Object::Dict(d) = resolved {
1405 out.set(name, Object::Dict(d));
1406 }
1407 }
1408 Ok(Some(out))
1409}
1410
1411fn resolve_font_resources(
1430 reader: &mut DocumentReader<'_>,
1431 resources: &Dict,
1432) -> Result<Option<Dict>, PdfError> {
1433 let font_obj = resources
1434 .entries()
1435 .iter()
1436 .find(|(k, _)| k == "Font")
1437 .map(|(_, v)| v.clone());
1438 let font_obj = match font_obj {
1439 Some(Object::Reference(id)) => reader.resolve(id)?,
1440 Some(other) => other,
1441 None => return Ok(None),
1442 };
1443 let Object::Dict(font_dict) = font_obj else {
1444 return Ok(None);
1445 };
1446 let mut out = Dict::new();
1447 for (name, value) in font_dict.entries() {
1448 let resolved = match value {
1449 Object::Reference(id) => reader.resolve(*id)?,
1450 other => other.clone(),
1451 };
1452 if let Object::Dict(mut d) = resolved {
1453 resolve_font_widths(reader, &mut d)?;
1459 out.set(name, Object::Dict(d));
1460 }
1461 }
1462 Ok(Some(out))
1463}
1464
1465fn resolve_font_widths(reader: &mut DocumentReader<'_>, font: &mut Dict) -> Result<(), PdfError> {
1478 if let Some(Object::Reference(id)) =
1480 font.entries()
1481 .iter()
1482 .find_map(|(k, v)| if k == "Widths" { Some(v.clone()) } else { None })
1483 {
1484 let resolved = reader.resolve(id)?;
1485 font.set("Widths", resolved);
1486 }
1487 if let Some(Object::Reference(id)) = font.entries().iter().find_map(|(k, v)| {
1489 if k == "FontDescriptor" {
1490 Some(v.clone())
1491 } else {
1492 None
1493 }
1494 }) {
1495 if let Ok(Object::Dict(d)) = reader.resolve(id) {
1496 font.set("FontDescriptor", Object::Dict(d));
1497 }
1498 }
1499 let descendant_ref = font.entries().iter().find_map(|(k, v)| {
1501 if k == "DescendantFonts" {
1502 Some(v.clone())
1503 } else {
1504 None
1505 }
1506 });
1507 if let Some(obj) = descendant_ref {
1508 let array_obj = match obj {
1509 Object::Reference(id) => reader.resolve(id)?,
1510 other => other,
1511 };
1512 let cid_ref = match array_obj {
1514 Object::Array(items) => items.into_iter().next(),
1515 Object::Dict(d) => Some(Object::Dict(d)),
1516 _ => None,
1517 };
1518 if let Some(cid_obj) = cid_ref {
1519 let cid_obj = match cid_obj {
1520 Object::Reference(id) => reader.resolve(id)?,
1521 other => other,
1522 };
1523 if let Object::Dict(mut cid_font) = cid_obj {
1524 if let Some(Object::Reference(id)) = cid_font.entries().iter().find_map(|(k, v)| {
1526 if k == "W" {
1527 Some(v.clone())
1528 } else {
1529 None
1530 }
1531 }) {
1532 let resolved = reader.resolve(id)?;
1533 cid_font.set("W", resolved);
1534 }
1535 font.set("DescendantFonts", Object::Dict(cid_font));
1536 }
1537 }
1538 }
1539 Ok(())
1540}
1541
1542fn resolve_shading_resources(
1560 reader: &mut DocumentReader<'_>,
1561 resources: &Dict,
1562) -> Result<Option<Dict>, PdfError> {
1563 let shading_obj = resources
1564 .entries()
1565 .iter()
1566 .find(|(k, _)| k == "Shading")
1567 .map(|(_, v)| v.clone());
1568 let shading_obj = match shading_obj {
1569 Some(Object::Reference(id)) => reader.resolve(id)?,
1570 Some(other) => other,
1571 None => return Ok(None),
1572 };
1573 let Object::Dict(shading_dict) = shading_obj else {
1574 return Ok(None);
1575 };
1576 let mut out = Dict::new();
1577 for (name, value) in shading_dict.entries() {
1578 let resolved = match value {
1579 Object::Reference(id) => reader.resolve(*id)?,
1580 other => other.clone(),
1581 };
1582 let d = match resolved {
1595 Object::Dict(d) => Some(d),
1596 Object::Stream(s) => {
1597 let mesh = decode_stream(&s)?;
1598 let mut d = s.dict;
1599 d.set("__MeshData", Object::HexString(mesh));
1600 Some(d)
1601 }
1602 _ => None,
1603 };
1604 if let Some(mut d) = d {
1605 if let Some((_, fobj)) = d
1606 .entries()
1607 .iter()
1608 .find(|(k, _)| k == "Function")
1609 .map(|(k, v)| (k.clone(), v.clone()))
1610 {
1611 let fobj = match fobj {
1618 Object::Reference(id) => reader.resolve(id)?,
1619 other => other,
1620 };
1621 let prepared = match fobj {
1622 Object::Array(items) => {
1623 let mut prepared = Vec::with_capacity(items.len());
1624 for f in items {
1625 prepared.push(prepare_function_object(reader, f)?);
1626 }
1627 Object::Array(prepared)
1628 }
1629 other => prepare_function_object(reader, other)?,
1630 };
1631 d.set("Function", prepared);
1632 }
1633 out.set(name, Object::Dict(d));
1634 }
1635 }
1636 Ok(Some(out))
1637}
1638
1639fn resolve_pattern_resources(
1654 reader: &mut DocumentReader<'_>,
1655 resources: &Dict,
1656) -> Result<Option<Dict>, PdfError> {
1657 let pat_obj = resources
1658 .entries()
1659 .iter()
1660 .find(|(k, _)| k == "Pattern")
1661 .map(|(_, v)| v.clone());
1662 let pat_obj = match pat_obj {
1663 Some(Object::Reference(id)) => reader.resolve(id)?,
1664 Some(other) => other,
1665 None => return Ok(None),
1666 };
1667 let Object::Dict(pat_dict) = pat_obj else {
1668 return Ok(None);
1669 };
1670 let mut out = Dict::new();
1671 for (name, value) in pat_dict.entries() {
1672 let resolved = match value {
1673 Object::Reference(id) => reader.resolve(*id)?,
1674 other => other.clone(),
1675 };
1676 let mut d = match resolved {
1680 Object::Dict(d) => d,
1681 Object::Stream(s) => s.dict,
1682 _ => continue,
1683 };
1684 if let Some((_, sh)) = d
1688 .entries()
1689 .iter()
1690 .find(|(k, _)| k == "Shading")
1691 .map(|(k, v)| (k.clone(), v.clone()))
1692 {
1693 let sh = match sh {
1694 Object::Reference(id) => reader.resolve(id)?,
1695 other => other,
1696 };
1697 if let Object::Dict(mut shading) = sh {
1698 if let Some((_, fobj)) = shading
1699 .entries()
1700 .iter()
1701 .find(|(k, _)| k == "Function")
1702 .map(|(k, v)| (k.clone(), v.clone()))
1703 {
1704 let fobj = match fobj {
1705 Object::Reference(id) => reader.resolve(id)?,
1706 other => other,
1707 };
1708 let prepared = match fobj {
1709 Object::Array(items) => {
1710 let mut prepared = Vec::with_capacity(items.len());
1711 for f in items {
1712 prepared.push(prepare_function_object(reader, f)?);
1713 }
1714 Object::Array(prepared)
1715 }
1716 other => prepare_function_object(reader, other)?,
1717 };
1718 shading.set("Function", prepared);
1719 }
1720 d.set("Shading", Object::Dict(shading));
1721 }
1722 }
1723 out.set(name, Object::Dict(d));
1724 }
1725 Ok(Some(out))
1726}
1727
1728fn resolve_tiling_patterns(
1744 reader: &mut DocumentReader<'_>,
1745 resources: &Dict,
1746 depth: usize,
1747) -> Result<Option<BTreeMap<String, TilingPattern>>, PdfError> {
1748 if depth >= MAX_XOBJECT_DEPTH {
1749 return Ok(None);
1750 }
1751 let pat_obj = resources
1752 .entries()
1753 .iter()
1754 .find(|(k, _)| k == "Pattern")
1755 .map(|(_, v)| v.clone());
1756 let pat_obj = match pat_obj {
1757 Some(Object::Reference(id)) => reader.resolve(id)?,
1758 Some(other) => other,
1759 None => return Ok(None),
1760 };
1761 let Object::Dict(pat_dict) = pat_obj else {
1762 return Ok(None);
1763 };
1764 let mut out: BTreeMap<String, TilingPattern> = BTreeMap::new();
1765 for (name, value) in pat_dict.entries() {
1766 let resolved = match value {
1767 Object::Reference(id) => reader.resolve(*id)?,
1768 other => other.clone(),
1769 };
1770 let Object::Stream(stream) = resolved else {
1773 continue;
1774 };
1775 if dict_int(&stream.dict, "PatternType") != Some(1) {
1776 continue;
1777 }
1778 if let Some(tp) = resolve_one_tiling_pattern(reader, &stream, depth)? {
1779 out.insert(name.clone(), tp);
1780 }
1781 }
1782 if out.is_empty() {
1783 Ok(None)
1784 } else {
1785 Ok(Some(out))
1786 }
1787}
1788
1789fn resolve_one_tiling_pattern(
1794 reader: &mut DocumentReader<'_>,
1795 stream: &Stream,
1796 depth: usize,
1797) -> Result<Option<TilingPattern>, PdfError> {
1798 let bbox = match read_rect(&stream.dict, "BBox") {
1799 Some(r) => r,
1800 None => return Ok(None),
1801 };
1802 let xstep = match dict_num(&stream.dict, "XStep") {
1803 Some(v) if v.is_finite() && v != 0.0 => v,
1804 _ => return Ok(None),
1805 };
1806 let ystep = match dict_num(&stream.dict, "YStep") {
1807 Some(v) if v.is_finite() && v != 0.0 => v,
1808 _ => return Ok(None),
1809 };
1810 let paint_type = dict_int(&stream.dict, "PaintType").unwrap_or(1);
1811 let matrix = form_matrix(&stream.dict);
1812
1813 let content_bytes = match decode_stream(stream) {
1814 Ok(b) => b,
1815 Err(_) => return Ok(None),
1816 };
1817
1818 let cell_resources = match stream.dict.entries().iter().find(|(k, _)| k == "Resources") {
1821 Some((_, Object::Reference(id))) => match reader.resolve(*id)? {
1822 Object::Dict(d) => Some(d),
1823 _ => None,
1824 },
1825 Some((_, Object::Dict(d))) => Some(d.clone()),
1826 _ => None,
1827 };
1828
1829 let ext_gstate_dict = match cell_resources.as_ref() {
1830 Some(r) => resolve_ext_gstate(reader, r)?,
1831 None => None,
1832 };
1833 let fonts_dict = match cell_resources.as_ref() {
1834 Some(r) => resolve_font_resources(reader, r)?,
1835 None => None,
1836 };
1837 let shading_dict = match cell_resources.as_ref() {
1838 Some(r) => resolve_shading_resources(reader, r)?,
1839 None => None,
1840 };
1841 let color_space_dict = match cell_resources.as_ref() {
1842 Some(r) => resolve_color_space_resources(reader, r)?,
1843 None => None,
1844 };
1845 let properties_dict = match cell_resources.as_ref() {
1846 Some(r) => resolve_properties_resources(reader, r)?,
1847 None => None,
1848 };
1849 let mut seen = HashSet::new();
1850 let nested_forms = match cell_resources.as_ref() {
1851 Some(r) => resolve_xobject_forms(reader, r, depth + 1, &mut seen)?,
1852 None => None,
1853 };
1854 let pattern_dict = match cell_resources.as_ref() {
1855 Some(r) => resolve_pattern_resources(reader, r)?,
1856 None => None,
1857 };
1858 let nested_tiling = match cell_resources.as_ref() {
1862 Some(r) => resolve_tiling_patterns(reader, r, depth + 1)?,
1863 None => None,
1864 };
1865
1866 let parsed = parse_content_stream_full_with_tiling(
1867 &content_bytes,
1868 ext_gstate_dict.as_ref(),
1869 fonts_dict.as_ref(),
1870 shading_dict.as_ref(),
1871 color_space_dict.as_ref(),
1872 properties_dict.as_ref(),
1873 nested_forms.as_ref(),
1874 pattern_dict.as_ref(),
1875 nested_tiling.as_ref(),
1876 )?;
1877 if parsed.root.children.is_empty() {
1878 return Ok(None);
1879 }
1880 Ok(Some(TilingPattern {
1881 cell: parsed.root,
1882 bbox,
1883 xstep,
1884 ystep,
1885 matrix,
1886 paint_type,
1887 }))
1888}
1889
1890fn resolve_type3_fonts(
1908 reader: &mut DocumentReader<'_>,
1909 resources: &Dict,
1910 depth: usize,
1911) -> Result<Option<BTreeMap<String, Type3Font>>, PdfError> {
1912 if depth >= MAX_XOBJECT_DEPTH {
1913 return Ok(None);
1914 }
1915 let font_obj = resources
1916 .entries()
1917 .iter()
1918 .find(|(k, _)| k == "Font")
1919 .map(|(_, v)| v.clone());
1920 let font_obj = match font_obj {
1921 Some(Object::Reference(id)) => reader.resolve(id)?,
1922 Some(other) => other,
1923 None => return Ok(None),
1924 };
1925 let Object::Dict(font_dict) = font_obj else {
1926 return Ok(None);
1927 };
1928 let mut out: BTreeMap<String, Type3Font> = BTreeMap::new();
1929 for (name, value) in font_dict.entries() {
1930 let resolved = match value {
1931 Object::Reference(id) => reader.resolve(*id)?,
1932 other => other.clone(),
1933 };
1934 let Object::Dict(fd) = resolved else {
1935 continue;
1936 };
1937 if !matches!(
1938 fd.entries().iter().find(|(k, _)| k == "Subtype"),
1939 Some((_, Object::Name(s))) if s == "Type3"
1940 ) {
1941 continue;
1942 }
1943 if let Some(font) = resolve_one_type3_font(reader, &fd, resources, depth)? {
1944 out.insert(name.clone(), font);
1945 }
1946 }
1947 if out.is_empty() {
1948 Ok(None)
1949 } else {
1950 Ok(Some(out))
1951 }
1952}
1953
1954fn resolve_one_type3_font(
1963 reader: &mut DocumentReader<'_>,
1964 fd: &Dict,
1965 enclosing_resources: &Dict,
1966 depth: usize,
1967) -> Result<Option<Type3Font>, PdfError> {
1968 let font_matrix = match fd.entries().iter().find(|(k, _)| k == "FontMatrix") {
1971 Some((_, obj @ Object::Array(items))) if items.len() == 6 => array_matrix(obj),
1972 _ => Transform2D {
1973 a: 0.001,
1974 b: 0.0,
1975 c: 0.0,
1976 d: 0.001,
1977 e: 0.0,
1978 f: 0.0,
1979 },
1980 };
1981
1982 let mut encoding: BTreeMap<u8, String> = BTreeMap::new();
1985 let enc_obj = match fd.entries().iter().find(|(k, _)| k == "Encoding") {
1986 Some((_, Object::Reference(id))) => Some(reader.resolve(*id)?),
1987 Some((_, other)) => Some(other.clone()),
1988 None => None,
1989 };
1990 if let Some(Object::Dict(enc)) = enc_obj {
1991 let diffs = match enc.entries().iter().find(|(k, _)| k == "Differences") {
1992 Some((_, Object::Reference(id))) => Some(reader.resolve(*id)?),
1993 Some((_, other)) => Some(other.clone()),
1994 None => None,
1995 };
1996 if let Some(arr) = diffs {
1997 if let Ok(parsed) = crate::reader::encoding::parse_encoding_differences(&arr) {
1998 for ov in parsed.overrides {
1999 encoding.insert(ov.code, ov.glyph_name);
2000 }
2001 }
2002 }
2003 }
2004 if encoding.is_empty() {
2005 return Ok(None);
2006 }
2007
2008 let glyph_resources = match fd.entries().iter().find(|(k, _)| k == "Resources") {
2013 Some((_, Object::Reference(id))) => match reader.resolve(*id)? {
2014 Object::Dict(d) => Some(d),
2015 _ => Some(enclosing_resources.clone()),
2016 },
2017 Some((_, Object::Dict(d))) => Some(d.clone()),
2018 _ => Some(enclosing_resources.clone()),
2019 };
2020
2021 let charprocs_obj = match fd.entries().iter().find(|(k, _)| k == "CharProcs") {
2023 Some((_, Object::Reference(id))) => reader.resolve(*id)?,
2024 Some((_, other)) => other.clone(),
2025 None => return Ok(None),
2026 };
2027 let Object::Dict(charprocs) = charprocs_obj else {
2028 return Ok(None);
2029 };
2030
2031 let mut glyphs: BTreeMap<String, Group> = BTreeMap::new();
2032 let mut shape_only: BTreeSet<String> = BTreeSet::new();
2033 let referenced: BTreeSet<&String> = encoding.values().collect();
2035 for (glyph_name, value) in charprocs.entries() {
2036 if !referenced.contains(glyph_name) {
2037 continue;
2038 }
2039 let resolved = match value {
2040 Object::Reference(id) => reader.resolve(*id)?,
2041 other => other.clone(),
2042 };
2043 let Object::Stream(stream) = resolved else {
2044 continue;
2045 };
2046 let content_bytes = match decode_stream(&stream) {
2047 Ok(b) => b,
2048 Err(_) => continue,
2049 };
2050 if charproc_is_shape_only(&content_bytes) {
2053 shape_only.insert(glyph_name.clone());
2054 }
2055 if let Some(group) =
2056 parse_glyph_description(reader, &content_bytes, glyph_resources.as_ref(), depth)?
2057 {
2058 if !group.children.is_empty() {
2059 glyphs.insert(glyph_name.clone(), group);
2060 }
2061 }
2062 }
2063 if glyphs.is_empty() {
2064 return Ok(None);
2065 }
2066 Ok(Some(Type3Font {
2067 font_matrix,
2068 encoding,
2069 glyphs,
2070 shape_only,
2071 }))
2072}
2073
2074fn charproc_is_shape_only(bytes: &[u8]) -> bool {
2080 let mut i = 0;
2081 while i < bytes.len() {
2082 let c = bytes[i];
2083 if c.is_ascii_whitespace()
2086 || c.is_ascii_digit()
2087 || c == b'+'
2088 || c == b'-'
2089 || c == b'.'
2090 || c == b'e'
2091 || c == b'E'
2092 {
2093 i += 1;
2094 continue;
2095 }
2096 if bytes[i..].starts_with(b"d1") {
2098 return true;
2099 }
2100 return false;
2101 }
2102 false
2103}
2104
2105fn parse_glyph_description(
2111 reader: &mut DocumentReader<'_>,
2112 content_bytes: &[u8],
2113 glyph_resources: Option<&Dict>,
2114 depth: usize,
2115) -> Result<Option<Group>, PdfError> {
2116 let ext_gstate_dict = match glyph_resources {
2117 Some(r) => resolve_ext_gstate(reader, r)?,
2118 None => None,
2119 };
2120 let fonts_dict = match glyph_resources {
2121 Some(r) => resolve_font_resources(reader, r)?,
2122 None => None,
2123 };
2124 let shading_dict = match glyph_resources {
2125 Some(r) => resolve_shading_resources(reader, r)?,
2126 None => None,
2127 };
2128 let color_space_dict = match glyph_resources {
2129 Some(r) => resolve_color_space_resources(reader, r)?,
2130 None => None,
2131 };
2132 let properties_dict = match glyph_resources {
2133 Some(r) => resolve_properties_resources(reader, r)?,
2134 None => None,
2135 };
2136 let mut seen = HashSet::new();
2137 let nested_forms = match glyph_resources {
2138 Some(r) => resolve_xobject_forms(reader, r, depth + 1, &mut seen)?,
2139 None => None,
2140 };
2141 let pattern_dict = match glyph_resources {
2142 Some(r) => resolve_pattern_resources(reader, r)?,
2143 None => None,
2144 };
2145 let tiling_patterns = match glyph_resources {
2146 Some(r) => resolve_tiling_patterns(reader, r, depth + 1)?,
2147 None => None,
2148 };
2149 let nested_type3 = match glyph_resources {
2153 Some(r) => resolve_type3_fonts(reader, r, depth + 1)?,
2154 None => None,
2155 };
2156
2157 let parsed = parse_content_stream_full_with_type3(
2158 content_bytes,
2159 ext_gstate_dict.as_ref(),
2160 fonts_dict.as_ref(),
2161 shading_dict.as_ref(),
2162 color_space_dict.as_ref(),
2163 properties_dict.as_ref(),
2164 nested_forms.as_ref(),
2165 pattern_dict.as_ref(),
2166 tiling_patterns.as_ref(),
2167 nested_type3.as_ref(),
2168 )?;
2169 if parsed.root.children.is_empty() {
2170 return Ok(None);
2171 }
2172 Ok(Some(parsed.root))
2173}
2174
2175fn array_matrix(obj: &Object) -> Transform2D {
2178 let Object::Array(items) = obj else {
2179 return Transform2D::identity();
2180 };
2181 let mut m = [0.0f32; 6];
2182 for (i, slot) in m.iter_mut().enumerate() {
2183 match items.get(i) {
2184 Some(Object::Integer(v)) => *slot = *v as f32,
2185 Some(Object::Real(v)) => *slot = *v as f32,
2186 _ => return Transform2D::identity(),
2187 }
2188 }
2189 Transform2D {
2190 a: m[0],
2191 b: m[1],
2192 c: m[2],
2193 d: m[3],
2194 e: m[4],
2195 f: m[5],
2196 }
2197}
2198
2199fn dict_num(dict: &Dict, key: &str) -> Option<f32> {
2202 match dict
2203 .entries()
2204 .iter()
2205 .find(|(k, _)| k == key)
2206 .map(|(_, v)| v)
2207 {
2208 Some(Object::Integer(v)) => Some(*v as f32),
2209 Some(Object::Real(v)) => Some(*v as f32),
2210 _ => None,
2211 }
2212}
2213
2214fn dict_int(dict: &Dict, key: &str) -> Option<i64> {
2216 match dict
2217 .entries()
2218 .iter()
2219 .find(|(k, _)| k == key)
2220 .map(|(_, v)| v)
2221 {
2222 Some(Object::Integer(v)) => Some(*v),
2223 _ => None,
2224 }
2225}
2226
2227fn read_rect(dict: &Dict, key: &str) -> Option<[f32; 4]> {
2231 let items = match dict
2232 .entries()
2233 .iter()
2234 .find(|(k, _)| k == key)
2235 .map(|(_, v)| v)
2236 {
2237 Some(Object::Array(items)) if items.len() == 4 => items,
2238 _ => return None,
2239 };
2240 let mut out = [0.0f32; 4];
2241 for (i, slot) in out.iter_mut().enumerate() {
2242 *slot = match &items[i] {
2243 Object::Integer(v) => *v as f32,
2244 Object::Real(v) => *v as f32,
2245 _ => return None,
2246 };
2247 if !slot.is_finite() {
2248 return None;
2249 }
2250 }
2251 Some(out)
2252}
2253
2254fn resolve_color_space_resources(
2283 reader: &mut DocumentReader<'_>,
2284 resources: &Dict,
2285) -> Result<Option<Dict>, PdfError> {
2286 let cs_obj = resources
2287 .entries()
2288 .iter()
2289 .find(|(k, _)| k == "ColorSpace")
2290 .map(|(_, v)| v.clone());
2291 let cs_obj = match cs_obj {
2292 Some(Object::Reference(id)) => reader.resolve(id)?,
2293 Some(other) => other,
2294 None => return Ok(None),
2295 };
2296 let Object::Dict(cs_dict) = cs_obj else {
2297 return Ok(None);
2298 };
2299 let mut out = Dict::new();
2300 for (name, value) in cs_dict.entries() {
2301 let resolved = match value {
2302 Object::Reference(id) => reader.resolve(*id)?,
2303 other => other.clone(),
2304 };
2305 let prepared = prepare_color_space_object(reader, resolved)?;
2306 out.set(name, prepared);
2307 }
2308 Ok(Some(out))
2309}
2310
2311fn resolve_properties_resources(
2326 reader: &mut DocumentReader<'_>,
2327 resources: &Dict,
2328) -> Result<Option<Dict>, PdfError> {
2329 let props_obj = resources
2330 .entries()
2331 .iter()
2332 .find(|(k, _)| k == "Properties")
2333 .map(|(_, v)| v.clone());
2334 let props_obj = match props_obj {
2335 Some(Object::Reference(id)) => reader.resolve(id)?,
2336 Some(other) => other,
2337 None => return Ok(None),
2338 };
2339 let Object::Dict(props_dict) = props_obj else {
2340 return Ok(None);
2341 };
2342 let mut out = Dict::new();
2343 for (name, value) in props_dict.entries() {
2344 let resolved = match value {
2345 Object::Reference(id) => reader.resolve(*id)?,
2346 other => other.clone(),
2347 };
2348 let d = match resolved {
2353 Object::Dict(d) => Some(d),
2354 Object::Stream(s) => Some(s.dict),
2355 _ => None,
2356 };
2357 if let Some(d) = d {
2358 out.set(name, Object::Dict(d));
2359 }
2360 }
2361 Ok(Some(out))
2362}
2363
2364const MAX_XOBJECT_DEPTH: usize = 12;
2371
2372fn resolve_xobject_forms(
2398 reader: &mut DocumentReader<'_>,
2399 resources: &Dict,
2400 depth: usize,
2401 visited: &mut HashSet<ObjectId>,
2402) -> Result<Option<BTreeMap<String, Group>>, PdfError> {
2403 if depth >= MAX_XOBJECT_DEPTH {
2404 return Ok(None);
2405 }
2406 let xobj_obj = resources
2407 .entries()
2408 .iter()
2409 .find(|(k, _)| k == "XObject")
2410 .map(|(_, v)| v.clone());
2411 let xobj_obj = match xobj_obj {
2412 Some(Object::Reference(id)) => reader.resolve(id)?,
2413 Some(other) => other,
2414 None => return Ok(None),
2415 };
2416 let Object::Dict(xobj_dict) = xobj_obj else {
2417 return Ok(None);
2418 };
2419 let mut out: BTreeMap<String, Group> = BTreeMap::new();
2420 for (name, value) in xobj_dict.entries() {
2421 let (form_id, resolved) = match value {
2424 Object::Reference(id) => (Some(*id), reader.resolve(*id)?),
2425 other => (None, other.clone()),
2426 };
2427 let Object::Stream(stream) = resolved else {
2428 continue;
2429 };
2430 let subtype = stream
2432 .dict
2433 .entries()
2434 .iter()
2435 .find(|(k, _)| k == "Subtype")
2436 .map(|(_, v)| v);
2437 if !matches!(subtype, Some(Object::Name(s)) if s == "Form") {
2438 continue;
2439 }
2440 if let Some(id) = form_id {
2441 if visited.contains(&id) {
2442 continue;
2443 }
2444 visited.insert(id);
2445 }
2446 let group = resolve_one_form_xobject(reader, &stream, depth, visited)?;
2447 if let Some(id) = form_id {
2448 visited.remove(&id);
2449 }
2450 if let Some(g) = group {
2451 if !g.children.is_empty() {
2452 out.insert(name.clone(), g);
2453 }
2454 }
2455 }
2456 if out.is_empty() {
2457 Ok(None)
2458 } else {
2459 Ok(Some(out))
2460 }
2461}
2462
2463fn resolve_one_form_xobject(
2471 reader: &mut DocumentReader<'_>,
2472 stream: &Stream,
2473 depth: usize,
2474 visited: &mut HashSet<ObjectId>,
2475) -> Result<Option<Group>, PdfError> {
2476 let content_bytes = match decode_stream(stream) {
2477 Ok(b) => b,
2478 Err(_) => return Ok(None),
2479 };
2480
2481 let form_resources = match stream.dict.entries().iter().find(|(k, _)| k == "Resources") {
2486 Some((_, Object::Reference(id))) => match reader.resolve(*id)? {
2487 Object::Dict(d) => Some(d),
2488 _ => None,
2489 },
2490 Some((_, Object::Dict(d))) => Some(d.clone()),
2491 _ => None,
2492 };
2493
2494 let ext_gstate_dict = match form_resources.as_ref() {
2495 Some(r) => resolve_ext_gstate(reader, r)?,
2496 None => None,
2497 };
2498 let fonts_dict = match form_resources.as_ref() {
2499 Some(r) => resolve_font_resources(reader, r)?,
2500 None => None,
2501 };
2502 let shading_dict = match form_resources.as_ref() {
2503 Some(r) => resolve_shading_resources(reader, r)?,
2504 None => None,
2505 };
2506 let color_space_dict = match form_resources.as_ref() {
2507 Some(r) => resolve_color_space_resources(reader, r)?,
2508 None => None,
2509 };
2510 let properties_dict = match form_resources.as_ref() {
2511 Some(r) => resolve_properties_resources(reader, r)?,
2512 None => None,
2513 };
2514 let nested_forms = match form_resources.as_ref() {
2515 Some(r) => resolve_xobject_forms(reader, r, depth + 1, visited)?,
2516 None => None,
2517 };
2518 let pattern_dict = match form_resources.as_ref() {
2519 Some(r) => resolve_pattern_resources(reader, r)?,
2520 None => None,
2521 };
2522 let tiling_patterns = match form_resources.as_ref() {
2523 Some(r) => resolve_tiling_patterns(reader, r, depth + 1)?,
2524 None => None,
2525 };
2526 let type3_fonts = match form_resources.as_ref() {
2527 Some(r) => resolve_type3_fonts(reader, r, depth + 1)?,
2528 None => None,
2529 };
2530
2531 let parsed = parse_content_stream_full_with_type3(
2532 &content_bytes,
2533 ext_gstate_dict.as_ref(),
2534 fonts_dict.as_ref(),
2535 shading_dict.as_ref(),
2536 color_space_dict.as_ref(),
2537 properties_dict.as_ref(),
2538 nested_forms.as_ref(),
2539 pattern_dict.as_ref(),
2540 tiling_patterns.as_ref(),
2541 type3_fonts.as_ref(),
2542 )?;
2543
2544 let inner = parsed.root;
2550 let matrix = form_matrix(&stream.dict);
2551 let clip = form_bbox_clip(&stream.dict);
2552 let children = if inner.transform == Transform2D::identity()
2553 && inner.clip.is_none()
2554 && inner.opacity == 1.0
2555 {
2556 inner.children
2559 } else {
2560 vec![Node::Group(inner)]
2561 };
2562 if children.is_empty() {
2563 return Ok(None);
2564 }
2565 Ok(Some(Group {
2566 transform: matrix,
2567 opacity: 1.0,
2568 clip,
2569 children,
2570 ..Group::default()
2571 }))
2572}
2573
2574fn form_matrix(dict: &Dict) -> Transform2D {
2577 let nums = match dict.entries().iter().find(|(k, _)| k == "Matrix") {
2578 Some((_, Object::Array(items))) if items.len() == 6 => items,
2579 _ => return Transform2D::identity(),
2580 };
2581 let mut m = [0.0f32; 6];
2582 for (i, slot) in m.iter_mut().enumerate() {
2583 match &nums[i] {
2584 Object::Integer(v) => *slot = *v as f32,
2585 Object::Real(v) => *slot = *v as f32,
2586 _ => return Transform2D::identity(),
2587 }
2588 }
2589 Transform2D {
2590 a: m[0],
2591 b: m[1],
2592 c: m[2],
2593 d: m[3],
2594 e: m[4],
2595 f: m[5],
2596 }
2597}
2598
2599fn form_bbox_clip(dict: &Dict) -> Option<Path> {
2606 let items = match dict.entries().iter().find(|(k, _)| k == "BBox") {
2607 Some((_, Object::Array(items))) if items.len() == 4 => items,
2608 _ => return None,
2609 };
2610 let mut v = [0.0f32; 4];
2611 for (i, slot) in v.iter_mut().enumerate() {
2612 match &items[i] {
2613 Object::Integer(n) => *slot = *n as f32,
2614 Object::Real(n) => *slot = *n as f32,
2615 _ => return None,
2616 }
2617 }
2618 let (x0, y0, x1, y1) = (v[0], v[1], v[2], v[3]);
2619 let (lx, rx) = (x0.min(x1), x0.max(x1));
2623 let (by, ty) = (y0.min(y1), y0.max(y1));
2624 if rx <= lx || ty <= by {
2625 return None;
2626 }
2627 let mut path = Path::new();
2628 path.commands.push(PathCommand::MoveTo(Point::new(lx, by)));
2629 path.commands.push(PathCommand::LineTo(Point::new(rx, by)));
2630 path.commands.push(PathCommand::LineTo(Point::new(rx, ty)));
2631 path.commands.push(PathCommand::LineTo(Point::new(lx, ty)));
2632 path.commands.push(PathCommand::Close);
2633 Some(path)
2634}
2635
2636fn dict_rect4(dict: &Dict, key: &str) -> Option<[f32; 4]> {
2642 let items = match dict.entries().iter().find(|(k, _)| k == key) {
2643 Some((_, Object::Array(items))) if items.len() == 4 => items,
2644 _ => return None,
2645 };
2646 let mut v = [0.0f32; 4];
2647 for (i, slot) in v.iter_mut().enumerate() {
2648 match &items[i] {
2649 Object::Integer(n) => *slot = *n as f32,
2650 Object::Real(n) => *slot = *n as f32,
2651 _ => return None,
2652 }
2653 }
2654 Some([
2655 v[0].min(v[2]),
2656 v[1].min(v[3]),
2657 v[0].max(v[2]),
2658 v[1].max(v[3]),
2659 ])
2660}
2661
2662fn resolve_annotation_appearances(
2672 reader: &mut DocumentReader<'_>,
2673 page_dict: &Dict,
2674 optional_content: Option<&crate::reader::ocg::OptionalContent>,
2675) -> Result<Vec<Group>, PdfError> {
2676 let annots = match page_dict
2677 .entries()
2678 .iter()
2679 .find(|(k, _)| k == "Annots")
2680 .map(|(_, v)| v.clone())
2681 {
2682 Some(Object::Reference(id)) => match reader.resolve(id) {
2683 Ok(o) => o,
2684 Err(_) => return Ok(Vec::new()),
2685 },
2686 Some(other) => other,
2687 None => return Ok(Vec::new()),
2688 };
2689 let Object::Array(items) = annots else {
2690 return Ok(Vec::new());
2691 };
2692 let mut out = Vec::new();
2693 for item in items {
2694 let annot = match item {
2695 Object::Reference(id) => match reader.resolve(id) {
2696 Ok(o) => o,
2697 Err(_) => continue,
2698 },
2699 other => other,
2700 };
2701 let Object::Dict(annot) = annot else {
2702 continue;
2703 };
2704 if !annotation_oc_visible(reader, &annot, optional_content) {
2709 continue;
2710 }
2711 if let Some(group) = annotation_appearance_group(reader, &annot)? {
2712 out.push(group);
2713 }
2714 }
2715 Ok(out)
2716}
2717
2718fn annotation_oc_visible(
2731 reader: &mut DocumentReader<'_>,
2732 annot: &Dict,
2733 optional_content: Option<&crate::reader::ocg::OptionalContent>,
2734) -> bool {
2735 let Some(entry) = annot
2736 .entries()
2737 .iter()
2738 .find(|(k, _)| k == "OC")
2739 .map(|(_, v)| v.clone())
2740 else {
2741 return true;
2742 };
2743 let Some(oc) = optional_content else {
2744 return true;
2745 };
2746 let (group_id, dict) = match entry {
2747 Object::Reference(id) => match reader.resolve(id) {
2748 Ok(Object::Dict(d)) => (Some(id), d),
2749 _ => return true,
2750 },
2751 Object::Dict(d) => (None, d),
2752 _ => return true,
2753 };
2754 let type_name = dict.entries().iter().find_map(|(k, v)| match (k, v) {
2757 (k, Object::Name(n)) if k == "Type" => Some(n.clone()),
2758 _ => None,
2759 });
2760 let is_ocmd = type_name.as_deref() == Some("OCMD")
2761 || (type_name.is_none() && dict.entries().iter().any(|(k, _)| k == "OCGs" || k == "VE"));
2762 if is_ocmd {
2763 match crate::reader::ocg::parse_membership(reader, &dict) {
2764 Ok(Some(mem)) => oc.evaluate_membership(&mem),
2765 _ => true,
2766 }
2767 } else {
2768 match group_id {
2769 Some(id) => oc.is_visible(id),
2770 None => true,
2773 }
2774 }
2775}
2776
2777fn annotation_appearance_group(
2801 reader: &mut DocumentReader<'_>,
2802 annot: &Dict,
2803) -> Result<Option<Group>, PdfError> {
2804 let flags = match annot.entries().iter().find(|(k, _)| k == "F") {
2809 Some((_, Object::Integer(v))) => *v,
2810 _ => 0,
2811 };
2812 const FLAG_HIDDEN: i64 = 1 << 1; const FLAG_NO_VIEW: i64 = 1 << 5; if flags & (FLAG_HIDDEN | FLAG_NO_VIEW) != 0 {
2815 return Ok(None);
2816 }
2817
2818 if matches!(
2823 annot.entries().iter().find(|(k, _)| k == "Subtype"),
2824 Some((_, Object::Name(s))) if s == "Popup"
2825 ) {
2826 return Ok(None);
2827 }
2828
2829 let Some(rect) = dict_rect4(annot, "Rect") else {
2832 return Ok(None);
2833 };
2834
2835 let ap = match annot
2837 .entries()
2838 .iter()
2839 .find(|(k, _)| k == "AP")
2840 .map(|(_, v)| v.clone())
2841 {
2842 Some(Object::Reference(id)) => match reader.resolve(id) {
2843 Ok(o) => o,
2844 Err(_) => return Ok(None),
2845 },
2846 Some(other) => other,
2847 None => return Ok(None),
2848 };
2849 let Object::Dict(ap) = ap else {
2850 return Ok(None);
2851 };
2852
2853 let n_entry = ap
2856 .entries()
2857 .iter()
2858 .find(|(k, _)| k == "N")
2859 .map(|(_, v)| v.clone());
2860 let (stream_id, n_obj) = match n_entry {
2861 Some(Object::Reference(id)) => match reader.resolve(id) {
2862 Ok(o) => (Some(id), o),
2863 Err(_) => return Ok(None),
2864 },
2865 Some(other) => (None, other),
2866 None => return Ok(None),
2867 };
2868 let (stream_id, stream) = match n_obj {
2869 Object::Stream(s) => (stream_id, s),
2870 Object::Dict(states) => {
2877 let Some(Object::Name(state)) = annot
2878 .entries()
2879 .iter()
2880 .find(|(k, _)| k == "AS")
2881 .map(|(_, v)| v.clone())
2882 else {
2883 return Ok(None);
2884 };
2885 let selected = states
2886 .entries()
2887 .iter()
2888 .find(|(k, _)| *k == state)
2889 .map(|(_, v)| v.clone());
2890 match selected {
2891 Some(Object::Reference(id)) => match reader.resolve(id) {
2892 Ok(Object::Stream(s)) => (Some(id), s),
2893 _ => return Ok(None),
2894 },
2895 Some(Object::Stream(s)) => (None, s),
2896 _ => return Ok(None),
2897 }
2898 }
2899 _ => return Ok(None),
2900 };
2901
2902 build_appearance_group(reader, &stream, stream_id, rect)
2903}
2904
2905fn build_appearance_group(
2910 reader: &mut DocumentReader<'_>,
2911 stream: &Stream,
2912 stream_id: Option<ObjectId>,
2913 rect: [f32; 4],
2914) -> Result<Option<Group>, PdfError> {
2915 let Some(bbox) = dict_rect4(&stream.dict, "BBox") else {
2918 return Ok(None);
2919 };
2920 let matrix = form_matrix(&stream.dict);
2921
2922 let mut visited = HashSet::new();
2923 if let Some(id) = stream_id {
2924 visited.insert(id);
2925 }
2926 let Some(form_group) = resolve_one_form_xobject(reader, stream, 0, &mut visited)? else {
2927 return Ok(None);
2928 };
2929
2930 let corners = [
2933 Point::new(bbox[0], bbox[1]),
2934 Point::new(bbox[2], bbox[1]),
2935 Point::new(bbox[2], bbox[3]),
2936 Point::new(bbox[0], bbox[3]),
2937 ];
2938 let (mut tx0, mut ty0, mut tx1, mut ty1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
2939 for c in corners {
2940 let p = matrix.apply(c);
2941 tx0 = tx0.min(p.x);
2942 ty0 = ty0.min(p.y);
2943 tx1 = tx1.max(p.x);
2944 ty1 = ty1.max(p.y);
2945 }
2946
2947 let (tw, th) = (tx1 - tx0, ty1 - ty0);
2952 let sx = if tw.is_finite() && tw > f32::EPSILON {
2953 (rect[2] - rect[0]) / tw
2954 } else {
2955 1.0
2956 };
2957 let sy = if th.is_finite() && th > f32::EPSILON {
2958 (rect[3] - rect[1]) / th
2959 } else {
2960 1.0
2961 };
2962 let a = Transform2D {
2963 a: sx,
2964 b: 0.0,
2965 c: 0.0,
2966 d: sy,
2967 e: rect[0] - sx * tx0,
2968 f: rect[1] - sy * ty0,
2969 };
2970
2971 Ok(Some(Group {
2974 transform: a,
2975 children: vec![Node::Group(form_group)],
2976 ..Group::default()
2977 }))
2978}
2979
2980fn prepare_color_space_object(
2986 reader: &mut DocumentReader<'_>,
2987 obj: Object,
2988) -> Result<Object, PdfError> {
2989 match obj {
2990 Object::Reference(id) => {
2991 let target = reader.resolve(id)?;
2992 prepare_color_space_object(reader, target)
2993 }
2994 Object::Array(items) => {
2995 let family = items.first().and_then(|o| match o {
2996 Object::Name(n) => Some(n.clone()),
2997 _ => None,
2998 });
2999 match family.as_deref() {
3000 Some("ICCBased") => {
3001 let mut out = vec![Object::Name("ICCBased".into())];
3005 let stream_obj = match items.into_iter().nth(1) {
3006 Some(Object::Reference(id)) => reader.resolve(id)?,
3007 Some(other) => other,
3008 None => return Ok(Object::Array(out)),
3009 };
3010 match stream_obj {
3011 Object::Stream(s) => {
3012 let mut dict = s.dict;
3015 if let Some((_, alt)) = dict
3016 .entries()
3017 .iter()
3018 .find(|(k, _)| k == "Alternate")
3019 .map(|(k, v)| (k.clone(), v.clone()))
3020 {
3021 let prepared_alt = prepare_color_space_object(reader, alt)?;
3022 dict.set("Alternate", prepared_alt);
3023 }
3024 out.push(Object::Dict(dict));
3025 }
3026 Object::Dict(d) => out.push(Object::Dict(d)),
3027 _ => {}
3028 }
3029 Ok(Object::Array(out))
3030 }
3031 Some("Indexed") => {
3032 let mut it = items.into_iter();
3034 let _family = it.next();
3035 let base = match it.next() {
3036 Some(b) => prepare_color_space_object(reader, b)?,
3037 None => return Ok(Object::Array(vec![Object::Name("Indexed".into())])),
3038 };
3039 let hival = it.next().unwrap_or(Object::Null);
3040 let hival = match hival {
3041 Object::Reference(id) => reader.resolve(id)?,
3042 other => other,
3043 };
3044 let lookup = match it.next() {
3045 Some(Object::Reference(id)) => reader.resolve(id)?,
3046 Some(other) => other,
3047 None => Object::Null,
3048 };
3049 let lookup = match lookup {
3052 Object::Stream(s) => Object::HexString(decode_stream(&s)?),
3053 other => other,
3054 };
3055 Ok(Object::Array(vec![
3056 Object::Name("Indexed".into()),
3057 base,
3058 hival,
3059 lookup,
3060 ]))
3061 }
3062 Some("Separation") => {
3063 let mut it = items.into_iter();
3073 let _family = it.next();
3074 let name = match it.next() {
3075 Some(Object::Reference(id)) => reader.resolve(id)?,
3076 Some(other) => other,
3077 None => return Ok(Object::Array(vec![Object::Name("Separation".into())])),
3078 };
3079 let alt = match it.next() {
3080 Some(a) => prepare_color_space_object(reader, a)?,
3081 None => Object::Null,
3082 };
3083 let tint = match it.next() {
3084 Some(f) => prepare_function_object(reader, f)?,
3085 None => Object::Null,
3086 };
3087 Ok(Object::Array(vec![
3088 Object::Name("Separation".into()),
3089 name,
3090 alt,
3091 tint,
3092 ]))
3093 }
3094 Some("DeviceN") => {
3095 let mut it = items.into_iter();
3106 let _family = it.next();
3107 let names = match it.next() {
3108 Some(Object::Reference(id)) => reader.resolve(id)?,
3109 Some(other) => other,
3110 None => return Ok(Object::Array(vec![Object::Name("DeviceN".into())])),
3111 };
3112 let names = match names {
3115 Object::Array(elems) => {
3116 let mut resolved = Vec::with_capacity(elems.len());
3117 for e in elems {
3118 let r = match e {
3119 Object::Reference(id) => reader.resolve(id)?,
3120 other => other,
3121 };
3122 resolved.push(r);
3123 }
3124 Object::Array(resolved)
3125 }
3126 other => other,
3127 };
3128 let alt = match it.next() {
3129 Some(a) => prepare_color_space_object(reader, a)?,
3130 None => Object::Null,
3131 };
3132 let tint = match it.next() {
3133 Some(f) => prepare_function_object(reader, f)?,
3134 None => Object::Null,
3135 };
3136 Ok(Object::Array(vec![
3137 Object::Name("DeviceN".into()),
3138 names,
3139 alt,
3140 tint,
3141 ]))
3142 }
3143 _ => Ok(Object::Array(items)),
3144 }
3145 }
3146 other => Ok(other),
3147 }
3148}
3149
3150fn prepare_function_object(
3174 reader: &mut DocumentReader<'_>,
3175 obj: Object,
3176) -> Result<Object, PdfError> {
3177 let obj = match obj {
3178 Object::Reference(id) => reader.resolve(id)?,
3179 other => other,
3180 };
3181 let mut dict = match obj {
3186 Object::Stream(s) => {
3187 let function_type = s
3188 .dict
3189 .entries()
3190 .iter()
3191 .find(|(k, _)| k == "FunctionType")
3192 .and_then(|(_, v)| match v {
3193 Object::Integer(n) => Some(*n),
3194 _ => None,
3195 });
3196 match function_type {
3197 Some(0) => {
3200 let samples = decode_stream(&s)?;
3201 let mut d = s.dict;
3202 d.set("__Samples", Object::HexString(samples));
3203 d
3204 }
3205 Some(4) => {
3209 let program = decode_stream(&s)?;
3210 let mut d = s.dict;
3211 d.set("__Program", Object::HexString(program));
3212 d
3213 }
3214 _ => s.dict,
3217 }
3218 }
3219 Object::Dict(d) => d,
3220 other => return Ok(other),
3221 };
3222 if let Some((_, Object::Array(funcs))) = dict
3225 .entries()
3226 .iter()
3227 .find(|(k, _)| k == "Functions")
3228 .map(|(k, v)| (k.clone(), v.clone()))
3229 {
3230 let mut prepared = Vec::with_capacity(funcs.len());
3231 for f in funcs {
3232 prepared.push(prepare_function_object(reader, f)?);
3233 }
3234 dict.set("Functions", Object::Array(prepared));
3235 }
3236 Ok(Object::Dict(dict))
3237}
3238
3239fn extract_stream_data(reader: &mut DocumentReader<'_>, id: ObjectId) -> Result<Vec<u8>, PdfError> {
3240 let obj = reader.resolve(id)?;
3241 let Object::Stream(s) = obj else {
3242 return Err(PdfError::other(format!(
3243 "PDF reader: object {id:?} expected to be a Stream (got {obj:?})"
3244 )));
3245 };
3246 decode_stream(&s)
3247}
3248
3249pub fn decode_stream(stream: &Stream) -> Result<Vec<u8>, PdfError> {
3264 let filter = stream
3265 .dict
3266 .entries()
3267 .iter()
3268 .find(|(k, _)| k == "Filter")
3269 .map(|(_, v)| v.clone());
3270 let parms = stream
3274 .dict
3275 .entries()
3276 .iter()
3277 .find(|(k, _)| k == "DecodeParms" || k == "DP")
3278 .map(|(_, v)| v.clone());
3279 match filter {
3280 None => Ok(stream.data.clone()),
3281 Some(Object::Name(name)) => apply_filter(&name, &stream.data, parms_for_index(&parms, 0)),
3282 Some(Object::Array(items)) => {
3283 let mut data = stream.data.clone();
3284 for (idx, item) in items.iter().enumerate() {
3285 let Object::Name(name) = item else {
3286 return Err(PdfError::other(format!(
3287 "PDF reader: /Filter chain item must be a Name (got {item:?})"
3288 )));
3289 };
3290 data = apply_filter(name, &data, parms_for_index(&parms, idx))?;
3291 }
3292 Ok(data)
3293 }
3294 Some(other) => Err(PdfError::other(format!(
3295 "PDF reader: /Filter must be a Name or array of Names (got {other:?})"
3296 ))),
3297 }
3298}
3299
3300fn parms_for_index(parms: &Option<Object>, idx: usize) -> Option<&Dict> {
3305 match parms {
3306 Some(Object::Dict(d)) if idx == 0 => Some(d),
3307 Some(Object::Array(items)) => match items.get(idx) {
3308 Some(Object::Dict(d)) => Some(d),
3309 _ => None,
3310 },
3311 _ => None,
3312 }
3313}
3314
3315fn apply_filter(name: &str, data: &[u8], parms: Option<&Dict>) -> Result<Vec<u8>, PdfError> {
3324 use crate::reader::filters;
3325 match name {
3326 "FlateDecode" | "Fl" => apply_predictor_post(filters::flate_decompress(data)?, parms),
3327 "LZWDecode" | "LZW" => {
3328 let early = parms
3331 .and_then(|d| d.entries().iter().find(|(k, _)| k == "EarlyChange"))
3332 .and_then(|(_, v)| match v {
3333 Object::Integer(n) => Some(*n != 0),
3334 _ => None,
3335 })
3336 .unwrap_or(true);
3337 apply_predictor_post(filters::lzw_decode_with_early_change(data, early)?, parms)
3338 }
3339 "ASCII85Decode" | "A85" => filters::ascii85_decode(data),
3340 "ASCIIHexDecode" | "AHx" => filters::ascii_hex_decode(data),
3341 "RunLengthDecode" | "RL" => filters::run_length_decode(data),
3342 other => Err(PdfError::other(format!(
3343 "PDF reader: filter `{other}` not yet supported by decode_stream \
3344 (image codec filters DCT/JPX/JBIG2/CCITTFax route through the image walkers)"
3345 ))),
3346 }
3347}
3348
3349fn apply_predictor_post(data: Vec<u8>, parms: Option<&Dict>) -> Result<Vec<u8>, PdfError> {
3354 use crate::reader::filters::{apply_predictor, PredictorParams};
3355 let Some(parms) = parms else {
3356 return Ok(data);
3357 };
3358 let int = |key: &str| -> Option<i64> {
3359 parms
3360 .entries()
3361 .iter()
3362 .find(|(k, _)| k == key)
3363 .and_then(|(_, v)| match v {
3364 Object::Integer(n) => Some(*n),
3365 _ => None,
3366 })
3367 };
3368 let predictor = int("Predictor").unwrap_or(1);
3369 if predictor <= 1 {
3370 return Ok(data);
3371 }
3372 let def = PredictorParams::default();
3373 let params = PredictorParams {
3374 predictor,
3375 colors: int("Colors")
3376 .map(|n| n.max(0) as usize)
3377 .unwrap_or(def.colors),
3378 bits_per_component: int("BitsPerComponent")
3379 .map(|n| n.max(0) as usize)
3380 .unwrap_or(def.bits_per_component),
3381 columns: int("Columns")
3382 .map(|n| n.max(0) as usize)
3383 .unwrap_or(def.columns),
3384 };
3385 apply_predictor(&data, ¶ms)
3386}
3387
3388fn decode_metadata(info: Object) -> Result<Metadata, PdfError> {
3389 let Object::Dict(d) = info else {
3390 return Err(PdfError::other(format!(
3391 "PDF reader: /Info must be a dict (got {info:?})"
3392 )));
3393 };
3394 let mut m = Metadata::default();
3395 for (k, v) in d.entries() {
3396 match k.as_str() {
3397 "Title" => m.title = decode_text(v),
3398 "Author" => m.author = decode_text(v),
3399 "Subject" => m.subject = decode_text(v),
3400 "Keywords" => {
3401 if let Some(s) = decode_text(v) {
3402 m.keywords = s.split(',').map(|p| p.trim().to_owned()).collect();
3406 }
3407 }
3408 "Creator" => m.creator = decode_text(v),
3409 "Producer" => m.producer = decode_text(v),
3410 "CreationDate" => m.created_at = decode_text(v).map(pdf_date_to_iso8601),
3411 "ModDate" => m.modified_at = decode_text(v).map(pdf_date_to_iso8601),
3412 other => {
3413 if let Some(s) = decode_text(v) {
3414 m.custom.insert(other.to_owned(), s);
3415 }
3416 }
3417 }
3418 }
3419 Ok(m)
3420}
3421
3422pub fn pdf_date_to_iso8601(s: String) -> String {
3426 let bytes = s.as_bytes();
3427 if !bytes.starts_with(b"D:") {
3428 return s;
3429 }
3430 let rest = &bytes[2..];
3431 if rest.len() < 4 {
3432 return s.clone();
3433 }
3434 let mut out = String::with_capacity(25);
3435 let year = &rest[0..4.min(rest.len())];
3436 out.push_str(&String::from_utf8_lossy(year));
3437 if rest.len() >= 6 {
3438 out.push('-');
3439 out.push_str(&String::from_utf8_lossy(&rest[4..6]));
3440 }
3441 if rest.len() >= 8 {
3442 out.push('-');
3443 out.push_str(&String::from_utf8_lossy(&rest[6..8]));
3444 }
3445 if rest.len() >= 10 {
3446 out.push('T');
3447 out.push_str(&String::from_utf8_lossy(&rest[8..10]));
3448 }
3449 if rest.len() >= 12 {
3450 out.push(':');
3451 out.push_str(&String::from_utf8_lossy(&rest[10..12]));
3452 }
3453 if rest.len() >= 14 {
3454 out.push(':');
3455 out.push_str(&String::from_utf8_lossy(&rest[12..14]));
3456 }
3457 if rest.len() == 15 && rest[14] == b'Z' {
3459 out.push('Z');
3460 } else if rest.len() >= 17 && (rest[14] == b'+' || rest[14] == b'-') {
3461 out.push(rest[14] as char);
3463 out.push_str(&String::from_utf8_lossy(&rest[15..17]));
3464 if rest.len() >= 20 && rest[17] == b'\'' {
3466 out.push(':');
3467 out.push_str(&String::from_utf8_lossy(&rest[18..20]));
3468 }
3469 }
3470 out
3471}
3472
3473fn decode_text(v: &Object) -> Option<String> {
3474 match v {
3475 Object::LiteralString(b) => Some(String::from_utf8_lossy(b).into_owned()),
3476 Object::HexString(b) => {
3477 if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
3480 let utf16: Vec<u16> = b[2..]
3481 .chunks_exact(2)
3482 .map(|c| u16::from_be_bytes([c[0], c[1]]))
3483 .collect();
3484 Some(String::from_utf16_lossy(&utf16))
3485 } else {
3486 Some(String::from_utf8_lossy(b).into_owned())
3487 }
3488 }
3489 _ => None,
3490 }
3491}
3492
3493fn number_to_f32(o: &Object) -> Result<f32, PdfError> {
3494 match o {
3495 Object::Integer(n) => Ok(*n as f32),
3496 Object::Real(f) => Ok(*f as f32),
3497 other => Err(PdfError::other(format!(
3498 "PDF reader: expected number, got {other:?}"
3499 ))),
3500 }
3501}
3502
3503#[allow(dead_code)]
3507fn empty_root() -> Group {
3508 Group::default()
3509}
3510
3511#[allow(dead_code)]
3512fn empty_path_node() -> PathNode {
3513 PathNode {
3514 path: Path {
3515 commands: vec![PathCommand::Close],
3516 },
3517 fill: Some(Paint::Solid(Rgba::opaque(0, 0, 0))),
3518 stroke: None,
3519 fill_rule: FillRule::NonZero,
3520 }
3521}
3522
3523#[allow(dead_code)]
3527fn _node_imported(_: Node) {}
3528
3529#[cfg(test)]
3530mod tests {
3531 use super::*;
3532 use crate::write_pdf_from_scene;
3533
3534 fn make_scene_with_one_red_rect() -> Scene {
3535 use oxideav_core::vector::{
3536 FillRule, Group, Node, Paint, Path, PathCommand, PathNode, Point, Rgba, VectorFrame,
3537 };
3538 let mut p = Path::new();
3539 p.commands.push(PathCommand::MoveTo(Point::new(10.0, 10.0)));
3540 p.commands.push(PathCommand::LineTo(Point::new(90.0, 10.0)));
3541 p.commands.push(PathCommand::LineTo(Point::new(90.0, 90.0)));
3542 p.commands.push(PathCommand::LineTo(Point::new(10.0, 90.0)));
3543 p.commands.push(PathCommand::Close);
3544 let frame = VectorFrame {
3545 width: 100.0,
3546 height: 100.0,
3547 view_box: None,
3548 root: Group {
3549 children: vec![Node::Path(PathNode {
3550 path: p,
3551 fill: Some(Paint::Solid(Rgba::opaque(255, 0, 0))),
3552 stroke: None,
3553 fill_rule: FillRule::NonZero,
3554 })],
3555 ..Group::default()
3556 },
3557 pts: None,
3558 time_base: TimeBase::new(1, 1),
3559 };
3560 let mut page = Page::new(100.0, 100.0);
3561 page.content = frame;
3562 Scene {
3563 pages: Some(vec![page]),
3564 ..Scene::default()
3565 }
3566 }
3567
3568 #[test]
3569 fn read_pdf_to_scene_roundtrip_single_page() {
3570 let scene = make_scene_with_one_red_rect();
3571 let pdf = write_pdf_from_scene(&scene).unwrap();
3572 let parsed = read_pdf_to_scene(&pdf).unwrap();
3573 let pages = parsed.pages.expect("scene has pages");
3574 assert_eq!(pages.len(), 1);
3575 assert_eq!(pages[0].width, 100.0);
3576 assert_eq!(pages[0].height, 100.0);
3577 let root = &pages[0].content.root;
3579 let path_node = find_first_path(root).expect("at least one PathNode in the tree");
3590 match &path_node.fill {
3591 Some(Paint::Solid(rgba)) => assert_eq!((rgba.r, rgba.g, rgba.b), (255, 0, 0)),
3592 other => panic!("expected solid red, got {other:?}"),
3593 }
3594 }
3595
3596 fn find_first_path(group: &Group) -> Option<&PathNode> {
3597 for child in &group.children {
3598 match child {
3599 Node::Path(p) => return Some(p),
3600 Node::Group(g) => {
3601 if let Some(p) = find_first_path(g) {
3602 return Some(p);
3603 }
3604 }
3605 _ => {}
3606 }
3607 }
3608 None
3609 }
3610
3611 #[test]
3612 fn read_pdf_to_scene_roundtrip_multi_page() {
3613 use oxideav_core::vector::Rgba;
3614 let mut scene = make_scene_with_one_red_rect();
3615 let mut p2 = Page::new(200.0, 100.0);
3616 p2.content.width = 200.0;
3617 p2.content.height = 100.0;
3618 use oxideav_core::vector::{Group, Node, Paint, Path, PathCommand, PathNode, Point};
3620 let mut path = Path::new();
3621 path.commands
3622 .push(PathCommand::MoveTo(Point::new(0.0, 0.0)));
3623 path.commands
3624 .push(PathCommand::LineTo(Point::new(50.0, 0.0)));
3625 path.commands
3626 .push(PathCommand::LineTo(Point::new(50.0, 50.0)));
3627 path.commands.push(PathCommand::Close);
3628 p2.content.root = Group {
3629 children: vec![Node::Path(PathNode {
3630 path,
3631 fill: Some(Paint::Solid(Rgba::opaque(0, 255, 0))),
3632 stroke: None,
3633 fill_rule: FillRule::NonZero,
3634 })],
3635 ..Group::default()
3636 };
3637 scene.pages.as_mut().unwrap().push(p2);
3638 let pdf = write_pdf_from_scene(&scene).unwrap();
3639 let parsed = read_pdf_to_scene(&pdf).unwrap();
3640 let pages = parsed.pages.expect("scene has pages");
3641 assert_eq!(pages.len(), 2);
3642 assert_eq!(pages[0].width, 100.0);
3643 assert_eq!(pages[1].width, 200.0);
3644 }
3645
3646 #[test]
3647 fn read_pdf_metadata_roundtrip() {
3648 let mut scene = make_scene_with_one_red_rect();
3649 scene.metadata = Metadata {
3650 title: Some("Round 3 Doc".into()),
3651 author: Some("Mark".into()),
3652 subject: Some("Reader test".into()),
3653 keywords: vec!["pdf".into(), "round3".into()],
3654 creator: Some("MyApp".into()),
3655 producer: Some("oxideav-pdf".into()),
3656 created_at: Some("2026-05-04T12:30:45Z".into()),
3657 modified_at: Some("2026-05-04T13:00:00Z".into()),
3658 ..Metadata::default()
3659 };
3660 let pdf = write_pdf_from_scene(&scene).unwrap();
3661 let parsed = read_pdf_to_scene(&pdf).unwrap();
3662 assert_eq!(parsed.metadata.title.as_deref(), Some("Round 3 Doc"));
3663 assert_eq!(parsed.metadata.author.as_deref(), Some("Mark"));
3664 assert_eq!(parsed.metadata.subject.as_deref(), Some("Reader test"));
3665 assert_eq!(parsed.metadata.creator.as_deref(), Some("MyApp"));
3666 assert_eq!(parsed.metadata.producer.as_deref(), Some("oxideav-pdf"));
3667 assert_eq!(
3668 parsed.metadata.keywords,
3669 vec!["pdf".to_string(), "round3".to_string()]
3670 );
3671 assert_eq!(
3673 parsed.metadata.created_at.as_deref(),
3674 Some("2026-05-04T12:30:45Z")
3675 );
3676 }
3677
3678 #[test]
3679 fn read_pdf_custom_metadata_roundtrip() {
3680 let mut scene = make_scene_with_one_red_rect();
3681 let mut custom = std::collections::BTreeMap::new();
3682 custom.insert("dc:rights".into(), "(c) 2026 Karpeles".into());
3683 custom.insert("Trapped".into(), "False".into());
3684 scene.metadata = Metadata {
3685 custom,
3686 ..Metadata::default()
3687 };
3688 let pdf = write_pdf_from_scene(&scene).unwrap();
3689 let parsed = read_pdf_to_scene(&pdf).unwrap();
3690 assert_eq!(
3691 parsed.metadata.custom.get("dc:rights").map(String::as_str),
3692 Some("(c) 2026 Karpeles")
3693 );
3694 assert_eq!(
3695 parsed.metadata.custom.get("Trapped").map(String::as_str),
3696 Some("False")
3697 );
3698 }
3699
3700 #[test]
3701 fn read_pdf_unicode_metadata_roundtrip() {
3702 let mut scene = make_scene_with_one_red_rect();
3703 scene.metadata = Metadata {
3704 title: Some("日本語".into()),
3705 ..Metadata::default()
3706 };
3707 let pdf = write_pdf_from_scene(&scene).unwrap();
3708 let parsed = read_pdf_to_scene(&pdf).unwrap();
3709 assert_eq!(parsed.metadata.title.as_deref(), Some("日本語"));
3710 }
3711
3712 #[test]
3713 fn pdf_date_to_iso8601_format() {
3714 assert_eq!(
3715 pdf_date_to_iso8601("D:20260504123045Z".to_string()),
3716 "2026-05-04T12:30:45Z"
3717 );
3718 assert_eq!(
3719 pdf_date_to_iso8601("D:20260504123045+09'00'".to_string()),
3720 "2026-05-04T12:30:45+09:00"
3721 );
3722 }
3723
3724 #[test]
3725 fn no_metadata_yields_default() {
3726 let scene = make_scene_with_one_red_rect();
3727 let pdf = write_pdf_from_scene(&scene).unwrap();
3728 let parsed = read_pdf_to_scene(&pdf).unwrap();
3729 assert!(parsed.metadata.title.is_none());
3730 assert!(parsed.metadata.custom.is_empty());
3731 }
3732
3733 #[test]
3738 fn decode_stream_applies_flate_png_predictor() {
3739 let predicted: &[u8] = &[
3743 0, 10, 20, 30, 2, 1, 2, 3, ];
3746 let compressed = crate::zlib::flate_compress(predicted);
3747
3748 let dict = Dict::new()
3749 .with("Filter", Object::Name("FlateDecode".into()))
3750 .with(
3751 "DecodeParms",
3752 Object::Dict(
3753 Dict::new()
3754 .with("Predictor", Object::Integer(12))
3755 .with("Columns", Object::Integer(3)),
3756 ),
3757 );
3758 let stream = Stream::new(dict, compressed);
3759 let out = decode_stream(&stream).unwrap();
3760 assert_eq!(out, [10u8, 20, 30, 11, 22, 33]);
3761 }
3762
3763 #[test]
3767 fn decode_stream_flate_without_predictor_is_passthrough() {
3768 let raw = b"hello predictor-free world";
3769 let compressed = crate::zlib::flate_compress(raw);
3770
3771 let dict = Dict::new().with("Filter", Object::Name("FlateDecode".into()));
3772 let stream = Stream::new(dict, compressed);
3773 assert_eq!(decode_stream(&stream).unwrap(), raw);
3774 }
3775
3776 #[test]
3783 fn resolve_shading_folds_mesh_stream_body() {
3784 use crate::reader::content::evaluate_mesh_shading_for_test;
3785 let mut body: Vec<u8> = Vec::new();
3788 for (x, y, r, g, b) in [
3789 (0u8, 0u8, 255u8, 0u8, 0u8),
3790 (255, 0, 0, 255, 0),
3791 (0, 255, 0, 0, 255),
3792 ] {
3793 body.extend_from_slice(&[0, x, y, r, g, b]); }
3795 let compressed = crate::zlib::flate_compress(&body);
3796
3797 let mut out: Vec<u8> = Vec::new();
3800 out.extend_from_slice(b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n");
3801 let mut offs: Vec<usize> = vec![0];
3802 offs.push(out.len());
3803 out.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
3804 offs.push(out.len());
3805 out.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n");
3806 offs.push(out.len());
3807 out.extend_from_slice(
3808 b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] \
3809 /Resources << /Shading << /Sh1 5 0 R >> >> >>\nendobj\n",
3810 );
3811 offs.push(out.len());
3812 out.extend_from_slice(b"4 0 obj\n<< >>\nendobj\n");
3813 offs.push(out.len());
3814 let header = format!(
3815 "5 0 obj\n<< /ShadingType 4 /ColorSpace /DeviceRGB \
3816 /BitsPerCoordinate 8 /BitsPerComponent 8 /BitsPerFlag 8 \
3817 /Decode [0 1 0 1 0 1 0 1 0 1] /Filter /FlateDecode /Length {} >>\nstream\n",
3818 compressed.len()
3819 );
3820 out.extend_from_slice(header.as_bytes());
3821 out.extend_from_slice(&compressed);
3822 out.extend_from_slice(b"\nendstream\nendobj\n");
3823
3824 let xref_off = out.len();
3825 out.extend_from_slice(b"xref\n0 6\n");
3826 out.extend_from_slice(b"0000000000 65535 f \n");
3827 for &o in &offs[1..] {
3828 out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
3829 }
3830 out.extend_from_slice(
3831 format!("trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF\n")
3832 .as_bytes(),
3833 );
3834
3835 let mut reader = DocumentReader::open(&out).expect("open");
3836 let resources = Dict::new().with(
3837 "Shading",
3838 Object::Dict(Dict::new().with("Sh1", Object::Reference(ObjectId::new(5)))),
3839 );
3840 let resolved = resolve_shading_resources(&mut reader, &resources)
3841 .expect("resolve")
3842 .expect("some shading dict");
3843 let sh1 = match resolved.entries().iter().find(|(k, _)| k == "Sh1") {
3844 Some((_, Object::Dict(d))) => d.clone(),
3845 _ => panic!("Sh1 not a dict"),
3846 };
3847 assert!(sh1.entries().iter().any(|(k, _)| k == "__MeshData"));
3849 let mesh = evaluate_mesh_shading_for_test(&sh1).expect("mesh");
3850 assert!(format!("{mesh:?}").contains("Triangles"));
3852 }
3853}