1use super::*;
8use crate::content::{Operator, TextElement};
9use crate::extractors::auto::{ImageCodecClass, PageSignals, ProducerPrior, ReasonCode};
10
11impl PdfDocument {
12 #[tracing::instrument(name = "pdf.from_bytes", skip_all, fields(bytes = data.len()))]
22 pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
23 match Self::open_from_bytes_inner(data) {
24 Ok(document) => Ok(document),
25 Err(error) => {
26 trace_open_error(&error);
27 Err(error)
28 }
29 }
30 }
31
32 #[deprecated(since = "0.3.15", note = "Use `from_bytes` instead")]
34 pub fn open_from_bytes(data: Vec<u8>) -> Result<Self> {
35 Self::from_bytes(data)
36 }
37
38 #[cfg(not(target_arch = "wasm32"))]
60 #[tracing::instrument(name = "pdf.open", skip_all, fields(path = %path.as_ref().display()))]
61 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
62 let data = match std::fs::read(path.as_ref()) {
72 Ok(data) => data,
73 Err(error) => {
74 let error = Error::Io(error);
75 trace_open_error(&error);
76 return Err(error);
77 }
78 };
79 Self::from_bytes(data)
80 }
81
82 fn open_from_bytes_inner(data: Vec<u8>) -> Result<Self> {
86 let mut reader = PdfReader::Memory(BufReader::new(Cursor::new(data)));
87 let (major, minor, header_offset) = parse_header(&mut reader, true)?;
89 let version = (major, minor);
90
91 let (mut xref, trailer, mut xref_reconstructed, mut synthetic_objects) =
92 Self::load_initial_xref_and_trailer(&mut reader)?;
93
94 if header_offset > 0 {
95 Self::adjust_xref_for_header_offset(&mut reader, &mut xref, &trailer, header_offset);
96 }
97
98 let (xref, trailer) = if validate_root_loadable(&mut reader, &xref, &trailer) {
101 (xref, trailer)
102 } else {
103 tracing::warn!(target: LOG_TARGET, "Root object not loadable after xref parse, falling back to xref reconstruction");
104 match Self::try_reconstruct_xref(&mut reader) {
105 Ok((x, t, syn)) => {
106 xref_reconstructed = true;
107 synthetic_objects = syn;
108 (x, t)
109 }
110 Err(_) => (xref, trailer),
111 }
112 };
113
114 let prepopulated_scan = Self::build_prepopulated_scan(&xref, xref_reconstructed);
115
116 let PdfReader::Memory(buffered) = reader;
119 let source_bytes = buffered.into_inner().into_inner();
120
121 let document = Self::build_document(
125 (source_bytes, version, xref, trailer),
126 (header_offset, xref_reconstructed, prepopulated_scan),
127 );
128
129 if !synthetic_objects.is_empty() {
134 let mut cache = document.object_cache.lock_or_recover();
135 for (obj_ref, obj) in synthetic_objects {
136 cache.insert(obj_ref, obj);
137 }
138 }
139
140 document.recover_object_streams_if_reconstructed(); if let Err(error) = document.ensure_encryption_initialized() {
143 trace_recoverable_pdf_error("initialize_encryption", &error);
144 }
147
148 Ok(document)
149 }
150
151 fn build_prepopulated_scan(xref: &CrossRefTable, xref_reconstructed: bool) -> Option<HashMap<u32, u64>> {
157 if !xref_reconstructed {
158 return None;
159 }
160 Some(
161 xref.all_object_numbers()
162 .filter_map(|id| {
163 xref.get(id).and_then(|e| {
164 (e.in_use && e.entry_type == crate::xref::XRefEntryType::Uncompressed).then_some((id, e.offset))
165 })
166 })
167 .collect(),
168 )
169 }
170
171 fn load_initial_xref_and_trailer<R: Read + Seek>(
172 reader: &mut R,
173 ) -> Result<(CrossRefTable, Object, bool, Vec<(ObjectRef, Object)>)> {
174 let mut xref_reconstructed = false;
175 let mut synthetic_objects: Vec<(ObjectRef, Object)> = Vec::new();
176
177 let (xref, trailer) = match Self::try_open_regular(reader) {
178 Ok((xref, trailer)) => {
179 if xref.is_empty() {
183 tracing::warn!(target: LOG_TARGET, "Regular xref parsing succeeded but table is empty, attempting reconstruction");
184 xref_reconstructed = true;
185 let (x, t, syn) = Self::try_reconstruct_xref(reader)?;
186 synthetic_objects = syn;
187 (x, t)
188 } else {
189 (xref, trailer)
193 }
194 }
195 Err(e) => {
196 trace_xref_parse_failure(&e);
197 match Self::try_reconstruct_xref(reader) {
198 Ok((reconstructed_xref, reconstructed_trailer, syn)) => {
199 tracing::info!(target: LOG_TARGET, "Successfully reconstructed xref table");
200 xref_reconstructed = true;
201 synthetic_objects = syn;
202 (reconstructed_xref, reconstructed_trailer)
203 }
204 Err(recon_err) => {
205 tracing::warn!(
206 target: crate::LOG_TARGET_ROOT,
207 operation = "reconstruct_xref",
208 primary_error_code = e.telemetry_code(),
209 primary_error_offset = ?e.telemetry_offset(),
210 reconstruction_error_code = recon_err.telemetry_code(),
211 reconstruction_error_offset = ?recon_err.telemetry_offset(),
212 "xref reconstruction failed; propagating original parse error"
213 );
214 return Err(e);
215 }
216 }
217 }
218 };
219
220 Ok((xref, trailer, xref_reconstructed, synthetic_objects))
221 }
222
223 fn adjust_xref_for_header_offset<R: Read + Seek>(
227 reader: &mut R,
228 xref: &mut CrossRefTable,
229 trailer: &Object,
230 header_offset: u64,
231 ) {
232 let probe = get_root_ref_from_trailer(trailer)
242 .filter(|r| {
243 xref.get(r.id)
244 .is_some_and(|e| e.in_use && e.entry_type == crate::xref::XRefEntryType::Uncompressed)
245 })
246 .or_else(|| first_in_use_uncompressed(xref));
247 if let Some(probe_ref) = probe
248 && !validate_object_at_offset(reader, xref, probe_ref)
249 {
250 tracing::warn!(target: LOG_TARGET,
251 "Probe object {} not loadable at xref offset, adjusting all offsets by header_offset={}",
252 probe_ref.id,
253 header_offset
254 );
255 xref.shift_offsets(header_offset);
256 }
257 }
258
259 fn build_document(
260 parsed: (Vec<u8>, (u8, u8), CrossRefTable, Object),
261 recovery: (u64, bool, Option<HashMap<u32, u64>>),
262 ) -> Self {
263 let (source_bytes, version, xref, trailer) = parsed;
264 let (header_offset, xref_reconstructed, prepopulated_scan) = recovery;
265 Self {
266 source_bytes,
267 version,
268 xref,
269 trailer,
270 object_cache: Mutex::new(BoundedObjectCache::new(DEFAULT_OBJECT_CACHE_MAX_BYTES)),
271 object_stream_cache: Mutex::new(BoundedObjectStreamCache::new(DEFAULT_OBJECT_STREAM_CACHE_MAX_BYTES)),
272 object_stream_telemetry_seen: Mutex::new(BoundedRecoveryTelemetry::new(
273 DEFAULT_OBJECT_STREAM_RECOVERY_MARKERS,
274 )),
275 encryption_handler: Mutex::new(None),
276 encrypt_dict_ref: Mutex::new(None),
277 options: ParserOptions::default(),
278 header_offset,
279 font_cache: Mutex::new(BoundedEntryCache::new(512)),
280 font_set_cache: Mutex::new(BoundedEntryCache::new(256)),
281 font_fingerprint_cache: Mutex::new(BoundedEntryCache::new(256)),
282 font_name_set_cache: Mutex::new(BoundedEntryCache::new(256)),
283 font_set_donated_cache: Mutex::new(BoundedEntryCache::new(256)),
284 font_fingerprint_donated_cache: Mutex::new(BoundedEntryCache::new(256)),
285 font_name_set_donated_cache: Mutex::new(BoundedEntryCache::new(256)),
286 #[cfg(test)]
287 donation_call_count: AtomicUsize::new(0),
288 #[cfg(test)]
289 donation_apply_count: AtomicUsize::new(0),
290 font_identity_cache: Mutex::new(BoundedEntryCache::new(512)),
291 font_id_hash_cache: Mutex::new(HashMap::new()),
292 font_reference_hash_cache: Mutex::new(BoundedEntryCache::new(FONT_IDENTITY_MAX_RESOLVED_REFERENCES)),
293 font_identity_hashed_bytes: AtomicUsize::new(0),
294 font_identity_shared_cache_enabled: AtomicBool::new(true),
295 structure_tree_cache: Mutex::new(None),
296 structure_content_cache: Mutex::new(None),
297 actualtext_index_cache: Mutex::new(None),
298 mc_actualtext_mcids: Mutex::new(HashMap::new()),
299 table_elements_cache: Mutex::new(None),
300 page_cache: Mutex::new(HashMap::new()),
301 page_cache_populated: AtomicBool::new(false),
302 scanned_object_offsets: Mutex::new(prepopulated_scan),
303 objstm_recovery_done: Mutex::new(false),
304 xref_reconstructed,
305 image_xobject_cache: Mutex::new(HashSet::new()),
306 xobject_text_free_cache: Mutex::new(HashSet::new()),
307 xobject_stream_cache: Mutex::new(HashMap::new()),
308 xobject_stream_cache_bytes: AtomicUsize::new(0),
309 xobject_spans_cache: Mutex::new(BoundedEntryCache::new(DEFAULT_XOBJECT_CACHE_MAX_ENTRIES)),
310 form_xobject_images_cache: Mutex::new(BoundedEntryCache::new(DEFAULT_XOBJECT_CACHE_MAX_ENTRIES)),
311 erase_regions: Mutex::new(HashMap::new()),
312 page_content_cache: Mutex::new(BoundedEntryCache::new(64)),
313 page_spans_cache: Mutex::new(BoundedEntryCache::new(8)),
314 search_index: Mutex::new(HashMap::new()),
315 page_chars_cache: Mutex::new(BoundedEntryCache::new(8)),
316 running_artifact_signatures: Mutex::new(None),
317 article_threads_cache: Mutex::new(None),
318 output_intent_cmyk_profile_cache: Mutex::new(None),
319 accumulated_warnings: Mutex::new(Vec::new()),
320 warning_sink: crate::extractors::warnings::WarningSink::new(),
321 recovery: std::sync::Arc::default(),
322 }
323 }
324
325 fn try_open_regular<R: Read + Seek>(reader: &mut R) -> Result<(CrossRefTable, Object)> {
327 let xref_offset = find_xref_offset(reader)?;
328
329 let xref = parse_xref(reader, xref_offset)?;
330
331 let trailer = if let Some(trailer_dict) = xref.trailer() {
332 Object::Dictionary(trailer_dict.clone())
333 } else {
334 reader.seek(SeekFrom::Start(xref_offset))?;
335 parse_trailer(reader)?
336 };
337
338 Ok((xref, trailer))
339 }
340
341 fn try_reconstruct_xref<R: Read + Seek>(
346 reader: &mut R,
347 ) -> Result<(CrossRefTable, Object, Vec<(ObjectRef, Object)>)> {
348 crate::xref_reconstruction::reconstruct_xref(reader)
349 }
350
351 pub(super) fn ensure_encryption_initialized(&self) -> Result<()> {
362 if self.encryption_handler.lock_or_recover().is_some() {
363 return Ok(());
364 }
365 let Some(trailer_dict) = self.trailer.as_dict() else {
366 return Ok(());
367 };
368 let encrypt_ref = match trailer_dict.get("Encrypt") {
369 Some(obj) => obj.clone(),
370 None => {
371 tracing::debug!(target: LOG_TARGET, "PDF is not encrypted (no /Encrypt entry)");
372 return Ok(());
373 }
374 };
375 let file_id = match trailer_dict.get("ID") {
376 Some(Object::Array(arr)) => match arr.first() {
377 Some(first_id) => match first_id.as_string() {
378 Some(id_bytes) => id_bytes.to_vec(),
379 None => {
380 tracing::warn!(target: LOG_TARGET, "Invalid /ID array entry (not a string), using empty file ID");
381 vec![]
382 }
383 },
384 None => {
385 tracing::warn!(target: LOG_TARGET, "Empty /ID array, using empty file ID");
386 vec![]
387 }
388 },
389 _ => {
390 tracing::warn!(target: LOG_TARGET, "Missing or invalid /ID entry in trailer, using empty file ID");
391 vec![]
392 }
393 };
394 let encrypt_obj = match encrypt_ref {
395 Object::Dictionary(_) => encrypt_ref,
396 Object::Reference(obj_ref) => {
397 tracing::debug!(target: LOG_TARGET,
398 object_id = obj_ref.id,
399 generation = obj_ref.generation,
400 "loading /Encrypt object reference"
401 );
402 *self.encrypt_dict_ref.lock_or_recover() = Some(obj_ref);
403 self.load_object(obj_ref)?
404 }
405 _ => {
406 return Err(Error::InvalidPdf(format!(
407 "Invalid /Encrypt entry type: {}",
408 encrypt_ref.type_name()
409 )));
410 }
411 };
412 let encrypt_obj = match encrypt_obj.as_dict() {
413 Some(dict) => Object::Dictionary(resolve_encrypt_dictionary_references(dict, |reference| {
414 self.load_object(reference)
415 })),
416 None => encrypt_obj,
417 };
418 let mut handler = EncryptionHandler::new(&encrypt_obj, file_id)?;
419
420 match handler.authenticate(b"") {
421 Ok(true) => {
422 tracing::info!(target: LOG_TARGET, "Successfully authenticated with empty password");
423 }
424 Ok(false) => {
425 tracing::warn!(target: LOG_TARGET, "PDF is encrypted and requires a password");
426 self.push_warning(
427 "PDF is encrypted and requires a password; call authenticate() before extracting text".to_string(),
428 );
429 }
430 Err(error) => {
431 trace_fatal_pdf_error("authenticate_empty_password", &error);
432 return Err(error);
433 }
434 }
435 *self.encryption_handler.lock_or_recover() = Some(handler);
436 Ok(())
437 }
438
439 pub(crate) fn decode_stream_with_encryption(&self, stream_obj: &Object, obj_ref: ObjectRef) -> Result<Vec<u8>> {
457 self.decode_stream_with_encryption_and_expected_size(stream_obj, obj_ref, None)
458 }
459
460 pub(crate) fn decode_image_stream_with_encryption(
461 &self,
462 stream_obj: &Object,
463 obj_ref: ObjectRef,
464 expected_filter_output_size: usize,
465 ) -> Result<Vec<u8>> {
466 self.decode_stream_with_encryption_and_expected_size(stream_obj, obj_ref, Some(expected_filter_output_size))
467 }
468
469 fn decode_stream_with_encryption_and_expected_size(
470 &self,
471 stream_obj: &Object,
472 obj_ref: ObjectRef,
473 expected_filter_output_size: Option<usize>,
474 ) -> Result<Vec<u8>> {
475 if matches!(stream_obj, Object::Null) {
476 return Ok(Vec::new());
477 }
478
479 let is_unencrypted_stream_type = if let Object::Stream { dict, .. } = stream_obj {
484 dict.get("Type")
485 .and_then(|t| t.as_name())
486 .map(|name| name == "ObjStm" || name == "XRef")
487 .unwrap_or(false)
488 } else {
489 false
490 };
491
492 let handler_ref = self.encryption_handler.lock_or_recover();
493 if let Some(handler) = handler_ref.as_ref() {
494 if is_unencrypted_stream_type {
495 drop(handler_ref);
496 return match expected_filter_output_size {
497 Some(expected_size) => stream_obj.decode_image_stream_data(expected_size),
498 None => stream_obj.decode_stream_data(),
499 };
500 }
501 let decrypt_fn = |data: &[u8]| -> Result<Vec<u8>> {
502 handler.decrypt_stream(data, obj_ref.id, obj_ref.generation as u32)
503 };
504 stream_obj.decode_stream_data_with_decryption_and_expected_size(
505 Some(&decrypt_fn),
506 obj_ref.id,
507 obj_ref.generation as u32,
508 expected_filter_output_size,
509 )
510 } else {
511 drop(handler_ref);
512 match expected_filter_output_size {
513 Some(expected_size) => stream_obj.decode_image_stream_data(expected_size),
514 None => stream_obj.decode_stream_data(),
515 }
516 }
517 }
518
519 #[cfg(not(target_arch = "wasm32"))]
524 pub fn open_with_config(path: impl AsRef<Path>, _config: impl std::any::Any) -> Result<Self> {
525 Self::open(path)
526 }
527
528 pub fn authenticate(&self, password: &[u8]) -> Result<bool> {
542 self.ensure_encryption_initialized()?;
543 let was_authenticated = self
551 .encryption_handler
552 .lock_or_recover()
553 .as_ref()
554 .map(|h| h.is_authenticated())
555 .unwrap_or(true);
556
557 let result = match self.encryption_handler.lock_or_recover().as_mut() {
558 Some(handler) => handler.authenticate(password),
559 None => return Ok(true),
560 };
561
562 if let Ok(true) = result
563 && !was_authenticated
564 {
565 self.object_cache.lock_or_recover().clear();
573 tracing::debug!(target: LOG_TARGET,
574 "authenticate(): object cache cleared after successful authentication \
575 to force re-decryption of any pre-auth cached objects"
576 );
577 }
578
579 result
580 }
581
582 pub fn is_encrypted(&self) -> bool {
598 if self.encryption_handler.lock_or_recover().is_some() {
599 return true;
600 }
601 self.trailer.as_dict().and_then(|d| d.get("Encrypt")).is_some()
602 }
603
604 #[must_use]
612 pub fn is_authenticated(&self) -> bool {
613 if self.ensure_encryption_initialized().is_err() {
619 return false;
620 }
621 !self.is_encrypted_and_unauthenticated()
622 }
623
624 #[must_use]
628 pub fn document_producer(&self) -> Option<String> {
629 self.document_info_string("Producer")
630 }
631
632 #[must_use]
635 pub fn document_creator(&self) -> Option<String> {
636 self.document_info_string("Creator")
637 }
638
639 fn document_info_string(&self, key: &str) -> Option<String> {
640 let info_raw = self.trailer.as_dict()?.get("Info")?;
641 let info = self.resolve_obj_ref(info_raw);
642 let val_raw = info.as_dict()?.get(key)?.clone();
643 let val = self.resolve_obj_ref(&val_raw);
644 let s = Self::decode_pdf_text_string(val.as_string()?);
645 let trimmed = s.trim();
646 (!trimmed.is_empty()).then(|| trimmed.to_string())
647 }
648
649 fn rect_isect_area(r: &crate::geometry::Rect, x0: f32, y0: f32, x1: f32, y1: f32) -> f32 {
652 let (rx1, ry1) = (r.x + r.width, r.y + r.height);
653 let ix = (rx1.min(x1) - r.x.max(x0)).max(0.0);
654 let iy = (ry1.min(y1) - r.y.max(y0)).max(0.0);
655 ix * iy
656 }
657
658 #[allow(clippy::type_complexity)]
659 fn accumulate_text_signals(
660 &self,
661 page: usize,
662 (px0, py0, px1, py1): (f32, f32, f32, f32),
663 page_area: f32,
664 ) -> (usize, f32, f32, f32, f32, String) {
665 let spans = self.extract_spans(page).unwrap_or_default();
666 let mut text = String::new();
667 let mut glyphs = 0usize;
668 let mut text_area = 0.0f32;
669 for s in &spans {
670 if s.artifact_type.is_some() {
671 continue;
672 }
673 let n = s.text.chars().count();
674 if n == 0 {
675 continue;
676 }
677 glyphs += n;
678 text.push_str(&s.text);
679 text.push(' ');
680 text_area += Self::rect_isect_area(&s.bbox, px0, py0, px1, py1);
681 }
682 let text_area_ratio = (text_area / page_area).clamp(0.0, 1.0);
683 let chars: Vec<char> = text.chars().collect();
684 let total = chars.len().max(1);
685 let bad = chars
686 .iter()
687 .filter(|&&c| c == '\u{FFFD}' || c.is_control() || ('\u{E000}'..='\u{F8FF}').contains(&c))
688 .count();
689 let garbled_ratio = bad as f32 / total as f32;
690 let word_text: String = self
691 .extract_words(page)
692 .unwrap_or_default()
693 .into_iter()
694 .map(|w| w.text)
695 .collect::<Vec<_>>()
696 .join(" ");
697 let words: Vec<&str> = word_text.split_whitespace().collect();
698 let (fragmented_word_ratio, consecutive_repeat_ratio) = if words.is_empty() {
699 (0.0, 0.0)
700 } else {
701 let frag = words.iter().filter(|w| w.chars().count() <= 2).count() as f32 / words.len() as f32;
702 let rep = words.windows(2).filter(|w| w[0] == w[1]).count() as f32 / words.len() as f32;
703 let frag = if crate::extractors::auto::is_cjk_dominant_text(&word_text) {
704 0.0
705 } else {
706 frag
707 };
708 (frag, rep)
709 };
710 (
711 glyphs,
712 text_area_ratio,
713 garbled_ratio,
714 fragmented_word_ratio,
715 consecutive_repeat_ratio,
716 word_text,
717 )
718 }
719
720 fn classify_page_images(
721 &self,
722 page: usize,
723 (px0, py0, px1, py1): (f32, f32, f32, f32),
724 page_area: f32,
725 ) -> (f32, ImageCodecClass, usize) {
726 let images = self.page_image_handles(page).unwrap_or_default();
733 let size_floor = ImageExtractFilter::default();
734 let mut img_area = 0.0f32;
735 let mut codec = ImageCodecClass::None;
736 for im in &images {
737 if i64::from(im.width) < size_floor.min_width || i64::from(im.height) < size_floor.min_height {
738 continue;
739 }
740 img_area += Self::rect_isect_area(&im.bbox, px0, py0, px1, py1);
741 let has_ccitt = im
742 .filter_chain
743 .iter()
744 .any(|f| matches!(f, crate::extractors::images::PdfFilter::CCITTFaxDecode));
745 let has_dct = im
746 .filter_chain
747 .iter()
748 .any(|f| matches!(f, crate::extractors::images::PdfFilter::DCTDecode));
749 let c = if has_ccitt {
750 ImageCodecClass::Ccitt
751 } else if has_dct {
752 ImageCodecClass::Dct
753 } else {
754 ImageCodecClass::Other
755 };
756 codec = match (codec, c) {
757 (ImageCodecClass::None, x) => x,
758 (_, ImageCodecClass::Ccitt) => ImageCodecClass::Ccitt,
759 (cur, _) => cur,
760 };
761 }
762 let image_area_ratio = (img_area / page_area).clamp(0.0, 1.0);
763 (image_area_ratio, codec, images.len())
764 }
765
766 fn compute_invisible_text_ratio(&self, page: usize) -> f32 {
767 let mut invisible = 0usize;
768 let mut glyph_bytes = 0usize;
769 if let Ok(data) = self.get_page_content_data(page)
770 && let Ok(ops) = crate::content::parse_content_stream(&data)
771 {
772 let mut rm: u8 = 0;
773 let mut stack: Vec<u8> = Vec::new();
774 for op in &ops {
775 match op {
776 Operator::SaveState => stack.push(rm),
777 Operator::RestoreState => {
778 if let Some(p) = stack.pop() {
779 rm = p;
780 }
781 }
782 Operator::Tr { render } => rm = *render,
783 Operator::Tj { text } => {
784 glyph_bytes += text.len();
785 if rm == 3 {
786 invisible += text.len();
787 }
788 }
789 Operator::TJ { array } => {
790 let g: usize = array
791 .iter()
792 .map(|e| match e {
793 TextElement::String(b) => b.len(),
794 TextElement::Offset(_) => 0,
795 })
796 .sum();
797 glyph_bytes += g;
798 if rm == 3 {
799 invisible += g;
800 }
801 }
802 _ => {}
803 }
804 }
805 }
806 if glyph_bytes == 0 {
807 0.0
808 } else {
809 invisible as f32 / glyph_bytes as f32
810 }
811 }
812
813 fn classify_producer_prior(&self) -> ProducerPrior {
814 let p = format!(
815 "{} {}",
816 self.document_producer().unwrap_or_default(),
817 self.document_creator().unwrap_or_default()
818 )
819 .to_lowercase();
820 const SCAN: &[&str] = &[
821 "scan",
822 "abbyy",
823 "tesseract",
824 "scansnap",
825 "finereader",
826 "ocr",
827 "lens",
828 "camscanner",
829 "kofax",
830 ];
831 const AUTH: &[&str] = &[
832 "word",
833 "libreoffice",
834 "latex",
835 "pdftex",
836 "chromium",
837 "skia",
838 "quartz",
839 "wkhtmltopdf",
840 "pdf_oxide",
845 "xberg-pdf-oxide",
846 "xberg-native-pdf",
847 "reportlab",
848 "prince",
849 "weasyprint",
850 "powerpoint",
851 "excel",
852 "indesign",
853 ];
854 if SCAN.iter().any(|k| p.contains(k)) {
855 ProducerPrior::Scanner
856 } else if AUTH.iter().any(|k| p.contains(k)) {
857 ProducerPrior::Authoring
858 } else {
859 ProducerPrior::Unknown
860 }
861 }
862
863 fn gather_page_signals(&self, page: usize) -> Result<(PageSignals, Option<ReasonCode>)> {
869 let (llx, lly, urx, ury) = self.get_page_media_box(page)?;
870 let rot = self.get_page_rotation(page).unwrap_or(0);
871 let (mut pw, mut ph) = ((urx - llx).abs(), (ury - lly).abs());
872 if rot % 180 != 0 {
873 std::mem::swap(&mut pw, &mut ph);
874 }
875 let page_area = (pw * ph).max(1.0);
876 let bbox = (llx.min(urx), lly.min(ury), llx.max(urx), lly.max(ury));
877
878 let (glyphs, text_area_ratio, garbled_ratio, fragmented_word_ratio, consecutive_repeat_ratio, word_text) =
879 self.accumulate_text_signals(page, bbox, page_area);
880 let (image_area_ratio, codec, images_len) = self.classify_page_images(page, bbox, page_area);
881 let invisible_text_ratio = self.compute_invisible_text_ratio(page);
882 let path_count = self.extract_paths(page).map(|p| p.len()).unwrap_or(0);
883 let vector_path_density = {
884 let denom = (path_count + glyphs + images_len).max(1) as f32;
885 (path_count as f32 / denom).clamp(0.0, 1.0)
886 };
887 let has_reliable_structure = self.mark_info().map(|m| m.is_structure_reliable()).unwrap_or(false);
888 let producer_prior = self.classify_producer_prior();
889 let page_is_empty = glyphs == 0 && image_area_ratio < 0.01 && path_count == 0;
890
891 let signals = PageSignals {
892 text_glyph_count: glyphs,
893 text_area_ratio,
894 image_area_ratio,
895 codec,
896 invisible_text_ratio,
897 garbled_ratio,
898 fragmented_word_ratio,
899 consecutive_repeat_ratio,
900 vector_path_density,
901 has_reliable_structure,
902 producer_prior,
903 page_is_empty,
904 };
905 let gate = crate::extractors::auto::text_quality_gate(&word_text);
906 Ok((signals, gate))
907 }
908
909 pub fn classify_page(&self, page: usize) -> Result<crate::extractors::auto::PageClassification> {
919 use crate::extractors::auto::{AutoExtractOptions, PageClassification, PageKind, classify_from_signals};
920 if !self.is_authenticated() {
921 return Err(Error::EncryptedPdf);
922 }
923 let (signals, gate) = self.gather_page_signals(page)?;
924 let opts = AutoExtractOptions::balanced();
925 let (mut kind, mut confidence, mut reason) = classify_from_signals(&signals, &opts);
926 if matches!(kind, PageKind::TextLayer)
927 && let Some(r) = gate
928 {
929 kind = PageKind::Scanned;
930 confidence = confidence.min(0.80);
931 reason = r;
932 }
933 Ok(PageClassification {
934 page,
935 kind,
936 confidence,
937 reason,
938 signals,
939 })
940 }
941
942 pub fn classify_document(&self) -> Result<crate::extractors::auto::DocumentClassification> {
952 use crate::extractors::auto::{DocumentClassification, PageKind, summarise};
953 let n = self.page_count()?;
954 let mut kinds = Vec::with_capacity(n);
955 let mut need = Vec::new();
956 for p in 0..n {
957 let k = match self.classify_page(p) {
958 Ok(c) => c.kind,
959 Err(e @ Error::EncryptedPdf) => return Err(e),
960 Err(_) => PageKind::Empty,
961 };
962 if matches!(k, PageKind::Scanned | PageKind::ImageText | PageKind::Mixed) {
963 need.push(p);
964 }
965 kinds.push(k);
966 }
967 let summary = summarise(&kinds);
968 Ok(DocumentClassification {
969 pages: kinds,
970 pages_needing_ocr: need,
971 summary,
972 })
973 }
974
975 pub fn extract_text_auto(&self, page: usize) -> Result<String> {
981 crate::extractors::auto::AutoExtractor::new().extract_text(self, page)
982 }
983
984 fn is_encrypted_and_unauthenticated(&self) -> bool {
990 if let Some(handler) = self.encryption_handler.lock_or_recover().as_ref() {
991 !handler.is_authenticated()
992 } else {
993 false
997 }
998 }
999
1000 pub(super) fn require_authenticated(&self) -> Result<()> {
1003 self.ensure_encryption_initialized()?;
1004 if self.is_encrypted_and_unauthenticated() {
1005 return Err(Error::EncryptedPdf);
1006 }
1007 Ok(())
1008 }
1009
1010 pub(super) fn is_encrypted_unreadable(&self) -> bool {
1015 let _ = self.ensure_encryption_initialized();
1016 self.is_encrypted_and_unauthenticated()
1017 }
1018}