Skip to content

fix(orders): count PDF pages by parsing the document, not scanning bytes#103

Merged
choden-dev merged 1 commit into
mainfrom
fix/pdf-page-count-accuracy
Jul 20, 2026
Merged

fix(orders): count PDF pages by parsing the document, not scanning bytes#103
choden-dev merged 1 commit into
mainfrom
fix/pdf-page-count-accuracy

Conversation

@choden-dev

Copy link
Copy Markdown
Owner

Problem

Server-side PDF page counting was inaccurate. A recent 200-page upload was counted as 467, inflating the order price (pricing is derived from the server-side page count, which never trusts the client).

Root cause

countPdfPages scanned the raw PDF bytes for /Type /Page markers. This overcounts real-world PDFs because that marker also appears in:

  • annotations / form-field (Widget) objects
  • object streams
  • orphaned page objects left behind by incremental updates
  • literal text inside content streams

The identical naive function was also duplicated in both app/api/shop/orders/route.ts and app/api/admin/orders/route.ts.

Fix

  • New shared helper lib/pdf.ts that parses the document with pdfjs-dist and returns the authoritative numPages.
    • pdfjs-dist is already a dependency, already used client-side by PdfOrder, and already configured for the Cloudflare Worker runtime via the canvas shim. We only parse structure here (no rendering).
    • Uses the legacy build and disables worker/eval/font-face for reliable server-side parsing.
    • Returns 0 on corrupt/encrypted/non-PDF input so callers keep rejecting invalid files.
  • Both order routes now call the shared helper (deduplicated), awaiting the now-async count.

Verification

  • pnpm typecheck ✅ and pnpm lint (biome) ✅
  • Functional test with a realistic 2-page PDF containing annotations:
    • old naive scanner: 4 (overcount)
    • new helper: 2 (correct)
  • Multi-page clean PDFs (1 / 5 / 200 pages) all return the exact count; invalid input returns 0.

The server-side page counter scanned raw PDF bytes for '/Type /Page'
markers to price orders. That badly overcounts real-world PDFs: the
marker also appears in annotations/form fields, object streams,
orphaned objects left by incremental updates, and inside content
streams. A recent 200-page upload was counted as 467.

Replace the naive scan with a shared lib/pdf.ts helper that parses the
document with pdfjs-dist (already a dependency, already used client-side
by PdfOrder and configured for the Worker runtime via the canvas shim)
and reads the authoritative numPages. Both /api/shop/orders and
/api/admin/orders now use the shared helper; invalid/corrupt files
still return 0 so callers reject them.
@choden-dev
choden-dev merged commit cc4b0b4 into main Jul 20, 2026
4 checks passed
@choden-dev
choden-dev deleted the fix/pdf-page-count-accuracy branch July 20, 2026 12:20
choden-dev added a commit that referenced this pull request Jul 20, 2026
…ng) (#104)

* fix(orders): make server-side PDF parsing work in the standalone build

Follow-up to #103. After switching page counting to pdfjs, valid PDFs
were being rejected as invalid in production.

Two root causes:

1. The pdfjs-dist *default* entry references browser-only globals
   (DOMMatrix) and throws 'ReferenceError: DOMMatrix is not defined'
   under Node. We already import the legacy build, but Next.js's
   standalone output-file tracing does not follow the dynamic subpath
   import, so the legacy build was missing from the deployed container.
   The import then threw and was swallowed as 'invalid PDF'. Fix: force
   the legacy build into the bundle via outputFileTracingIncludes.

2. countPdfPages swallowed *all* errors and returned 0, so a failure to
   load the parser (a server misconfiguration) was indistinguishable
   from a genuinely corrupt file. Fix: load the parser outside the
   try/catch and throw a distinct PdfParserUnavailableError so it
   surfaces as a 500 instead of telling the user their file is invalid;
   log genuine parse failures so real bad files can be told apart from
   parser regressions.

* fix(orders): count PDF pages with pdf-lib instead of pdfjs

Replace the server-side pdfjs-based page counter with pdf-lib.

pdfjs-dist is designed for the browser: its default build references
DOM-only globals (DOMMatrix) and crashes under Node, and its legacy
build was unreliable to trace into the Next.js standalone/container
bundle — so valid PDFs were being rejected as invalid in production.

pdf-lib is a dependency-free, pure-TypeScript PDF library that runs
anywhere Node runs (no native addons, no DOM globals, no web worker, no
bundler/file-tracing special-casing). It parses the document object
graph and returns an authoritative page count via getPageCount().

- Accurate on real-world PDFs (annotations/incremental updates that made
  the original byte-scanner overcount 200 -> 467).
- ignoreEncryption: true lets us still count pages of password/permission
  -protected PDFs instead of falsely rejecting them.
- Invalid/corrupt input is caught and logged, returning 0 so callers
  reject it with a clear message.

Also drops the now-unnecessary outputFileTracingIncludes workaround for
the pdfjs legacy build. pdfjs-dist remains a dependency for the
client-side PdfOrder preview only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant