Repository navigation
Table.extract(): group cell text into lines by the font-metric line top (#5198) - #5243
Merged
Merged
Conversation
…f#5198) find_tables() sets small glyph heights so that every character on a line shares one top, but that only takes effect while quad corrections are on. With corrections off, which is the state after importing pymupdf4llm and always the state on the layout path, each character keeps its exact glyph box. The "." of "0.74" then starts about 7 pt below the digits, the pdfplumber-derived word extractor puts it on a line of its own, and Table.extract() returns "0 74\n.". Space characters with empty glyph boxes land in another line cluster the same way, so words lose their spaces at small font sizes. On rotated pages the same boxes scramble the word order and split "12.5" into "12 . 5". make_chars() now stores a "line_top" for every character: the top of the text line computed from the character's baseline and the span's normalised ascender, which is where the quad correction with small glyph heights puts the top of the character box. For rotated lines, whose text lines stack along x, it stores the same distance along x as "line_x". WordExtractor groups characters and words into lines by these values instead of the glyph box edges, a word carrying the smallest value of its characters as it carries the smallest top, and extract_text() clusters and orders words by them too. With corrections on the values equal the existing box edges, so output stays the same there, except for glyphs that lie entirely below the baseline (underscores): the correction boxes those as upside-down text, below the line, and they used to come out on a line of their own ("Prem Written\n_"); they now stay in their word ("Prem_Written"). Cell membership and the glyph boxes themselves are untouched; the text strategy and the layout=True text map consume the same words and see merged numbers with corrections off. The layout-branch expectations of seven tests in tests/test_tables.py recorded the split output. They are regenerated from the output of this change, with and without pymupdf4llm installed, and now agree with the non-layout branches of the same tests. test_4017 had two layout sub-branches, for a process with and without pymupdf4llm; since pymupdf#5205 find_tables() turns corrections off before get_layout() in both cases and they produce the same four tables, so they are now one expectation. tests/test_5198.py covers the word extractor with hand-made characters, the decimal point with corrections on and off for all four page rotations and on the layout path, a raised footnote marker, and spaces between small words.
veget-able
force-pushed
the
table/line-grouping-5198
branch
from
November 2, 2026 15:48
10933f3 to
cab22dc
Compare
JorjMcKie
approved these changes
Nov 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #5198. This complements #5213, which re-enables quad corrections for the line-based finder. It addresses text grouping when corrections are off, including the layout path.
I use line coordinates derived from font metrics to group and order cell text, while keeping the existing glyph boxes for cell membership and detection. This keeps decimal points, spaces and other small glyphs with the text they belong to, without a second text extraction or a change to the quad-correction setting.
Why this happens
find_tables()enablesTOOLS.set_small_glyph_heights(True)so characters on the same line share a common top coordinate. This depends on quad corrections being enabled. When they are disabled,JM_char_quad()returns the raw glyph quad instead, which is the tight glyph box whenTEXT_ACCURATE_BBOXESis set, astextpage_flags()does when the layout package is available.The layout path deliberately disables corrections before
get_layout(). Importing pymupdf4llm also disables them. In these cases, the.in0.74can start about 7 pt below the digits, outside the 3 pt y tolerance used byextract_text(), so it becomes a separate line.Empty glyph boxes for spaces cause similar grouping problems. Small text can lose spaces, as in
ClassA/BOvercollateralizationintest_4017. On rotated pages, the same geometry can change word order or split numbers into text such as12 . 5 mg.Thomas traced the cause in #5198 and suggested grouping by baseline. This follows that approach, with font size taken into account.
Changes
make_chars()records aline_topfor each character, derived from its baseline and the span's font metrics using the same normalisation and glyphless-font fallback as quad correction.For horizontal text, the calculation is
origin.y - size * asc / (asc - dsc). With corrections off, the reported ascender and descender defaults of 0.8 and -0.2 giveorigin.y - 0.8 * size.For vertical lines on pages rotated by 90 or 270 degrees, it records the corresponding x edges as
line_x0andline_x1. The distance between these edges equals the font size. Oblique lines, which the extractor already treats as vertical, retain their glyph box edges.WordExtractor.iter_sort_chars(),char_begins_new_word()andextract_text()use these coordinates in place of glyph box edges for line grouping and ordering. Words carry the minimumline_topandline_x0, and the maximumline_x1, of their characters.With corrections on, the derived coordinates match the corrected box edges for ordinary text. I also checked this on a rotated page with mixed font sizes. There is one known difference for glyphs entirely below the baseline, such as underscores. The flip test in
JM_char_quad()treats these as upside-down text and places their boxes below the rest of the line. Usingline_topkeepsPrem_Writtentogether instead of producingPrem Written\n_, matching the span text.Using the baseline alone would still split raised footnote markers. In the test case, a marker raised by 3.5 pt falls outside the 3 pt tolerance, producing
a\nTotal. Its font-metric line top is within 0.3 pt of the main text, so it stays on the same line asTotala, matching the 1.28 result.char_in_bbox,has_textand header detection continue to use the existing glyph boxes. The text strategy (words_to_edges_h/v) and thelayout=Truetext map consume the regrouped words, so their output can change when previously split words are joined.Tests
tests/test_tables.pythat recorded split punctuation or missing spaces. The updated results agree with the non-layout expectations and were checked both with and without pymupdf4llm installed.test_4017. Since Lazy Import of heavy weight modules #5205,find_tables()disables corrections beforeget_layout()in both environments, and both produce the same four tables.tests/test_5198.pyfor low glyphs, word-level grouping, decimal points with corrections on and off, all four page rotations on the line-based path, the layout path, raised footnote markers and spaces between 8 pt words. Nine of the sixteen cases fail on main.Validation
I compared current main (
16c664fd, whose table code is what I measured, using the 2.0 binary) with the same environment plus this branch'stable.py.For direct
find_tables()calls, I sampled every third page of the ParseBench table group, giving 168 pages. “Lone punctuation” counts cells containing a line made only of punctuation such as.,,or-.The correction setting below is the initial process setting. The layout path disables corrections internally in both cases.
The 179 remaining flagged cells contain text such as
S.p.A.orS.c.a.r.l.. The six changed cells with corrections on are the underscore cases described above.On the sample's 270-degree page, the no-layout output changes from
THERIFLEMAN 00 Feb19/20andM -F/3-4PtoTHE RIFLEMAN 0.0 Feb19/20andM-F/3-4P.I also compared corrections on and off using the same code in no-layout mode. On main, 115 pages and 4,501 cells differ between the settings. With this change, that falls to 2 pages and 36 cells. The remaining differences concern glyphs near cell borders, where membership still depends on the glyph boxes. The five table boxes and one grid that differ between the settings on main still differ with this change alone, since detection reads the boxes. With #5213 applied as well, there are no differences.
strategy="text"can produce different grids because it derives edges from words. In the layout-mode run with corrections off, 56 of 152 grids change, and lone-punctuation cells fall from 10,273 to 577. With corrections on, 3 grids change, on pages where underscores inside identifiers were split into separate words before.As a separate non-regression check, I ran all 503 ParseBench table pages through pymupdf4llm Markdown extraction with HTML tables. The score remains 0.758617, with no Markdown changes on any page. That path builds cell text through
extract_cellsfrom the TextPage and did not show this symptom.Scope
The extra empty column also reported in #5198 is unrelated to the correction setting and is not addressed here.
#5204 addresses the punctuation symptom by obtaining character boxes from a second extraction. This approach instead changes the coordinates used for grouping and ordering, while preserving the boxes used for cell membership and detection.