Skip to content

fix(web): gate PDF print-ready handshake on a usable content size (#4458)#4545

Open
YOMXXX wants to merge 2 commits into
nexu-io:mainfrom
YOMXXX:fix/issue-4458-pdf-blank
Open

fix(web): gate PDF print-ready handshake on a usable content size (#4458)#4545
YOMXXX wants to merge 2 commits into
nexu-io:mainfrom
YOMXXX:fix/issue-4458-pdf-blank

Conversation

@YOMXXX

@YOMXXX YOMXXX commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Refs #4458

Why

#4458 reports PDF export still producing a one-page, all-white PDF even though the artifact visibly rendered in Open Design. @lefarcen's triage pointed at the desktop printToPDF readiness path, and reading it confirmed one concrete, non-speculative blank path:

For the sandboxed-preview export, the desktop bridge prints a wrapper document whose <body> is overflow:hidden around a sandbox="allow-scripts" iframe it cannot read. It therefore depends on the in-iframe print-ready handshake reporting the artifact's content size (__odPrintSize). That handshake measured the size after a fixed two animation frames — which can fire before a heavier artifact has finished layout, so it reports width/height = 0. The parent cache only stores a positive-finite size, so __odPrintSize stays null, and inferPageSize then falls back to measuring the wrapper viewport — which, per that function's own docstring, "blanks … artifacts whose visible content sits below the fold".

What users will see

A heavier or slower-laying-out artifact that previously exported as a blank page is more likely to export with its real content, because the desktop PDF bridge now receives the artifact's true content size instead of falling back to the wrapper viewport.

Surface area

  • None — internal change to the PDF export print-ready handshake in apps/web/src/runtime/exports.ts. No UI/CLI/API/i18n surface.

Screenshots

N/A — no UI. (Visual confirmation requires a real desktop PDF export; see Bug fix verification.)

Bug fix verification

  • Test path: apps/web/tests/runtime/exports.test.ts
    • reportPrintSizeWhenStable (#4458) — reports only once content has a usable (non-zero) size by polling animation frames; reports best-effort when the frame budget is exhausted (genuinely empty content).
    • isUsablePrintSize (#4458) — rejects zero / negative / non-finite / non-number dimensions.
    • The existing PDF export can differ from the webpage and may produce a blank white page #4067 size-reporting test is updated to assert the handshake is wired to reportPrintSizeWhenStable and the cache gates on isUsablePrintSize.
  • Did the tests go red on main and green on this branch? yes — both new functions are missing on main (import is undefined, tests throw); green after the fix. These are real-logic behavior assertions on the exact functions injected into the handshake (via toString()), not string-presence checks.
  • Honest scope / limitation: this fixes one root-cause path (late layout → size 0 → viewport fallback → blank). PDF export can still produce a blank white page after #4329 #4458 may also cover a second mode where __odPrintSize is valid but Electron's printToPDF captures before the iframe composites a paint; distinguishing/validating that needs a real-Electron repro with an artifact that reproduces the blank (the reporter noted it was hard to recreate), which this PR does not attempt. Hence Refs rather than Fixes — leaving the issue open for end-to-end confirmation.

Validation

  • pnpm --filter @open-design/web exec vitest run tests/runtime/exports.test.ts → 55 passed (4 new/updated specs RED→GREEN).
  • tsc --noEmit clean for exports.ts.
  • pnpm guard → no new violations.

…xu-io#4458)

One blank-PDF path in nexu-io#4458: the in-iframe print-ready handshake reported the
artifact's content size after a fixed two animation frames, which can fire
before a heavier artifact has finished layout (size still 0). With no usable
__odPrintSize, the desktop bridge's inferPageSize falls back to measuring the
wrapper viewport — which, per its own docs, blanks artifacts whose visible
content sits below the fold.

- Add reportPrintSizeWhenStable(): poll animation frames until the measured
  size is usable (positive, finite), then report it; bounded so a genuinely
  empty artifact reports best-effort instead of hanging the handshake.
- Add isUsablePrintSize() and use it in the parent readiness cache's size
  validation. Both are injected via toString() and unit-tested directly
  (real-logic behavior assertions, not just string-presence).

Scoped to this one root-cause path. A separate blank-page mode where the
reported size is valid but printToPDF captures before the iframe composites
needs a real-Electron repro to confirm and is left for follow-up.
@lefarcen

Copy link
Copy Markdown
Contributor

Hi @YOMXXX! 👋

Thanks for tightening the PDF export readiness path here — the write-up around the zero-size fallback path is clear, and it helps that you called out the remaining end-to-end limitation explicitly.

I've routed this PR into review and will keep the PR metadata / follow-up in sync while it moves through the queue.

@lefarcen lefarcen requested a review from mrcfps June 19, 2026 01:19
@lefarcen lefarcen added size/M PR changes 100-300 lines risk/medium Medium risk: regular code changes type/bugfix Bug fix needs-validation Runtime change detected; needs human or /explore agent validation. labels Jun 19, 2026
@lefarcen

Copy link
Copy Markdown
Contributor

🧪 This PR has changes that need a manual QA pass before merge — please hold off self-merging for now; we'll loop QA in once it's merge-ready.

…eady cache (nexu-io#4458)

nexu-io#4458 notes the existing coverage only proves the print-ready scripts are
injected, never that they behave. Extract the real parent-cache <script> from
a live export, run it against a minimal fake window, and drive it with
postMessage events to assert the runtime size gate: a usable size is cached
for inferPageSize(), while a zero/non-finite size or a wrong-nonce message is
rejected so it cannot blank or poison the PDF page.

@mrcfps mrcfps left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YOMXXX I reviewed the updated print-ready handshake and the added runtime coverage in apps/web/src/runtime/exports.ts / apps/web/tests/runtime/exports.test.ts. The bounded polling plus parent-cache size gate line up with inferPageSize()'s existing contract, and I did not find any changed-range issues that look merge-blocking. Thanks for tightening this PDF-export path and for documenting the remaining limitation clearly. 🙏

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

@lefarcen lefarcen requested a review from AmyShang-alt June 19, 2026 01:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-validation Runtime change detected; needs human or /explore agent validation. risk/medium Medium risk: regular code changes size/M PR changes 100-300 lines type/bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants