Implement offscreen rendering with workers
Move the work of drawing the PDF onto the cavas to a worker thread using OffscreenCanvas. This should free up the main thread a bit by moving all of the CanvasGraphics operations to this "renderer" worker.
This commit is contained in:
parent
e1de28c866
commit
82661bdbf4
@ -129,10 +129,11 @@ class Page {
|
||||
};
|
||||
}
|
||||
|
||||
#createPartialEvaluator(handler) {
|
||||
#createPartialEvaluator(handler, rendererHandler) {
|
||||
return new PartialEvaluator({
|
||||
xref: this.xref,
|
||||
handler,
|
||||
rendererHandler,
|
||||
pageIndex: this.pageIndex,
|
||||
idFactory: this._localIdFactory,
|
||||
fontCache: this.fontCache,
|
||||
@ -438,6 +439,7 @@ class Page {
|
||||
|
||||
async getOperatorList({
|
||||
handler,
|
||||
rendererHandler,
|
||||
sink,
|
||||
task,
|
||||
intent,
|
||||
@ -448,7 +450,10 @@ class Page {
|
||||
const contentStreamPromise = this.getContentStream();
|
||||
const resourcesPromise = this.loadResources(RESOURCES_KEYS_OPERATOR_LIST);
|
||||
|
||||
const partialEvaluator = this.#createPartialEvaluator(handler);
|
||||
const partialEvaluator = this.#createPartialEvaluator(
|
||||
handler,
|
||||
rendererHandler
|
||||
);
|
||||
|
||||
const newAnnotsByPage = !this.xfaFactory
|
||||
? getNewAnnotationsMap(annotationStorage)
|
||||
@ -1331,7 +1336,7 @@ class PDFDocument {
|
||||
this.xfaFactory.setImages(xfaImages);
|
||||
}
|
||||
|
||||
async #loadXfaFonts(handler, task) {
|
||||
async #loadXfaFonts(handler, task, rendererHandler) {
|
||||
const acroForm = await this.pdfManager.ensureCatalog("acroForm");
|
||||
if (!acroForm) {
|
||||
return;
|
||||
@ -1357,6 +1362,7 @@ class PDFDocument {
|
||||
const partialEvaluator = new PartialEvaluator({
|
||||
xref: this.xref,
|
||||
handler,
|
||||
rendererHandler,
|
||||
pageIndex: -1,
|
||||
idFactory: this._globalIdFactory,
|
||||
fontCache,
|
||||
@ -1469,9 +1475,9 @@ class PDFDocument {
|
||||
this.xfaFactory.appendFonts(pdfFonts, reallyMissingFonts);
|
||||
}
|
||||
|
||||
loadXfaResources(handler, task) {
|
||||
loadXfaResources(handler, task, rendererHandler) {
|
||||
return Promise.all([
|
||||
this.#loadXfaFonts(handler, task).catch(() => {
|
||||
this.#loadXfaFonts(handler, task, rendererHandler).catch(() => {
|
||||
// Ignore errors, to allow the document to load.
|
||||
}),
|
||||
this.#loadXfaImages(),
|
||||
|
||||
@ -222,6 +222,7 @@ class PartialEvaluator {
|
||||
constructor({
|
||||
xref,
|
||||
handler,
|
||||
rendererHandler,
|
||||
pageIndex,
|
||||
idFactory,
|
||||
fontCache,
|
||||
@ -234,6 +235,7 @@ class PartialEvaluator {
|
||||
}) {
|
||||
this.xref = xref;
|
||||
this.handler = handler;
|
||||
this.rendererHandler = rendererHandler;
|
||||
this.pageIndex = pageIndex;
|
||||
this.idFactory = idFactory;
|
||||
this.fontCache = fontCache;
|
||||
@ -553,13 +555,19 @@ class PartialEvaluator {
|
||||
const transfers = imgData ? [imgData.bitmap || imgData.data.buffer] : null;
|
||||
|
||||
if (this.parsingType3Font || cacheGlobally) {
|
||||
return this.handler.send(
|
||||
this.handler.send("commonobj", [objId, "Image", imgData], transfers);
|
||||
return this.rendererHandler.send(
|
||||
"commonobj",
|
||||
[objId, "Image", imgData],
|
||||
transfers
|
||||
);
|
||||
}
|
||||
return this.handler.send(
|
||||
this.handler.send(
|
||||
"obj",
|
||||
[objId, this.pageIndex, "Image", imgData],
|
||||
transfers
|
||||
);
|
||||
return this.rendererHandler.send(
|
||||
"obj",
|
||||
[objId, this.pageIndex, "Image", imgData],
|
||||
transfers
|
||||
@ -787,11 +795,10 @@ class PartialEvaluator {
|
||||
// globally, check if the image is still cached locally on the main-thread
|
||||
// to avoid having to re-parse the image (since that can be slow).
|
||||
if (w * h > 250000 || hasMask) {
|
||||
const localLength = await this.handler.sendWithPromise("commonobj", [
|
||||
objId,
|
||||
"CopyLocalImage",
|
||||
{ imageRef },
|
||||
]);
|
||||
const localLength = await this.rendererHandler.sendWithPromise(
|
||||
"commonobj",
|
||||
[objId, "CopyLocalImage", { imageRef }]
|
||||
);
|
||||
|
||||
if (localLength) {
|
||||
this.globalImageCache.setData(imageRef, globalCacheData);
|
||||
@ -1021,6 +1028,7 @@ class PartialEvaluator {
|
||||
|
||||
state.font = translated.font;
|
||||
translated.send(this.handler);
|
||||
translated.send(this.rendererHandler);
|
||||
return translated.loadedName;
|
||||
}
|
||||
|
||||
@ -1040,7 +1048,7 @@ class PartialEvaluator {
|
||||
PartialEvaluator.buildFontPaths(
|
||||
font,
|
||||
glyphs,
|
||||
this.handler,
|
||||
this.rendererHandler,
|
||||
this.options
|
||||
);
|
||||
}
|
||||
@ -1518,8 +1526,15 @@ class PartialEvaluator {
|
||||
|
||||
if (this.parsingType3Font) {
|
||||
this.handler.send("commonobj", [id, "Pattern", patternIR]);
|
||||
this.rendererHandler.send("commonobj", [id, "Pattern", patternIR]);
|
||||
} else {
|
||||
this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]);
|
||||
this.rendererHandler.send("obj", [
|
||||
id,
|
||||
this.pageIndex,
|
||||
"Pattern",
|
||||
patternIR,
|
||||
]);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
@ -82,6 +82,8 @@ class WorkerMessageHandler {
|
||||
|
||||
static setup(handler, port) {
|
||||
let testMessageProcessed = false;
|
||||
let rendererHandler = null;
|
||||
|
||||
handler.on("test", data => {
|
||||
if (testMessageProcessed) {
|
||||
return; // we already processed 'test' message once
|
||||
@ -94,12 +96,19 @@ class WorkerMessageHandler {
|
||||
|
||||
handler.on("configure", data => {
|
||||
setVerbosityLevel(data.verbosity);
|
||||
rendererHandler = new MessageHandler(
|
||||
"worker-channel",
|
||||
"renderer-channel",
|
||||
data.channelPort
|
||||
);
|
||||
});
|
||||
|
||||
handler.on("GetDocRequest", data => this.createDocumentHandler(data, port));
|
||||
handler.on("GetDocRequest", data =>
|
||||
this.createDocumentHandler(data, port, rendererHandler)
|
||||
);
|
||||
}
|
||||
|
||||
static createDocumentHandler(docParams, port) {
|
||||
static createDocumentHandler(docParams, port, rendererHandler) {
|
||||
// This context is actually holds references on pdfManager and handler,
|
||||
// until the latter is destroyed.
|
||||
let pdfManager;
|
||||
@ -173,7 +182,11 @@ class WorkerMessageHandler {
|
||||
const task = new WorkerTask("loadXfaResources");
|
||||
startWorkerTask(task);
|
||||
|
||||
await pdfManager.ensureDoc("loadXfaResources", [handler, task]);
|
||||
await pdfManager.ensureDoc("loadXfaResources", [
|
||||
handler,
|
||||
task,
|
||||
rendererHandler,
|
||||
]);
|
||||
finishWorkerTask(task);
|
||||
}
|
||||
|
||||
@ -776,6 +789,7 @@ class WorkerMessageHandler {
|
||||
page
|
||||
.getOperatorList({
|
||||
handler,
|
||||
rendererHandler,
|
||||
sink,
|
||||
task,
|
||||
intent: data.intent,
|
||||
@ -859,8 +873,8 @@ class WorkerMessageHandler {
|
||||
.then(page => pdfManager.ensure(page, "getStructTree"));
|
||||
});
|
||||
|
||||
handler.on("FontFallback", function (data) {
|
||||
return pdfManager.fontFallback(data.id, handler);
|
||||
rendererHandler.on("FontFallback", function (data) {
|
||||
return pdfManager.fontFallback(data.id, rendererHandler);
|
||||
});
|
||||
|
||||
handler.on("Cleanup", function (data) {
|
||||
|
||||
@ -41,6 +41,7 @@ import {
|
||||
isDataScheme,
|
||||
isValidFetchUrl,
|
||||
PageViewport,
|
||||
PDFObjects,
|
||||
RenderingCancelledException,
|
||||
StatTimer,
|
||||
} from "./display_utils.js";
|
||||
@ -75,7 +76,6 @@ import { PDFDataTransportStream } from "./transport_stream.js";
|
||||
import { PDFFetchStream } from "display-fetch_stream";
|
||||
import { PDFNetworkStream } from "display-network";
|
||||
import { PDFNodeStream } from "display-node_stream";
|
||||
import { PDFObjects } from "./pdf_objects.js";
|
||||
import { TextLayer } from "./text_layer.js";
|
||||
import { XfaText } from "./xfa_text.js";
|
||||
|
||||
@ -390,16 +390,23 @@ function getDocument(src = {}) {
|
||||
: new WasmFactory({ baseUrl: wasmUrl }),
|
||||
};
|
||||
|
||||
const workerChannel = new MessageChannel();
|
||||
|
||||
if (!worker) {
|
||||
// Worker was not provided -- creating and owning our own. If message port
|
||||
// is specified in global worker options, using it.
|
||||
worker = PDFWorker.create({
|
||||
verbosity,
|
||||
port: GlobalWorkerOptions.workerPort,
|
||||
channelPort: workerChannel.port1,
|
||||
});
|
||||
task._worker = worker;
|
||||
}
|
||||
|
||||
const renderer = new RendererWorker(workerChannel.port2, enableHWA);
|
||||
const rendererHandler = renderer.handler;
|
||||
task.renderer = renderer;
|
||||
|
||||
const docParams = {
|
||||
docId,
|
||||
apiVersion:
|
||||
@ -504,10 +511,12 @@ function getDocument(src = {}) {
|
||||
networkStream,
|
||||
transportParams,
|
||||
transportFactory,
|
||||
enableHWA
|
||||
enableHWA,
|
||||
rendererHandler
|
||||
);
|
||||
task._transport = transport;
|
||||
messageHandler.send("Ready", null);
|
||||
rendererHandler.send("Ready", null);
|
||||
});
|
||||
})
|
||||
.catch(task._capability.reject);
|
||||
@ -544,6 +553,8 @@ class PDFDocumentLoadingTask {
|
||||
*/
|
||||
_worker = null;
|
||||
|
||||
#renderer = null;
|
||||
|
||||
/**
|
||||
* Unique identifier for the document loading task.
|
||||
* @type {string}
|
||||
@ -603,6 +614,8 @@ class PDFDocumentLoadingTask {
|
||||
|
||||
this._worker?.destroy();
|
||||
this._worker = null;
|
||||
this.#renderer.destroy();
|
||||
this.#renderer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -614,6 +627,17 @@ class PDFDocumentLoadingTask {
|
||||
async getData() {
|
||||
return this._transport.getData();
|
||||
}
|
||||
|
||||
get renderer() {
|
||||
return this.#renderer;
|
||||
}
|
||||
|
||||
set renderer(renderer) {
|
||||
if (this.#renderer) {
|
||||
throw new Error("PDFDocumentLoadingTask.renderer: already set.");
|
||||
}
|
||||
this.#renderer = renderer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1574,7 +1598,7 @@ class PDFPageProxy {
|
||||
useRequestAnimationFrame: !intentPrint,
|
||||
pdfBug: this._pdfBug,
|
||||
pageColors,
|
||||
enableHWA: this._transport.enableHWA,
|
||||
rendererHandler: this._transport.rendererHandler,
|
||||
operationsFilter,
|
||||
});
|
||||
|
||||
@ -1992,6 +2016,12 @@ class PDFPageProxy {
|
||||
get stats() {
|
||||
return this._stats;
|
||||
}
|
||||
|
||||
resetCanvas(taskID) {
|
||||
this._transport.rendererHandler.send("resetCanvas", {
|
||||
taskID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2000,6 +2030,8 @@ class PDFPageProxy {
|
||||
* @property {Worker} [port] - The `workerPort` object.
|
||||
* @property {number} [verbosity] - Controls the logging level;
|
||||
* the constants from {@link VerbosityLevel} should be used.
|
||||
* @property {MessagePort} [channelPort] - The channel port to use for
|
||||
* communication with the renderer thread.
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -2081,10 +2113,12 @@ class PDFWorker {
|
||||
name = null,
|
||||
port = null,
|
||||
verbosity = getVerbosityLevel(),
|
||||
channelPort = null,
|
||||
} = {}) {
|
||||
this.name = name;
|
||||
this.destroyed = false;
|
||||
this.verbosity = verbosity;
|
||||
this.channelPort = channelPort;
|
||||
|
||||
if (port) {
|
||||
if (PDFWorker.#workerPorts.has(port)) {
|
||||
@ -2117,9 +2151,14 @@ class PDFWorker {
|
||||
#resolve() {
|
||||
this.#capability.resolve();
|
||||
// Send global setting, e.g. verbosity level.
|
||||
this.#messageHandler.send("configure", {
|
||||
verbosity: this.verbosity,
|
||||
});
|
||||
this.#messageHandler.send(
|
||||
"configure",
|
||||
{
|
||||
verbosity: this.verbosity,
|
||||
channelPort: this.channelPort,
|
||||
},
|
||||
[this.channelPort]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2355,6 +2394,34 @@ class PDFWorker {
|
||||
}
|
||||
}
|
||||
|
||||
class RendererWorker {
|
||||
#worker;
|
||||
|
||||
#handler;
|
||||
|
||||
constructor(channelPort, enableHWA) {
|
||||
this.#worker = new Worker("../src/display/renderer_worker.js", {
|
||||
type: "module",
|
||||
});
|
||||
this.#handler = new MessageHandler("main", "renderer", this.#worker);
|
||||
this.#handler.send("configure", { channelPort, enableHWA }, [channelPort]);
|
||||
this.#handler.on("ready", () => {
|
||||
// DO NOTHING
|
||||
});
|
||||
}
|
||||
|
||||
get handler() {
|
||||
return this.#handler;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#worker.terminate();
|
||||
this.#worker = null;
|
||||
this.#handler.destroy();
|
||||
this.#handler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For internal use only.
|
||||
* @ignore
|
||||
@ -2376,9 +2443,11 @@ class WorkerTransport {
|
||||
networkStream,
|
||||
params,
|
||||
factory,
|
||||
enableHWA
|
||||
enableHWA,
|
||||
rendererHandler
|
||||
) {
|
||||
this.messageHandler = messageHandler;
|
||||
this.rendererHandler = rendererHandler;
|
||||
this.loadingTask = loadingTask;
|
||||
this.commonObjs = new PDFObjects();
|
||||
this.fontLoader = new FontLoader({
|
||||
@ -2544,8 +2613,13 @@ class WorkerTransport {
|
||||
const terminated = this.messageHandler.sendWithPromise("Terminate", null);
|
||||
waitOn.push(terminated);
|
||||
|
||||
const terminatedRenderer = this.rendererHandler.sendWithPromise(
|
||||
"Terminate",
|
||||
null
|
||||
);
|
||||
waitOn.push(terminatedRenderer);
|
||||
|
||||
Promise.all(waitOn).then(() => {
|
||||
this.commonObjs.clear();
|
||||
this.fontLoader.clear();
|
||||
this.#methodPromises.clear();
|
||||
this.filterFactory.destroy();
|
||||
@ -2564,7 +2638,13 @@ class WorkerTransport {
|
||||
}
|
||||
|
||||
setupMessageHandler() {
|
||||
const { messageHandler, loadingTask } = this;
|
||||
const { messageHandler, loadingTask, rendererHandler } = this;
|
||||
|
||||
rendererHandler.on("continue", ({ taskID, arg }) => {
|
||||
const continueFn = InternalRenderTask.continueFnMap.get(taskID);
|
||||
assert(continueFn, `No continue function for taskID: ${taskID}`);
|
||||
continueFn.call(arg);
|
||||
});
|
||||
|
||||
messageHandler.on("GetReader", (data, sink) => {
|
||||
assert(
|
||||
@ -3166,6 +3246,10 @@ class RenderTask {
|
||||
(separateAnnots.canvas && annotationCanvasMap?.size > 0)
|
||||
);
|
||||
}
|
||||
|
||||
get taskID() {
|
||||
return this.#internalRenderTask.taskID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3177,6 +3261,10 @@ class InternalRenderTask {
|
||||
|
||||
static #canvasInUse = new WeakSet();
|
||||
|
||||
static #taskCounter = 0n;
|
||||
|
||||
static continueFnMap = new Map();
|
||||
|
||||
constructor({
|
||||
callback,
|
||||
params,
|
||||
@ -3190,9 +3278,10 @@ class InternalRenderTask {
|
||||
useRequestAnimationFrame = false,
|
||||
pdfBug = false,
|
||||
pageColors = null,
|
||||
enableHWA = false,
|
||||
rendererHandler,
|
||||
operationsFilter = null,
|
||||
}) {
|
||||
this.taskID = InternalRenderTask.#taskCounter++;
|
||||
this.callback = callback;
|
||||
this.params = params;
|
||||
this.objs = objs;
|
||||
@ -3221,7 +3310,9 @@ class InternalRenderTask {
|
||||
this._nextBound = this._next.bind(this);
|
||||
this._canvas = params.canvas;
|
||||
this._canvasContext = params.canvas ? null : params.canvasContext;
|
||||
this._enableHWA = enableHWA;
|
||||
this._renderInWorker = this._canvasContext === null;
|
||||
this.rendererHandler = rendererHandler;
|
||||
InternalRenderTask.continueFnMap.set(this.taskID, this._continueBound);
|
||||
this._dependencyTracker = params.dependencyTracker;
|
||||
this._operationsFilter = operationsFilter;
|
||||
}
|
||||
@ -3256,31 +3347,47 @@ class InternalRenderTask {
|
||||
const { viewport, transform, background, dependencyTracker } = this.params;
|
||||
|
||||
// When printing in Firefox, we get a specific context in mozPrintCallback
|
||||
// which cannot be created from the canvas itself.
|
||||
const canvasContext =
|
||||
this._canvasContext ||
|
||||
this._canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
willReadFrequently: !this._enableHWA,
|
||||
// which cannot be created from the canvas itself. In this case, we don't
|
||||
// render in the worker and use the context directly.
|
||||
if (this._renderInWorker) {
|
||||
const offscreen = this._canvas.transferControlToOffscreen();
|
||||
this.rendererHandler.send(
|
||||
"init",
|
||||
{
|
||||
pageIndex: this._pageIndex,
|
||||
canvas: offscreen,
|
||||
map: this.annotationCanvasMap,
|
||||
colors: this.pageColors,
|
||||
taskID: this.taskID,
|
||||
transform,
|
||||
viewport,
|
||||
transparency,
|
||||
background,
|
||||
optionalContentConfig,
|
||||
dependencyTracker,
|
||||
},
|
||||
[offscreen]
|
||||
);
|
||||
} else {
|
||||
this.gfx = new CanvasGraphics(
|
||||
this._canvasContext,
|
||||
this.commonObjs,
|
||||
this.objs,
|
||||
this.canvasFactory,
|
||||
this.filterFactory,
|
||||
{ optionalContentConfig },
|
||||
this.annotationCanvasMap,
|
||||
this.pageColors,
|
||||
dependencyTracker
|
||||
);
|
||||
this.gfx.beginDrawing({
|
||||
transform,
|
||||
viewport,
|
||||
transparency,
|
||||
background,
|
||||
});
|
||||
}
|
||||
|
||||
this.gfx = new CanvasGraphics(
|
||||
canvasContext,
|
||||
this.commonObjs,
|
||||
this.objs,
|
||||
this.canvasFactory,
|
||||
this.filterFactory,
|
||||
{ optionalContentConfig },
|
||||
this.annotationCanvasMap,
|
||||
this.pageColors,
|
||||
dependencyTracker
|
||||
);
|
||||
this.gfx.beginDrawing({
|
||||
transform,
|
||||
viewport,
|
||||
transparency,
|
||||
background,
|
||||
});
|
||||
this.operatorListIdx = 0;
|
||||
this.graphicsReady = true;
|
||||
this.graphicsReadyCallback?.();
|
||||
@ -3289,7 +3396,12 @@ class InternalRenderTask {
|
||||
cancel(error = null, extraDelay = 0) {
|
||||
this.running = false;
|
||||
this.cancelled = true;
|
||||
this.gfx?.endDrawing();
|
||||
if (this._renderInWorker) {
|
||||
this.rendererHandler.send("end", { taskID: this.taskID });
|
||||
} else {
|
||||
this.gfx.endDrawing();
|
||||
}
|
||||
InternalRenderTask.continueFnMap.delete(this.taskID);
|
||||
if (this.#rAF) {
|
||||
window.cancelAnimationFrame(this.#rAF);
|
||||
this.#rAF = null;
|
||||
@ -3348,19 +3460,35 @@ class InternalRenderTask {
|
||||
if (this.cancelled) {
|
||||
return;
|
||||
}
|
||||
this.operatorListIdx = this.gfx.executeOperatorList(
|
||||
this.operatorList,
|
||||
this.operatorListIdx,
|
||||
this._continueBound,
|
||||
this.stepper,
|
||||
this._operationsFilter
|
||||
);
|
||||
const { operatorList, operatorListIdx, taskID } = this;
|
||||
if (this._renderInWorker) {
|
||||
this.operatorListIdx = await this.rendererHandler.sendWithPromise(
|
||||
"execute",
|
||||
{
|
||||
operatorList,
|
||||
operatorListIdx,
|
||||
taskID,
|
||||
operationsFilter: this._operationsFilter,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.operatorListIdx = this.gfx.executeOperatorList(
|
||||
this.operatorList,
|
||||
this.operatorListIdx,
|
||||
this._continueBound,
|
||||
this.stepper,
|
||||
this._operationsFilter
|
||||
);
|
||||
}
|
||||
if (this.operatorListIdx === this.operatorList.argsArray.length) {
|
||||
this.running = false;
|
||||
if (this.operatorList.lastChunk) {
|
||||
this.gfx.endDrawing();
|
||||
if (this._renderInWorker) {
|
||||
this.rendererHandler.send("end", { taskID });
|
||||
} else {
|
||||
this.gfx.endDrawing();
|
||||
}
|
||||
InternalRenderTask.#canvasInUse.delete(this._canvas);
|
||||
|
||||
this.callback();
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,4 +89,14 @@ class DOMCanvasFactory extends BaseCanvasFactory {
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseCanvasFactory, DOMCanvasFactory };
|
||||
class OffscreenCanvasFactory extends BaseCanvasFactory {
|
||||
constructor({ enableHWA = false }) {
|
||||
super({ enableHWA });
|
||||
}
|
||||
|
||||
_createCanvas(width, height) {
|
||||
return new OffscreenCanvas(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseCanvasFactory, DOMCanvasFactory, OffscreenCanvasFactory };
|
||||
|
||||
@ -995,6 +995,115 @@ function renderRichText({ html, dir, className }, container) {
|
||||
container.append(fragment);
|
||||
}
|
||||
|
||||
const INITIAL_DATA = Symbol("INITIAL_DATA");
|
||||
|
||||
/**
|
||||
* A PDF document and page is built of many objects. E.g. there are objects for
|
||||
* fonts, images, rendering code, etc. These objects may get processed inside of
|
||||
* a worker. This class implements some basic methods to manage these objects.
|
||||
*/
|
||||
class PDFObjects {
|
||||
#objs = Object.create(null);
|
||||
|
||||
/**
|
||||
* Ensures there is an object defined for `objId`.
|
||||
*
|
||||
* @param {string} objId
|
||||
* @returns {Object}
|
||||
*/
|
||||
#ensureObj(objId) {
|
||||
return (this.#objs[objId] ||= {
|
||||
...Promise.withResolvers(),
|
||||
data: INITIAL_DATA,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If called *without* callback, this returns the data of `objId` but the
|
||||
* object needs to be resolved. If it isn't, this method throws.
|
||||
*
|
||||
* If called *with* a callback, the callback is called with the data of the
|
||||
* object once the object is resolved. That means, if you call this method
|
||||
* and the object is already resolved, the callback gets called right away.
|
||||
*
|
||||
* @param {string} objId
|
||||
* @param {function} [callback]
|
||||
* @returns {any}
|
||||
*/
|
||||
get(objId, callback = null) {
|
||||
// If there is a callback, then the get can be async and the object is
|
||||
// not required to be resolved right now.
|
||||
if (callback) {
|
||||
const obj = this.#ensureObj(objId);
|
||||
obj.promise.then(() => callback(obj.data));
|
||||
return null;
|
||||
}
|
||||
// If there isn't a callback, the user expects to get the resolved data
|
||||
// directly.
|
||||
const obj = this.#objs[objId];
|
||||
// If there isn't an object yet or the object isn't resolved, then the
|
||||
// data isn't ready yet!
|
||||
if (!obj || obj.data === INITIAL_DATA) {
|
||||
throw new Error(`Requesting object that isn't resolved yet ${objId}.`);
|
||||
}
|
||||
return obj.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} objId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(objId) {
|
||||
const obj = this.#objs[objId];
|
||||
return !!obj && obj.data !== INITIAL_DATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} objId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
delete(objId) {
|
||||
const obj = this.#objs[objId];
|
||||
if (!obj || obj.data === INITIAL_DATA) {
|
||||
// Only allow removing the object *after* it's been resolved.
|
||||
return false;
|
||||
}
|
||||
delete this.#objs[objId];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the object `objId` with optional `data`.
|
||||
*
|
||||
* @param {string} objId
|
||||
* @param {any} [data]
|
||||
*/
|
||||
resolve(objId, data = null) {
|
||||
const obj = this.#ensureObj(objId);
|
||||
obj.data = data;
|
||||
obj.resolve();
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const objId in this.#objs) {
|
||||
const { data } = this.#objs[objId];
|
||||
data?.bitmap?.close(); // Release any `ImageBitmap` data.
|
||||
}
|
||||
this.#objs = Object.create(null);
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
for (const objId in this.#objs) {
|
||||
const { data } = this.#objs[objId];
|
||||
|
||||
if (data === INITIAL_DATA) {
|
||||
continue;
|
||||
}
|
||||
yield [objId, data];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
applyOpacity,
|
||||
ColorScheme,
|
||||
@ -1016,6 +1125,7 @@ export {
|
||||
OutputScale,
|
||||
PageViewport,
|
||||
PDFDateString,
|
||||
PDFObjects,
|
||||
PixelsPerInch,
|
||||
RenderingCancelledException,
|
||||
renderRichText,
|
||||
|
||||
243
src/display/renderer_worker.js
Normal file
243
src/display/renderer_worker.js
Normal file
@ -0,0 +1,243 @@
|
||||
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,
|
||||
}) => {
|
||||
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
|
||||
);
|
||||
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("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 };
|
||||
@ -90,6 +90,7 @@ class MessageHandler {
|
||||
comObj.addEventListener("message", this.#onMessage.bind(this), {
|
||||
signal: this.#messageAC.signal,
|
||||
});
|
||||
comObj.start?.();
|
||||
}
|
||||
|
||||
#onMessage({ data }) {
|
||||
|
||||
@ -50,6 +50,8 @@ class BasePDFPageView {
|
||||
|
||||
renderTask = null;
|
||||
|
||||
renderTaskID = null;
|
||||
|
||||
resume = null;
|
||||
|
||||
constructor(options) {
|
||||
@ -162,7 +164,7 @@ class BasePDFPageView {
|
||||
|
||||
if (prevCanvas) {
|
||||
prevCanvas.replaceWith(canvas);
|
||||
prevCanvas.width = prevCanvas.height = 0;
|
||||
this.pdfPage.resetCanvas(this.renderTaskID);
|
||||
} else {
|
||||
onShow(canvas);
|
||||
}
|
||||
@ -190,7 +192,7 @@ class BasePDFPageView {
|
||||
return;
|
||||
}
|
||||
canvas.remove();
|
||||
canvas.width = canvas.height = 0;
|
||||
this.pdfPage.resetCanvas(this.renderTaskID);
|
||||
this.canvas = null;
|
||||
this.#resetTempCanvas();
|
||||
}
|
||||
@ -212,6 +214,8 @@ class BasePDFPageView {
|
||||
}
|
||||
};
|
||||
|
||||
this.renderTaskID = renderTask.taskID;
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
await renderTask.promise;
|
||||
|
||||
@ -303,7 +303,7 @@ class PDFThumbnailView {
|
||||
await renderTask.promise;
|
||||
} catch (e) {
|
||||
if (e instanceof RenderingCancelledException) {
|
||||
zeroCanvas(canvas);
|
||||
pdfPage.resetCanvas(renderTask.taskID);
|
||||
return;
|
||||
}
|
||||
error = e;
|
||||
@ -318,7 +318,7 @@ class PDFThumbnailView {
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
|
||||
this.#convertCanvasToImage(canvas);
|
||||
zeroCanvas(canvas);
|
||||
pdfPage.resetCanvas(renderTask.taskID);
|
||||
|
||||
this.eventBus.dispatch("thumbnailrendered", {
|
||||
source: this,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user