pdf.js/src/display/renderer_worker.js
2025-10-06 15:15:43 +05:30

258 lines
7.3 KiB
JavaScript

import { assert, warn } from "../shared/util.js";
import { FontFaceObject, FontLoader } from "./font_loader.js";
import { CanvasGraphics } from "./canvas.js";
import { DOMFilterFactory } from "./filter_factory.js";
import { MessageHandler } from "../shared/message_handler.js";
import { OffscreenCanvasFactory } from "./canvas_factory.js";
import { PDFObjects } from "./display_utils.js";
class RendererMessageHandler {
static #commonObjs = new PDFObjects();
static #objsMap = new Map();
static #tasks = new Map();
static #fontLoader = new FontLoader({
ownerDocument: self,
});
static #canvasFactory;
static #filterFactory;
static #enableHWA = false;
static {
this.initializeFromPort(self);
}
static pageObjs(pageIndex) {
let objs = this.#objsMap.get(pageIndex);
if (!objs) {
objs = new PDFObjects();
this.#objsMap.set(pageIndex, objs);
}
return objs;
}
static initializeFromPort(port) {
let terminated = false;
let mainHandler = new MessageHandler("renderer", "main", port);
mainHandler.send("ready", null);
mainHandler.on("Ready", function () {
// DO NOTHING
});
mainHandler.on("configure", ({ channelPort, enableHWA }) => {
this.#enableHWA = enableHWA;
const workerHandler = new MessageHandler(
"renderer-channel",
"worker-channel",
channelPort
);
this.#canvasFactory = new OffscreenCanvasFactory({
enableHWA,
});
this.#filterFactory = new DOMFilterFactory({});
workerHandler.on("commonobj", ([id, type, data]) => {
if (terminated) {
throw new Error("Renderer worker has been terminated.");
}
this.handleCommonObj(id, type, data, workerHandler);
});
workerHandler.on("obj", ([id, pageIndex, type, data]) => {
if (terminated) {
throw new Error("Renderer worker has been terminated.");
}
this.handleObj(pageIndex, id, type, data);
});
});
mainHandler.on(
"init",
({
pageIndex,
canvas,
map,
colors,
taskID,
transform,
viewport,
transparency,
background,
optionalContentConfig,
dependencyTracker,
}) => {
assert(!this.#tasks.has(taskID), "Task already initialized");
const ctx = canvas.getContext("2d", {
alpha: false,
willReadFrequently: this.#enableHWA,
});
const objs = this.pageObjs(pageIndex);
const gfx = new CanvasGraphics(
ctx,
this.#commonObjs,
objs,
this.#canvasFactory,
this.#filterFactory,
{ optionalContentConfig },
map,
colors,
dependencyTracker
);
gfx.beginDrawing({ transform, viewport, transparency, background });
this.#tasks.set(taskID, { canvas, gfx });
}
);
mainHandler.on(
"execute",
async ({ operatorList, operatorListIdx, taskID }) => {
if (terminated) {
throw new Error("Renderer worker has been terminated.");
}
const task = this.#tasks.get(taskID);
assert(task !== undefined, "Task not initialized");
return task.gfx.executeOperatorList(
operatorList,
operatorListIdx,
arg => mainHandler.send("continue", { taskID, arg })
);
}
);
mainHandler.on(
"growOperationsCount",
({ taskID, newOperatorListLength }) => {
if (terminated) {
throw new Error("Renderer worker has been terminated.");
}
const task = this.#tasks.get(taskID);
assert(task !== undefined, "Task not initialized");
task.gfx.dependencyTracker?.growOperationsCount(newOperatorListLength);
}
);
mainHandler.on("end", ({ taskID }) => {
if (terminated) {
throw new Error("Renderer worker has been terminated.");
}
const task = this.#tasks.get(taskID);
assert(task !== undefined, "Task not initialized");
task.gfx.endDrawing();
});
mainHandler.on("resetCanvas", ({ taskID }) => {
if (terminated) {
throw new Error("Renderer worker has been terminated.");
}
const task = this.#tasks.get(taskID);
assert(task !== undefined, "Task not initialized");
const canvas = task.canvas;
canvas.width = canvas.height = 0;
});
mainHandler.on("Terminate", async () => {
terminated = true;
this.#commonObjs.clear();
for (const objs of this.#objsMap.values()) {
objs.clear();
}
this.#objsMap.clear();
this.#tasks.clear();
this.#fontLoader.clear();
mainHandler.destroy();
mainHandler = null;
});
}
static handleCommonObj(id, type, exportedData, handler) {
if (this.#commonObjs.has(id)) {
return null;
}
switch (type) {
case "Font":
if ("error" in exportedData) {
const exportedError = exportedData.error;
warn(`Error during font loading: ${exportedError}`);
this.#commonObjs.resolve(id, exportedError);
break;
}
// TODO: Make FontInspector work again.
const inspectFont = null;
// this._params.pdfBug && globalThis.FontInspector?.enabled
// ? (font, url) => globalThis.FontInspector.fontAdded(font, url)
// : null;
const font = new FontFaceObject(exportedData, inspectFont);
this.#fontLoader
.bind(font)
.catch(() => handler.sendWithPromise("FontFallback", { id }))
.finally(() => {
if (!font.fontExtraProperties && font.data) {
// Immediately release the `font.data` property once the font
// has been attached to the DOM, since it's no longer needed,
// rather than waiting for a `PDFDocumentProxy.cleanup` call.
// Since `font.data` could be very large, e.g. in some cases
// multiple megabytes, this will help reduce memory usage.
font.data = null;
}
this.#commonObjs.resolve(id, font);
});
break;
case "CopyLocalImage":
const { imageRef } = exportedData;
assert(imageRef, "The imageRef must be defined.");
for (const objs of this.#objsMap.values()) {
for (const [, data] of objs) {
if (data?.ref !== imageRef) {
continue;
}
if (!data.dataLen) {
return null;
}
this.#commonObjs.resolve(id, structuredClone(data));
return data.dataLen;
}
}
break;
case "FontPath":
case "Image":
case "Pattern":
this.#commonObjs.resolve(id, exportedData);
break;
default:
throw new Error(`Got unknown common object type ${type}`);
}
return null;
}
static handleObj(pageIndex, id, type, exportedData) {
const objs = this.pageObjs(pageIndex);
if (objs.has(id)) {
return;
}
switch (type) {
case "Image":
case "Pattern":
objs.resolve(id, exportedData);
break;
default:
throw new Error(
`Got unknown object type ${type} id ${id} for page ${pageIndex} data ${JSON.stringify(exportedData)}`
);
}
}
}
export { RendererMessageHandler };