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.
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 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
pip install pdf-table-gate
Requires Python 3.9+ and PyMuPDF. Nothing else — no ML runtime, no model download.
API
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.
table_pages(doc, config=DEFAULT) -> list[int]
0-based indexes of the pages worth extracting.
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.
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_tablereturnsFalse. 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
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'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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pdf_table_gate-0.1.0.tar.gz.
File metadata
- Download URL: pdf_table_gate-0.1.0.tar.gz
- Upload date:
- Size: 9.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
022e6058eab9aa7fa7f007f1051afa7258a75b7ce132cdcee0adf8d1a1cfa41c
|
|
| MD5 |
35ed1614ce32b32688c13717b9f163bc
|
|
| BLAKE2b-256 |
4704ee8c3e401f46c716ee3f2072d97d42307dd7c9e00581f08be803c90ec35a
|
Provenance
The following attestation bundles were made for pdf_table_gate-0.1.0.tar.gz:
Publisher:
publish.yml on creatorx808-hub/pdf-table-gate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pdf_table_gate-0.1.0.tar.gz -
Subject digest:
022e6058eab9aa7fa7f007f1051afa7258a75b7ce132cdcee0adf8d1a1cfa41c - Sigstore transparency entry: 2292326568
- Sigstore integration time:
-
Permalink:
creatorx808-hub/pdf-table-gate@0d7e7fdaa1af53eb536538b9b46abb7c8cf62ed3 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/creatorx808-hub
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0d7e7fdaa1af53eb536538b9b46abb7c8cf62ed3 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pdf_table_gate-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pdf_table_gate-0.1.0-py3-none-any.whl
- Upload date:
- Size: 8.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
945ee3e5289977e70ab6312c9b20c055d1dec39de29dc757cbe05c7ccb435bc7
|
|
| MD5 |
0f483610eeee55960b5e759bf7667bdc
|
|
| BLAKE2b-256 |
507718cebf38849b5c09dd9d53ccbb3beacbec7f46b7d94f52c08b14d29eeff2
|
Provenance
The following attestation bundles were made for pdf_table_gate-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on creatorx808-hub/pdf-table-gate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pdf_table_gate-0.1.0-py3-none-any.whl -
Subject digest:
945ee3e5289977e70ab6312c9b20c055d1dec39de29dc757cbe05c7ccb435bc7 - Sigstore transparency entry: 2292326609
- Sigstore integration time:
-
Permalink:
creatorx808-hub/pdf-table-gate@0d7e7fdaa1af53eb536538b9b46abb7c8cf62ed3 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/creatorx808-hub
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0d7e7fdaa1af53eb536538b9b46abb7c8cf62ed3 -
Trigger Event:
release
-
Statement type: