Skip to content

fix: keep y0/y1 in sync when clipping objects in clip_obj#1382

Open
otiscuilei wants to merge 1 commit into
jsvine:developfrom
otiscuilei:fix/clip-obj-updates-y0-y1
Open

fix: keep y0/y1 in sync when clipping objects in clip_obj#1382
otiscuilei wants to merge 1 commit into
jsvine:developfrom
otiscuilei:fix/clip-obj-updates-y0-y1

Conversation

@otiscuilei

Copy link
Copy Markdown

Who hits this

Anyone reading y0/y1 off an object that was clipped by .crop(...), .within_bbox(...), or .outside_bbox(...). Those fields are part of the public per-object schema (exported via to_dict, .objects, and the CSV/JSON output, documented as "distance from bottom of page"), so consumers that read y0/y1 — or compute geometry/height from them — on a cropped page get silently wrong numbers. Text extraction is unaffected (it uses top/doctop/x0), so this is silent corruption of the coordinate fields rather than a crash.

Root cause

clip_obj in pdfplumber/utils/geometry.py rewrites an object's top-based coordinates (x0, x1, top, bottom, doctop, width, height) but never updates the PDF-space y0/y1. pdfplumber objects carry the same box in two coordinate systems with the invariant top == page_bottom_ref - y1 and bottom == page_bottom_ref - y0. After a vertical clip, top/bottom shift but y0/y1 keep their pre-crop values, so the two representations describe different boxes and y1 - y0 no longer equals height. The sibling helpers move_object and resize_object both already update y0/y1 when present; only clip_obj was missing it.

The fix

After computing the clipped top/bottom, also update y0/y1 when present, mirroring resize_object: y1 tracks top, y0 tracks bottom.

if "y1" in copy:
    copy["y1"] = obj["y1"] - diff
if "y0" in copy:
    copy["y0"] = obj["y0"] - (dims["bottom"] - obj["bottom"])

Before / after

Cropping a page so chars[0] (top=117.18, bottom=135.18, y0=656.82, y1=674.82, height=18) is cut in half vertically:

  • Before: top=126.18, bottom=135.18, height=9.0, but y0=656.82, y1=674.82 -> y1-y0 = 18.0 (stale); page.height - y1 = 117.18 != top
  • After: y1=665.82, y0=656.82 -> y1-y0 = 9.0 = height, and page.height - y1 = 126.18 = top, page.height - y0 = 135.18 = bottom

Notes

  • Adds a regression test test_crop_preserves_y0_y1 in tests/test_basics.py that crops through a char vertically and asserts y1-y0 == height and the top/bottom to y0/y1 invariant. It fails on the current code and passes with this change.
  • Updates CHANGELOG.md (Unreleased / Fixed) and adds a contributor entry to README.md.
  • make lint passes (flake8, mypy, black, isort).

clip_obj (behind Page.crop / within_bbox / outside_bbox) rewrote an
object's top-based coordinates (top, bottom, doctop, height) but left
the PDF-space y0/y1 untouched. For an object clipped along the vertical
axis this broke the top/bottom <-> y0/y1 invariant, so y1 - y0 no longer
matched height and consumers reading y0/y1 off a cropped object got
stale values.

Update y0/y1 alongside top/bottom, mirroring move_object/resize_object:
y1 tracks top, y0 tracks bottom. Adds a regression test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant