Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ pub struct PdfProcessResult {
pub ocr_reasons_by_page: Vec<PageOcrReasons>,
/// Title from PDF metadata (if available).
pub title: Option<String>,
/// 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,
Expand Down Expand Up @@ -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,
Expand Down
53 changes: 52 additions & 1 deletion tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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()));
Expand Down