diff --git a/src/lib.rs b/src/lib.rs index ded492bc..fa14ae93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -149,7 +149,9 @@ pub struct PdfProcessResult { pub ocr_reasons_by_page: Vec, /// Title from PDF metadata (if available). pub title: Option, - /// Detection confidence score (0.0–1.0). + /// Confidence score for the returned result (0.0–1.0). In + /// [`ProcessMode::Full`], this is 0.0 when a TextBased PDF yields no + /// usable Markdown. pub confidence: f32, /// Layout complexity analysis (tables, multi-column detection). pub layout: LayoutComplexity, @@ -4413,6 +4415,27 @@ fn process_document( markdown }; + // Full mode returns Markdown for a document classified as TextBased. If + // extraction-quality gates leave that output empty, retaining the + // detector's 1.0 confidence makes the silent-empty failure look certain to + // callers. Analyze mode intentionally omits Markdown, so its confidence + // remains the detector result. + let confidence = if options.mode == ProcessMode::Full + && pdf_type == PdfType::TextBased + && page_count > 0 + && markdown + .as_ref() + .is_none_or(|markdown| markdown.trim().is_empty()) + { + log::debug!( + "TextBased PDF produced no usable markdown — reducing confidence from {:.2} to 0", + confidence + ); + 0.0 + } else { + confidence + }; + Ok(PdfProcessResult { pdf_type, markdown, diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 44282d58..599e79e6 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -10,7 +10,7 @@ use pdf_inspector::{ extract_text_in_regions_mem, extract_text_with_positions, extract_text_with_positions_mem, process_pdf_mem, process_pdf_mem_with_options, process_pdf_with_options, to_markdown, to_markdown_from_items_with_rects_and_page_count, MarkdownOptions, PdfError, PdfOptions, - PdfType, TextItem, + PdfType, ProcessMode, TextItem, }; use std::collections::HashSet; @@ -1151,6 +1151,57 @@ fn test_process_pdf_mem_repairs_truncated_eof_marker() { ); } +#[test] +fn test_process_pdf_mem_suppressed_extraction_has_zero_confidence() { + let buf = std::fs::read("tests/fixtures/shinagawa_identity_h.pdf").unwrap(); + let result = process_pdf_mem(&buf).unwrap(); + + assert_eq!(result.pdf_type, PdfType::TextBased); + assert!( + result + .markdown + .as_deref() + .unwrap_or_default() + .trim() + .is_empty(), + "suppressed undecodable text should not claim usable markdown" + ); + assert!(result.pages_needing_ocr.contains(&1)); + assert_eq!( + result.confidence, 0.0, + "a TextBased result with no usable markdown must not claim certainty" + ); +} + +#[test] +fn test_process_pdf_mem_analyze_mode_retains_detection_confidence() { + let buf = std::fs::read("tests/fixtures/shinagawa_identity_h.pdf").unwrap(); + let result = + process_pdf_mem_with_options(&buf, PdfOptions::new().mode(ProcessMode::Analyze)).unwrap(); + + assert_eq!(result.markdown, None); + assert_eq!( + result.confidence, 1.0, + "Analyze mode intentionally omits markdown and keeps detector confidence" + ); +} + +#[test] +fn test_process_pdf_mem_sparse_usable_text_retains_confidence() { + let result = process_pdf_mem(&make_minimal_text_pdf()).unwrap(); + + assert_eq!(result.pdf_type, PdfType::TextBased); + assert!(result + .markdown + .as_deref() + .unwrap_or_default() + .contains("Hello World")); + assert_eq!( + result.confidence, 1.0, + "sparse but usable text should retain TextBased confidence" + ); +} + #[test] fn test_process_pdf_mem_repairs_leading_tab_and_truncated_eof() { let pdf = add_leading_tab(truncate_eof_marker(make_minimal_text_pdf()));