Engineering Journal
Pdf Processor
Pdf Processor

Three Ways Multi-Column PDF Parsing Breaks (and the Fixes for Each)

2026-05-15

TLDR

Histogram-based column split detection fails in three predictable ways: page-frame outer border rectangles claiming all text items as giant BOX regions, full-width section headers covering column gutters in X-coverage histograms, and pre-column BOX classifications claiming right-column text items before column detection runs. Resolving these defects requires page-frame dimensions guards, post-split full-width item rescue loops, and full-sample fallback re-detection passes.
Defect PatternFailure MechanismStructural Remediation
1. Page-Frame ClaimOuter border classified as giant BOXGuard: (bx < 4%W && bw > 65%W) \
2. Header Gutter MaskWide headers block coverage zero-gapsRescue: Scan fullWidthIndices after split found
3. Pre-Column Box ClaimSide annotation boxes claim right col textFallback: Rerun _detectPageColumns on all textMeta

Technical defect remediation patterns

1. Page-Frame outer border guard

Filter out page outer border rectangles before BOX region assignment:
// Filter out full-page border rectangles from BOX classification
export function isPageFrameRectangle(rectX, rectWidth, viewportWidth) {
  return (rectX < viewportWidth  0.04 && rectWidth > viewportWidth  0.65) ||
         rectWidth > viewportWidth * 0.88;
}

2. Post-Split full-width index rescue loop

Re-evaluate items initially classified as full-width headers once a column split coordinate is identified:
export function rescueFalseFullWidthItems(fullWidthIndices, textMetaList, splitX) {
  for (let i = fullWidthIndices.length - 1; i >= 0; i--) {
    const item = textMetaList[fullWidthIndices[i]];
    const rightEdge = item.vx + (item.vWidth || 0);

if (rightEdge < splitX) { fullWidthIndices.splice(i, 1); item.columnIndex = 0; // Assigned to left column } else if (item.vx > splitX) { fullWidthIndices.splice(i, 1); item.columnIndex = 1; // Assigned to right column } } }


3. Full-Sample fallback column re-detection

If unclaimed text items yield no column splits, rerun column detection over the complete text sample:
export function executeColumnDetectionFallback(unclaimedMeta, allTextMeta, viewport, scale) {
  let { splits } = _detectPageColumns(unclaimedMeta, viewport, scale);

// Fallback: If unclaimed items yield zero splits but full text set is significantly larger if (splits.length === 0 && allTextMeta.length > unclaimedMeta.length + 4) { const fallbackResult = _detectPageColumns(allTextMeta, viewport, scale); splits = fallbackResult.splits; }

return splits; }

Rule of thumb: Filter out page-frame rectangles, rescue single-column items from fullWidthIndices post-split, and fall back to full-sample text arrays if unclaimed items yield no column splits.
Read this post in the full Engineering Journal →