Table Structure Parsing#
Table structure parsing in RAGFlow converts raw bounding-box detections from a vision model into a structured HTML table or natural-language sentence list. The pipeline runs through four stages: TSR inference → coordinate alignment → row/column assignment → span/header reconstruction.
The entry point is TableStructureRecognizer in deepdoc/vision/table_structure_recognizer.py, a subclass of Recognizer that classifies regions within a cropped table image into six labels: table, table column, table row, table column header, table projected row header, and table spanning cell.
Coordinate System Alignment#
TSR outputs coordinates relative to the cropped table image; OCR text boxes in self.boxes carry page-cumulative Y coordinates (absolute offsets across all preceding pages). These two spaces must be reconciled before overlap matching .
Key mechanics:
page_cum_heightis built in__images__()as a cumulative sum of per-page heights (in PDF-point units at72 × zoominDPI) .- OCR boxes are lifted into cumulative space in
_layouts_rec()viabox["top"] += page_cum_height[page_number - 1]. - TSR components stored in
self.tb_cpnsretain image-local coordinates — only text boxes are in cumulative space.
⚠️ Active bug (as of 2026-07):
find_overlapped_with_thresholdis called with text boxes (cumulative space) against TSR row structures (image-local space). Row-index ("R") assignment is only accidentally correct when the table sits near the top of the page where the two coordinate origins nearly coincide. Mid-page tables degrade more gracefully (nullR, header span missed) but top-page tables can silently bind cells to the wrong rows .
Row, Column, and Spanning Cell Assignment#
_table_transformer_job() orchestrates the assignment:
| Tag | Method | Threshold | Purpose |
|---|---|---|---|
R (row) | find_overlapped_with_threshold | 0.3 | Assigns each text box to its TSR row |
H (header row) | find_overlapped_with_threshold | 0.3 | Flags text boxes in header rows |
SP (spanning cell) | find_overlapped_with_threshold | 0.3 | Flags text boxes in spanning cells |
C (column) | find_horizontally_tightest_fit | — | Assigns text box to the horizontally closest column |
These tags (R, H, C, SP) written onto each text box drive the downstream construct_table() call.
⚠️ Multi-page/multi-table column identity bug (as of 2026-07):
layoutnois assigned per-page viaenumerate(tbls), so every page's first table is"table-0".find_horizontally_tightest_fitonly checkslayoutnowithout the page number — a text box on page 2's"table-0"can incorrectly match columns from page 1's"table-0". The proposed fix is to also comparepage_numbervspnin the matching guard.
After raw TSR output is normalized, rows are aligned left/right and columns are aligned top/bottom using median/mean across detected elements .
construct_table(): Row/Column Grid, Span Calculation, and Header Inference#
construct_table() takes tagged text boxes and produces an HTML table or text sentences.
Grid Assembly#
Boxes are sorted by row (sort_R_firstly) then by column (sort_C_firstly for single-page, sort_X_firstly for cross-page tables) . Cross-page detection is a simple len(set([b["page_number"] for b in boxes])) > 1 check .
Single-value rows or columns (with ≥ 4 rows/columns total) are relocated to the nearest neighbor based on geometric distance, preventing phantom columns or rows from OCR noise .
Spanning Cell Calculation (__cal_spans)#
__cal_spans() computes colspan and rowspan for cells tagged "SP":
- Compute per-column mean left/right bounds (
clft,crgt) and per-row mean top/bottom bounds (rtop,rbtm) from the actual cell coordinates. - For each spanning-cell box, test every other column: a column
jis included in the span if its midpoint falls inside the spanning cell's horizontal extent (H_left…H_right). The same midpoint test applies for rows usingH_top…H_bott. - Convert the collected index lists into contiguous ranges and null-out covered cells in the grid (HTML mode) or aggregate their text (text mode) .
Header Inference#
Headers are inferred heuristically in construct_table():
- Content-type majority voting:
blockType()classifies each cell as one ofDt(date),Nu(numeric),Ca(code/catalog),En(English),NE(numeric+text),Sg(single char),Tx/Lx(short/long text),Nr(name), orOt(other) . The table's dominant type (max_type) is determined byCounter. - A row is a header if more than 50% of its cells are either tagged
Hby TSR or contain non-numeric content in a predominantly numeric table .
Multi-Level Header Merging (__desc_table)#
In text (non-HTML) output mode, __desc_table() merges adjacent header rows:
- Missing cell fill: empty header cells are filled from the previous header row's values .
- Multi-level concatenation: for consecutive header rows, if
headers[j][k]doesn't already containheaders[j-1][k], the two are concatenated with的(Chinese) orfor(English) as separator, with the shorter string made the prefix .
Rotated Table Path#
When TABLE_AUTO_ROTATE=true, _ocr_rotated_tables() evaluates 0°/90°/180°/270° rotations via OCR confidence, then maps coordinates back through three stages: rotated image → original image (via _map_rotated_point()) → page-local (÷ ZM + table offset) → page-cumulative (+ page_cum_height[page_index]) .
Key Files#
| File | Role |
|---|---|
deepdoc/vision/table_structure_recognizer.py | TableStructureRecognizer, construct_table(), __cal_spans(), blockType() |
deepdoc/parser/pdf_parser.py | _table_transformer_job(), _ocr_rotated_tables(), page_cum_height construction |
deepdoc/vision/recognizer.py | overlapped_area(), find_overlapped_with_threshold(), find_horizontally_tightest_fit(), layouts_cleanup() |