系列:一張投影片背後的工程:從零打造網頁簡報編輯器
對應程式:2026-10-04 工作目錄,
package.json版本0.20.0;不是v0.4.0的歷史操作紀錄。
今天要交付的是一個可以帶走的檔案。我沿用上一篇的研究分享簡報:第一頁有中文與一張合成長條圖,第二頁整理分享方式,第三頁是略過的補充資料;前兩頁另外保存只有講者需要的備註。
最後產生的 PDF 應有兩頁。觀眾能看到標題和圖表,卻不該看到編輯工具、第三頁或講者備註。

2026-10-04 的實際列印 DOM 截圖,使用與 PDF 相同的列印樣式;這張圖不是原生列印對話框,也不是 PDF 檔案的轉圖。圖表為合成示意資料。
| 簡報裡的內容 | 交付時要看到什麼 |
|---|---|
| 三張投影片,其中一張略過 | 列印頁面與 PDF 都只有兩頁 |
| 中文標題與 PNG 圖表 | 字型準備完成,圖片成功解碼 |
| 講者備註 | 留在可編輯文件,排除於投影片輸出 |
| 淡入轉場、選取框與工具列 | 固定頁面中不出現這些互動效果 |
本次自動化確實產生 demo-two-slides.pdf,檢查到兩個 PDF 頁面;輸出前也確認列印 DOM 的中文標題存在、圖片可解碼、備註和補充頁已排除。這些是不同層次的檢查,不能只看檔案副檔名就算成功。
使用者按「輸出 PDF」時,程式先呼叫 preparePrint(),等待列印頁面可用,再開啟瀏覽器列印對話框。主要流程是:
if (document.fonts) await document.fonts.ready
// 等待 IndexedDB 圖片讀取完成;這段設有五秒期限。
while (root.querySelector('.image-placeholder.is-loading')) {
// 檢查期限,稍後再查詢。
}
const images = Array.from(root.querySelectorAll('img'))
await Promise.allSettled(images.map((image) => image.decode()))
字型準備與圖片解碼是兩件事。DOM 裡已經有文字或 img,不代表排版與圖像都準備好。字型等待使用 document.fonts.ready,圖片則先等資產讀取佔位消失,再等 decode()。MDN:FontFaceSet.ready、HTMLImageElement.decode()
目前仍有一個明確限制:五秒期限只管「圖片還在讀取」的佔位,沒有包住字型等待或全部解碼。Promise.allSettled() 也會容許解碼失敗,因此產品流程仍可能進入含破圖的列印畫面。
本次取證腳本採用較嚴格的 Promise.all(images.map(img => img.decode())),有圖片解碼失敗就中止。這能確保示範素材通過檢查,並不表示產品已經有同樣的失敗處理。
列印頁採用約 16:9 的 13.333in × 7.5in,邊界設為零。1600 × 900 的邏輯畫布以 0.8 倍放入頁面;換算後是 1280 × 720 CSS px,約等於上述尺寸。
每頁都使用 break-after: page,最後一頁改成 auto,避免尾端多出空白頁。投影片本身的固定尺寸與列印分頁在這裡各負責一件事,不必重新計算每個物件的位置。
@page {
size: 13.333in 7.5in;
margin: 0;
}
.print-page:last-child {
break-after: auto;
page-break-after: auto;
}
這裡只是節錄,完整尺寸與列印專用規則放在 src/styles.css。印出來的 PDF 也不會保存播放轉場。
這次腳本透過 Edge 的 Page.printToPDF 產生檔案,保留背景並採用 CSS 頁面尺寸。使用者操作產品時,走的仍是 window.print() 與瀏覽器的「另存成 PDF」。
window.print() 會開啟列印對話框,但不會回報使用者最後選擇儲存或取消。因此程式不能在函式返回後顯示「PDF 已成功儲存」。MDN:Window.print()
自己操作時,可按以下順序確認:
這四步是讀者的人工重現方式。本次新增的是瀏覽器自動化與檔案證據,沒有把人工對話框驗收登記成已通過。