Engineering Journal
Pdf Processor
Pdf Processor

Reprocessing is only valid when the pipeline owns the input

2026-08-17

TLDR: Re-processing a page is only safe when the pipeline that runs the re-process is the same pipeline that owns the document. When a scanned document is extracted by an OCR backend and then "re-extracted" by a local vector-geometry pipeline that never read that document, the re-run silently destroys content that the first run got right. The fix is a guard on the reprocess entry point, not a repair of the degraded output.

The problem class

Every extraction tool exposes a "re-extract" affordance: the user changed a parameter (skip this layer, adjust a threshold, move a column split) and wants the page rebuilt. The naive contract is "re-run the extractor with the new parameters."

That contract has a hidden precondition. A re-run only improves a page when the extractor that runs the re-run is the same extractor that produced the page, or at least a pipeline that can read the same substrate. If the first extraction came from a different engine with a different substrate model, the re-run is not a refinement. It is a fresh, worse interpretation of the same document, and it replaces a good result with a bad one.

The naive approach

Model extraction as a pure function: pageHtml = extract(bytes, page, params). Re-extraction is then just calling the function again with new parameters and swapping the output.

function reextract(page) {
  worker.post({ type: "reprocess", page, pipeline: params });
}

The result handler applies whatever html comes back. No questions asked about where the current html came from.

Why it fails

Extraction pipelines are not interchangeable implementations of one function. They consume different substrates:

A scanned page has almost no vector substrate: no text items, a handful of segments, mostly image area. Sent through a vector pipeline, it classifies into a few low-confidence zones and loses the image regions the OCR service found. The re-extract button replaced 3 extracted pictures with 1, and rewrote the page markup in a degraded layout. The user's report: "the extracted HTML does not show the image after re-extraction."

The re-run was not idempotent. It was destructive, because the pipeline that re-ran did not own the input.

The better model

The reprocess entry point must know, per page, which engine produced that page. Reprocessing is only offered when the local pipeline can actually serve it. Three cases fall out:

const pgInfo = analysis.pages?.[currentPage];

// Page has no vector substrate for this pipeline. if (pgInfo?.scanned && !pgInfo?.ocrLayer) { keepCurrentPage(); explainWhy(); return; }

// Page came from the local OCR bridge, which cached its synthetic // inputs in the worker. The local pipeline CAN re-run it. if (pgInfo?.scanned && pgInfo?.ocrLayer) { worker.post({ type: "reprocess", page, pipeline: params }); return; }

// Ordinary vector page. Re-run freely. worker.post({ type: "reprocess", page, pipeline: params });

The discriminator is not "can the pipeline produce something" (it always can, and usually something bad). It is "does the pipeline own the inputs for this page." Ownership is tracked as a per-page flag set during pre-flight classification, before any engine runs.

Implementation evidence

The pipeline parameters (skip layers, thresholds, splits) were meaningless for the OCR-extracted page anyway: the backend service that produced it ignores them. So the honest re-extract on a scanned page is to keep the current page and tell the user why. The toast reads: "Scanned page, images are OCR-extracted and cannot be re-extracted by the geometry pipeline. The page is kept as-is."

The guard had to sit at the entry point, not the result handler, for one reason: the result is already garbage by the time it arrives. Intercepting a bad result to keep the old html is possible, but then the region metadata, the canvas overlay, and the undo stack all diverge. Guarding before the request means nothing downstream needs to be rolled back.

Verified in a real browser on both sides of the guard:

Scanned doc, page 1:  before 3 inline images, len 60217
                    after  3 inline images, len 60217   (kept, toast shown)

Vector doc, page 11: before 5 inline images, len 850943 after 5 inline images, len 852003 (reprocessed, kept)

The vector page reprocessed and produced a fresh result with all five images. The scanned page was untouched. Both behaviors are correct.

Tradeoffs

The guard gives up "re-extraction" as a universal affordance. A scanned page cannot be re-pipelined locally, and that is the correct outcome: the parameters do not apply, and the alternative was silent content loss. The local OCR path (a scanned page whose synthetic inputs are cached in the worker) still re-runs and honors the parameters, so the affordance is not gone, it is routed.

The cost is a new per-page flag contract between pre-flight and the panel. It is one boolean, and it is set where the substrate is already measured, so it adds no second measurement of the document.

Read this post in the full Engineering Journal →