-
Notifications
You must be signed in to change notification settings - Fork 0
PDF Generator
Upstream PR #367 (DriveThruCards PDF export). Four real bugs were found and fixed here — the first three verified deployed and working end-to-end (Playwright, real headed Chromium + Firefox, real backend + image-cdn); the fourth (silent blank-card data loss) shipped 2026-07-17, verified via a mocked-CDN Playwright suite in a sandbox with no live-backend access — see its own entry below for what that leaves unverified.
-
Phantom PDF download on opening the editor, before ever touching the PDF tab.
@react-pdf/renderereagerly instantiates a Yoga WASM binary at import time, not render time, andPDFGeneratorwas statically imported. Fixed vianext/dynamic({ssr: false})+mountOnEnteron theTab.Panes that mount it (PDFGeneratorModal.tsx,FinishedMyProject.tsx,ProjectEditor.tsx). -
Live preview auto-downloading in Firefox / eating too much space in Chrome. The native
<iframe>/<object>embed either triggers Firefox's "download instead of render" behavior for blob PDFs, or (once switched to<object>) pulls in the browser's own PDF viewer chrome (toolbar, thumbnail sidebar) that isn't controllable. Replaced entirely with a pdf.js canvas renderer (PDFCanvasPreview.tsx) — zero chrome, works identically in every browser.pdfjs-dist's worker script can't be resolved by Next's webpack via the normalimport.meta.urlpattern, so it's copied intopublic/as a static asset by a postinstall script (frontend/scripts/copy-pdf-worker.js) — gitignored, regenerated on everynpm install, always matches the installedpdfjs-distversion. -
PDF card images never rendered at all (both preview and the actual download — only cut lines showed), because
pdfImage.tsrouted thumbnail-quality previews throughgetBucketImageURL, and no image CDN was configured for this fork at all. See Image-CDN. -
Silent blank-card data loss on a failed image fetch, both in the live preview and the actual download (frontend-polish package item 1/5, 2026-07-17).
@react-pdf/renderer's<Image>fetches itssrcURL internally and silently skips a card it can't fetch rather than failing the render — a real production risk, since a user could send a print-ready file to MakePlayingCards/PringlePrints/NotMPC with blank cards and only find out after physical printing.Fixed by having
pdfImage.tsfetch the image itself instead of handing<Image>a bare remote URL to fetch blind (fetchAsObjectURL: a GET request, anokcheck, then ablob:object URL — the same pattern theLocalFilesource type already used). A genuine failure now rejects instead of resolving to a URL that might fail silently later.PDFCardImageandSCMCardcatch that rejection and callreportImageFailure, a callback threaded throughPDFPropsandSCMPDFPropsthatpdf.worker.tssupplies per render call (not something any caller of the public render hooks passes in itself).renderPDF/renderPDFInWorkernow return that render's failures alongside the blob, asRenderPDFResult.failures.The live preview in
PDFGenerator.tsxshows a warningAlertnaming the failed cards (test idpdf-preview-image-failures) whenever any failures came back, and a separate dangerAlert(test idpdf-preview-error) when the render itself throws — that'suseRenderPDF's pre-existingerrorvalue, which used to be computed and then never actually rendered anywhere. The download and Save-to-Drive paths (downloadPDF/saveToDrivePDF) block behind awindow.confirmnaming the failed cards before callingdownloadFile/uploading, the same pattern already used for the irreversible-action confirms inDrivesPanel.tsx. Cancelling that confirm dispatches a "Download Cancelled"/"Save Cancelled" toast and returns without writing anything.
See Image-CDN's "What it does" section for the R2 bucket/Worker
split. pdfImage.ts tries the R2 bucket first for small/large tiers,
falling back to the Worker on a miss; full-resolution always goes through
the Worker, matching upstream. As of the bug-4 fix above, both legs are a
real GET + response.ok check (not a HEAD probe) — the fetched body
becomes a blob: object URL handed to <Image>, so a fetch failure on
either domain is something calling code can actually observe and report,
rather than being resolved into an unvalidated URL for <Image> to fail on
silently later. This also means the "cheap check without a body" rationale
a HEAD request had is gone — the body was going to be fetched by <Image>
anyway on a hit, so fetching it once ourselves is a real efficiency win, not
just a correctness one.
A large real export (~104 cards) failed almost every full-resolution image fetch, all reporting
as blank in the confirm dialog. Root cause: the image-CDN Worker's full-resolution tier shares
ONE global 3-req/s rate limiter across every caller (see Image-CDN's "What it does"
section), enforced server-side with its own internal retry/backoff - but nothing on the CLIENT
paced how many concurrent full-resolution fetches it fired at once.
@react-pdf/renderer's own internal scheduler resolves every card's <Image src={async () => ...}> callback with its own concurrency, entirely outside this codebase's control - a large
export could trigger dozens of simultaneous fetches, each independently exhausting its own
server-side retry budget under that contention and coming back as a permanent per-card failure.
Fix (pdfImage.ts's fetchFullResolutionImageAsBlob, used by both getPDFImageURL's and
getPDFImageBlob's full-resolution branches - the risk applies to any full-resolution export,
not just Proposal B's bleed-normalized cards):
- A shared
Semaphore(common/semaphore.ts, new - a plain acquire/release concurrency gate for gating an unbounded stream of ad-hoc calls from a scheduler this codebase doesn't control, distinct fromconcurrencyLimit.ts'smapWithConcurrencyLimit, which needs a known, finite item list) caps client-side full-resolution fetches toFULL_RESOLUTION_FETCH_CONCURRENCY = 3, matching the server's own limit. - Retries a 429 or 5xx (transient) up to
FULL_RESOLUTION_FETCH_MAX_RETRIES = 3times with exponential backoff + jitter - a non-retryable 4xx (a real dead link) still fails on the first attempt, so a genuinely broken image doesn't burn retry budget that delays every other card queued behind the concurrency gate. -
Live progress: a large export paced to 3 req/s can now take several minutes (honestly
reported, not hidden) -
PDFProps.reportImageProgress(mirroring the existingreportImageFailurepattern, threaded throughpdf.worker.ts→ comlink'sonImageProgress→pdfRenderService→PDFGenerator.tsx) drives a "Fetching images: N/M" indicator so the wait reads as working, not hung.totalis an approximation (unique card count, not slot count - a duplicate card in the deck fetches once per slot, socompletedcan end up slightly ahead of it), intentionally not presented as an exact fraction for that reason. -
In-app confirm modal, not
window.confirm(): the incident's own screenshot showed Firefox's "allow notifications?" anti-spam chrome sitting next to the native confirm dialog - a browser can silently start auto-suppressing FUTUREwindow.confirm()calls on an origin once enough of them fire near other browser-level prompts, which would turn this safeguard off with no visible warning.ImageFailureConfirmModal(a real React-rendered BootstrapModal,PDFGenerator.tsx) can't be affected by that heuristic at all.
A cross-session report once flagged failed HEAD requests to
img.proxyprints.ca/<id>-small-google_drive when opening the PDF tab live,
traced to an R2 custom-domain quirk (net::ERR_FAILED on a HEAD-on-missing-
object, not a clean 404) that the existing bucket→worker fallback already
absorbed harmlessly. The bug-4 fix above replaced the HEAD check with a
real GET, so this specific console-noise pattern no longer occurs — kept
here as a historical note in case an old bug report referencing it resurfaces.
Full spec + approval record: docs/proposals/proposal-b-bleed-normalization.md. Core algorithm (bleedNormalize.ts: probe-median measurement per side, IQR ambiguity, fallback + manual-override plan resolution) and canvas synthesis (bleedExtension.ts: pure crop/extend geometry + normalizeCardBleed's decode→measure→plan→draw→encode→release pipeline) are built and unit tested (12 tests across the two modules, plus 4 new pdfImage.test.ts tests for the getPDFImageBlob split). Wired into PDF.tsx's PDFCardImage: full-resolution Google Drive/local-file images run through normalization instead of the old uniform proportional rescale; SCM mode and the thumbnail tiers are untouched (out of scope per the proposal doc).
Shipped and tested: the measurement/plan/extension math end-to-end, real per-card wiring in the standard (non-SCM) render path, PDFProps.bleedPriors/bleedOverrides (both optional maps keyed by card identifier, safely defaulting to "unresolved"/"auto" when absent), the main-thread batch resolution of bleedPriors from APIGetTagConsensus (bounded concurrency, per-card failure tolerance — frontend/src/common/concurrencyLimit.ts + bleedPriorResolution.ts), the manual-override UI (Auto/Force bleed/Force trimmed per card, PDFGenerator.tsx's "Bleed Overrides" panel) with its projectSlice/localStorage persistence, and the hedged WYSIWYG preview badge ("bleed will be generated", PagePreview.tsx + willLikelyGenerateBleed). Proposal B is complete end to end — see docs/proposals/proposal-b-bleed-normalization.md's "Shipped vs. not yet built" for the full per-PR breakdown.
Not yet built (both intentionally out of scope, not silently dropped): the merge-time server-side calibration pass for the four named measurement constants, and the XML round-trip field for a persisted override (flagged per the owner's own instruction, not built).
PDF.tsx's per-card eligibility check (isBleedNormalizationEligible - full-resolution Google Drive/local-file images only) is now exported and shared, rather than re-derived: PDFGenerator.tsx's BleedOverrideSettings panel and the display page's rail Print Options section (frontend/src/features/display/PrintOptionsSection.tsx - Proposal H pane migration, left-panel unification, issue #164) both call the same function, so the two surfaces' eligibility rule can't silently drift apart.
PDFCardImage's effective-dpi derivation (imageDPI when it's set and lower than cardDocument.dpi, else cardDocument.dpi) handles the case where a lower imageDPI setting makes the Worker serve a downscaled image - measurement always converts px→mm against the resolution of what was actually decoded, not assumed.
A real crash caught only by running tests/PDFGenerator.spec.ts, not by tsc/jest: the first version skipped the old proportional rescale by setting transform: "none" when normalized. @react-pdf/renderer's own stylesheet parser (@react-pdf/stylesheet) has a bug where any single-token transform value throws deep inside its internals (see docs/lessons.md's entry for the exact mechanism) - and their custom reconciler doesn't propagate that as a rejection anywhere, so pdf(...).toBlob() just hangs forever with zero console/page error. All 3 download-path Playwright tests hung at their timeout; a stashed pre-Proposal-B baseline confirmed they pass cleanly with no other changes. Fixed by using transform: undefined (omitting the key) instead of "none" - all 4 tests pass afterward, matching baseline timing.
-
frontend/src/features/pdf/PDFGenerator.tsx,frontend/src/features/pdf/pdfImage.ts(+pdfImage.test.ts) -
frontend/src/features/pdf/PDF.tsx,frontend/src/features/pdf/scm/SCMPDF.tsx(both threadreportImageFailuredown to their per-card<Image>) -
frontend/src/features/pdf/pdf.worker.ts(owns the per-renderfailuresarray — see bug 4),pdfRenderService.ts,useRenderPDF.ts frontend/src/features/pdf/PDFCanvasPreview.tsxfrontend/scripts/copy-pdf-worker.js-
frontend/src/features/pdf/PDFGeneratorModal.tsx,frontend/src/features/export/FinishedMyProject.tsx,frontend/src/components/ProjectEditor.tsx -
frontend/tests/PDFGenerator.spec.ts— mocked-CDN Playwright coverage for bug 4 (preview warning, confirm-gated download/cancel, and a real-image success-path regression check)
All four bugs verified fixed. The first three are deployed and confirmed live; bug 4 is verified only in a mocked sandbox (see its merge-time checklist item in the frontend-polish PR) pending a live-backend check. Upstream PR #463 (lazy WASM load fix) is open; #464 (canvas preview) and #466 (thumbnail routing) were closed after the maintainer said the existing upstream behavior is deliberate design for their codebase, not a bug — see Infrastructure for PR status details. Don't "fix" this fork's PDF tab implementation to match upstream's on those two points; both are correct for their own codebase.
See also Google-Drive-Connect for the separate "Save PDF directly to Google Drive" upload feature on this same tab.
Understanding the system
- Overview
- Documentation-Process
- Theory
- Identification-Pipeline
- Pipeline-Fidelity-Gate
- Federation-v1
- Vote-System
- Readiness-Audit
- License-Provenance
- Upstreaming-Conventions
- Drift-Log
- Upstream-Wiki-Drift
- Printing-Tags
- Catalog-Completion-Plan
- Moderation
- Card-DOM-API
- PDF-Generator
- Print-Export-Page
- Google-Drive-Connect
- Grid-Selector
- Image-CDN
- Local-File-Source
Using it
Operating it
Folded into other pages