Walk away from the caption in ``direction`` ('up' or 'down') and collect logical rows that look like part of a tabular layout. Returns the list of accepted row bboxes (caption excluded) and the number of rows confirmed as table body rows. The caller decides which direction wins.
(
page,
caption_anchor: dict,
rows: list[dict],
paragraph_blocks: list[tuple[float, float, float, float, str]],
*,
direction: str,
upper_bound: float,
lower_bound: float,
)
| 837 | |
| 838 | |
| 839 | def _grow_table_region( |
| 840 | page, |
| 841 | caption_anchor: dict, |
| 842 | rows: list[dict], |
| 843 | paragraph_blocks: list[tuple[float, float, float, float, str]], |
| 844 | *, |
| 845 | direction: str, |
| 846 | upper_bound: float, |
| 847 | lower_bound: float, |
| 848 | ) -> tuple[list[tuple[float, float, float, float]], int]: |
| 849 | """Walk away from the caption in ``direction`` ('up' or 'down') and collect |
| 850 | logical rows that look like part of a tabular layout. |
| 851 | |
| 852 | Returns the list of accepted row bboxes (caption excluded) and the number |
| 853 | of rows confirmed as table body rows. The caller decides which direction |
| 854 | wins. |
| 855 | """ |
| 856 | caption_y0 = caption_anchor["bbox"][1] |
| 857 | caption_y1 = caption_anchor["bbox"][3] |
| 858 | |
| 859 | accepted: list[tuple[float, float, float, float]] = [] |
| 860 | data_row_count = 0 |
| 861 | consecutive_non_data = 0 |
| 862 | seen_data = False |
| 863 | last_accepted_edge = caption_y1 if direction == "down" else caption_y0 |
| 864 | |
| 865 | if direction == "down": |
| 866 | candidates = [r for r in rows if r["bbox"][1] > caption_y1 + 0.5] |
| 867 | candidates.sort(key=lambda r: r["bbox"][1]) |
| 868 | boundary_check = lambda ly0, ly1: ly1 >= lower_bound |
| 869 | else: |
| 870 | candidates = [r for r in rows if r["bbox"][3] < caption_y0 - 0.5] |
| 871 | candidates.sort(key=lambda r: r["bbox"][3], reverse=True) |
| 872 | boundary_check = lambda ly0, ly1: ly0 <= upper_bound |
| 873 | |
| 874 | for row in candidates: |
| 875 | restricted_row = _restrict_row_to_caption_column(row, caption_anchor["bbox"], page.rect) |
| 876 | if restricted_row is None: |
| 877 | continue |
| 878 | row = restricted_row |
| 879 | rx0, ry0, rx1, ry1 = row["bbox"] |
| 880 | if boundary_check(ry0, ry1): |
| 881 | break |
| 882 | text = row["text"] |
| 883 | is_table_row = _row_is_table_like(row) |
| 884 | in_paragraph_block = _line_is_inside_any_block(row["bbox"], paragraph_blocks) |
| 885 | row_gap = ry0 - last_accepted_edge if direction == "down" else last_accepted_edge - ry1 |
| 886 | row_height = max(ry1 - ry0, 1.0) |
| 887 | if seen_data and row_gap > max(12.0, row_height * 1.8) and not is_table_row: |
| 888 | break |
| 889 | if ( |
| 890 | seen_data |
| 891 | and len(row.get("members", []) or []) <= 1 |
| 892 | and in_paragraph_block |
| 893 | and not _looks_like_data_row(text) |
| 894 | ): |
| 895 | break |
| 896 | # If this row sits entirely inside a real prose paragraph and does not |