test: add extract_pdf_text unit tests (DOC-FULL-023)

- pdf_with_text_stream helper: build minimal PDFs with BT/ET text
- single/multiple text blocks, no BT/ET→empty, empty PDF→empty
- MAX_SCAN_BYTES limit respected, empty text filtered
- page_count from pages dict, default to 1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-18 13:32:34 +08:00
parent 1c93308e8b
commit 7570717ec8

View File

@ -344,6 +344,104 @@ mod tests {
} }
} }
fn pdf_with_text_stream(text_content: &str) -> Vec<u8> {
// Build a minimal PDF with a stream object containing BT/ET text
let stream_text = format!("BT\n({})\nET", text_content);
let obj = format!(
"1 0 obj\n<< /Length {} >>\nstream\n{}\nendstream\nendobj\n",
stream_text.len(),
stream_text
);
let obj_bytes = obj.as_bytes();
let header = b"%PDF-1.4\n%\x80\x80\x80\x80\n";
let header_len = header.len() as u64;
let obj_len = obj_bytes.len() as u64;
let trailer = format!(
"xref\n0 2\n0000000000 65535 f \n{:010} 00000 n \ntrailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n",
header_len,
header_len + obj_len
);
let mut pdf = Vec::new();
pdf.extend_from_slice(header);
pdf.extend_from_slice(obj_bytes);
pdf.extend_from_slice(trailer.as_bytes());
pdf
}
#[test]
fn test_extract_pdf_text_single_bt_et_block() {
let pdf = pdf_with_text_stream("Hello World");
let path = write_temp(&pdf, "text_single.pdf");
let pages = extract_pdf_text(&path).unwrap();
assert_eq!(pages.len(), 1);
assert_eq!(pages[0].text, "Hello World");
}
#[test]
fn test_extract_pdf_text_multiple_blocks() {
let stream = "BT\n(First line)\nET\nBT\n(Second line)\nET";
let obj = format!("1 0 obj\n<< /Length {} >>\nstream\n{}\nendstream\nendobj\n", stream.len(), stream);
let header = b"%PDF-1.4\n";
let pdf: Vec<u8> = [header.as_slice(), obj.as_bytes()].concat();
// This is a slightly malformed PDF but we're testing the BT/ET scanner
let path = write_temp(&pdf, "text_multi.pdf");
let pages = extract_pdf_text(&path).unwrap();
// The scanner finds bt\n...\net blocks in sequence
assert!(pages.len() >= 1, "Should find at least one text block");
}
#[test]
fn test_extract_pdf_text_no_bt_et_returns_empty() {
let pdf = b"%PDF-1.4\nNo text blocks here\n%%EOF".to_vec();
let path = write_temp(&pdf, "no_text.pdf");
let pages = extract_pdf_text(&path).unwrap();
assert!(pages.is_empty());
}
#[test]
fn test_extract_pdf_text_empty_pdf_returns_empty() {
let pdf = b"%PDF-1.4\n".to_vec();
let path = write_temp(&pdf, "empty.pdf");
let pages = extract_pdf_text(&path).unwrap();
assert!(pages.is_empty());
}
#[test]
fn test_extract_pdf_text_max_scan_limit_respected() {
// Create a PDF larger than MAX_SCAN_BYTES (8MB) by padding zeros
let mut pdf = pdf_with_text_stream("Hello");
pdf.resize(MAX_SCAN_BYTES + 1024, 0);
let path = write_temp(&pdf, "large.pdf");
let result = extract_pdf_text(&path);
assert!(result.is_ok());
}
#[test]
fn test_extract_pdf_text_empty_text_filtered() {
let pdf = pdf_with_text_stream("");
let path = write_temp(&pdf, "empty_text.pdf");
let pages = extract_pdf_text(&path).unwrap();
// Empty text blocks are filtered out
assert!(pages.is_empty());
}
#[test]
fn test_extract_pdf_text_page_count_from_pages_dict() {
let data = minimal_pdf(3);
let path = write_temp(&data, "count_pages.pdf");
let meta = read_pdf_metadata(&path).unwrap();
assert_eq!(meta.page_count, 3);
}
#[test]
fn test_extract_pdf_text_default_to_one_page() {
let pdf = b"%PDF-1.4\n".to_vec();
let path = write_temp(&pdf, "default_page.pdf");
let meta = read_pdf_metadata(&path).unwrap();
assert_eq!(meta.page_count, 1);
}
#[test] #[test]
fn test_fixture_scanned_pdf() { fn test_fixture_scanned_pdf() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))