Change PDF rendering

This commit is contained in:
2026-03-30 00:03:19 +02:00
parent 7f9d766ce0
commit d1bb79570d
9 changed files with 891 additions and 307 deletions

View File

@@ -0,0 +1,87 @@
/**
* render-worker.js — Web Worker for off-main-thread PDF rendering.
*
* Has its own PDF.js instance. Renders pages via OffscreenCanvas and transfers
* ImageBitmap objects back to the main thread with zero-copy transfer.
*
* Message protocol:
*
* Main → Worker:
* { type: "init", pdfData: ArrayBuffer } (transferred, not copied)
* { type: "render", pageNum, scale, gen }
* { type: "cancel", gen } — renderGen check handles this implicitly
* { type: "cleanup" } — pdfDoc.cleanup() (free caches)
* { type: "destroy" } — pdfDoc.destroy(); self.close()
*
* Worker → Main:
* { type: "ready", numPages }
* { type: "rendered", pageNum, gen, bitmap } (bitmap as transferable)
* { type: "error", message }
*/
importScripts("brittle://app/pdfjs/build/pdf.min.js");
const pdfjsLib = globalThis.pdfjsLib;
// Do NOT set workerSrc to a brittle:// URL here. When PDF.js tries to spawn its
// own sub-worker with new Worker("brittle://…") and that fails, it falls back to
// a "fake worker" that injects a <script> tag — which throws because `document`
// does not exist inside a Web Worker.
//
// Instead, we fetch pdf.worker.min.js in handleInit() and hand PDF.js a blob URL
// it can actually use with new Worker(blobUrl). Blob URLs created inside a worker
// are same-origin and can be used for nested workers.
let pdfDoc = null;
self.onmessage = async function (ev) {
const msg = ev.data;
switch (msg.type) {
case "init": await handleInit(msg); break;
case "render": await handleRender(msg); break;
case "cleanup":
if (pdfDoc) await pdfDoc.cleanup();
break;
case "destroy":
if (pdfDoc) { await pdfDoc.destroy(); pdfDoc = null; }
self.close();
break;
}
};
async function handleInit({ pdfData }) {
try {
// Fetch pdf.worker.min.js and create a blob URL so PDF.js can spawn its
// own sub-worker without relying on brittle:// for new Worker().
const resp = await fetch("brittle://app/pdfjs/build/pdf.worker.min.js");
const text = await resp.text();
const blob = new Blob([text], { type: "application/javascript" });
pdfjsLib.GlobalWorkerOptions.workerSrc = URL.createObjectURL(blob);
pdfDoc = await pdfjsLib.getDocument({ data: new Uint8Array(pdfData) }).promise;
self.postMessage({ type: "ready", numPages: pdfDoc.numPages });
} catch (e) {
self.postMessage({ type: "error", message: e.message });
}
}
async function handleRender({ pageNum, scale, gen }) {
if (!pdfDoc) return;
let page = null;
try {
page = await pdfDoc.getPage(pageNum);
const vp = page.getViewport({ scale });
const width = Math.round(vp.width);
const height = Math.round(vp.height);
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d");
await page.render({ canvasContext: ctx, viewport: vp }).promise;
const bitmap = canvas.transferToImageBitmap();
self.postMessage({ type: "rendered", pageNum, gen, bitmap }, [bitmap]);
} catch (e) {
if (e?.name !== "RenderingCancelledException") {
console.warn("[render-worker] render error page", pageNum, e);
}
} finally {
if (page) page.cleanup();
}
}