Metadata-Version: 2.4
Name: pdf-table-gate
Version: 0.1.0
Summary: Decide whether a PDF page actually contains a table, before you try to extract one.
License: MIT
Project-URL: Homepage, https://github.com/creatorx808-hub/pdf-table-gate
Project-URL: Issues, https://github.com/creatorx808-hub/pdf-table-gate/issues
Keywords: pdf,table,extraction,pymupdf,table-detection,ocr
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Text Processing :: Markup
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyMuPDF>=1.23
Dynamic: license-file

# pdf-table-gate

**Decide whether a PDF page actually contains a table — before you try to extract one.**

PyMuPDF's table finder is very good at finding tables. It is also very good at finding tables in pages that do not have any. This library answers the prior question, so the extractor is only ever asked where the columns are on pages that genuinely have columns.

```python
import fitz
from pdftablegate import has_table

doc = fitz.open("report.pdf")
for page in doc:
    if has_table(page):
        tables = page.find_tables(strategy="text", min_words_vertical=2)
```

## The problem

PyMuPDF extracts ruled tables (`strategy="lines_strict"`) reliably. For tables held together by alignment alone — which is most real-world statements, invoices and reports — it offers `strategy="text"`, which splits on whitespace.

That strategy cannot tell a table from a paragraph. A paragraph is also words separated by spaces.

Run [`examples/measure.py`](examples/measure.py) and you get this, on a document containing **no tables at all**:

```
Document: 7 pages of prose, zero tables.

  find_tables(strategy='text') alone : 7 'tables' found
    largest hallucination            : 35 rows x 17 cols
  with has_table() as a gate         : 0 tables found
```

Each of those "rows" is a sentence shredded at its spaces:

```
Paragraph | number | 0 w | ith | s | ome | fi | ller | ...
```

Nothing in the output signals low confidence. You get a workbook of confetti under a cheerful *"7 tables found."* The extractor isn't broken — it is answering a question that has no answer on that page.

## The approach

A real table without ruling lines has one property prose never has: **a band of blank space running top to bottom that every row respects.** A column gutter.

Prose fills the full measure on nearly every line, so no vertical channel survives more than a line or two. That is a physical fact about the layout, not a heuristic about content, which is why it is what gets tested.

The implementation clusters words into visual rows, then sweeps the page width with a difference array to find channels no row crosses — tolerating a small fraction of rows that do, because real tables have merged cells, section headers and totals lines that reach across columns.

**Use both halves for what each is good at.** The gutter detector is too coarse to place columns; a wide cell reaching across a gutter will merge two columns into one. `find_tables` resolves columns correctly but hallucinates on prose. So the gate decides *whether*, and `find_tables` decides *where*.

## Install

```bash
pip install pdf-table-gate
```

Requires Python 3.9+ and PyMuPDF. Nothing else — no ML runtime, no model download.

## API

```python
has_table(page, config=DEFAULT) -> bool
```
True when the page has a real column gutter. One text extraction and a linear sweep, so it is cheap enough to call before every extraction rather than after.

```python
table_pages(doc, config=DEFAULT) -> list[int]
```
0-based indexes of the pages worth extracting.

```python
gutters(page, config=DEFAULT) -> list[tuple[float, float]]
```
The detected gutters as `(x_start, x_end)`. Draw them on the page and you can see exactly what the gate reacted to; an empty list is *why* `has_table` said no.

```python
GateConfig(row_tol_frac=0.6, min_gutter_pt=5.0, gutter_leak=0.04, min_rows=3)
```

| Knob | Default | What it does |
|---|---|---|
| `row_tol_frac` | `0.6` | Words join a row when their vertical centres fall within this fraction of median glyph height. Keeps a subscript with its baseline without swallowing the next row. |
| `min_gutter_pt` | `5.0` | Narrower than this is an inter-word space, not a column gap. Below ~5pt it starts finding "columns" between words of a sentence. |
| `gutter_leak` | `0.04` | Fraction of rows allowed to span a gutter. `0` rejects nearly every real table (merged cells); large values start accepting prose. |
| `min_rows` | `3` | Fewer visual rows than this isn't evidence of anything. |

## What this is not

- **Not an extractor.** It returns a boolean. Use PyMuPDF, Camelot or pdfplumber for the data.
- **Not a content classifier.** Aligned code, a two-column CV, or a columnar form will read as tabular — structurally, they are. That is the honest boundary of a geometric test.
- **Not useful on scans.** A scanned page holds a picture of a table and no words to measure, so `has_table` returns `False`. Correct, but run OCR first and re-check.
- **Not a replacement for `lines_strict`.** If your tables have ruling lines, use that directly; it is already reliable and you don't need a gate.

## Why not just use a layout model?

`pymupdf-layout` was evaluated against this exact problem. It pulls in onnxruntime — roughly 40MB of ML runtime — and on the documents measured here the `find_tables` results were **byte-identical** with and without it. It does not change this behaviour. If a ~40MB dependency earns its place in your project for other reasons, fine; it will not fix this.

## Tests

```bash
pip install -e . pytest
pytest
```

The negative cases are the point. Any detector finds a table in a table; this library exists because the obvious approach also finds them in paragraphs. Every positive test has a prose counterpart, and fixtures are generated rather than committed so you can read exactly what each page looks like.

## Provenance

Extracted from the table-extraction engine behind [PDF Cubby](https://pdfcubby.com)'s PDF-to-Excel tool, where the failure above was found on a real 7-page prose document during testing — it returned seven confident tables of roughly 29 × 24 shredded words. The numbers in this README come from the reproducible synthetic fixture in `examples/`, which is a little different but the same failure.

## Licence

MIT.
