88 lines
3.2 KiB
JavaScript
88 lines
3.2 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
}
|