Document the y0/y1 vs top/bottom/doctop coordinate systems in README - #1387
Document the y0/y1 vs top/bottom/doctop coordinate systems in README#1387soodoku wants to merge 2 commits into
y0/y1 vs top/bottom/doctop coordinate systems in README#1387Conversation
The README defines y0/y1/top/bottom/doctop once per object type, in five separate property tables, but never explains why there are two vertical coordinate systems, which one bounding boxes use, or how to convert between them. That gap has repeatedly been reported as a bug: jsvine#198, jsvine#845, jsvine#1332, and most recently jsvine#1369. Add a "Coordinates" section above the per-object tables covering which properties are measured from the page bottom versus the page and document top, that bounding boxes are (x0, top, x1, bottom), why the top-left origin was chosen, the conversion between the systems and its MediaBox caveat, and which objects carry which properties. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
||
| Not every object carries both varieties. Objects parsed directly from the PDF — `char`, `line`, `rect`, `curve`, `image`, `annot`, and `hyperlink` — have all of the properties above. Objects that `pdfplumber` computes for you use only the top-down properties: the dicts returned by `.extract_words(...)` provide `top`, `bottom`, and `doctop` but no `y0`/`y1`, and those returned by `.search(...)` provide `top` and `bottom`. Relatedly, a `line`'s or `curve`'s `pts` are `(x, top)` tuples. | ||
|
|
||
| Because object bounding boxes are normalized, `top` is never greater than `bottom`, and `height` (equal to `bottom - top`) is never negative. |
There was a problem hiding this comment.
I think we can probably cut this line. Or does it clarify something that is a common source of confusion? (Perhaps I'm just overlooking that.)
|
|
||
| The top-left origin was chosen because ([#198](https://github.com/jsvine/pdfplumber/issues/198)) people generally read PDFs from the top-left toward the bottom-right; because measuring from the top is what makes `doctop` — the distance from the top of the *whole document* — able to distinguish objects at similar heights on different pages; and because most adjacent layout systems (SVG, Canvas, PIL/Pillow, which `pdfplumber` itself uses for visual debugging) also put the origin in the top-left. | ||
|
|
||
| For a page whose `MediaBox` begins at the origin — the typical case — the two systems convert simply: |
There was a problem hiding this comment.
Since the y-flavored variables are the "original" ones, and top/bottom the conversions, I might suggest framing the code block below in those terms, instead of vice versa.
|
|
||
| The `y0`/`y1` pair follows the PDF specification's own coordinate system, and is retained for compatibility with `pdfminer.six`. Everything else in `pdfplumber` places the origin in the __top-left__ corner. In particular, bounding boxes are expressed as `(x0, top, x1, bottom)` — *not* `(x0, y0, x1, y1)` — which is what `.crop(...)`, `.within_bbox(...)`, and the visual-debugging methods expect. | ||
|
|
||
| The top-left origin was chosen because ([#198](https://github.com/jsvine/pdfplumber/issues/198)) people generally read PDFs from the top-left toward the bottom-right; because measuring from the top is what makes `doctop` — the distance from the top of the *whole document* — able to distinguish objects at similar heights on different pages; and because most adjacent layout systems (SVG, Canvas, PIL/Pillow, which `pdfplumber` itself uses for visual debugging) also put the origin in the top-left. |
There was a problem hiding this comment.
The doctop part of the explanation, although true, feels like a bit of a distraction, and slightly difficult to follow. Maybe just keep this paragraph to the first and third parts?
| |`top`, `bottom`| Top of the page | Downward | | ||
| |`doctop`| Top of the *document* | Downward | | ||
|
|
||
| The `y0`/`y1` pair follows the PDF specification's own coordinate system, and is retained for compatibility with `pdfminer.six`. Everything else in `pdfplumber` places the origin in the __top-left__ corner. In particular, bounding boxes are expressed as `(x0, top, x1, bottom)` — *not* `(x0, y0, x1, y1)` — which is what `.crop(...)`, `.within_bbox(...)`, and the visual-debugging methods expect. |
There was a problem hiding this comment.
I'd cut the , and is retained for ... part of the first sentence. I'd also cut the part inside the em-dashes.
|
|
||
| #### Coordinates | ||
|
|
||
| Horizontal coordinates are unambiguous: `x0` and `x1` are both measured from the left edge of the page. Vertical coordinates come in two varieties, and mixing them is a common source of confusion: |
There was a problem hiding this comment.
I might reframe this slightly. It's less that the coordinates "come in two varieties", but rather that pdfplumber uses top/bottom but retains reference to the original y0/y1 coords.
|
Thanks, @soodoku! I do think it's a good idea to clarify the coordinate system. I've made some specific suggestions in the line-comments above. More broadly: The location in the README feels a bit off, since it breaks up the discussion of object types. This might go better at the end of that section, right before Visual Debugging. What do you think? |
|
Thanks for the helpful suggestions — I’ve addressed all three in |
|
Wow, quick work! I'm curious: Any chance this is an LLM responding / committing? |
Closes #389.
Adds a
#### Coordinatessection to the README, directly above the per-object property tables, explaining the two vertical coordinate systems and how they relate.The README currently defines
y0/y1/top/bottom/doctopfive separate times — once per object type — but never explains why there are two systems, which one bounding boxes use, or how to convert between them. That gap keeps generating issues: #198, #845, #1332, and as recently as this April, #1369, where a user reported as a bug that "y-coordinates are top-to-bottom for words but bottom-to-top for line points."The section covers:
y0/y1) versus the page top (top/bottom) and the document top (doctop);(x0, top, x1, bottom), not(x0, y0, x1, y1), and that this is what.crop(...)/.within_bbox(...)expect;top == page.height - y1, with the caveat that a non-zeroMediaBoxorigin shifts it bypage.mediabox[1];.extract_words(...)and.search(...)results carry only the top-down ones, and thatptsare(x, top)tuples.Every factual claim was checked against the library rather than against the existing prose:
top == page.height - y1andbottom == page.height - y0hold exactly on a standard page, and fail by exactlypage.mediabox[1]ontests/pdfs/issue-1181.pdf(MediaBoxorigin aty = -200), which is what the caveat records.char/line/rect/curve/image/annot/hyperlinkcarry all seven;.extract_words(...)returnstop/bottom/doctopand noy0/y1;.search(...)returnstop/bottom.Docs-only, so no tests are added.
make lintandmake testsboth pass locally (172 passed).