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() {