Skip to content

2.0: Table.extract() moves the decimal point out of numbers ("0.74" comes back as "0 74", newline, ".") — regression from 1.28.2 #5198

Open

Description

@thomas-villani

Description of the bug

Since 2.0, Table.extract() returns cell text with decimal numbers broken apart: the
digits are split by a space and the decimal point is moved onto a line of its own after
them. 0.74 comes back as '0 74\n.' and 12.5 mg as '12 5 mg\n.'. This happens in
the native table finder too (with pymupdf._get_layout = None). With the layout hook
that 2.0 installs on import, the same table also comes back with an extra empty column.
page.get_text("words") on the same page is correct, and 1.28.2 extracts the same table
correctly.

How to reproduce the bug

To reproduce (no input file needed)

import pymupdf

doc = pymupdf.open()
page = doc.new_page(width=595, height=842)
x0, y0, x1, y1, xm, ym = 72, 72, 472, 152, 272, 112
for a, b in [((x0, y0), (x1, y0)), ((x0, ym), (x1, ym)), ((x0, y1), (x1, y1)),
             ((x0, y0), (x0, y1)), ((xm, y0), (xm, y1)), ((x1, y0), (x1, y1))]:
    page.draw_line(a, b)
for text, point in [("P for interaction", (80, 100)), ("Cooking fuel type", (280, 100)),
                     ("0.74", (80, 140)), ("12.5 mg", (280, 140))]:
    page.insert_text(point, text, fontsize=11)

print(pymupdf.__version__, [t.extract() for t in page.find_tables().tables])
print([w[4] for w in page.get_text("words")])

Output (the second line, get_text("words"), is
['P', 'for', 'interaction', 'Cooking', 'fuel', 'type', '0.74', '12.5', 'mg'] in every case)

Version extract()
1.28.2 [[['P for interaction', 'Cooking fuel type'], ['0.74', '12.5 mg']]]
2.0 [[['P for interaction', '', 'Cooking fuel type'], ['0 74\n.', '', '12 5 mg\n.']]]
2.0 with pymupdf._get_layout = None [[['P for interaction', 'Cooking fuel type'], ['0 74\n.', '12 5 mg\n.']]]

In a real document the same regression also drops the spaces between words. On the
landscape (/Rotate 90) table pages 11-14 of the PubMed Central open-access article
PMC12000001, find_tables() returns the same grid (same bbox) on both versions, with the
native finder (pymupdf._get_layout = None), but extract() gives:

Version header row data row
1.28.0 [None, None, 'β (95%CI)', 'FDR', 'P for interaction', ...] ['Cooking fuel type', '', '', '', '0.74', ...]
2.0 [None, None, 'β(95%CI)', 'FDR', 'Pforinteraction', ...] ['Cookingfueltype', '', '', '', '074\n.', ...]

Environment: PyMuPDF 2.0 (MuPDF 1.28.5), Python 3.12, Windows 11 and Ubuntu 24.04.
Not seen on 1.28.0 or 1.28.2.

PyMuPDF version

2.0

Operating system

Linux

Python version

3.12

Activity

  1. added a commit that references this issue on Oct 25, 2026
    80ad995
  2. thomas-villani commented on Oct 25, 2026

    @thomas-villani
    Author

    I traced this to the quad-corrections toggle rather than to anything in Table.extract(), and opened #5213 with a fix for the line-based finder.

    Cause. find_tables() sets TOOLS.set_small_glyph_heights(True), but small glyph heights only apply while quad corrections are on (JM_char_quad() returns ch->quad unchanged when g_skip_quad_corrections is set). pymupdf4llm's helpers call pymupdf.TOOLS.unset_quad_corrections(True) at import time, and 2.0 imports pymupdf4llm at the end of import pymupdf, so in 2.0 every process has corrections off. Each character then keeps its exact glyph box, the "." of "0.74" sits about 7pt below the digits, and extract_text() puts it on a line of its own. The same exact boxes drop the spaces on the rotated pages.

    One line confirms it on 2.0:

    import pymupdf
    print(pymupdf.TOOLS.unset_quad_corrections())   # True right after import (0 on 1.28)
    pymupdf.TOOLS.unset_quad_corrections(False)
    # the reproducer above now gives [['P for interaction', 'Cooking fuel type'], ['0.74', '12.5 mg']]

    After that reset, the rotated pages of PMC12000001 extract as '0.47' and 'P for interaction' again.

    After #5205. Deferring the pymupdf4llm import fixes a plain import pymupdf, but not a process that imports pymupdf4llm. Also, find_tables() now turns corrections off before get_layout(), so the line-based finder runs without them whenever layout provides no boxes. The PR turns corrections on before make_chars() in that case. On 66 PubMed Central articles, the result is identical to 1.28.0 for every table and cell.

    The layout path is a separate question, so it is not in the PR. It turns corrections off on purpose, and has split decimals since layout support was added (reproduced on 1.28.2 with pymupdf.layout imported). The layout expectations in tests/test_tables.py now record that output: '2755 9\n.', '269\n-' for -269, '13144%\n.' for 131.44%. Turning corrections on there fixes the text but merges rows in test_4017 and splits a header in test_paths_param, so row segmentation seems to rely on the exact glyph boxes. A fix would probably build cell text from separately corrected boxes, or group a cell's characters by baseline rather than by top. I'm happy to work on that if you agree on the direction.

    The extra empty column with the layout hook, from the original report, is unrelated to this, and the PR does not change it.

  3. added 2 commits that reference this issue on Oct 25, 2026
    d6480df
    87c293b
  4. veget-able commented on Nov 2, 2026

    @veget-able
    Collaborator

    Thanks for the analysis, it matches what I found. I approved #5213 for the line-based finder. For the layout path I'd like to open a separate PR myself and will leave a note here once it is up.

  5. veget-able commented on Nov 2, 2026

    @veget-able
    Collaborator

    The layout-path fix is up as #5243. It groups cell text by the font-metric line top instead of the glyph top, so it no longer depends on the quad-corrections toggle, and it covers rotated pages too.

  6. JorjMcKie commented on Nov 11, 2026

    @JorjMcKie
    Collaborator

    Thanks for appreciating that we have developed a fix, @thomas-villani !
    It is however our policy to keep issues open beyond fix development, until the corresponding version has been published on PyPI.
    So, I am going to reopen it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    fix developedrelease schedule to be determined

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions