Pdf Processor
Three Ways Multi-Column PDF Parsing Breaks (and the Fixes for Each)
TLDR
Histogram-based column split detection fails in three predictable ways: page-frame outer border rectangles claiming all text items as giantBOX 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 Pattern | Failure Mechanism | Structural Remediation |
|---|---|---|
| 1. Page-Frame Claim | Outer border classified as giant BOX | Guard: (bx < 4%W && bw > 65%W) \ |
| 2. Header Gutter Mask | Wide headers block coverage zero-gaps | Rescue: Scan fullWidthIndices after split found |
| 3. Pre-Column Box Claim | Side annotation boxes claim right col text | Fallback: Rerun _detectPageColumns on all textMeta |
Technical defect remediation patterns
1. Page-Frame outer border guard
Filter out page outer border rectangles beforeBOX 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 →