Skip to content

Repository files navigation

TableFinder vs TableHunter — a table-region-recovery win, a grid-quality gap

This note is for pymupdf/mupdf upstream developers evaluating whether MuPDF's native table detector (fz_table_hunt / fz_find_table_within_bounds, "TableHunter" below) can replace pymupdf's Python TableFinder (page.find_tables(), slated for removal) as the table-detection backend for a downstream table-to-HTML pipeline.

Short version: TableHunter, driven by GNN-layout table boxes, finds the same table regions TableFinder finds — that part works well. But on those same regions it produces a much finer grid than TableFinder's ruling-line grid (4x-16x more cells across every example below), and substituting it in measurably hurts table-extraction quality on a 503-page benchmark. There is also a latent type-mismatch bug in the existing make_table_from_bbox Python wrapper that makes it silently unusable today.

Everything with a number attached below was produced by running the two scripts in this directory in a fresh shell against plain pymupdf — see Provenance for exactly what was re-run this session vs. cited from a prior 503-page benchmark.

Contents

tablehunter-comparison/
├── README.md                              -- this file
├── compare_finder_hunter.py                -- finder vs hunter grid comparison (standalone)
├── repro_make_table_from_bbox_bug.py       -- minimal make_table_from_bbox bug repro
├── pdfs/                                   -- the five gallery source pages (single-page PDFs)
└── examples/                               -- dumped finder/hunter HTML grid pairs (14 files)

TL;DR

TableFinder (page.find_tables, lines_strict) TableHunter (fz_find_table_within_bounds, GNN-box-bounded)
Region recovery (finder-only bordered candidates, given a GNN table box) reference 22/22 recovered, IoU 0.867-0.999 vs. finder's bbox
Grid quality on those same recovered regions reference (ruling-line-exact grid) 4.1x-15.7x more cells than finder, on every one of the 5 examples tested below (over-segmentation)
GTRM (structural-similarity score, 0-1, higher is better; 503-page ParseBench table benchmark; TableHunter used as a union candidate source) 0.7211 0.5206 (-0.2005)
pymupdf.table.make_table_from_bbox wrapper n/a broken: always returns [] (type-mismatch bug, see below)
Verdict keep as the default/primary detector correctly proposes where tables are; does not yet produce finder-grade grids there

Update (2026-07-28): detector flags matched to the layout stext option set

Robin reproduced gallery case 1 with the stext option set pymupdf-layout uses when extracting the page and got the correct 4x4 grid, suspecting an option mismatch on our side. He was right. TABLE_DETECTOR_FLAGS used a reduced set (accurate-bboxes, segment, vectors, mediabox-clip); it now matches the layout set (preserve-images, preserve-whitespace, preserve-ligatures, accurate-bboxes, vectors, collect-styles, segment, paragraph-break, structured, clip, fuzzy-vectors, lazy-vectors), with table-hunt excluded because the bounded detector is invoked explicitly per box.

With the matched flags (fresh finder/hunter HTML pairs in examples-layout-flags/):

case reduced flags (old) layout-matched flags (new)
1657221585_page62 wrong grid 4x4 = 16 TDs, matches the finder exactly (resolved)
1634690602_page7 no table found, 28 rows / 165 TDs vs finder 5x3 = 15 cells (11.0x)
1634690602_page2 no table found, 27 rows / 161 TDs vs finder 4x3 = 12 cells (13.4x)
1653739079_page39 no table all 3 tables found, 12 rows / 96-108 TDs each vs finder 12-14 cells (7.7-8.0x)
203924_..._page27 no table found, 14 rows / 91 TDs vs finder 6x5 = 22 cells (4.1x)

GTRM on the 503-page table benchmark: 0.5206 -> 0.5185 (unchanged in substance; finder path 0.7211). With the right options the failures collapse into the single over-segmentation mode. Region recovery stops being an issue for the bounded calls, which also recover the page39/page27 regions that a page-wide hunt misses.

Ground-truth grids for the five gallery pages are bundled under gt/ (extracted from the ParseBench table dataset) so they can be opened next to the finder/hunter dumps.

How to reproduce

Prerequisites: pymupdf >= 1.28 with a build that ships the table detector (pymupdf.mupdf.fz_find_table_within_bounds, pymupdf.table.TABLE_DETECTOR_FLAGS) and some PDFs with tables. pymupdf.layout (the GNN layout wheel) is optional — both scripts detect its absence and fall back automatically (see "bounds modes" below). We ran everything with PYTHONUTF8=1 set (Windows console; avoids UnicodeEncodeError on non-ASCII table text) — harmless, and unnecessary on most Unix shells.

# 1. side-by-side grid comparison for one PDF (first page by default)
PYTHONUTF8=1 python compare_finder_hunter.py path/to/your.pdf

# a specific page, and HTML dumps of every finder/hunter grid found
PYTHONUTF8=1 python compare_finder_hunter.py path/to/your.pdf --page 3 --dump-html out/

# 2. the make_table_from_bbox wrapper bug, against your own PDF
PYTHONUTF8=1 python repro_make_table_from_bbox_bug.py path/to/your.pdf
# (or with no argument at all -- it defaults to pdfs/1657221585_page6.pdf)

No PDFs of your own needed: the gallery's five source pages (plus the bug repro's default page) ship in pdfs/ — single-page extracts from ParseBench's public table document set — so every number and HTML pair in this document regenerates from a bare checkout:

for f in pdfs/*.pdf; do PYTHONUTF8=1 python compare_finder_hunter.py "$f" --dump-html examples/; done
PYTHONUTF8=1 python repro_make_table_from_bbox_bug.py

With pymupdf.layout installed the hunter bounds come from the GNN table boxes (the mode the committed gallery used); without it the scripts fall back to finder-bbox bounds automatically (see the bounds-mode note below).

Exactly as run for this document (from inside tablehunter-comparison/, against the envs/improved pymupdf 1.28.0 + pymupdf-layout 1.28.0 environment used throughout our investigation):

PYTHONUTF8=1 ../envs/improved/Scripts/python.exe compare_finder_hunter.py \
    "../ParseBench/data/docs/table/1657221585_page62.pdf" --dump-html examples

Bounds modes (both exercised this session — see Provenance):

  • pymupdf.layout importable → the script calls page.get_layout() (the official pymupdf surface, no fork/helper involved) and uses the GNN "table"-class box with the best IoU against each TableFinder bbox as the bounds passed to fz_find_table_within_bounds. Printed as bounds mode: gnn.
  • pymupdf.layout not installed → falls back to the TableFinder bbox itself (padded 3pt) as the bounds. Printed as bounds mode: finder-bbox. We ran both canonical gallery examples this way under envs/nolayout (a real pymupdf install with no layout wheel present, not a simulated flag) and found the fallback is not always equivalent to GNN-bounded mode: on 1657221585_page62.pdf (GNN IoU 1.00 against the finder bbox) both modes produced an identical grid (251 TDs, 15.7x). On 1634690602_page7.pdf (GNN IoU 0.94 — the GNN box is measurably larger than the finder bbox) the fallback bounds gave a different, smaller grid: 75 TDs (5.0x) vs. 105 TDs (7.0x) in GNN mode, because the two bounds genuinely differ in extent (the GNN-mode hunter bbox starts at x=20.9 vs. x=57.2 for the finder-bbox-mode bbox — the GNN box includes extra page content the finder bbox does not). Practical implication: which bounds you feed the detector measurably changes its output, on top of the over-segmentation problem itself. The gallery below uses GNN-bounded mode throughout (envs/improved, pymupdf.layout present) — call that out if you re-run it without the layout wheel and see different numbers on some PDFs.

Over-segmentation gallery

Five examples, chosen for large hunter/finder cell-count ratios and visually unambiguous splits, spanning three unrelated source documents. All numbers below are copy-pasted from actual compare_finder_hunter.py runs (see Provenance) — none are hand-computed or carried over unverified from earlier notes. "Grid RxC" is TableFinder's nominal grid shape; the cell count can be below R x C when TableFinder itself merges spanned cells (fewer non-empty cells than the nominal grid size) — that's TableFinder's own span handling, not a hunter artifact. TableHunter's stext output has no such nominal rectangular shape (see Mechanism), so it's reported as "R rows (TR elements), N TD cells total."

1. 1657221585_page62.pdf, page 0 — 16 cells -> 251 TDs (15.7x)

bbox grid cells
finder (68.0, 90.9, 527.2, 792.1) 4x4 16
hunter (65.0, 91.0, 530.2, 792.0) 47 rows (TR) 251

region IoU (hunter bbox vs finder bbox) = 0.99. This is the largest ratio we found on the corpus: a regulatory-table page with long wrapped-paragraph cells.

finder excerpt (examples/1657221585_page62_table0_finder.html):

<tr>
  <td>12.1.12</td>
  <td>Yeasts products</td>
  <td>All yeasts32 and parts (1) thereof obtained by cracking and/or fractionation of
      yeast cells from Saccharomyces bayanus, ... vitamins and minerals</td>
  <td>Moisture if &lt; 75 % or &gt; 97 %</td>
</tr>

hunter excerpt (examples/1657221585_page62_table0_hunter.html) — the SAME row, sliced along every line-wrap/alignment gap the uncertainty-based segmentation treats as a candidate cut, splitting the running sentence apart (note and/or and fractionation land in separate TDs, and the last TD before the wrap even loses "the moisture" clause's punctuation grouping):

<tr>
  <td>12.1.12</td>
  <td>Yeasts products</td>
  <td>All yeasts32 and parts (1) thereof obtained by cracking</td>
  <td>Moisture if &lt; 75 % or &gt;</td>
</tr>
<tr>
  <td></td><td></td>
  <td>and/or</td><td>fractionation</td><td></td><td>of</td><td>yeast</td>
  <td>cells from</td><td>97 %</td>
</tr>

2. 1634690602_page7.pdf, page 0 — 15 cells -> 105 TDs (7.0x)

bbox grid cells
finder (56.9, 71.1, 779.7, 519.1) 5x3 15
hunter (20.9, 71.4, 779.7, 525.9) 27 rows (TR) 105

region IoU = 0.94. A veterinary-medicine CVMP recommendations table (wide page, 3 wide text columns).

finder excerpt:

<tr>
  <td>Product (active substance(s))</td>
  <td>CVMP meeting date</td>
  <td>Recommendation - SPC change (additions to text in bold, deletions in strikethrough)</td>
</tr>

hunter excerpt — the header text itself gets duplicated/re-cut across two TR's (the "(active substance(s))" fragment recurs as its own TD in the next row):

<tr>
  <td></td>
  <td>Product (active substance(s))</td>
  <td>CVMP meeting date</td>
  <td>Recommendation - SPC change (additions to text in bold, deletions in strikethrough)</td>
</tr>
<tr>
  <td></td>
  <td>(active substance(s))</td>
  <td></td>
  <td>(additions to text in bold, deletions in strikethrough)</td>
</tr>

3. 1634690602_page2.pdf, page 0 — 12 cells -> 103 TDs (8.6x)

bbox grid cells
finder (56.9, 71.1, 779.7, 521.1) 4x3 12
hunter (19.4, 71.4, 779.7, 525.9) 26 rows (TR) 103

region IoU = 0.94. Same source document family as example 2, different page — same header-duplication artifact, confirming it's systematic, not a one-off:

<!-- finder -->
<tr>
  <td>Product (active substance(s))</td>
  <td>CVMP meeting date</td>
  <td>Recommendation - SPC change (additions to text in bold, deletions in strikethrough)</td>
</tr>
<!-- hunter -->
<tr>
  <td></td>
  <td>Product (active substance(s))</td>
  <td>CVMP meeting date</td>
  <td>Recommendation - SPC change (additions to text in bold, deletions in strikethrough)</td>
</tr>
<tr>
  <td></td><td>(active substance(s))</td><td></td>
  <td>(additions to text in bold, deletions in strikethrough)</td>
</tr>

4. 1653739079_page39.pdf, page 0, table 0 of 3 — 14 cells -> 108 TDs (7.7x)

bbox grid cells
finder (45.0, 103.2, 567.0, 233.8) 3x6 14
hunter (42.3, 104.6, 572.2, 233.0) 12 rows (TR) 108

region IoU = 0.97. This page has three side-by-side small tables; all three show the same 7.7x-8.0x ratio (table 1: 14 -> 108, table 2: 12 -> 96 — see examples/1653739079_page39_table{0,1,2}_*.html). This example shows mid-word splitting, not just mid-sentence: "Department" becomes two TDs, Depar / rtment.

<!-- finder -->
<tr>
  <td>Year</td><td>Assessments</td><td>Department</td><td>Public</td>
</tr>
<!-- hunter -->
<tr>
  <td></td><td>Year</td><td>Assessments</td><td>Depar</td><td>rtment</td>
  <td></td><td>Pub</td><td>blic</td><td></td>
</tr>

5. 203924_fb04e77929bf4bbc93dbd659653a4f01_page27.pdf, page 0 — 22 cells -> 91 TDs (4.1x)

bbox grid cells
finder (62.6, 71.1, 532.8, 637.6) 6x5 22
hunter (62.6, 71.2, 532.8, 637.6) 14 rows (TR) 91

region IoU = 1.00 (exact bbox match) — the smallest ratio in the gallery, and useful evidence that even a perfect region match still over-segments: a narrower, denser policy-document table.

<!-- finder -->
<tr>
  <td></td><td></td>
  <td>for veterinary medicinal products (to cover technologies such as mobile devices
      and apps) (D; F) 3. Investigate and develop methods to optimise the dissemination
      of updated safety information to practising veterinarians and animal owners
      (food-producing animals and companion animals) (B; G)</td>
  <td></td><td></td>
</tr>
<!-- hunter -->
<tr>
  <td></td><td></td><td>3.</td>
  <td>for veterinary medicinal products (to cover technologies such as mobile devices
      and apps) (D; F) Investigate and develop methods to optimise the dissemination
      of updated safety information to practising veterinarians and animal owners
      (food- producing animals and companion animals) (B; G)</td>
  <td></td><td></td><td></td>
</tr>

(the leading list marker "3." is sheared off into its own TD)

Mechanism: why the grid comes out finer

TableHunter is not "worse" at finding tables — section above shows it recovers the same regions TableFinder does. The gap is in how it partitions a recovered region into cells, and it comes down to three things:

1. It does consume ruling lines. pymupdf.table.TABLE_DETECTOR_FLAGS (PyMuPDF-private/src/table.py:148-154) is TEXT_ACCURATE_BBOXES | TEXT_SEGMENT | TEXT_COLLECT_VECTORS | TEXT_MEDIABOX_CLIPTEXT_COLLECT_VECTORS is what hands MuPDF's stext-table detector the page's drawn vector lines/rects as candidate ruling lines. Per our source-level investigation notes, this consumption happens in MuPDF's stext-table.c (h_line/v_line accumulation, roughly lines 1107-1563 in the version we looked at) — you have that file locally; we don't vendor mupdf's C source in this checkout, so treat that line range as a pointer to check against your own tree, not something re-verified in this session.

2. But segmentation is uncertainty-scored, not "is there a ruling line here." The block MuPDF's grid detector actually builds carries, per axis, a list of (pos, uncertainty) candidate cut positions plus a max_uncertainty (see JM_make_grid_block, PyMuPDF-private/src/extra.i:3689-3711, which is what would expose a FZ_STEXT_BLOCK_GRID block's xpos/ypos/max_uncertain to Python — see the bug section below for why that path is currently dead). pymupdf's own (also-dead) make_table_from_bbox logic (PyMuPDF-private/src/table.py:227-240) confirms this reading: it explicitly throws away any y position with yunc > 0 before treating it as a row boundary. In other words, MuPDF's own model of this data is "some of these candidate cuts are real grid lines and some aren't; you must filter." The fz_find_table_within_bounds -> stext-XML path that is actually usable today (what both scripts in this repo use) does not apply any such filter before emitting TDs — every candidate cut becomes a real cell boundary in the output.

3. stext has no rowspan/colspan representation. We dumped the raw stext XML this session and checked directly: a TD struct element carries exactly idx, bbox, raw, std — no span attribute of any kind:

TD attribs: {'idx': '0', 'bbox': '68 90.7 110.2845 108.728359', 'raw': 'TD', 'std': 'TD'}

Per our source-level investigation notes, mupdf's stext-table.c documents this limitation directly in a code comment around line 472. Practically, this cuts both ways: a genuinely merged source cell becomes one big TD (that part is fine — see the finder's single wrapped-paragraph cells above), but nothing in the output tells a consumer "these dozen narrow TDs were really one logical cell that merely wrapped across several text lines." Combined with (2), every alignment gap inside a real prose cell is free to become its own TD, which is exactly what the gallery above shows — right down to "Depar" / "rtment" being split mid-word in example 4.

Net effect measured on the 503-page ParseBench table benchmark (details in Provenance): substituting TableHunter (GNN-box-bounded) for TableFinder as the union-candidate source in our table-to-HTML engine takes GTRM from 0.7211 to 0.5206 — a 0.2005 drop — even though region coverage barely changes. The over-segmented grids actively hurt downstream structural quality; they are not merely "more detail."

The make_table_from_bbox bug

pymupdf.table.make_table_from_bbox (and get_table_dict_from_rect / pymupdf.extra.make_table_dict underneath it) is pymupdf's existing Python entry point for consuming fz_find_table_within_bounds. It silently returns no cells for every table, always, on this build.

Root cause — PyMuPDF-private/src/extra.i:3717-3738:

void make_table_dict(fz_stext_page *tp, PyObject *table_dict, PyObject *bbox)
{
    fz_rect bounds = JM_rect_from_py(bbox);
    fz_stext_block *block;
    try {
        block = mupdf::ll_fz_find_table_within_bounds(tp, bounds);
    } catch (std::exception&) {
        return;   /* Ignore failure to find a table structure. */
    }
    // Check if a table structure was found
    if (block && block->type == FZ_STEXT_BLOCK_GRID) {   // <-- type 4
        JM_make_grid_block(block, table_dict);
    }
    // else: table_dict silently stays {} -- no exception, no signal.
}

and PyMuPDF-private/src/table.py:207-215:

def make_table_from_bbox(textpage, word_rects, rect):
    cells = []
    block = get_table_dict_from_rect(textpage, rect)     # calls make_table_dict above
    if block.get("type") != mupdf.FZ_STEXT_BLOCK_GRID:
        return cells                                      # always [] in practice
    ...

On this pymupdf/mupdf build, fz_find_table_within_bounds never returns a FZ_STEXT_BLOCK_GRID (type 4) block — it returns a Table STRUCT block (FZ_STEXT_BLOCK_STRUCT, type 2). The == FZ_STEXT_BLOCK_GRID check is therefore always false, and both wrappers fail silently. This is not a one-off: a scan across 40 ParseBench PDFs found 0/36 finder-bbox trials ever produced a grid through this path.

Repro (repro_make_table_from_bbox_bug.py, run this session, envs/improved and envs/nolayout both shown):

PYTHONUTF8=1 python repro_make_table_from_bbox_bug.py

observed output:

file: 1657221585_page6.pdf   TableFinder bbox=(68.03..., 190.06..., 527.24..., 629.05...)  (60 cells -- a table IS genuinely there)

T.make_table_from_bbox(...) -> 0 cells   (expected > 0; got silent [])
T.get_table_dict_from_rect(...) -> {}   (empty: make_table_dict only fills it when block.type == FZ_STEXT_BLOCK_GRID)
raw fz_find_table_within_bounds(...) block.type = 2   (FZ_STEXT_BLOCK_GRID=4, FZ_STEXT_BLOCK_STRUCT=2)
  -> block.type != FZ_STEXT_BLOCK_GRID, so make_table_dict's `if` never fires: bug confirmed.

(also reproduced against 1657221585_page62.pdf under envs/nolayout, a real pymupdf install with no layout wheel, confirming it's independent of the layout wheel being present)

The workaround both scripts in this repo use instead: call fz_find_table_within_bounds directly, then walk textpage.extractXML() for the Table / TR / TD struct elements it wrote into the stext tree (see hunt_within_bounds() in compare_finder_hunter.py).

What would make TableHunter a drop-in replacement

None of this is a dead end — the region-recovery result means the hard part (knowing where to look) already works. Grid quality is a narrower, more mechanical gap. In rough order of expected impact:

  1. Ruling-line-authoritative segmentation. When TEXT_COLLECT_VECTORS finds a (near-)complete ruling-line grid inside the bounds, snap segmentation to exactly those lines and treat them as ground truth row/column boundaries, falling back to today's uncertainty/alignment-based cuts only where no line exists. All 5 gallery examples above are ruled tables where this alone should collapse most of the extra TDs.
  2. A cell-merge pass keyed on the uncertainty score MuPDF already computes. JM_make_grid_block shows the detector already produces a per-position uncertainty value for every candidate cut — it just isn't used to gate which cuts survive into the TD struct path. Merging TDs across a low-confidence internal cut (no ruling line, no consistent alignment evidence across rows) would directly target the "Depar"/"rtment"-style splits.
  3. Rowspan/colspan on TD (or an adjacency side-channel next to the struct tree), so a real spanned source cell round-trips as one logical cell instead of forcing a choice between "one oversized TD" and (via 1+2 done wrong) "many split TDs."
  4. Fix the type-mismatch bug as a low-effort first step: reconcile make_table_dict's FZ_STEXT_BLOCK_GRID check with what fz_find_table_within_bounds actually returns today (FZ_STEXT_BLOCK_STRUCT) — either by having the detector still populate a FZ_STEXT_BLOCK_GRID-shaped result the existing wrapper expects, or by updating make_table_dict/make_table_from_bbox to read the STRUCT block and expose its (already-computed) per-cut uncertainty at the Python level. That would give downstream consumers the raw material for (2) without any new MuPDF-side computation.

Measuring a hunter fix at benchmark scale (GTRM)

The 0.7211 / 0.5206 numbers come from LlamaIndex's ParseBench, table group (503 pages, GTRM = a composite of GriTS and TableRecordMatch). Everything needed to reproduce them — and to score a modified hunter build in the same pipeline — is public. The key piece is the branch experiment/hunter-union-source at https://github.com/veget-able/PyMuPDF: it is the review branch (single-diff/table-refine-html) plus one commit adding an environment switch, PYMUPDF_TABLE_UNION_SOURCE=hunter, that routes the fusion stage's candidate source through fz_find_table_within_bounds (per layout table box) instead of the Python line finder. Everything downstream — the GNN primaries, the replace/split/append gates, grid refinement, span resolution, header tagging, HTML output — is identical between the two settings, so the score difference isolates the candidate grids themselves.

  1. PyMuPDF from the branch. No compiled parts differ from 1.28.0, so the quick path is the official wheel plus a pure-Python overlay:

    pip install pymupdf==1.28.0 pymupdf-layout==1.28.0
    git clone -b experiment/hunter-union-source https://github.com/veget-able/PyMuPDF
    PKG=$(python -c "import pymupdf, os; print(os.path.dirname(pymupdf.__file__))")
    for f in table _table_refine _table_spans _table_union _table_headers; do
        cp PyMuPDF/src/$f.py "$PKG/"
    done

    For a modified MuPDF, build PyMuPDF from that branch against your MuPDF checkout as usual — the Python files ride along, and your detector changes flow in through fz_find_table_within_bounds.

  2. pymupdf4llm with the HTML table output (pure Python):

    pip install git+https://github.com/veget-able/pymupdf4llm@single-diff/html-tables-v2
  3. ParseBench with the table_output wiring:

    git clone https://github.com/veget-able/ParseBench
    cd ParseBench && pip install -e .
    parse-bench run pymupdf4llm_v4 --group table --max_concurrent 1 --open_report False

    The dataset downloads on first run. --max_concurrent 1 matters (the layout engine carries global state). The score is aggregate_metrics.avg_grits_trm_composite in <output_dir>/pymupdf4llm_v4/_evaluation_report.json. On Windows set PYTHONUTF8=1; unnecessary elsewhere.

  4. Flip the candidate source and re-run:

    PYMUPDF_TABLE_UNION_SOURCE=hunter parse-bench run pymupdf4llm_v4 \
        --group table --max_concurrent 1 --open_report False --output_dir ./output-hunter

    Expected on the unmodified 1.28.0 detector: ~72.1 with the line finder, ~52.1 with the hunter (the branch as pushed re-measures at 0.7211 / 0.5205 on our machine, matching the original investigation's 0.5206). A hunter fix that closes the over-segmentation shows up directly in this number — and we are happy to run candidate builds through this harness on our side too.

Provenance

What is asserted in this document and how it was checked, so nothing here is a copied-but-unverified number:

  • Region recovery, 22/22, IoU 0.867-0.999: recomputed this session directly from a prior investigation's raw per-candidate JSON (22 finder-only bordered candidates, all recovered). Two of the 22 (1657221585_page62.pdf, 1634690602_page7.pdf) were independently re-run live this session with compare_finder_hunter.py and match exactly (IoU 1.00 and 0.94 respectively, cell counts as in the gallery).
  • All 5 gallery examples (cell counts, bboxes, IoU, HTML excerpts): run live this session with compare_finder_hunter.py against envs/improved/Scripts/python.exe (pymupdf 1.28.0 + pymupdf-layout 1.28.0), PYTHONUTF8=1. The examples/*.html files are the exact --dump-html output of those runs, not hand-edited.
  • Gallery PDFs (pdfs/): the source pages themselves, included verbatim (single-page extracts from ParseBench's public table document set) so the gallery reproduces from a bare checkout. Re-verified after bundling: compare_finder_hunter.py pdfs/1657221585_page62.pdf reproduces the committed pair's numbers exactly (4x4 = 16 vs 47 rows / 251 TDs, region IoU 0.99, GNN-bounds IoU 1.00), and the no-argument bug repro runs against pdfs/1657221585_page6.pdf.
  • Bounds-mode fallback (no pymupdf.layout): run live this session against envs/nolayout/Scripts/python.exe, a real pymupdf install with no layout wheel installed (not a simulated flag), on both canonical gallery PDFs — this is what surfaced the GNN-vs-fallback divergence on 1634690602_page7.pdf documented under "Bounds modes" above.
  • The make_table_from_bbox bug repro and its observed output: run live this session, both environments, both a bundled default PDF and an explicit path argument.
  • GTRM 0.7211 (finder) vs. 0.5206 (TableHunter, GNN-box-bounded, used as a union candidate source): cited from our prior 503-page ParseBench table benchmark (GTRM = average of GriTS and TableRecordMatch structural-match scores over ParseBench's 503-page / 284-document table set). Not rerun in this session — a full 503-page benchmark pass is out of scope for this comparison doc; treat it as a reference data point, reproducible with the ParseBench harness in this repo if you want to re-derive it yourself.
  • Mechanism section: the TABLE_DETECTOR_FLAGS composition, the JM_make_grid_block uncertainty fields, the dead filtering logic in make_table_from_bbox, and the "no rowspan/colspan attribute on TD" claim were all checked directly against PyMuPDF-private/src/extra.i and PyMuPDF-private/src/table.py in this checkout, plus a live stext-XML dump (shown verbatim above) confirming TD attributes. The stext-table.c line references (ruling-line consumption via FZ_STEXT_BLOCK_VECTOR walks walk_grid_lines/walk_grid_lines2 ~1722-1812 feeding add_h_line2/ add_v_line2 ~1515-1563 into grid cells ~1107-1128; border-favoring scoring ~3726-3759; hunt entries ~4337/4468/4514; the "no explicit rowspan/colspan in stext" comment ~472-474) were read and verified against a local MuPDF source checkout during the investigation that produced this document. Line numbers drift across MuPDF versions — treat them as anchors, and re-locate the symbols in your tree if they have moved.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages