How pdf-inspector Converts Extracted PDF Text to Markdown: A 4-Stage Pipeline Explained
pdf-inspector transforms raw PDF text into structured Markdown by detecting document type, analyzing layout and typography, classifying structural elements, and streaming a conversion loop that emits clean, hierarchical Markdown tokens.
pdf-inspector is a Rust-based PDF parsing engine developed by Firecrawl that converts PDF documents into Markdown format. This article explains how pdf-inspector converts extracted text to Markdown by walking through its four-stage pipeline, examining the actual source files from the firecrawl/pdf-inspector repository where this conversion logic is implemented.
Stage 1: PDF Type Detection and Text Extraction
The conversion process begins in src/detector.rs and src/extractor/mod.rs. The detect_pdf_type function first determines whether the PDF contains a usable text layer, is scanned, or mixed. This detection step prevents wasted processing on image-only documents.
Once detected, extract_text_with_positions produces a flat list of TextItem structs. Each TextItem carries:
- Unicode string content
- Font name, size, and style flags
- Physical coordinates (
x,y,width,height)
The extractor then groups nearby TextItems into TextLine objects, preserving reading order while handling multi-column layouts. This positional metadata becomes critical for the layout analysis that follows.
Stage 2: Layout and Font Analysis
In src/markdown/analysis.rs, pdf-inspector computes font statistics that drive structural inference. The calculate_font_stats function builds a histogram of font-size frequencies across the document. From this, it derives font_size_rarity scores—uncommon large fonts indicate potential headings.
The analysis module also implements:
bold_heading_level– assigns heading levels based on bold-only formattingdetect_overused_struct_heading_levels– suppresses repeated structural headings (e.g., page headers styled as<H2>)is_toc_entry_lineandis_toc_marker_heading– identifies table-of-contents lines to avoid false heading detection
This statistical approach allows pdf-inspector to adapt to document-specific typographic conventions rather than relying on fixed thresholds.
Stage 3: Classification and Pre-processing
The pipeline moves to src/markdown/classify.rs and src/markdown/preprocess.rs for semantic labeling and cleanup.
Classification (classify.rs) tags each TextLine with structural roles:
is_heading_fragment– detects split heading linesis_list_itemandstarts_with_bullet_marker– identifies unordered lists- Caption and code block detection via heuristic patterns
Pre-processing (preprocess.rs) then repairs common PDF artifacts:
merge_drop_capsjoins large initial letters with following linesmerge_heading_linescollapses fragmented headings into single lines
This stage ensures that downstream conversion receives coherent, properly labeled text blocks.
Stage 4: Core Conversion Loop and Post-Processing
The final transformation happens in src/markdown/convert.rs and src/markdown/postprocess.rs.
Chart-Prose Order Interleaving
The convert.rs module handles positioned blocks—tables and images that must be interleaved with text at correct vertical positions. The ChartProseOrder system uses chart_stream_position to reconcile the logical reading order, ensuring that tables in newspaper-style columns appear where readers expect them.
Markdown Token Emission
For each element in the sorted stream, the conversion loop emits appropriate Markdown syntax:
| Element | Markdown Output | Implementation |
|---|---|---|
| Headings | # to ###### prefix |
Level determined by font rarity and bold analysis |
| List items | - or * prefix |
format_list_item normalizes indentation |
| Code blocks | Triple backtick fences | Detected via monospace font patterns |
| Tables | Pipe-delimited syntax | src/tables/format.rs renders with 25-column cap, merges continuation rows |
| Images |  syntax |
Extraction handled in extractor/xobjects.rs |
Final Cleanup
clean_markdown in postprocess.rs performs terminal fixes:
- Removes dot-leaders from TOC lines
- Repairs hyphenation across line breaks
- Canonicalizes URL formatting
- Trims excess whitespace
Public API and Usage
All stages are orchestrated from src/lib.rs through the to_markdown function:
pub fn to_markdown(items: &[TextItem]) -> Result<String> {
// 1️⃣ extract → 2️⃣ analyze → 3️⃣ classify → 4️⃣ convert
let lines = extractor::group_items_into_lines(items)?;
let stats = markdown::analysis::calculate_font_stats(&lines);
let classified = markdown::classify::classify_lines(&lines, &stats)?;
let preprocessed = markdown::preprocess::run(classified)?;
let raw_md = markdown::convert::run(preprocessed)?;
Ok(markdown::postprocess::clean_markdown(raw_md))
}
Full Pipeline Example
use pdf_inspector::{process_pdf, PdfOptions, ProcessMode};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Full pipeline: detection → extraction → markdown
let result = process_pdf("sample.pdf")?;
println!("Detected type: {:?}", result.pdf_type);
if let Some(md) = result.markdown {
println!("--- Markdown Output ---\n{md}");
}
// Custom options – only run the analyzer, no markdown generation
let opts = PdfOptions::new()
.mode(ProcessMode::Analyze) // stop after analysis
.pages([1, 2, 3]); // process a subset of pages
let analysis = pdf_inspector::process_pdf_with_options("sample.pdf", opts)?;
println!("Page count: {}", analysis.page_count);
Ok(())
}
Low-Level Conversion API
use pdf_inspector::markdown::{to_markdown_from_items, MarkdownOptions};
let items = pdf_inspector::extractor::extract_text("sample.pdf")?;
let md = to_markdown_from_items(&items, MarkdownOptions::default())?;
println!("{md}");
Summary
pdf-inspector converts PDF text to Markdown through a carefully sequenced pipeline:
- Detection and extraction (
detector.rs,extractor/mod.rs) identifies PDF type and extracts positioned text items - Layout analysis (
analysis.rs) computes font statistics and rarity scores to infer document structure - Classification and pre-processing (
classify.rs,preprocess.rs) labels lines semantically and repairs PDF artifacts like drop-caps - Conversion and post-processing (
convert.rs,postprocess.rs) streams Markdown tokens in correct reading order and cleans final output
The result is token-efficient Markdown that preserves headings, lists, tables, images, and multi-column reading order without the noise typical of raw PDF text extraction.
Frequently Asked Questions
What PDF types can pdf-inspector handle?
pdf-inspector handles text-based PDFs, scanned documents, and mixed documents through its detection system in detector.rs. For scanned PDFs without text layers, the engine would require OCR preprocessing—though the current implementation focuses on documents with extractable text content.
How does pdf-inspector distinguish headings from regular text?
Headings are detected through font-size rarity analysis in analysis.rs. Unusually large fonts receive higher rarity scores, which combine with bold formatting heuristics (bold_heading_level) to assign appropriate heading levels. The system also suppresses overused structural headings and excludes TOC lines from heading classification.
Can pdf-inspector preserve complex table structures?
Yes. Tables are detected as positioned blocks and rendered by src/tables/format.rs into Markdown pipe tables. The formatter handles continuation rows, caps column counts at 25 for readability, and maintains proper vertical ordering through the ChartProseOrder system that interleaves tables with surrounding prose.
Is the conversion customizable?
Partially. The PdfOptions builder exposed in lib.rs allows selecting processing modes (full conversion versus analysis-only) and specifying page subsets. Lower-level APIs like to_markdown_from_items accept MarkdownOptions for controlling output behavior, though the core heuristics remain fixed to ensure consistent structural inference.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →