DEV Community

JamesAnderson121
JamesAnderson121

Posted on

Debugging Wrong-Order Merged PDF Bundles — A 5-Checkpoint Walkthrough

The safest fix for a merged PDF with pages in the wrong order is to make the ordered input list the contract, then verify that contract before and after the merge. Do not start by re-rendering every page: rendering can reveal visual defects, but it adds cost and usually cannot explain why a lease rider appeared before the lease. For a property-management bundle, preserve original PDF pages during the fast path, record a page-level manifest, and reserve rendering for a small fidelity sample or for files that fail structural checks.

Short answer: freeze the intended document order as data, never infer it from filenames or asynchronous completion order, append pages in one explicit loop, and compare the output's page sequence with a manifest derived from the same immutable plan. The five checkpoints are selection, normalization, scheduling, assembly, and verification.

How do you debug a merged PDF whose pages are in the wrong order?

Most wrong-order PDFs are ordering bugs upstream of the PDF writer. A property bundle may begin as a database query for a cover sheet, lease, inspection report, and addenda. The query has no guaranteed application-level order unless it declares one. A directory scan returns paths, but its iteration order is not a business rule. A lexical filename sort also puts unit-10.pdf before unit-2.pdf, while a timestamp sort silently turns upload timing into document policy.

Concurrency creates a subtler version. Suppose four source files are downloaded or prepared in parallel. If the program appends each result when its task completes, the network or conversion latency becomes the page order. The code can pass repeatedly on a laptop and still produce a different bundle under load.

That failure moves.

The correction is small but important: assign every source a stable ordinal when the bundle plan is created. Keep that ordinal through retrieval, validation, retries, and assembly. If business rules require the cover first and notices last, encode those rules in the plan rather than hoping path names happen to express them.

This is the key distinction: completion order is execution metadata; document order is domain data.

Reproduce the ordering contract in Python

Before touching a production pipeline, reduce the failure to a tiny, deterministic program. The following example models a property bundle without depending on a particular PDF package. fetch_source stands in for storage retrieval, while append_pdf and read_page_labels are deliberately generic adapters around whichever conforming PDF implementation the system already uses.

from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol, Sequence


@dataclass(frozen=True)
class BundleItem:
    ordinal: int
    kind: str
    source: Path


class PdfAssembler(Protocol):
    def append_pdf(self, source: Path) -> int:
        """Append every source page and return the number appended."""

    def write(self, destination: Path) -> None:
        """Write the assembled document."""


def fetch_source(item: BundleItem) -> tuple[int, Path]:
    # A real adapter can download, decrypt, or validate the source here.
    return item.ordinal, item.source


def ordered_sources(items: Sequence[BundleItem]) -> list[Path]:
    ordinals = [item.ordinal for item in items]
    if len(ordinals) != len(set(ordinals)):
        raise ValueError("bundle ordinals must be unique")
    if sorted(ordinals) != list(range(len(items))):
        raise ValueError("bundle ordinals must be contiguous from zero")

    retrieved: dict[int, Path] = {}
    with ThreadPoolExecutor(max_workers=4) as pool:
        futures = [pool.submit(fetch_source, item) for item in items]
        for future in as_completed(futures):
            ordinal, path = future.result()
            retrieved[ordinal] = path

    return [retrieved[index] for index in range(len(items))]


def merge_bundle(
    items: Sequence[BundleItem],
    assembler: PdfAssembler,
    destination: Path,
) -> list[tuple[int, Path, int]]:
    manifest = []
    output_page = 0
    for source in ordered_sources(items):
        page_count = assembler.append_pdf(source)
        manifest.append((output_page, source, page_count))
        output_page += page_count
    assembler.write(destination)
    return manifest


bundle = [
    BundleItem(0, "cover", Path("cover.pdf")),
    BundleItem(1, "lease", Path("lease.pdf")),
    BundleItem(2, "inspection", Path("inspection.pdf")),
    BundleItem(3, "rider", Path("pet-rider.pdf")),
]
Enter fullscreen mode Exit fullscreen mode

There are two useful assertions here. Ordinals must be unique, and they must form a contiguous zero-based sequence. Without the second check, a missing item can disappear quietly: a plan containing 0, 1, 3 is sorted, but it is incomplete. Fail before creating a plausible-looking bundle.

Notice where parallel work stops. Retrieval can finish in any order because its results are stored by ordinal. Assembly remains a single ordered loop. This keeps throughput work separate from the semantic operation, and it is much easier to reason about in an eval harness.

For a concrete regression fixture, use source PDFs whose pages contain large labels such as COVER-1, LEASE-1, LEASE-2, INSPECTION-1, and RIDER-1. The expected output sequence is then unambiguous. A five-page fixture is cheap enough for every commit; a representative large bundle belongs in a less frequent suite.

Trace all 5 checkpoints, not just the writer

Start with selection. Log or persist the record identifiers and ordinals returned by the bundle-planning step. The evidence should answer one question without opening the PDFs: which source was supposed to occupy each position? Avoid relying on a map or set as the carrier of this contract. Even where a language preserves insertion order, a map communicates lookup, not an intentional sequence. Consider a renewal packet whose query returns a cover, a 12-page lease, two inspection reports, and three riders. If one rider is filtered as a duplicate after ordinals are assigned, silently renumbering the survivors may produce a valid PDF that no longer matches the approved plan. Keeping the gap and rejecting the plan creates more immediate work, but it exposes the disagreement at the point where the record identity and business rule are still available.

Normalization comes next. Paths may be normalized, duplicate documents removed, and optional attachments filtered. Each transformation needs a test that includes the ordinal. Deduplication by filename is especially risky in property workflows because two units can legitimately contain inspection.pdf. Deduplicate only by a stable content or record identity that matches the business rule, and decide explicitly whether removing an item closes the ordinal gap or leaves the plan invalid.

At scheduling, capture both ordinal and completed_at, but never sort by the latter. A retry must replace the result at the same ordinal. It must not append another entry. This is also where a compact structured event earns its keep: bundle ID, source record ID, ordinal, attempt, result, and page count are enough to diagnose ordering without logging tenant document contents.

Assembly should expose source boundaries. Record the output start page and appended page count for every source, as the example manifest does. If the second source starts at output page 1 and contributes 12 pages, the next source must start at 13 when using zero-based indexes. That arithmetic catches dropped or duplicated pages without rasterizing anything.

Finally, verification must inspect the artifact that will actually be delivered, not an in-memory object from before serialization. Reopen the written PDF, confirm its total page count, and compare page-level markers when the workflow has reliable markers. PDF page labels can help navigation, but visible text and page-label dictionaries are different things; neither should be assumed to exist in arbitrary source files. For controlled fixtures, put a unique visible token on each page and extract it after writing. For uncontrolled customer documents, retain the structural manifest and perform sampled visual comparison where fidelity risk warrants the render cost.

One warning deserves its own line.

Do not “fix” order by sorting extracted page text. Text extraction can be absent, ambiguous, or arranged differently from visual reading order, and document semantics are not recoverable from alphabetical content.

Preserve pages first, render only with a reason

PDF is a structured page-description format, and merging page objects is different from converting pages to images. Rasterizing every source can flatten interactive or structural features, enlarge files depending on resolution and compression choices, and consume CPU and memory proportional to rendered pixels. It may still be appropriate when the output contract is explicitly image-only, when a damaged source must be normalized through a controlled conversion path, or when visual comparison is the required acceptance test.

For ordinary lease bundles, start with structural preservation. The fast path should copy or append pages while retaining the source page content and resources according to the chosen implementation's documented behavior. Then budget rendering where it produces evidence: first and last pages at source boundaries, pages with rotations or unusual boxes, and a stable fixture set containing annotations, forms, fonts, and transparency.

The decision table is short because the rule should be easy to operate:

Signal Verification method Cost profile What it proves
Source ordinal and output page range Manifest comparison Low Planned sources occupy the expected ranges
Output page count Reopen and count Low No page was silently lost or duplicated in aggregate
Controlled visible page token Text extraction in fixtures Low to moderate Fixture pages appear in exact sequence
Rendered boundary-page comparison Pixel or perceptual comparison Higher Selected pages retain expected visual appearance
Full-document rendering Page-by-page visual evaluation Highest Visual fidelity across the entire tested artifact

The manifest approach has a real limitation: it proves sequence and page ranges, not visual equivalence. It is the wrong sole verifier when pages are regenerated from office documents, when fonts can be substituted, or when the delivery contract requires flattened appearance. In those cases, choose a controlled renderer and compare the rendered output against approved fixtures, accepting the extra compute and storage cost. Conversely, full rendering is a poor default for large, structurally sound archival bundles when the immediate question is only whether inputs were appended in the wrong order. This is an explicit fidelity-versus-render-cost trade-off, not a universal preference for one pipeline.

No single check proves both properties.

This is where eval-driven development pays off. Keep ordering and fidelity as separate scores. An output can have perfect pixels in the wrong sequence, or correct order with a rotated inspection page. Combining both into one pass/fail result hides the diagnosis and encourages expensive reruns.

It also keeps prompt and model usage out of the critical path. A language model can classify an ambiguous attachment before the plan is frozen, but it should not be asked to reconstruct deterministic page order after assembly. If classification is necessary, store its output, confidence policy, and resulting ordinal as inputs to a reproducible plan; do not spend tokens repeatedly on a question the database can answer exactly.

Make the fix survive production

The production checklist works best as prose attached to the bundle state machine. Create an immutable plan with a version and contiguous ordinals. Validate every source before assembly, then retrieve concurrently only if results are re-associated by ordinal. Append in plan order, record each output range, write to a temporary destination, reopen the completed artifact, and publish it only after structural verification succeeds. A failed attempt should leave the previous deliverable untouched and produce enough metadata for a targeted retry.

Add three regression cases around the happy path. First, delay the earliest input so it completes last; output order must remain unchanged. Second, omit ordinal 2 from a four-item plan; assembly must stop rather than compress the gap silently. Third, retry one retrieval; the manifest must contain one source at that ordinal, not two.

Then add fidelity fixtures based on actual document mechanics, without using private tenant files: mixed portrait and landscape pages, differing page boxes, rotated pages, annotations, form fields, and embedded fonts. PDF processors do not all preserve every feature identically, so the adapter's behavior must be tested against the output contract rather than inferred from a successful write() call. ISO 32000-2 defines the PDF format; the selected library's documentation defines its API boundaries; the regression corpus defines what this application accepts.

Operationally, watch ratios and counts instead of document content. Useful signals include planned sources versus assembled sources, planned pages versus written pages, duplicate ordinals, verification failures by stage, and render time for the sampled fidelity path. Alerting on “bundle failed” alone sends the next engineer back to manual PDF inspection. Stage-specific evidence points directly to selection, normalization, scheduling, assembly, or verification.

The final rule is plain: make order explicit before bytes move. Once an immutable plan, ordered assembly loop, and post-write manifest check agree, a wrong-order merged PDF becomes a localized contract failure instead of a mysterious rendering problem. Rendering remains valuable, but for fidelity evidence—not for guessing what the input list should have been.

Sources

Top comments (0)