//! CLI tool for detecting PDF type (text-based vs scanned) use pdf_inspector::{ detect_pdf_type, detector::estimate_page_count_from_bytes, process_pdf_with_options, PdfOptions, PdfType, ProcessMode, }; use std::env; use std::fmt::Write; use std::fs; use std::process; use std::time::Instant; /// Escape a string for embedding in a JSON string value. fn json_escape(s: &str) -> String { let mut out = String::with_capacity(s.len() + 16); for ch in s.chars() { match ch { '\\' => out.push_str("\\\\"), '"' => out.push_str("\\\""), '\n' => out.push_str("\\n"), '\r' => out.push_str("\\r"), '\t' => out.push_str("\\t"), '\x08' => out.push_str("\\b"), '\x0C' => out.push_str("\\f"), c if c < '\x20' => { let _ = write!(out, "\\u{:04x}", c as u32); } c => out.push(c), } } out } fn main() { env_logger::init(); let args: Vec = env::args().collect(); if args.len() < 2 { eprintln!("Usage: {} ", args[0]); eprintln!(" {} --json", args[0]); eprintln!(" {} --analyze", args[0]); eprintln!(); eprintln!("Options:"); eprintln!(" --json Output result as JSON"); eprintln!(" --analyze Also run layout analysis (tables, columns)"); process::exit(1); } let pdf_path = &args[1]; let json_output = args.iter().any(|a| a == "--json"); let analyze = args.iter().any(|a| a == "--analyze"); let start = Instant::now(); if analyze { run_analyze(pdf_path, json_output, start); } else { run_detect_only(pdf_path, json_output, start); } } fn pdf_type_str(pdf_type: &PdfType) -> &'static str { match pdf_type { PdfType::TextBased => "text_based", PdfType::Scanned => "scanned", PdfType::ImageBased => "image_based", PdfType::Mixed => "mixed", } } fn page_count_hint(pdf_path: &str) -> Option { fs::read(pdf_path) .ok() .map(|bytes| estimate_page_count_from_bytes(&bytes)) .filter(|&count| count > 0) } fn print_error(e: &pdf_inspector::PdfError, pdf_path: &str, json_output: bool) { if json_output { if let Some(count) = page_count_hint(pdf_path) { println!( r#"{{"error":"{}","page_count_hint":{}}}"#, json_escape(&e.to_string()), count ); } else { println!(r#"{{"error":"{}"}}"#, json_escape(&e.to_string())); } } else { eprintln!("Error: {}", e); if let Some(count) = page_count_hint(pdf_path) { eprintln!("Page count hint: {}", count); } } } fn run_analyze(pdf_path: &str, json_output: bool, start: Instant) { match process_pdf_with_options(pdf_path, PdfOptions::new().mode(ProcessMode::Analyze)) { Ok(result) => { let elapsed = start.elapsed(); if json_output { let ocr_pages: Vec = result .pages_needing_ocr .iter() .map(|p| p.to_string()) .collect(); let table_pages: Vec = result .layout .pages_with_tables .iter() .map(|p| p.to_string()) .collect(); let col_pages: Vec = result .layout .pages_with_columns .iter() .map(|p| p.to_string()) .collect(); println!( r#"{{"pdf_type":"{}","page_count":{},"pages_needing_ocr":[{}],"is_complex":{},"pages_with_tables":[{}],"pages_with_columns":[{}],"detection_time_ms":{}}}"#, pdf_type_str(&result.pdf_type), result.page_count, ocr_pages.join(","), result.layout.is_complex, table_pages.join(","), col_pages.join(","), elapsed.as_millis() ); } else { println!("PDF Type Detection + Layout Analysis"); println!("====================================="); println!("File: {}", pdf_path); println!(); println!( "Type: {}", match result.pdf_type { PdfType::TextBased => "TEXT-BASED (extractable text)", PdfType::Scanned => "SCANNED (OCR needed)", PdfType::ImageBased => "IMAGE-BASED (mostly images, OCR may help)", PdfType::Mixed => "MIXED (some text, some images)", } ); println!("Page count: {}", result.page_count); if !result.pages_needing_ocr.is_empty() { println!("Pages needing OCR: {:?}", result.pages_needing_ocr); } println!(); if result.layout.is_complex { println!("Layout: COMPLEX"); if !result.layout.pages_with_tables.is_empty() { println!(" Pages with tables: {:?}", result.layout.pages_with_tables); } if !result.layout.pages_with_columns.is_empty() { println!( " Pages with columns: {:?}", result.layout.pages_with_columns ); } } else { println!("Layout: simple"); } println!(); println!("Detection time: {}ms", elapsed.as_millis()); } } Err(e) => { print_error(&e, pdf_path, json_output); process::exit(1); } } } fn run_detect_only(pdf_path: &str, json_output: bool, start: Instant) { // Use the low-level detect_pdf_type for richer output (pages_sampled etc.), // but also call detect_pdf to demonstrate the unified API. match detect_pdf_type(pdf_path) { Ok(result) => { let elapsed = start.elapsed(); if json_output { let ocr_pages: Vec = result .pages_needing_ocr .iter() .map(|p| p.to_string()) .collect(); println!( r#"{{"pdf_type":"{}","page_count":{},"pages_sampled":{},"pages_with_text":{},"confidence":{:.2},"title":{},"ocr_recommended":{},"pages_needing_ocr":[{}],"detection_time_ms":{}}}"#, pdf_type_str(&result.pdf_type), result.page_count, result.pages_sampled, result.pages_with_text, result.confidence, result .title .as_ref() .map(|t| format!("\"{}\"", json_escape(t))) .unwrap_or_else(|| "null".to_string()), result.ocr_recommended, ocr_pages.join(","), elapsed.as_millis() ); } else { println!("PDF Type Detection Results"); println!("=========================="); println!("File: {}", pdf_path); println!(); println!( "Type: {}", match result.pdf_type { PdfType::TextBased => "TEXT-BASED (extractable text)", PdfType::Scanned => "SCANNED (OCR needed)", PdfType::ImageBased => "IMAGE-BASED (mostly images, OCR may help)", PdfType::Mixed => "MIXED (some text, some images)", } ); println!("Confidence: {:.0}%", result.confidence * 100.0); println!(); println!("Page count: {}", result.page_count); println!("Pages sampled: {}", result.pages_sampled); println!("Pages with text: {}", result.pages_with_text); println!( "OCR recommended: {}", if result.ocr_recommended { "YES" } else { "NO" } ); if !result.pages_needing_ocr.is_empty() { if result.pages_needing_ocr.len() == result.page_count as usize { println!("Pages needing OCR: all (of {})", result.page_count); } else { println!( "Pages needing OCR: {:?} (of {})", result.pages_needing_ocr, result.page_count ); } } if let Some(title) = &result.title { println!("Title: {}", title); } println!(); println!("Detection time: {}ms", elapsed.as_millis()); println!(); // Recommendations if result.ocr_recommended { match result.pdf_type { PdfType::Mixed => { println!("Recommendation: Use OCR - images provide essential context (template PDF)"); } PdfType::Scanned => { println!("Recommendation: Use OCR (MinerU or similar)"); } PdfType::ImageBased => { println!("Recommendation: Use OCR for best results"); } _ => { println!("Recommendation: Use OCR for complete extraction"); } } } else { println!("Recommendation: Use direct text extraction (fast)"); } } } Err(e) => { print_error(&e, pdf_path, json_output); process::exit(1); } } }