# Extracting Per-Page Markdown from PDFs with pdf-inspector: A Complete Guide

> Easily extract per-page Markdown from PDFs using pdf-inspector. Generate JSON output with isolated markdown blocks, tables, and links for each page. Get the complete guide now.

- Repository: [Firecrawl/pdf-inspector](https://github.com/firecrawl/pdf-inspector)
- Tags: how-to-guide
- Published: 2026-08-10

---

**Use `pdf2md --json` to extract per-page Markdown from PDFs with `pdf-inspector`, producing page-level JSON output with isolated markdown blocks, tables, and links for each page.**

PDF-inspector is a Rust-based tool from the Firecrawl ecosystem that converts PDF files into clean, structured Markdown on a per-page basis. Unlike simple text extractors, it performs sophisticated layout analysis—including column detection, reading order reconstruction, and table recognition—before emitting semantic Markdown. For workflows that need page-granular content, such as LLM prompting or paginated UIs, `pdf2md` with the `--json` flag provides exactly that structure.

## How pdf-inspector Converts PDFs to Per-Page Markdown

The conversion pipeline in `pdf-inspector` is organized into three distinct stages, each implemented in dedicated Rust modules:

| Stage | Purpose | Key Module |
|-------|---------|------------|
| **Detection** | Classifies PDF type (text-based, scanned, mixed, image-based) | [`src/detector.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/detector.rs) |
| **Extraction & Layout** | Parses content streams, builds font maps, detects columns and reading order | [`src/extractor/content_stream.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/content_stream.rs), [`src/extractor/layout.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/layout.rs), [`src/extractor/fonts.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/fonts.rs) |
| **Markdown Generation** | Converts logical lines to Markdown, classifies elements, cleans output | [`src/markdown/convert.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/markdown/convert.rs), [`src/markdown/classify.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/markdown/classify.rs), [`src/markdown/postprocess.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/markdown/postprocess.rs) |

The **public API** exposed in [`src/lib.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs) provides the `process_pdf_with_options` function, which both CLI binaries and language bindings consume.

## CLI Usage for Per-Page Markdown Extraction

### Basic JSON Output with Page Boundaries

The `pdf2md` binary in [`src/bin/pdf2md.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/bin/pdf2md.rs) handles full extraction. Adding `--json` triggers per-page segmentation:

```bash
pdf2md --json quarterly-report.pdf > report.json

```

The resulting JSON structure wraps each page as an independent object:

```json
{
  "pages": [
    {
      "page_number": 1,
      "markdown": "# Q3 Financial Results\n\nRevenue increased by 15%...",

      "tables": [],
      "links": [{"url": "https://example.com", "text": "Investor Relations"}]
    },
    {
      "page_number": 2,
      "markdown": "## Detailed Breakdown\n\n| Region | Q3 Sales | Growth |\n|---|---|---|\n| North America | $12.4M | +18% |",

      "tables": [{"rows": 5, "columns": 3, "markdown": "..."}],
      "links": []
    }
  ]
}

```

This structure is intentionally designed for downstream consumption: each `markdown` field contains complete, page-isolated content suitable for direct LLM ingestion or UI rendering.

### Extracting Specific Page Ranges

For targeted extraction, use the `--pages` flag:

```bash
pdf2md --json --pages 10-15 contract.pdf > pages-10-15.json

```

Only the specified pages traverse the full pipeline, reducing processing time for large documents.

## Programmatic Per-Page Markdown Access

### Rust Library API

Call `process_pdf_with_options` directly from [`src/lib.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs) for embedded use:

```rust
use pdf_inspector::{process_pdf_with_options, ProcessOptions};

let opts = ProcessOptions {
    json_output: true,
    page_range: Some("1,3,5-10".to_string()),
    ..Default::default()
};

let result = process_pdf_with_options("document.pdf", opts)?;
// result contains the same JSON structure as CLI --json output

```

### Python Bindings

The PyO3-based Python interface in [`src/python.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/python.rs) mirrors the CLI options:

```python
from pdf_inspector import process_pdf

result = process_pdf(
    "document.pdf",
    json_output=True,      # Enables per-page JSON structure

    page_range="1-5"       # Optional: subset of pages

)

# Access individual page markdown

for page in result["pages"]:
    print(f"Page {page['page_number']}: {len(page['markdown'])} chars")
    if page["tables"]:
        print(f"  Contains {len(page['tables'])} table(s)")

```

## Layout Analysis That Preserves Per-Page Integrity

### Column Detection and Reading Order

The layout engine in [`src/extractor/layout.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/layout.rs) uses **horizontal projection histograms** with pre-masking to identify column structures. It classifies documents into two reading modes:

- **Newspaper layout**: Multiple independent columns read sequentially down each column
- **Tabular layout**: Y-interleaved rows across columns to preserve logical row order

This analysis runs per-page, ensuring the extracted `markdown` field reflects the correct visual reading sequence for that specific page.

### Table Detection Across Pages

Three detectors execute in priority order for each page ([`src/tables/detect_rects.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_rects.rs), [`src/tables/detect_lines.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_lines.rs), [`src/tables/detect_heuristic.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_heuristic.rs)). When tables span page boundaries, each page's `tables` array contains only the portion present on that page—maintaining the per-page isolation guarantee.

## Handling Complex PDF Types Per Page

The detector in [`src/detector.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/detector.rs) runs independently on each page to classify:

- **Tiled scans**: Multi-image scans assembled into single pages
- **Mixed PDFs**: Pages combining text and images requiring selective OCR
- **Low-text-quality PDFs**: Content that needs OCR fallback

This per-page classification means a single document can mix processing strategies: page 1 might use direct text extraction while page 2 triggers OCR, with both contributing clean Markdown to their respective JSON entries.

## Unicode and Post-Processing Guarantees

Before Markdown reaches the JSON output, several cleanup stages ensure quality:

- [`src/tounicode.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tounicode.rs): Parses ToUnicode CMaps for correct CID font mapping
- [`src/text_utils.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/text_utils.rs): Applies NFKC normalization, ligature expansion, and RTL handling
- [`src/markdown/postprocess.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/markdown/postprocess.rs): Removes dot leaders, fixes hyphenation, strips page numbers

These steps run per-page, so each `markdown` string is immediately usable without additional cleanup.

## Summary

- **`pdf2md --json`** produces structured JSON with isolated per-page Markdown, tables, and links—ideal for LLM workflows and paginated UIs
- **Page ranges** (`--pages`) enable efficient partial document processing without full pipeline overhead
- **Rust API** (`process_pdf_with_options` in [`src/lib.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs)) and **Python bindings** ([`src/python.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/python.rs)) provide programmatic access with identical output structure
- **Per-page layout analysis** (columns, reading order, tables) ensures each page's Markdown reflects correct visual structure
- **Detection and cleanup pipelines** operate at page granularity, supporting mixed-content documents

## Frequently Asked Questions

### How do I get Markdown separated by page instead of one continuous file?

Use `pdf2md --json your.pdf`. This outputs JSON where each page has its own `markdown` field, rather than concatenating all pages into a single stream. Access individual pages via `result["pages"][n]["markdown"]` in Python or equivalent in other languages.

### Does pdf-inspector handle multi-column layouts correctly per page?

Yes. The layout engine in [`src/extractor/layout.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/layout.rs) detects columns via horizontal projection histograms and determines proper reading order for each page independently. Newspaper-style columns are read sequentially; tabular layouts use Y-interleaving to preserve row relationships.

### What happens to tables that span multiple pages?

Tables spanning pages are split at page boundaries. Each page's JSON entry contains the table portion present on that page in its `tables` array. The [`src/tables/format.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/format.rs) module handles cell spanning within-page; cross-page table reconstruction requires downstream logic.

### Can I extract only specific pages without processing the entire PDF?

Yes. Both CLI (`--pages 5-10`) and programmatic API (`page_range: Some("5-10".to_string())`) accept page specifications. The pipeline skips non-selected pages entirely, improving performance for large documents.