# How pdf-inspector Handles Table Detection Strategies: Three Complementary Pipelines Explained

> Discover how pdf-inspector uses three prioritized table detection strategies rectangle line-based and heuristic text-only analysis to accurately extract tables from PDFs.

- Repository: [Firecrawl/pdf-inspector](https://github.com/firecrawl/pdf-inspector)
- Tags: deep-dive
- Published: 2026-09-01

---

**pdf-inspector uses three prioritized detection strategies—rectangle-based, line-based, and heuristic text-only analysis—to extract tables from PDFs, running them in sequence and falling back to later methods when earlier ones fail.**

The [firecrawl/pdf-inspector](https://github.com/firecrawl/pdf-inspector) Rust library implements a multi-stage **table detection** system designed to handle the diverse ways PDFs encode tabular data. Unlike single-strategy approaches, pdf-inspector's architecture recognizes that tables can be rendered as vector rectangles, ruled lines, or even pure text alignment—and processes all three possibilities in a fixed priority order.

## The Three Table Detection Strategies

pdf-inspector executes its detection pipelines in strict sequence. Each strategy is implemented in its own source module and contributes fallback hints to subsequent stages.

| Priority | Strategy | Source File | Input Signals |
|----------|----------|-------------|---------------|
| 1 | **Rectangle-based detection** | [`src/tables/detect_rects.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_rects.rs) | PDF `re` (rectangle) operators |
| 2 | **Line-based detection** | [`src/tables/detect_lines.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_lines.rs) | Path operators (`m`/`l`/`S` for lines) |
| 3 | **Heuristic text-only detection** | [`src/tables/detect_heuristic.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_heuristic.rs) | `TextItem` positions and font properties |

A valid result from any stage can short-circuit later pipelines, though hint regions from failed stages improve downstream accuracy.

## Rectangle-Based Table Detection

The fastest and most precise pipeline, **rectangle-based detection** in [`src/tables/detect_rects.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_rects.rs) exploits explicit table borders drawn as PDF rectangles.

### Normalization and Clustering

Raw rectangles undergo preprocessing before analysis:

- **Normalization** – Negative widths/heights are flipped; tiny decorative rects and page-spanning backgrounds are filtered using `DOMINANT_PAGE_BACKGROUND_MIN_REPETITIONS`
- **Spatial clustering** – The `cluster_rects` union-find structure merges overlapping rectangles (with tolerance) into component groups

### Grid Construction and Validation

For each cluster containing ≥6 rectangles, `detect_table_from_rect_group` attempts full grid validation:

- Rectangles are assigned to table cells based on spatial intersection
- Valid grids require consistent row/column structure per `COMPETING_TABLE_MIN_ROWS`

### Fallback Mechanisms

When rectangle clusters fail strict validation, the pipeline provides intelligent recovery:

- **Wide cluster splitting** – `split_wide_cluster` divides clusters at the largest X-gap to isolate adjacent tables
- **Hint region generation** – Failed rectangle groups pass boundary hints to the heuristic detector
- **Merged cluster fallback** – `detect_merged_cluster_table` attempts row-stripe detection across all clusters when individual groups yield only narrow tables

The `MAX_CLUSTER_RECTS` constant prevents runaway processing on vector-heavy pages.

## Line-Based Table Detection

When rectangle detection fails or produces weak results, **line-based detection** in [`src/tables/detect_lines.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_lines.rs) analyzes explicit ruled borders constructed from PDF path operations.

### Segment Extraction and Merging

1. **Path parsing** – Horizontal and vertical segments are extracted from `m` (move), `l` (line), and `S` (stroke) operators
2. **Horizontal merging** – `merge_horizontal_segments` combines collinear horizontal segments that share Y coordinates
3. **Rule formation** – Merged segments become *logical rules* grouped by similar X-span via `group_rules_by_span`

### Independent Table Regions

Large vertical gaps or explicit "Table N" captions trigger `split_independent_rule_runs`, isolating separate tables before construction begins.

### Multiple Construction Strategies

Each rule run attempts several specialized constructors:

- **Text-anchor tables** – `build_text_anchor_table` infers column edges from anchored text row X-positions
- **Open-edge grids** – `derive_columns_from_horizontal_segments` synthesizes column boundaries from horizontal line endpoints when vertical rules are absent
- **Dense-row anchors** – `build_dense_row_anchor_table` handles tables with many vertical rules but sparse horizontals, using dense text rows as fallback anchors

### Scoring and Selection

Candidate tables receive `table_evidence_score` ratings. The `select_non_overlapping_hypotheses` function retains the highest-scoring, mutually exclusive table set.

## Heuristic Text-Only Table Detection

The final **heuristic detection** stage in [`src/tables/detect_heuristic.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_heuristic.rs) requires no vector graphics—only extracted text positions. This handles PDFs where tables exist purely as aligned text.

### Preprocessing Pipeline

Before structural analysis, text items undergo extensive normalization:

- **Redline handling** – `redline_edit_regions` identifies and excludes strikethrough/underline edit overlaps
- **Underline-based columns** – `underlined_table_columns` discovers columns with sufficient underlined items
- **Glyph merging** – `merge_adjacent_items_preserving` combines adjacent single-character items into words
- **Financial expansion** – `expand_consolidated_items` splits consolidated strings like "$ 1,234" into sub-items for proper column alignment

### Region Discovery and Boundary Inference

Two complementary passes locate candidate table regions:

- `find_table_regions` – Small-font pass for tables with reduced font size
- `find_column_boundaries` – Derives column edges from non-script item X-positions
- `find_row_boundaries` – Establishes row divisions from Y-position clustering

### Structural Validation

Every candidate table must satisfy validation functions in the detection pipeline:

- Column count between 2–25
- Minimum 2 rows
- Column alignment consistency (`check_column_alignment`)
- Multi-column occupancy patterns
- Numeric-like content detection (`has_table_like_content`)
- Exclusion of key-value layouts (`is_key_value_layout`, `is_paragraph_layout`)
- Column consistency across rows (`has_consistent_columns`)

### Two-Pass Architecture

| Pass | Target | Font Size Window | Region Criteria |
|------|--------|------------------|-----------------|
| Small-font | Tables in reduced type | ≤ 0.9 × base size | Standard (`find_table_regions`) |
| Body-font | Primary content tables | 0.85–1.05 × base size | Strict (`find_table_regions_strict`) |

The body-font pass auto-skips pages where it would generate excessive false positives. Both passes map indices back to original `TextItem` positions before returning final `Table` structs.

## Using pdf-inspector's Table Detection

### Command-Line Extraction

```bash

# Extract PDF to Markdown with automatic table detection

pdf2md --json annual-report.pdf > annual-report.md

```

The `pdf2md` binary internally executes all three pipelines in priority order and merges non-conflicting results.

### Rust Library Integration

```rust
use pdf_inspector::lib::{process_pdf_with_options, PdfOptions};
use pdf_inspector::types::Table;

fn extract_document(path: &str) -> Result<Vec<Table>, Box<dyn std::error::Error>> {
    let options = PdfOptions::default();
    let result = process_pdf_with_options(path, &options)?;
    
    // Tables contain rows, columns, and cell content vectors
    for table in &result.tables {
        println!("Detected: {} rows × {} columns", 
            table.rows.len(), 
            table.columns.len()
        );
    }
    
    Ok(result.tables)
}

```

### Direct Strategy Invocation (Advanced)

```rust
use pdf_inspector::tables::{
    detect_tables_from_rects, 
    detect_tables_from_lines, 
    detect_tables
};

// Strategy 1: Rectangle-based detection
let (rect_tables, rect_hints) = detect_tables_from_rects(
    &text_items, 
    &pdf_rects, 
    page_number
);

// Strategy 2: Line-based detection  
let line_tables = detect_tables_from_lines(
    &text_items, 
    &pdf_lines, 
    page_number
);

// Strategy 3: Heuristic text-only detection
// rect_hints improves accuracy on hinted regions
let heuristic_tables = detect_tables(
    &text_items, 
    base_font_size,
    /*skip_body_font=*/ false
);

```

## Key Source Files and Architecture

| File | Responsibility |
|------|--------------|
| [`src/tables/detect_rects.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_rects.rs) | `cluster_rects`, `detect_table_from_rect_group`, `detect_merged_cluster_table` |
| [`src/tables/detect_lines.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_lines.rs) | `merge_horizontal_segments`, `build_text_anchor_table`, `table_evidence_score` |
| [`src/tables/detect_heuristic.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_heuristic.rs) | `expand_consolidated_items`, `find_column_boundaries`, validation suite |
| [`src/tables/grid.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/grid.rs) | `join_cell_items`, boundary helpers, header recovery |
| [`src/types.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/types.rs) | `PdfRect`, `PdfLine`, `TextItem`, `Table` struct definitions |
| [`src/lib.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs) | `process_pdf_with_options` orchestration API |
| [`docs/rust-api.md`](https://github.com/firecrawl/pdf-inspector/blob/main/docs/rust-api.md) | Library documentation for Rust consumers |

The modular architecture allows each detection strategy to evolve independently while sharing validated `Table` output structures.

## Summary

- **pdf-inspector implements three prioritized table detection strategies**: rectangle-based, line-based, and heuristic text-only analysis, defined in [`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), and [`src/tables/detect_heuristic.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_heuristic.rs) respectively
- **Rectangle detection runs first** and provides the fastest path for explicitly bordered tables, with `cluster_rects` and grid validation as core operations
- **Line detection serves as secondary fallback**, using `merge_horizontal_segments` and multiple constructor strategies (`build_text_anchor_table`, `build_dense_row_anchor_table`) for ruled tables
- **Heuristic detection handles pure text tables** through two-pass analysis with preprocessing (`expand_consolidated_items`, `merge_adjacent_items_preserving`) and strict validation (`has_consistent_columns`, `is_key_value_layout`)
- **Hint passing between stages** improves overall accuracy: failed rectangle clusters guide heuristic region selection
- **The public API** (`process_pdf_with_options`) abstracts pipeline orchestration, while individual strategies remain accessible for specialized use cases

## Frequently Asked Questions

### What order does pdf-inspector run its table detection strategies?

pdf-inspector always executes detection in fixed priority: **rectangle-based first, line-based second, heuristic text-only last**. This ordering prioritizes the most explicit visual signals (drawn rectangles and lines) before computationally intensive text analysis. Each stage can produce valid tables that prevent later pipelines from running, though hint regions improve downstream accuracy when early stages fail partially.

### How does pdf-inspector handle tables without any borders or lines?

Tables rendered purely as aligned text trigger the **heuristic detection pipeline** in [`src/tables/detect_heuristic.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/tables/detect_heuristic.rs). This strategy analyzes `TextItem` positions through glyph merging, financial string expansion, and column/row boundary inference. A two-pass approach (small-font then body-font) catches tables at different scales, while validation functions like `check_column_alignment` and `has_table_like_content` filter false positives from paragraph text.

### Can I use only one detection strategy instead of all three?

Yes—individual strategies are directly callable through `detect_tables_from_rects()`, `detect_tables_from_lines()`, and `detect_tables()`. However, the full three-pipeline approach in `process_pdf_with_options()` is recommended for production use, as automatic fallback and hint passing between stages substantially improves extraction accuracy across diverse PDF sources.

### Why does rectangle detection sometimes fail on obvious tables?

Rectangle detection fails when PDFs use **line operators** (`m`/`l`/`S`) rather than rectangle operators (`re`) to draw borders, or when decorative vector elements trigger filtering constants like `DOMINANT_PAGE_BACKGROUND_MIN_REPETITIONS`. The `split_wide_cluster` logic may also over-segment adjacent tables. These cases are intentionally delegated to line-based or heuristic detection, which handle border styles and explicit validation scoring that rectangles alone cannot express.