Skip to content

[BUG] Coordinate key mismatch in pymupdf.table.chars_in_rect() drops table candidates and rows #5092

Description

@Hao-ActuaViz

Description of the bug

In pymupdf/table.py, the internal helper function chars_in_rect(CHARS, rect) compares a top-down bounding box rect against character coordinates in CHARS.

However, while rect uses top-down Y coordinates (rect[1] is top, rect[3] is bottom) and the horizontal check uses top-down x0/x1, the vertical check in chars_in_rect compares rect[1] and rect[3] against c["y0"] and c["y1"] instead of c["top"] and c["bottom"].

def chars_in_rect(CHARS, rect):
    return any(
        1
        and rect[0] <= c["x0"]
        and c["x1"] <= rect[2]
        and rect[1] <= c["y0"]    # ❌ rect[1] is top-down Y, but c["y0"] is CTM / bottom-up
        and rect[3] >= c["y1"]    # ❌ rect[3] is top-down Y, but c["y1"] is CTM / bottom-up
        for c in CHARS
    )

Impact on Table Detection (find_tables()):
In table.py::clean_graphics(), candidate table boundary rectangles (prect0) are filtered using chars_in_rect(CHARS, prect0) to determine if a candidate box contains text.

  • On standard full-page tables, c["y0"] and c["top"] happen to numerically overlap by coincidence, masking the issue.
  • On short tables near the bottom of a page, continuation tables across page breaks, or pages with non-identity CTMs, c["y0"]/c["y1"] do not overlap with top-down rect[1]/rect[3].
  • As a result, chars_in_rect returns False, clean_graphics() discards the candidate boundary box, line synthesis fails, and entire table rows (or short tables) disappear from find_tables().

Note that elsewhere in table.py (e.g. in has_text() around line 1554), top-down coordinates are correctly paired with c["top"] and c["bottom"]:

text_bboxes = [
    (c["x0"], c["top"], c["x1"], c["bottom"]) # ✅ Correct top-down pair used in has_text()
    for c in CHARS
    if c["text"] not in white_spaces
]

How to reproduce the bug

Minimal Code Example 1 (Direct Unit Test):

import pymupdf.table

# A character positioned at top=100.0, bottom=110.0 on an 800px page.
# Top-down coordinates: top=100, bottom=110
# CTM / bottom-up coordinates: y0=690, y1=700
char = {
    "x0": 10.0, "x1": 20.0,
    "top": 100.0, "bottom": 110.0,
    "y0": 690.0, "y1": 700.0
}

# Top-down bounding box clearly surrounding the character (top=90, bottom=120)
rect = (0.0, 90.0, 30.0, 120.0)

# Expected: True (character is inside the top-down bounding box)
# Actual: False (because chars_in_rect compares top-down rect bounds with y0/y1)
result = pymupdf.table.chars_in_rect([char], rect)
print("chars_in_rect result:", result)  # Outputs: False

Proposed Fix:

In src/table.py, update chars_in_rect to use c["top"] and c["bottom"]:

def chars_in_rect(CHARS, rect):
    """Check whether any of the chars in CHAR are inside rectangle 'rect'."""
    return any(
        1
        and rect[0] <= c["x0"]
        and c["x1"] <= rect[2]
-       and rect[1] <= c["y0"]
-       and rect[3] >= c["y1"]
+       and rect[1] <= c["top"]
+       and rect[3] >= c["bottom"]
        for c in CHARS
    )

PyMuPDF version

1.27.2.3

Operating system

MacOS

Python version

3.12

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions