Skip to content

fix(tables): reject layout bands in the cell-rect fallback - #460

Open
flowjzh wants to merge 1 commit into
firecrawl:mainfrom
flowjzh:fix/layout-band-stripe-fallback
Open

fix(tables): reject layout bands in the cell-rect fallback#460
flowjzh wants to merge 1 commit into
firecrawl:mainfrom
flowjzh:fix/layout-band-stripe-fallback

Conversation

@flowjzh

@flowjzh flowjzh commented Aug 24, 2026

Copy link
Copy Markdown

Summary

Resume- and form-template PDFs commonly carry decorative fill rects: a
page background, a full-content-band rect drawn several times (the
template's page frame, no interior y-edges), and a few header chips.
None of them are table borders, but the cell-rect fallback
(detect_row_stripe_table_from_cell_rects) serializes the whole page
as a markdown table:

  • rows come from the rect y-edges — the content band has no interior
    y-edges, so the entire page body lands in one table row;
  • columns come from text x-clustering, so the prose is bucketed by
    x position into pseudo-cells;
  • text within a cell keeps stream order, not visual order.

The reading order of the document is destroyed.

Why the existing guards miss it

The full-grid pass correctly rejects the cluster (sparse content — it
knows this is not a data table), but the fallback re-accepts it on cell
density. The fallback's paragraph-wall guard
(max_cell_len > 500 && non_empty_rows < 4) is bypassed whenever the
header chips above the band contribute 4 non-empty rows — exactly the
layout these templates have.

The fix

Add has_dominant_prose_row, the row-granular sibling of the existing
has_dominant_prose_cell helper (which the other rect-detection paths
already call): after cell assignment, reject the table when one row
holds more than 500 chars and at least half the region's text
.
Genuine stripe tables spread their items across rows; a rect band
boxing flowing prose concentrates it in one row. The signal has to be
row-granular: the columns carved from the band split the prose into
sub-threshold cells, so every per-cell check stays blind.

Repro

tests/fixtures/template_layout_band.pdf (new, synthetic, PII-free):
2 page backgrounds + 3 header-chip rows + one content-band rect drawn
4 times + ordinary resume-shaped text inside the band.

Before:

|Test User||||
|

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Rejects layout-band rectangles in the cell-rect stripe fallback to preserve reading order. Previously, the fallback accepted a content-band rect and collapsed the page body into one giant table row; now it rejects when one row dominates the text.

- Adds `has_dominant_prose_row`: reject if any row has >500 characters and at least half of the region’s text.
- Applies only in `detect_row_stripe_table_from_cell_rects`; the full-grid path is unchanged.
- Updates snapshots: `td9264` now extracts flowing text at the document head. Adds `template_layout_band.pdf` fixture, a snapshot, and a unit test that ensures the fallback rejects the band case.
- No migration required; low risk to genuine stripe tables.

<sup>Written for commit 064e0d2b8914ec5ae634abd5ef1a9b99c089902b. Summary will update on new commits.</sup>

<a href="https://cubic.dev/pr/firecrawl/pdf-inspector/pull/460?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
A template content band (one rect drawn repeatedly, no interior
y-edges) plus header chips lets the stripe fallback serialize a
whole text page as a table: the band's flowing prose lands in one
giant row and reading order is destroyed. The existing
paragraph-wall guard only fires under 4 non-empty rows, and the
header chips supply those rows.

Add has_dominant_prose_row, the row-granular sibling of
has_dominant_prose_cell: reject when one row holds more than 500
chars and at least half the region's text. Per-cell signals stay
blind here because the columns carve the prose into sub-threshold
cells; only the row sum sees it.

Regenerate the td9264 snapshot: the band-row pseudo-table at the
document head now extracts as flowing text. A second pseudo-table
further down (prose wrapped into rows by the body-font heuristic
pass) is a separate pre-existing issue and is unchanged.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Shadow auto-approve: would auto-approve. Focused parser bug fix: adds a dominant-prose-row guard to the cell-rect fallback so layout-band rects aren't serialized as tables, with unit and snapshot tests pinning the corrected extraction and no public/security/rollout tradeoffs.

Re-trigger cubic

@flowjzh
flowjzh force-pushed the fix/layout-band-stripe-fallback branch from ed5ed6c to 064e0d2 Compare August 24, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant