1use super::*;
8
9impl PdfDocument {
10 pub fn check_for_circular_references(&self) -> Vec<(ObjectRef, ObjectRef)> {
33 let mut cycles = Vec::new();
34 let mut visited = HashSet::new();
35 let mut path = Vec::new();
36
37 let obj_nums: Vec<u32> = self.xref.entries.keys().copied().collect();
38 for obj_num in obj_nums {
39 let obj_ref = ObjectRef::new(obj_num, 0);
40 if !visited.contains(&obj_ref) {
41 self.dfs_check_cycles(obj_ref, &mut visited, &mut path, &mut cycles);
42 }
43 }
44
45 cycles
46 }
47
48 fn dfs_check_cycles(
50 &self,
51 obj_ref: ObjectRef,
52 visited: &mut HashSet<ObjectRef>,
53 path: &mut Vec<ObjectRef>,
54 cycles: &mut Vec<(ObjectRef, ObjectRef)>,
55 ) {
56 if path.contains(&obj_ref) {
57 if let Some(&prev) = path.last() {
58 cycles.push((prev, obj_ref));
59 }
60 return;
61 }
62
63 if visited.contains(&obj_ref) {
64 return;
65 }
66
67 visited.insert(obj_ref);
68 path.push(obj_ref);
69
70 if let Ok(obj) = self.load_object(obj_ref) {
71 for ref_found in Self::find_references(&obj) {
72 self.dfs_check_cycles(ref_found, visited, path, cycles);
73 }
74 }
75
76 path.pop();
77 }
78
79 pub(super) fn find_references(obj: &Object) -> Vec<ObjectRef> {
81 let mut refs = Vec::new();
82
83 match obj {
84 Object::Reference(r) => refs.push(*r),
85 Object::Array(arr) => {
86 for item in arr {
87 refs.extend(Self::find_references(item));
88 }
89 }
90 Object::Dictionary(dict) => {
91 for value in dict.values() {
92 refs.extend(Self::find_references(value));
93 }
94 }
95 Object::Stream { dict, .. } => {
96 for value in dict.values() {
97 refs.extend(Self::find_references(value));
98 }
99 }
100 _ => {}
101 }
102
103 refs
104 }
105
106 pub fn extract_images(&self, page_index: usize) -> Result<Vec<crate::extractors::PdfImage>> {
146 self.require_authenticated()?;
147 self.extract_images_filtered(page_index, &ImageExtractFilter::default())
148 }
149
150 pub fn extract_embedded_files(&self) -> Result<Vec<(String, Vec<u8>)>> {
157 self.require_authenticated()?;
158 let catalog = self.catalog()?;
159 let Some(cat_dict) = catalog.as_dict() else {
160 return Ok(Vec::new());
161 };
162 let names = cat_dict.get("Names").and_then(|n| self.resolve_object(n).ok());
163 let Some(ef_root) = names
164 .as_ref()
165 .and_then(|n| n.as_dict())
166 .and_then(|d| d.get("EmbeddedFiles"))
167 .and_then(|e| self.resolve_object(e).ok())
168 else {
169 return Ok(Vec::new());
170 };
171
172 let mut filespecs: Vec<Object> = Vec::new();
173 self.collect_embedded_filespecs(&ef_root, &mut filespecs, 0);
174
175 let mut out = Vec::new();
176 for fs in &filespecs {
177 let Some(fs_dict) = fs.as_dict() else {
178 continue;
179 };
180 let filename = fs_dict
181 .get("UF")
182 .or_else(|| fs_dict.get("F"))
183 .and_then(|n| self.resolve_object(n).ok())
184 .and_then(|n| n.as_string().map(|s| String::from_utf8_lossy(s).into_owned()))
185 .unwrap_or_else(|| "attachment".to_string());
186 let Some(ef) = fs_dict.get("EF").and_then(|e| self.resolve_object(e).ok()) else {
187 continue;
188 };
189 let Some(ef_dict) = ef.as_dict() else {
190 continue;
191 };
192 let Some(stream_ref) = ef_dict
193 .get("F")
194 .or_else(|| ef_dict.get("UF"))
195 .and_then(|r| r.as_reference())
196 else {
197 continue;
198 };
199 if let Ok(stream_obj) = self.load_object(stream_ref)
200 && let Ok(bytes) = self.decode_stream_with_encryption(&stream_obj, stream_ref)
201 {
202 out.push((filename, bytes));
203 }
204 }
205 Ok(out)
206 }
207
208 fn collect_embedded_filespecs(&self, node: &Object, out: &mut Vec<Object>, depth: u8) {
211 if depth > 32 {
212 return;
213 }
214 let Ok(node) = self.resolve_object(node) else {
215 return;
216 };
217 let Some(dict) = node.as_dict() else {
218 return;
219 };
220 if let Some(names) = dict.get("Names").and_then(|n| self.resolve_object(n).ok())
221 && let Some(arr) = names.as_array()
222 {
223 let mut i = 1;
225 while i < arr.len() {
226 if let Ok(fs) = self.resolve_object(&arr[i]) {
227 out.push(fs);
228 }
229 i += 2;
230 }
231 }
232 if let Some(kids) = dict.get("Kids").and_then(|k| self.resolve_object(k).ok())
233 && let Some(arr) = kids.as_array()
234 {
235 for kid in arr {
236 self.collect_embedded_filespecs(kid, out, depth + 1);
237 }
238 }
239 }
240
241 fn build_color_space_map(&self, resources: Option<&Object>) -> std::collections::HashMap<String, Object> {
249 let mut map = std::collections::HashMap::new();
250 let Some(res) = resources else {
251 return map;
252 };
253 let res = if let Some(r) = res.as_reference() {
254 match self.load_object(r) {
255 Ok(o) => o,
256 Err(_) => return map,
257 }
258 } else {
259 res.clone()
260 };
261 let Some(res_dict) = res.as_dict() else {
262 return map;
263 };
264 let Some(cs_entry) = res_dict.get("ColorSpace") else {
265 return map;
266 };
267 let cs_obj = if let Some(r) = cs_entry.as_reference() {
268 match self.load_object(r) {
269 Ok(o) => o,
270 Err(_) => return map,
271 }
272 } else {
273 cs_entry.clone()
274 };
275 let Some(cs_dict) = cs_obj.as_dict() else {
276 return map;
277 };
278 for (name, value) in cs_dict.iter() {
279 let resolved = if let Some(r) = value.as_reference() {
280 self.load_object(r).unwrap_or_else(|_| value.clone())
281 } else {
282 value.clone()
283 };
284 map.insert(name.clone(), resolved);
285 }
286 map
287 }
288
289 pub fn page_image_handles(&self, page_index: usize) -> Result<Vec<crate::extractors::images::PdfImageHandle<'_>>> {
321 use crate::content::Operator;
322 use crate::content::parse_content_stream_images_only;
323 use crate::extractors::images::image_handle_from_inline;
324
325 self.require_authenticated()?;
326
327 let page = self.get_page(page_index)?;
328 let page_dict = page.as_dict().ok_or_else(|| Error::ParseError {
329 offset: 0,
330 reason: "Page is not a dictionary".to_string(),
331 })?;
332
333 let content_data = self.get_page_content_data(page_index)?;
334
335 let resources = match page_dict.get("Resources") {
336 Some(res) => {
337 if let Some(ref_obj) = res.as_reference() {
338 Some(self.load_object(ref_obj)?)
339 } else {
340 Some(res.clone())
341 }
342 }
343 None => None,
344 };
345
346 let operators = match parse_content_stream_images_only(&content_data) {
347 Ok(ops) => ops,
348 Err(_) => return Ok(Vec::new()),
349 };
350
351 let cs_map = self.build_color_space_map(resources.as_ref());
353
354 let xobject_dict = if let Some(ref res) = resources {
355 if let Some(res_dict) = res.as_dict() {
356 if let Some(xobj_entry) = res_dict.get("XObject") {
357 let resolved = if let Some(ref_obj) = xobj_entry.as_reference() {
358 self.load_object(ref_obj)?
359 } else {
360 xobj_entry.clone()
361 };
362 resolved.as_dict().cloned()
363 } else {
364 None
365 }
366 } else {
367 None
368 }
369 } else {
370 None
371 };
372
373 let mut handles = Vec::new();
374 let mut ctm_stack = vec![crate::content::Matrix::identity()];
375 let mut paint_order: usize = 0;
376 let mut xobject_stack: Vec<crate::object::ObjectRef> = Vec::new();
377
378 for op in operators {
379 match op {
380 Operator::SaveState => {
381 if let Some(current) = ctm_stack.last() {
382 ctm_stack.push(*current);
383 }
384 }
385 Operator::RestoreState => {
386 if ctm_stack.len() > 1 {
387 ctm_stack.pop();
388 }
389 }
390 Operator::Cm { a, b, c, d, e, f } => {
391 if let Some(current) = ctm_stack.last_mut() {
392 let m = crate::content::Matrix { a, b, c, d, e, f };
393 *current = m.multiply(current);
394 }
395 }
396 Operator::Do { name } => {
397 if let Some(ref xobj_dict_map) = xobject_dict {
398 let ctm = ctm_stack
399 .last()
400 .copied()
401 .unwrap_or_else(crate::content::Matrix::identity);
402 if let Ok(mut more) = self.collect_handles_from_do(
403 &name,
404 xobj_dict_map,
405 resources.as_ref(),
406 ctm,
407 &mut paint_order,
408 &mut xobject_stack,
409 ) {
410 handles.append(&mut more);
411 }
412 }
413 }
414 Operator::InlineImage { dict, data } => {
415 let ctm = ctm_stack
416 .last()
417 .copied()
418 .unwrap_or_else(crate::content::Matrix::identity);
419 if let Some(handle) = image_handle_from_inline(self, &dict, data, ctm, paint_order, &cs_map) {
420 handles.push(handle);
421 paint_order += 1;
422 }
423 }
424 _ => {}
425 }
426 }
427
428 Ok(handles)
429 }
430
431 fn collect_handles_from_do<'s>(
438 &'s self,
439 name: &str,
440 xobject_dict: &std::collections::HashMap<String, Object>,
441 resources: Option<&Object>,
442 ctm: crate::content::Matrix,
443 paint_order: &mut usize,
444 xobject_stack: &mut Vec<crate::object::ObjectRef>,
445 ) -> Result<Vec<crate::extractors::images::PdfImageHandle<'s>>> {
446 use crate::extractors::images::image_handle_from_xobject;
447
448 let xobject_ref_obj = match xobject_dict.get(name) {
449 Some(o) => o,
450 None => return Ok(Vec::new()),
451 };
452
453 let xobject_ref_opt = xobject_ref_obj.as_reference();
454 let xobject = if let Some(ref_obj) = xobject_ref_opt {
455 self.load_object(ref_obj)?
456 } else {
457 xobject_ref_obj.clone()
458 };
459 let xobj_dict = match xobject.as_dict() {
460 Some(d) => d,
461 None => return Ok(Vec::new()),
462 };
463
464 let subtype = xobj_dict.get("Subtype").and_then(|s| s.as_name()).unwrap_or("");
465
466 match subtype {
467 "Image" => {
468 if let Some(ref_obj) = xobject_ref_opt {
469 let cs_map = self.build_color_space_map(resources);
470 match image_handle_from_xobject(self, ref_obj, xobj_dict, ctm, *paint_order, &cs_map) {
471 Some(h) => {
472 *paint_order += 1;
473 let mut handles = vec![h];
474 handles.extend(self.collect_referenced_mask_handles(xobj_dict, ctm, paint_order, &cs_map));
475 Ok(handles)
476 }
477 _ => Ok(Vec::new()),
478 }
479 } else {
480 Ok(Vec::new())
481 }
482 }
483 "Form" => {
484 if let (Some(ref_obj), Some(parent_res)) = (xobject_ref_opt, resources) {
485 self.collect_image_handles_from_form_xobject(
486 ref_obj,
487 &xobject,
488 parent_res,
489 ctm,
490 paint_order,
491 xobject_stack,
492 )
493 } else {
494 Ok(Vec::new())
495 }
496 }
497 _ => Ok(Vec::new()),
498 }
499 }
500
501 fn collect_referenced_mask_handles<'s>(
502 &'s self,
503 image_dict: &std::collections::HashMap<String, Object>,
504 ctm: crate::content::Matrix,
505 paint_order: &mut usize,
506 color_spaces: &std::collections::HashMap<String, Object>,
507 ) -> Vec<crate::extractors::images::PdfImageHandle<'s>> {
508 use crate::extractors::images::image_handle_from_xobject;
509
510 let mut handles = Vec::new();
511 for key in ["Mask", "SMask"] {
512 let Some(mask_ref) = image_dict.get(key).and_then(Object::as_reference) else {
513 continue;
514 };
515 let Ok(mask) = self.load_object(mask_ref) else {
516 continue;
517 };
518 let Some(mask_dict) = mask.as_dict() else {
519 continue;
520 };
521 if mask_dict.get("Subtype").and_then(Object::as_name) != Some("Image") {
522 continue;
523 }
524 if let Some(handle) = image_handle_from_xobject(self, mask_ref, mask_dict, ctm, *paint_order, color_spaces)
525 {
526 handles.push(handle);
527 *paint_order += 1;
528 }
529 }
530 handles
531 }
532
533 fn collect_image_handles_from_form_xobject<'s>(
547 &'s self,
548 xobject_ref: crate::object::ObjectRef,
549 xobject: &Object,
550 parent_resources: &Object,
551 parent_ctm: crate::content::Matrix,
552 paint_order: &mut usize,
553 xobject_stack: &mut Vec<crate::object::ObjectRef>,
554 ) -> Result<Vec<crate::extractors::images::PdfImageHandle<'s>>> {
555 use crate::content::Operator;
556 use crate::content::parse_content_stream_images_only;
557 use crate::extractors::images::image_handle_from_inline;
558
559 if xobject_stack.contains(&xobject_ref) || xobject_stack.len() >= 100 {
561 return Ok(Vec::new());
562 }
563
564 xobject_stack.push(xobject_ref);
565
566 let xobj_dict = match xobject.as_dict() {
567 Some(d) => d,
568 None => {
569 xobject_stack.pop();
570 return Ok(Vec::new());
571 }
572 };
573
574 let form_resources = if let Some(form_res) = xobj_dict.get("Resources") {
575 if let Some(ref_obj) = form_res.as_reference() {
576 self.load_object(ref_obj)?
577 } else {
578 form_res.clone()
579 }
580 } else {
581 parent_resources.clone()
582 };
583
584 let form_xobject_dict = if let Some(res_dict) = form_resources.as_dict() {
585 if let Some(xobj_entry) = res_dict.get("XObject") {
586 let resolved = if let Some(ref_obj) = xobj_entry.as_reference() {
587 self.load_object(ref_obj)?
588 } else {
589 xobj_entry.clone()
590 };
591 resolved.as_dict().cloned()
592 } else {
593 None
594 }
595 } else {
596 None
597 };
598
599 let form_matrix = if let Some(matrix_obj) = xobj_dict.get("Matrix") {
600 self.parse_matrix_from_object(matrix_obj)
601 .unwrap_or_else(crate::content::Matrix::identity)
602 } else {
603 crate::content::Matrix::identity()
604 };
605
606 let cached_stream = self.xobject_stream_cache.lock_or_recover().get(&xobject_ref).cloned();
607 let stream_data = if let Some(cached) = cached_stream {
608 cached.as_ref().clone()
609 } else {
610 match self.decode_stream_with_encryption(xobject, xobject_ref) {
611 Ok(data) => {
612 const MAX_STREAM_CACHE_BYTES: usize = 50 * 1024 * 1024;
613 let current_bytes = self.xobject_stream_cache_bytes.load(Ordering::Relaxed);
614 if current_bytes + data.len() <= MAX_STREAM_CACHE_BYTES {
615 self.xobject_stream_cache_bytes
616 .store(current_bytes + data.len(), Ordering::Relaxed);
617 self.xobject_stream_cache
618 .lock_or_recover()
619 .insert(xobject_ref, std::sync::Arc::new(data.clone()));
620 }
621 data
622 }
623 Err(error) => {
624 tracing::warn!(target: LOG_TARGET,
625 error_code = error.telemetry_code(),
626 error_offset = ?error.telemetry_offset(),
627 "failed to decode Form XObject stream; skipping"
628 );
629 xobject_stack.pop();
630 return Ok(Vec::new());
631 }
632 }
633 };
634
635 let operators = match parse_content_stream_images_only(&stream_data) {
636 Ok(ops) => ops,
637 Err(_) => {
638 xobject_stack.pop();
639 return Ok(Vec::new());
640 }
641 };
642
643 let start_ctm = form_matrix.multiply(&parent_ctm);
652 let mut ctm_stack = vec![start_ctm];
653 let mut handles = Vec::new();
654
655 for op in operators {
656 match op {
657 Operator::SaveState => {
658 if let Some(current) = ctm_stack.last() {
659 ctm_stack.push(*current);
660 }
661 }
662 Operator::RestoreState => {
663 if ctm_stack.len() > 1 {
664 ctm_stack.pop();
665 }
666 }
667 Operator::Cm { a, b, c, d, e, f } => {
668 if let Some(current) = ctm_stack.last_mut() {
669 let m = crate::content::Matrix { a, b, c, d, e, f };
670 *current = m.multiply(current);
671 }
672 }
673
674 Operator::Do { name } => {
675 if let Some(ref xobj_d) = form_xobject_dict {
676 let current_ctm = ctm_stack
677 .last()
678 .copied()
679 .unwrap_or_else(crate::content::Matrix::identity);
680 if let Ok(mut more) = self.collect_handles_from_do(
681 &name,
682 xobj_d,
683 Some(&form_resources),
684 current_ctm,
685 paint_order,
686 xobject_stack,
687 ) {
688 handles.append(&mut more);
689 }
690 }
691 }
692
693 Operator::InlineImage { dict, data } => {
694 let current_ctm = ctm_stack
695 .last()
696 .copied()
697 .unwrap_or_else(crate::content::Matrix::identity);
698 let cs_map = self.build_color_space_map(Some(&form_resources));
699 if let Some(h) = image_handle_from_inline(self, &dict, data, current_ctm, *paint_order, &cs_map) {
700 handles.push(h);
701 *paint_order += 1;
702 }
703 }
704
705 _ => {}
706 }
707 }
708
709 xobject_stack.pop();
710 Ok(handles)
711 }
712
713 fn extract_images_filtered(
720 &self,
721 page_index: usize,
722 filter: &ImageExtractFilter,
723 ) -> Result<Vec<crate::extractors::PdfImage>> {
724 use crate::content::Operator;
725 use crate::content::parse_content_stream_images_only;
726
727 let page = self.get_page(page_index)?;
728 let page_dict = page.as_dict().ok_or_else(|| Error::ParseError {
729 offset: 0,
730 reason: "Page is not a dictionary".to_string(),
731 })?;
732
733 let content_data = self.get_page_content_data(page_index)?;
734
735 let resources = match page_dict.get("Resources") {
736 Some(res) => {
737 if let Some(ref_obj) = res.as_reference() {
738 Some(self.load_object(ref_obj)?)
739 } else {
740 Some(res.clone())
741 }
742 }
743 None => None,
744 };
745
746 let operators = match parse_content_stream_images_only(&content_data) {
748 Ok(ops) => ops,
749 Err(_) => {
750 return Ok(Vec::new());
751 }
752 };
753
754 let mut images = Vec::new();
755 let mut ctm_stack = vec![crate::content::Matrix::identity()];
756 let mut xobject_stack = Vec::new();
760
761 let xobject_dict = if let Some(ref res) = resources {
763 if let Some(res_dict) = res.as_dict() {
764 if let Some(xobj_entry) = res_dict.get("XObject") {
765 let resolved = if let Some(ref_obj) = xobj_entry.as_reference() {
766 self.load_object(ref_obj)?
767 } else {
768 xobj_entry.clone()
769 };
770 resolved.as_dict().cloned()
771 } else {
772 None
773 }
774 } else {
775 None
776 }
777 } else {
778 None
779 };
780
781 for op in operators {
782 match op {
783 Operator::SaveState => {
784 if let Some(current_ctm) = ctm_stack.last() {
785 ctm_stack.push(*current_ctm);
786 }
787 }
788 Operator::RestoreState => {
789 if ctm_stack.len() > 1 {
790 ctm_stack.pop();
791 }
792 }
793 Operator::Cm { a, b, c, d, e, f } => {
794 if let Some(current_ctm) = ctm_stack.last_mut() {
795 let matrix = crate::content::Matrix { a, b, c, d, e, f };
796 *current_ctm = matrix.multiply(current_ctm);
798 }
799 }
800
801 Operator::Do { name } => {
802 if let Some(ref xobj_dict) = xobject_dict {
803 let current_ctm = ctm_stack
804 .last()
805 .copied()
806 .unwrap_or_else(crate::content::Matrix::identity);
807 if let Ok(mut xobj_images) = self.extract_images_from_xobject_do(
808 &name,
809 xobj_dict,
810 resources.as_ref(),
811 current_ctm,
812 &mut xobject_stack,
813 filter,
814 ) {
815 images.append(&mut xobj_images);
816 }
817 }
818 }
819
820 Operator::InlineImage { dict, data } => {
821 let current_ctm = ctm_stack
822 .last()
823 .copied()
824 .unwrap_or_else(crate::content::Matrix::identity);
825 if let Ok(image) = self.extract_image_from_inline(&dict, &data, current_ctm) {
826 images.push(image);
827 }
828 }
829
830 _ => {}
831 }
832 }
833
834 Ok(images)
835 }
836
837 fn extract_images_from_xobject_do(
842 &self,
843 name: &str,
844 xobject_dict: &std::collections::HashMap<String, Object>,
845 resources: Option<&Object>,
846 ctm: crate::content::Matrix,
847 xobject_stack: &mut Vec<ObjectRef>,
848 filter: &ImageExtractFilter,
849 ) -> Result<Vec<crate::extractors::PdfImage>> {
850 use crate::extractors::extract_image_from_xobject;
851
852 let mut images = Vec::new();
853
854 let xobject_ref_obj = match xobject_dict.get(name) {
855 Some(obj) => obj,
856 None => return Ok(images),
857 };
858
859 let xobject_ref_opt = xobject_ref_obj.as_reference();
860 let xobject = if let Some(ref_obj) = xobject_ref_opt {
861 self.load_object(ref_obj)?
862 } else {
863 xobject_ref_obj.clone()
864 };
865 let xobject_dict = xobject.as_dict().ok_or_else(|| Error::ParseError {
866 offset: 0,
867 reason: "XObject is not a dictionary".to_string(),
868 })?;
869
870 let subtype = xobject_dict.get("Subtype").and_then(|s| s.as_name()).unwrap_or("");
871
872 match subtype {
873 "Image" => {
874 let resolve_int = |o: &Object| -> Option<i64> {
884 match o.as_reference() {
885 Some(r) => self.load_object(r).ok().and_then(|v| v.as_integer()),
886 None => o.as_integer(),
887 }
888 };
889 let w = xobject_dict.get("Width").and_then(resolve_int).unwrap_or(0);
890 let h = xobject_dict.get("Height").and_then(resolve_int).unwrap_or(0);
891 if w < filter.min_width || h < filter.min_height {
892 return Ok(images);
893 }
894 if (w as u64) * (h as u64) > filter.max_pixels {
895 return Ok(images);
896 }
897 if filter.skip_indexed_small > 0
899 && (w < filter.skip_indexed_small || h < filter.skip_indexed_small)
900 && let Some(cs_obj) = xobject_dict.get("ColorSpace")
901 {
902 let is_indexed = match cs_obj {
903 Object::Name(n) => n == "Indexed",
904 Object::Array(arr) if !arr.is_empty() => arr[0].as_name() == Some("Indexed"),
905 _ => false,
906 };
907 if is_indexed {
908 return Ok(images);
909 }
910 }
911
912 let needs_cs_resolve = matches!(
914 &xobject,
915 Object::Stream { dict, .. } if matches!(dict.get("ColorSpace"), Some(Object::Reference(_)))
916 );
917
918 let resolved_xobject;
919 let xobject_for_extract = if needs_cs_resolve {
920 if let Object::Stream { dict, data } = &xobject {
921 let mut new_dict = dict.clone();
922 if let Some(Object::Reference(cs_ref)) = dict.get("ColorSpace")
923 && let Ok(resolved_cs) = self.load_object(*cs_ref)
924 {
925 new_dict.insert("ColorSpace".to_string(), resolved_cs);
926 }
927 resolved_xobject = Object::Stream {
928 dict: new_dict,
929 data: data.clone(),
930 };
931 &resolved_xobject
932 } else {
933 &xobject
934 }
935 } else {
936 &xobject
937 };
938
939 if let Ok(mut image) =
940 extract_image_from_xobject(Some(self), xobject_for_extract, xobject_ref_opt, None)
941 {
942 let unit_rect = crate::geometry::Rect::new(0.0, 0.0, 1.0, 1.0);
944 let bbox = self.transform_bbox_with_ctm(&unit_rect, ctm);
945 image.set_bbox(bbox);
946
947 image.set_matrix([ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]);
948 image.set_rotation_degrees(Self::matrix_to_rotation(ctm));
949
950 images.push(image);
951 }
952 }
953 "Form" => {
954 if let (Some(ref_obj), Some(parent_res)) = (xobject_ref_opt, resources)
955 && let Ok(mut form_images) =
956 self.extract_images_from_form_xobject(ref_obj, &xobject, parent_res, ctm, xobject_stack, filter)
957 {
958 images.append(&mut form_images);
959 }
960 }
961 _ => {}
962 }
963
964 Ok(images)
965 }
966
967 fn extract_images_from_form_xobject(
973 &self,
974 xobject_ref: ObjectRef,
975 xobject: &Object,
976 parent_resources: &Object,
977 parent_ctm: crate::content::Matrix,
978 xobject_stack: &mut Vec<ObjectRef>,
979 filter: &ImageExtractFilter,
980 ) -> Result<Vec<crate::extractors::PdfImage>> {
981 use crate::content::Operator;
982 use crate::content::parse_content_stream_images_only;
983
984 if xobject_stack.contains(&xobject_ref) || xobject_stack.len() >= 100 {
985 return Ok(Vec::new());
986 }
987
988 {
991 if let Some(cached_images) = self.form_xobject_images_cache.lock_or_recover().get(&xobject_ref) {
992 let images = cached_images
993 .iter()
994 .map(|img| {
995 let mut cloned = img.clone();
996 if let Some(rect) = cloned.bbox() {
997 cloned.set_bbox(self.transform_bbox_with_ctm(rect, parent_ctm));
998 }
999 cloned
1000 })
1001 .collect();
1002 return Ok(images);
1003 }
1004 }
1005
1006 xobject_stack.push(xobject_ref);
1007
1008 let xobj_dict = xobject.as_dict().ok_or_else(|| Error::ParseError {
1009 offset: 0,
1010 reason: "Form XObject is not a dictionary".to_string(),
1011 })?;
1012
1013 let form_resources = if let Some(form_res) = xobj_dict.get("Resources") {
1014 if let Some(ref_obj) = form_res.as_reference() {
1015 self.load_object(ref_obj)?
1016 } else {
1017 form_res.clone()
1018 }
1019 } else {
1020 parent_resources.clone()
1021 };
1022
1023 let form_xobject_dict = if let Some(res_dict) = form_resources.as_dict() {
1024 if let Some(xobj_entry) = res_dict.get("XObject") {
1025 let resolved = if let Some(ref_obj) = xobj_entry.as_reference() {
1026 self.load_object(ref_obj)?
1027 } else {
1028 xobj_entry.clone()
1029 };
1030 resolved.as_dict().cloned()
1031 } else {
1032 None
1033 }
1034 } else {
1035 None
1036 };
1037
1038 let form_matrix = if let Some(matrix_obj) = xobj_dict.get("Matrix") {
1039 self.parse_matrix_from_object(matrix_obj)
1040 .unwrap_or_else(crate::content::Matrix::identity)
1041 } else {
1042 crate::content::Matrix::identity()
1043 };
1044
1045 let cached_stream = self.xobject_stream_cache.lock_or_recover().get(&xobject_ref).cloned();
1046 let stream_data = if let Some(cached) = cached_stream {
1047 cached.as_ref().clone()
1048 } else {
1049 match self.decode_stream_with_encryption(xobject, xobject_ref) {
1050 Ok(data) => {
1051 const MAX_STREAM_CACHE_BYTES: usize = 50 * 1024 * 1024;
1052 let current_bytes = self.xobject_stream_cache_bytes.load(Ordering::Relaxed);
1053 if current_bytes + data.len() <= MAX_STREAM_CACHE_BYTES {
1054 self.xobject_stream_cache_bytes
1055 .store(current_bytes + data.len(), Ordering::Relaxed);
1056 self.xobject_stream_cache
1057 .lock_or_recover()
1058 .insert(xobject_ref, std::sync::Arc::new(data.clone()));
1059 }
1060 data
1061 }
1062 Err(error) => {
1063 tracing::warn!(target: LOG_TARGET,
1064 error_code = error.telemetry_code(),
1065 error_offset = ?error.telemetry_offset(),
1066 "failed to decode Form XObject stream; skipping"
1067 );
1068 xobject_stack.pop();
1069 return Ok(Vec::new());
1070 }
1071 }
1072 };
1073
1074 let operators = match parse_content_stream_images_only(&stream_data) {
1076 Ok(ops) => ops,
1077 Err(_) => {
1078 xobject_stack.pop();
1079 return Ok(Vec::new());
1080 }
1081 };
1082
1083 let mut raw_images = Vec::new();
1086 let mut ctm_stack = vec![form_matrix];
1087
1088 for op in operators {
1089 match op {
1090 Operator::SaveState => {
1091 if let Some(current_ctm) = ctm_stack.last() {
1092 ctm_stack.push(*current_ctm);
1093 }
1094 }
1095 Operator::RestoreState => {
1096 if ctm_stack.len() > 1 {
1097 ctm_stack.pop();
1098 }
1099 }
1100 Operator::Cm { a, b, c, d, e, f } => {
1101 if let Some(current_ctm) = ctm_stack.last_mut() {
1102 let matrix = crate::content::Matrix { a, b, c, d, e, f };
1103 *current_ctm = matrix.multiply(current_ctm);
1105 }
1106 }
1107
1108 Operator::Do { name } => {
1109 if let Some(ref xobj_d) = form_xobject_dict {
1110 let current_ctm = ctm_stack
1111 .last()
1112 .copied()
1113 .unwrap_or_else(crate::content::Matrix::identity);
1114 if let Ok(mut xobj_images) = self.extract_images_from_xobject_do(
1117 &name,
1118 xobj_d,
1119 Some(&form_resources),
1120 current_ctm,
1121 xobject_stack,
1122 filter,
1123 ) {
1124 raw_images.append(&mut xobj_images);
1125 }
1126 }
1127 }
1128
1129 Operator::InlineImage { dict, data } => {
1130 let current_ctm = ctm_stack
1131 .last()
1132 .copied()
1133 .unwrap_or_else(crate::content::Matrix::identity);
1134 if let Ok(image) = self.extract_image_from_inline(&dict, &data, current_ctm) {
1135 raw_images.push(image);
1136 }
1137 }
1138
1139 _ => {}
1140 }
1141 }
1142
1143 xobject_stack.pop();
1144
1145 self.form_xobject_images_cache
1147 .lock_or_recover()
1148 .insert(xobject_ref, raw_images.clone());
1149
1150 let images = raw_images
1151 .into_iter()
1152 .map(|mut img| {
1153 if let Some(rect) = img.bbox() {
1154 img.set_bbox(self.transform_bbox_with_ctm(rect, parent_ctm));
1155 }
1156 img
1157 })
1158 .collect();
1159
1160 Ok(images)
1161 }
1162
1163 fn extract_image_from_inline(
1165 &self,
1166 dict: &std::collections::HashMap<String, Object>,
1167 data: &[u8],
1168 ctm: crate::content::Matrix,
1169 ) -> Result<crate::extractors::PdfImage> {
1170 use crate::extractors::expand_inline_image_dict;
1171
1172 let expanded_dict = expand_inline_image_dict(dict.clone());
1173
1174 let stream_obj = Object::Stream {
1175 dict: expanded_dict,
1176 data: bytes::Bytes::copy_from_slice(data),
1177 };
1178
1179 let mut image = crate::extractors::extract_image_from_xobject(Some(self), &stream_obj, None, None)?;
1180
1181 let unit_rect = crate::geometry::Rect::new(0.0, 0.0, 1.0, 1.0);
1183 let bbox = self.transform_bbox_with_ctm(&unit_rect, ctm);
1184 image.set_bbox(bbox);
1185
1186 image.set_matrix([ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]);
1187 image.set_rotation_degrees(Self::matrix_to_rotation(ctm));
1188
1189 Ok(image)
1190 }
1191
1192 fn matrix_to_rotation(m: crate::content::Matrix) -> i32 {
1194 let angle_rad = m.b.atan2(m.a);
1195 let angle_deg = (angle_rad.to_degrees().round() as i32) % 360;
1196 if angle_deg < 0 { angle_deg + 360 } else { angle_deg }
1197 }
1198
1199 pub(super) fn transform_bbox_with_ctm(
1204 &self,
1205 rect: &crate::geometry::Rect,
1206 ctm: crate::content::Matrix,
1207 ) -> crate::geometry::Rect {
1208 let x0 = rect.x;
1209 let y0 = rect.y;
1210 let x1 = rect.x + rect.width;
1211 let y1 = rect.y + rect.height;
1212
1213 let tx0 = ctm.a * x0 + ctm.c * y0 + ctm.e;
1214 let ty0 = ctm.b * x0 + ctm.d * y0 + ctm.f;
1215
1216 let tx1 = ctm.a * x1 + ctm.c * y0 + ctm.e;
1217 let ty1 = ctm.b * x1 + ctm.d * y0 + ctm.f;
1218
1219 let tx2 = ctm.a * x0 + ctm.c * y1 + ctm.e;
1220 let ty2 = ctm.b * x0 + ctm.d * y1 + ctm.f;
1221
1222 let tx3 = ctm.a * x1 + ctm.c * y1 + ctm.e;
1223 let ty3 = ctm.b * x1 + ctm.d * y1 + ctm.f;
1224
1225 let min_x = tx0.min(tx1).min(tx2).min(tx3);
1226 let max_x = tx0.max(tx1).max(tx2).max(tx3);
1227 let min_y = ty0.min(ty1).min(ty2).min(ty3);
1228 let max_y = ty0.max(ty1).max(ty2).max(ty3);
1229
1230 crate::geometry::Rect {
1231 x: min_x,
1232 y: min_y,
1233 width: max_x - min_x,
1234 height: max_y - min_y,
1235 }
1236 }
1237
1238 pub(super) fn parse_matrix_from_object(&self, obj: &Object) -> Option<crate::content::Matrix> {
1240 if let Some(array) = obj.as_array()
1241 && array.len() >= 6
1242 {
1243 let mut values = [0.0f32; 6];
1244 for (i, val) in array.iter().take(6).enumerate() {
1245 let num = if let Some(f) = val.as_real() {
1246 f as f32
1247 } else {
1248 let i_val = val.as_integer()?;
1249 i_val as f32
1250 };
1251 values[i] = num;
1252 }
1253
1254 return Some(crate::content::Matrix {
1255 a: values[0],
1256 b: values[1],
1257 c: values[2],
1258 d: values[3],
1259 e: values[4],
1260 f: values[5],
1261 });
1262 }
1263 None
1264 }
1265
1266 #[cfg(not(target_arch = "wasm32"))]
1271 pub fn extract_images_to_files(
1272 &self,
1273 page_index: usize,
1274 output_dir: impl AsRef<Path>,
1275 prefix: Option<&str>,
1276 start_index: Option<usize>,
1277 ) -> Result<Vec<ExtractedImageRef>> {
1278 use std::fs;
1279
1280 let images = self.extract_images(page_index)?;
1281
1282 let output_dir = output_dir.as_ref();
1283 if !output_dir.exists() {
1284 fs::create_dir_all(output_dir).map_err(Error::Io)?;
1285 }
1286
1287 let prefix = prefix.unwrap_or("img");
1288 let mut index = start_index.unwrap_or(1);
1289 let mut result = Vec::new();
1290
1291 for image in images {
1292 let (format, extension) = match image.data() {
1293 crate::extractors::ImageData::Jpeg(_) => (ImageFormat::Jpeg, "jpg"),
1294 _ => (ImageFormat::Png, "png"),
1295 };
1296
1297 let filename = format!("{}_{:03}.{}", prefix, index, extension);
1298 let filepath = output_dir.join(&filename);
1299
1300 match format {
1301 ImageFormat::Jpeg => image.save_as_jpeg(&filepath)?,
1302 ImageFormat::Png => image.save_as_png(&filepath)?,
1303 }
1304
1305 result.push(ExtractedImageRef {
1306 filename,
1307 format,
1308 width: image.width(),
1309 height: image.height(),
1310 bbox: image.bbox().cloned(),
1311 rotation: image.rotation_degrees(),
1312 matrix: image.matrix(),
1313 });
1314
1315 index += 1;
1316 }
1317
1318 Ok(result)
1319 }
1320
1321 pub fn get_page_for_debug(&self, page_index: usize) -> Result<Object> {
1324 self.get_page(page_index)
1325 }
1326
1327 pub fn may_contain_text_public(data: &[u8]) -> bool {
1330 Self::may_contain_text(data)
1331 }
1332}