From f3c697fcb183ea734dbcdefe55cdd38074cac3d2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 26 May 2026 14:31:50 -0400 Subject: [PATCH] Deck PDF export: page = slide canvas size + wait for images to load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in the first deck-PDF cut: 1. PDF page didn't match the slide canvas. The page was sized to the PREVIEW_BASE_PX-wide preview (7.5in × 4.22in for a 16:9 slide), which gave a tiny PDF page that the browser sometimes scaled awkwardly. Now the page is sized to the slide's natural design dimensions (wPx / 96 × hPx / 96 inches — e.g. 20 × 11.25in for a default 1920×1080 slide), so the PDF is 1:1 with the canvas. A CSS `transform: scale(wPx / PREVIEW_BASE_PX)` enlarges the PREVIEW_BASE_PX-wide preview to fill the larger page; transform scales text proportionally, so font sizes stay correct. 2. Some images weren't rendering. The export called window.print() one RAF after attaching the print container — slower remote image URLs hadn't finished loading yet, so they missed the print snapshot. Now we await every 's load/error event (with a 5s per-image cap so one hung URL can't block forever) before calling print. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 78 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/src/index.html b/src/index.html index f159e88..79e2f6b 100644 --- a/src/index.html +++ b/src/index.html @@ -14570,37 +14570,43 @@ // Export the current deck as a multi-page PDF — one page per slide. // Each slide is rendered via the same `renderCustomArtPreviewDOM` the - // Deck Builder thumbnails use (themed at render time, slide source is + // Deck Builder thumbnails use (themed at render time; slide source is // never mutated). The browser handles the actual PDF emission via its // Save-as-PDF print path. - function exportDeckAsPdf() { + // + // Page sizing matches the slide canvas's natural design dimensions: + // a 1920×1080 slide → 20in × 11.25in PDF page at the CSS 96 dpi + // baseline, so the PDF is the slide at 1:1 scale. The preview itself + // is always rendered at PREVIEW_BASE_PX wide internally, so we apply + // a CSS `transform: scale(...)` to enlarge it to fill the page. + async function exportDeckAsPdf() { const deck = ensureDeck(); const slides = deck?.slides || []; if (!slides.length) { toastError('No slides to export.'); return; } - // Match the print page to the preview's native size (PREVIEW_BASE_PX - // CSS pixels wide at the slide's aspect). At the browser's 96 dpi - // assumption, PREVIEW_BASE_PX = 720 → 7.5 inches wide, with height - // derived from the first slide's aspect ratio. Mixed-aspect decks - // still print on this page size — non-matching slides letterbox. const firstSlide = slides[0].slide || {}; const wPx = Math.max(1, Number(firstSlide.width) || 1920); const hPx = Math.max(1, Number(firstSlide.height) || 1080); - const pageWidthIn = PREVIEW_BASE_PX / 96; - const pageHeightIn = (PREVIEW_BASE_PX * hPx / wPx) / 96; + // CSS uses a 96 dpi baseline, so px / 96 → in. + const pageW = wPx / 96; + const pageH = hPx / 96; + // The preview renders at PREVIEW_BASE_PX wide; scale it up so it + // fills the page exactly. + const previewScale = wPx / PREVIEW_BASE_PX; const fmt = (n) => Math.round(n * 1000) / 1000; const container = document.createElement('div'); container.id = 'deckPrintContainer'; - // The container is hidden from normal flow (taken out of the body - // visual layout) but stays in the DOM so print can find it. container.style.cssText = 'position:fixed;left:-99999px;top:0;'; slides.forEach((entry, i) => { const page = document.createElement('div'); page.className = 'deckPrintPage'; - // Page break after each slide except the last — gives N pages for - // N slides without a trailing blank. if (i < slides.length - 1) page.style.pageBreakAfter = 'always'; + // Inner wrapper hosts the transform; the preview itself is + // appended inside it. + const inner = document.createElement('div'); + inner.className = 'deckPrintInner'; const composed = composeSlideForDeck(entry, deck.theme || {}); - renderCustomArtPreviewDOM(page, composed); + renderCustomArtPreviewDOM(inner, composed); + page.appendChild(inner); container.appendChild(page); }); document.body.appendChild(container); @@ -14608,18 +14614,27 @@ styleEl.id = 'deckPrintStyle'; styleEl.textContent = ` @media print { - @page { size: ${fmt(pageWidthIn)}in ${fmt(pageHeightIn)}in; margin: 0; } + @page { size: ${fmt(pageW)}in ${fmt(pageH)}in; margin: 0; } body > *:not(#deckPrintContainer) { display: none !important; } #deckPrintContainer { position: static !important; left: 0 !important; top: 0 !important; } .deckPrintPage { - width: ${fmt(pageWidthIn)}in; height: ${fmt(pageHeightIn)}in; - display: flex; align-items: center; justify-content: center; + width: ${fmt(pageW)}in; height: ${fmt(pageH)}in; + position: relative; box-sizing: border-box; padding: 0; margin: 0; background: #fff; overflow: hidden; } - /* The preview wrapper inside each page — strip caps and borders so - the slide fills the page exactly. */ - .deckPrintPage > div { + /* Scale the natively-PREVIEW_BASE_PX-wide preview up to the + slide's natural design dimensions. transform-origin: top left + keeps everything anchored at (0,0). */ + .deckPrintInner { + position: absolute; top: 0; left: 0; + transform: scale(${fmt(previewScale)}); + transform-origin: top left; + } + /* The preview's own root div has inline width/aspect-ratio rules + and a border + box-shadow for the editor view — strip the + decoration in print. */ + .deckPrintInner > div { margin: 0 !important; border: 0 !important; box-shadow: none !important; } } @@ -14634,12 +14649,23 @@ window.removeEventListener('afterprint', cleanup); }; window.addEventListener('afterprint', cleanup); - // One animation frame lets the preview DOM layout settle before the - // print snapshot is taken. - requestAnimationFrame(() => { - try { window.print(); } - catch (e) { cleanup(); toastError('Could not open the print dialog.'); } - }); + // Wait for every in the print container to finish loading + // (or error). Without this, slow remote images miss the print + // snapshot and the PDF page renders without them. Cap each wait at + // 5s so one hung URL can't block the export forever. + const imgs = Array.from(container.querySelectorAll('img')); + await Promise.all(imgs.map(img => { + if (img.complete && img.naturalWidth) return Promise.resolve(); + return new Promise(resolve => { + let done = false; + const finish = () => { if (done) return; done = true; resolve(); }; + img.addEventListener('load', finish); + img.addEventListener('error', finish); + setTimeout(finish, 5000); + }); + })); + try { window.print(); } + catch (e) { cleanup(); toastError('Could not open the print dialog.'); } } function printWithCurrentFilename() {