Skip to content

Markdown mode: a single sparse row transposes the table into column-wise prose and orphans values #400

Description

@knealfortegra

Summary

With output_format="markdown", a table containing a single sparse row (a row where only some of the columns are populated) is not emitted as a table at all. Instead the table is transposed into column-wise prose blocks (Order Date 03/14/2024 05/02/2024 06/08/2024, Item Code IC-1048 IC-1049 IC-1051, …), a fragment of it re-emerges as a 4-column table with no header, and the remaining values are left orphaned on a trailing line with no row association at all.

The same PDF renders correctly in the default text mode — every column spatially aligned, every row present, including the sparse row. So the geometry is available; it's the Markdown structuring step that loses it.

This is output_format="markdown" like #395, but a different trigger and a different failure, so I've filed it separately rather than as a comment there. #395 is free text interleaved at the head of the table, where the header becomes prose and the separator shifts down — but every data row survives as a table row. Here the trigger is a row with missing cells, and the result is that values are silently detached from their rows. A fix targeting #395's header/separator handling would not obviously address this.

Reproduction

Fully self-contained (pip install liteparse reportlab):

from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas

W, H = letter
COLS = [48, 108, 190, 300, 360, 424, 486, 545]
HEADERS = ["Order Date", "Item Code", "Description", "Status", "Unit Cost", "Qty", "Tax", "Ref"]

c = canvas.Canvas("sparse_row.pdf", pagesize=letter)
c.setFont("Helvetica-Bold", 12)
c.drawString(48, H - 56, "Order Detail Report by Account")
c.setFont("Helvetica", 8)

y = H - 96
for x, h in zip(COLS, HEADERS):
    c.drawString(x, y, h)
y -= 22

def row(cells, cols=COLS):
    global y
    for x, cell in zip(cols, cells):
        c.drawString(x, y, cell)
    y -= 16

row(["03/14/2024", "IC-1048", "WIDGET ASSEMBLY", "Shipped", "$482,110.40", "$0.00", "$9,215.75", "N/A"])
row(["05/02/2024", "IC-1049", "BRACKET SET", "Shipped", "$0.00", "$0.00", "$0.00", "N/A"])

# A SPARSE row: only 3 of the 8 columns are populated.
row(["06/08/2024", "IC-1051", "Open"], [COLS[0], COLS[1], COLS[3]])
y -= 6

# A free-text note, indented under the Description column.
c.drawString(190, y, "Unit was delayed in transit and re-routed through the regional hub.")
y -= 22

row(["06/08/2024", "IC-1051", "GEARBOX HOUSING", "Open", "$918,340.55", "$0.00", "$12,004.10", "N/A"])
row(["07/19/2024", "IC-1050", "CONTROL MODULE", "Open", "$31,905.22", "$4,100.00", "$0.00", "N/A"])

c.showPage()
c.save()

from liteparse import LiteParse

data = open("sparse_row.pdf", "rb").read()
print("--- output_format='text' ---")
print(LiteParse(ocr_enabled=False, quiet=True).parse(data).pages[0].text)
print("--- output_format='markdown' ---")
print(LiteParse(ocr_enabled=False, quiet=True, output_format="markdown").parse(data).pages[0].markdown)

Actual output

output_format="text" — correct. All 8 columns aligned, all rows present, the sparse row and the note both in the right place:

Order Date   Item Code   Description       Status    Unit Cost     Qty                      Tax          Ref

03/14/2024   IC-1048     WIDGET ASSEMBLY   Shipped   $482,110.40   $0.00                    $9,215.75    N/A
05/02/2024   IC-1049     BRACKET SET       Shipped   $0.00         $0.00                    $0.00        N/A
06/08/2024   IC-1051                       Open

                         Unit was delayed in transit and re-routed through the regional hub.

06/08/2024   IC-1051     GEARBOX HOUSING   Open      $918,340.55   $0.00                    $12,004.10   N/A
07/19/2024   IC-1050     CONTROL MODULE    Open      $31,905.22    $4,100.00                $0.00        N/A

output_format="markdown" — the table is gone:

# Order Detail Report by Account

Order Date 03/14/2024 05/02/2024 06/08/2024

Item Code IC-1048 IC-1049 IC-1051

06/08/2024 IC-1051 07/19/2024 IC-1050

Description WIDGET ASSEMBLY BRACKET SET

Status Shipped Shipped Open

Unit Cost $482,110.40 $0.00

Qty $0.00 $0.00

Tax $9,215.75 $0.00

Ref N/A N/A

Unit was delayed in transit and re-routed through the regional hub.

| GEARBOX HOUSING | Open | $918,340.55 | $0.00 |
|---|---|---|---|
| CONTROL MODULE | Open | $31,905.22 | $4,100.00 |

$12,004.10 N/A $0.00 N/A

What's wrong with it

Three distinct problems, all from the one sparse row:

  1. The table is transposed into column-wise prose. Unit Cost $482,110.40 $0.00 groups the column's values together. A reader has no way to know which order each value belongs to.
  2. The surviving table fragment has lost its identifying columns. | GEARBOX HOUSING | Open | $918,340.55 | $0.00 | has no Order Date and no Item Code, and no header row — the separator sits above the first row, so a Markdown parser reads GEARBOX HOUSING as a column name.
  3. Values are orphaned entirely. The trailing $12,004.10 N/A $0.00 N/A belongs to the last two rows (Tax and Ref), but nothing in the output associates them with a row.

The failure is silent — the output is well-formed Markdown, and every value is still present somewhere on the page, so a consumer has no signal that the row/column association has been destroyed. That's what makes it worse than an outright parse error.

Expected

The sparse row should be emitted as a table row with empty cells:

| Order Date | Item Code | Description | Status | Unit Cost | Qty | Tax | Ref |
|---|---|---|---|---|---|---|---|
| 03/14/2024 | IC-1048 | WIDGET ASSEMBLY | Shipped | $482,110.40 | $0.00 | $9,215.75 | N/A |
| 05/02/2024 | IC-1049 | BRACKET SET | Shipped | $0.00 | $0.00 | $0.00 | N/A |
| 06/08/2024 | IC-1051 |  | Open |  |  |  |  |
| 06/08/2024 | IC-1051 | GEARBOX HOUSING | Open | $918,340.55 | $0.00 | $12,004.10 | N/A |
| 07/19/2024 | IC-1050 | CONTROL MODULE | Open | $31,905.22 | $4,100.00 | $0.00 | N/A |

Sparse rows are common in real reports — subtotal lines, continuation rows, and records where a field simply doesn't apply. The text output shows the column geometry is already recovered correctly, so this looks like it's in the table-structuring step rather than in layout analysis.

Environment

  • liteparse 2.11.0
  • Python 3.13.13
  • macOS 26.6, arm64

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