Engineering Journal
Pdf Processor
Pdf Processor

Direct Manipulation on a Spatial Canvas: How to Build a Correction Layer Over an Automatic Pipeline

2026-06-01

TLDR

Tuning global threshold sliders to fix region classification errors on specific document pages introduces side effects on other pages. A more effective solution is providing a direct spatial canvas editing layer where users can select, resize, reclassify, or suppress specific bounding boxes on screen. Normalizing canvas interaction coordinates into worker space guarantees that user corrections persist accurately across display resizes and re-extractions.

Correction StrategyModification ScopeSide Effect RiskPrecision
Global Threshold SlidersEntire document / All pagesHigh (Fixing page 3 breaks page 7)Low (Imprecise parameter tuning)
Canvas Region EditorSingle target bounding boxZero (Local override isolation)High (Pixel-exact direct manipulation)

Problem statement: the fallacy of global threshold tuning

When an automated PDF Processor misclassifies a table on page 3, adjusting global parameters (e.g., column gap distance, Y-band tolerances) alters extraction logic for all pages in the document.

A parameter value that fixes page 3 often corrupts previously clean layouts on page 7.

Local structural errors require direct spatial correction tools rather than global parameter adjustments.


Technical architecture & implementation

1. Dual-Mode canvas interaction model

Maintain two distinct interaction modes via toolbar state:

2. Coordinate space normalization

Convert UI canvas display pixels to normalized pipeline worker space at interaction time to ensure corrections survive canvas resizes:

// Convert display canvas pixels to worker viewport space
export function canvasPxToWorkerPx(canvasPx, canvasWidthPx, workerWidthPx) {
  return (canvasPx / canvasWidthPx) * workerWidthPx;
}

// Convert worker viewport space back to display canvas pixels for rendering export function workerPxToCanvasPx(workerPx, workerWidthPx, canvasWidthPx) { return (workerPx / workerWidthPx) * canvasWidthPx; }

3. Interactive eight-handle bounding box resizing

Render corner and midpoint handles for selected regions, mapping cursor styles to handle directions:

const HANDLE_CURSORS = {
  nw: "nw-resize",
  n: "n-resize",
  ne: "ne-resize",
  w: "w-resize",
  e: "e-resize",
  sw: "sw-resize",
  s: "s-resize",
  se: "se-resize",
};

export function resizeRegionBoundingBox( region, activeHandle, deltaX, deltaY, minSizePx = 4, ) { const updated = { ...region.bbox };

if (activeHandle.includes("w")) { const newWidth = updated.w - deltaX; if (newWidth >= minSizePx) { updated.x += deltaX; updated.w = newWidth; } } if (activeHandle.includes("e")) { updated.w = Math.max(minSizePx, updated.w + deltaX); } if (activeHandle.includes("n")) { const newHeight = updated.h - deltaY; if (newHeight >= minSizePx) { updated.y += deltaY; updated.h = newHeight; } } if (activeHandle.includes("s")) { updated.h = Math.max(minSizePx, updated.h + deltaY); }

return updated; }

4. Manual column split bypass

When users place manual vertical split lines, skip automatic column scanning entirely:

export function resolvePageColumnSplits(
  manualSplits,
  textMeta,
  viewport,
  scale,
) {
  // Manual Split Bypass: If user drew split lines, use them exclusively
  if (manualSplits && manualSplits.length > 0) {
    const validSplits = manualSplits
      .filter((s) => s.x > viewport.width  0.05 && s.x < viewport.width  0.95)
      .sort((a, b) => a.x - b.x);

return buildColumnZonesFromManualSplits(validSplits, viewport.width); }

// Fallback to automatic bipartite column detector return detectPageColumns(textMeta, viewport, scale); }

Rule of thumb: Store spatial UI canvas corrections in worker coordinate space, and allow manual split overlays to bypass automated layout detectors.
Read this post in the full Engineering Journal →