Initial commit: Rust PDF-to-Markdown library
- Smart PDF type detection (text vs scanned) without full document load - Text extraction using lopdf directly - Markdown conversion with header/list/code detection - CLI tools: detect-pdf, pdf2md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+150
@@ -0,0 +1,150 @@
|
||||
//! Smart PDF detection and text extraction using lopdf
|
||||
//!
|
||||
//! This module provides:
|
||||
//! - Fast detection of scanned vs text-based PDFs without full document load
|
||||
//! - Direct text extraction from text-based PDFs
|
||||
//! - Markdown conversion with structure detection
|
||||
|
||||
pub mod detector;
|
||||
pub mod extractor;
|
||||
pub mod markdown;
|
||||
|
||||
pub use detector::{detect_pdf_type, PdfType, PdfTypeResult};
|
||||
pub use extractor::{extract_text, extract_text_with_positions, TextItem};
|
||||
pub use markdown::{to_markdown, MarkdownOptions};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// High-level PDF processing result
|
||||
#[derive(Debug)]
|
||||
pub struct PdfProcessResult {
|
||||
/// The detected PDF type
|
||||
pub pdf_type: PdfType,
|
||||
/// Extracted text (if text-based PDF)
|
||||
pub text: Option<String>,
|
||||
/// Markdown output (if text-based PDF)
|
||||
pub markdown: Option<String>,
|
||||
/// Page count
|
||||
pub page_count: u32,
|
||||
/// Processing time in milliseconds
|
||||
pub processing_time_ms: u64,
|
||||
}
|
||||
|
||||
/// Process a PDF file with smart detection and extraction
|
||||
///
|
||||
/// This function will:
|
||||
/// 1. Quickly detect if the PDF is text-based or scanned
|
||||
/// 2. If text-based, extract text and convert to markdown
|
||||
/// 3. If scanned, return early indicating OCR is needed
|
||||
pub fn process_pdf<P: AsRef<Path>>(path: P) -> Result<PdfProcessResult, PdfError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// Step 1: Smart detection (fast, no full load)
|
||||
let detection = detect_pdf_type(&path)?;
|
||||
|
||||
let result = match detection.pdf_type {
|
||||
PdfType::TextBased => {
|
||||
// Step 2: Full extraction for text-based PDFs
|
||||
let text = extract_text(&path)?;
|
||||
let markdown = to_markdown(&text, MarkdownOptions::default());
|
||||
|
||||
PdfProcessResult {
|
||||
pdf_type: PdfType::TextBased,
|
||||
text: Some(text),
|
||||
markdown: Some(markdown),
|
||||
page_count: detection.page_count,
|
||||
processing_time_ms: start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
PdfType::Scanned | PdfType::ImageBased => {
|
||||
// Return early - OCR needed
|
||||
PdfProcessResult {
|
||||
pdf_type: detection.pdf_type,
|
||||
text: None,
|
||||
markdown: None,
|
||||
page_count: detection.page_count,
|
||||
processing_time_ms: start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
PdfType::Mixed => {
|
||||
// Try to extract what we can
|
||||
let text = extract_text(&path).ok();
|
||||
let markdown = text.as_ref().map(|t| to_markdown(t, MarkdownOptions::default()));
|
||||
|
||||
PdfProcessResult {
|
||||
pdf_type: PdfType::Mixed,
|
||||
text,
|
||||
markdown,
|
||||
page_count: detection.page_count,
|
||||
processing_time_ms: start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Process PDF from memory buffer
|
||||
pub fn process_pdf_mem(buffer: &[u8]) -> Result<PdfProcessResult, PdfError> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// Step 1: Smart detection (fast, no full load)
|
||||
let detection = detector::detect_pdf_type_mem(buffer)?;
|
||||
|
||||
let result = match detection.pdf_type {
|
||||
PdfType::TextBased => {
|
||||
// Step 2: Full extraction for text-based PDFs
|
||||
let text = extractor::extract_text_mem(buffer)?;
|
||||
let markdown = to_markdown(&text, MarkdownOptions::default());
|
||||
|
||||
PdfProcessResult {
|
||||
pdf_type: PdfType::TextBased,
|
||||
text: Some(text),
|
||||
markdown: Some(markdown),
|
||||
page_count: detection.page_count,
|
||||
processing_time_ms: start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
PdfType::Scanned | PdfType::ImageBased => {
|
||||
PdfProcessResult {
|
||||
pdf_type: detection.pdf_type,
|
||||
text: None,
|
||||
markdown: None,
|
||||
page_count: detection.page_count,
|
||||
processing_time_ms: start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
PdfType::Mixed => {
|
||||
let text = extractor::extract_text_mem(buffer).ok();
|
||||
let markdown = text.as_ref().map(|t| to_markdown(t, MarkdownOptions::default()));
|
||||
|
||||
PdfProcessResult {
|
||||
pdf_type: PdfType::Mixed,
|
||||
text,
|
||||
markdown,
|
||||
page_count: detection.page_count,
|
||||
processing_time_ms: start.elapsed().as_millis() as u64,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PdfError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("PDF parsing error: {0}")]
|
||||
Parse(String),
|
||||
#[error("PDF is encrypted")]
|
||||
Encrypted,
|
||||
#[error("Invalid PDF structure")]
|
||||
InvalidStructure,
|
||||
}
|
||||
|
||||
impl From<lopdf::Error> for PdfError {
|
||||
fn from(e: lopdf::Error) -> Self {
|
||||
PdfError::Parse(e.to_string())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user