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:
Abimael Martell
2026-02-06 11:51:41 -08:00
co-authored by Claude Opus 4.5
commit 135ce518c1
8 changed files with 1574 additions and 0 deletions
+150
View File
@@ -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())
}
}