mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-11-03 04:00:39 +00:00
Deck PDF export: page = slide canvas size + wait for images to load
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 <img>'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 <[email protected]>
This commit is contained in:
1 parent
84ad374f2d
commit
f3c697fcb1
1 file changed
+52
-26
+52
-26
@@ -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 <img> 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() {
|
||||
|
||||
Reference in new issue
Block a user