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.
Release files for pdf-table-gate 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pdf_table_gate-0.1.0.tar.gz | 9.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pdf_table_gate-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 17.7 kB
Release files / pdf_table_gate-0.1.0.tar.gz
| Download URL | pdf_table_gate-0.1.0.tar.gz |
|---|---|
| Size | 9.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
022e6058eab9aa7fa7f007f1051afa7258a75b7ce132cdcee0adf8d1a1cfa41c
|
|
BLAKE2b-256 checksum How to use checksums |
4704ee8c3e401f46c716ee3f2072d97d42307dd7c9e00581f08be803c90ec35a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.
Transparency logRelease files / pdf_table_gate-0.1.0-py3-none-any.whl
| Download URL | pdf_table_gate-0.1.0-py3-none-any.whl |
|---|---|
| Size | 8.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
945ee3e5289977e70ab6312c9b20c055d1dec39de29dc757cbe05c7ccb435bc7
|
|
BLAKE2b-256 checksum How to use checksums |
507718cebf38849b5c09dd9d53ccbb3beacbec7f46b7d94f52c08b14d29eeff2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.
Transparency log