From 82661bdbf4ee06365ca5511ba4986e5fdf8ef424 Mon Sep 17 00:00:00 2001 From: Ujjwal Sharma Date: Mon, 12 May 2025 11:17:53 +0200 Subject: [PATCH 1/5] 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. --- src/core/document.js | 16 ++- src/core/evaluator.js | 31 +++-- src/core/worker.js | 24 +++- src/display/api.js | 216 +++++++++++++++++++++++------ src/display/canvas_factory.js | 12 +- src/display/display_utils.js | 110 +++++++++++++++ src/display/renderer_worker.js | 243 +++++++++++++++++++++++++++++++++ src/shared/message_handler.js | 1 + web/base_pdf_page_view.js | 8 +- web/pdf_thumbnail_view.js | 4 +- 10 files changed, 598 insertions(+), 67 deletions(-) create mode 100644 src/display/renderer_worker.js diff --git a/src/core/document.js b/src/core/document.js index a3892751f03b2..239ae333f9116 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -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(), diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 256a506268a06..6f369c1a5e226 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -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; } diff --git a/src/core/worker.js b/src/core/worker.js index b94fb706849d4..34e920b24dba2 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -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) { diff --git a/src/display/api.js b/src/display/api.js index 9283adecbdb47..572cfed9355c7 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -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(); } } diff --git a/src/display/canvas_factory.js b/src/display/canvas_factory.js index 988e764859828..fbe02d27348a6 100644 --- a/src/display/canvas_factory.js +++ b/src/display/canvas_factory.js @@ -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 }; diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 29c16c2ecddad..9ecd1cddac090 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -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, diff --git a/src/display/renderer_worker.js b/src/display/renderer_worker.js new file mode 100644 index 0000000000000..6b7e7c96c76b9 --- /dev/null +++ b/src/display/renderer_worker.js @@ -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 }; diff --git a/src/shared/message_handler.js b/src/shared/message_handler.js index a1af0aab27b54..633534087deda 100644 --- a/src/shared/message_handler.js +++ b/src/shared/message_handler.js @@ -90,6 +90,7 @@ class MessageHandler { comObj.addEventListener("message", this.#onMessage.bind(this), { signal: this.#messageAC.signal, }); + comObj.start?.(); } #onMessage({ data }) { diff --git a/web/base_pdf_page_view.js b/web/base_pdf_page_view.js index 4c8ab5e953e39..1290dbfc392e2 100644 --- a/web/base_pdf_page_view.js +++ b/web/base_pdf_page_view.js @@ -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; diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js index 625b75278bda9..a3591404037a4 100644 --- a/web/pdf_thumbnail_view.js +++ b/web/pdf_thumbnail_view.js @@ -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, From a163821501764a74118b0a551e1e1f3bfe13f766 Mon Sep 17 00:00:00 2001 From: Ujjwal Sharma Date: Tue, 1 Jul 2025 13:57:33 +0200 Subject: [PATCH 2/5] temporarily disable tempCanvas --- web/base_pdf_page_view.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/base_pdf_page_view.js b/web/base_pdf_page_view.js index 1290dbfc392e2..52ce1a5fcd96c 100644 --- a/web/base_pdf_page_view.js +++ b/web/base_pdf_page_view.js @@ -114,7 +114,8 @@ class BasePDFPageView { // In HCM, a final filter is applied on the canvas which means that // before it's applied we've normal colors. Consequently, to avoid to // have a final flash we just display it once all the drawing is done. - const updateOnFirstShow = !prevCanvas && !hasHCM && !hideUntilComplete; + // const updateOnFirstShow = !prevCanvas && !hasHCM && !hideUntilComplete; + const updateOnFirstShow = false; let canvas = (this.canvas = document.createElement("canvas")); From 4734888685505cf786c03bb2a2616e5a46df58e6 Mon Sep 17 00:00:00 2001 From: Ujjwal Sharma Date: Mon, 14 Jul 2025 23:08:36 +0200 Subject: [PATCH 3/5] Add a pdf.renderer.js entrypoint and gulp function to build renderer --- gulpfile.mjs | 20 ++++++++++++++++++++ src/display/api.js | 11 ++++++++--- src/pdf.renderer.js | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/pdf.renderer.js diff --git a/gulpfile.mjs b/gulpfile.mjs index 4f790b8ef0719..87fb3c6dcb022 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -589,6 +589,18 @@ function createWorkerBundle(defines) { .pipe(webpack2Stream(workerFileConfig)); } +function createRendererWorkerBundle(defines) { + const rendererWorkerFileConfig = createWebpackConfig(defines, { + filename: defines.MINIFIED ? "pdf.renderer.min.mjs" : "pdf.renderer.mjs", + library: { + type: "module", + }, + }); + return gulp + .src("./src/pdf.renderer.js", { encoding: false }) + .pipe(webpack2Stream(rendererWorkerFileConfig)); +} + function createWebBundle(defines, options) { const viewerFileConfig = createWebpackConfig( defines, @@ -1103,6 +1115,7 @@ function buildGeneric(defines, dir) { return ordered([ createMainBundle(defines).pipe(gulp.dest(dir + "build")), createWorkerBundle(defines).pipe(gulp.dest(dir + "build")), + createRendererWorkerBundle(defines).pipe(gulp.dest(dir + "build")), createSandboxBundle(defines).pipe(gulp.dest(dir + "build")), createWebBundle(defines, { defaultPreferencesDir: defines.SKIP_BABEL @@ -1294,6 +1307,7 @@ function buildMinified(defines, dir) { return ordered([ createMainBundle(defines).pipe(gulp.dest(dir + "build")), createWorkerBundle(defines).pipe(gulp.dest(dir + "build")), + createRendererWorkerBundle(defines).pipe(gulp.dest(dir + "build")), createSandboxBundle(defines).pipe(gulp.dest(dir + "build")), createImageDecodersBundle({ ...defines, IMAGE_DECODERS: true }).pipe( gulp.dest(dir + "image_decoders") @@ -1439,6 +1453,9 @@ gulp.task( createWorkerBundle(defines).pipe( gulp.dest(MOZCENTRAL_CONTENT_DIR + "build") ), + createRendererWorkerBundle(defines).pipe( + gulp.dest(MOZCENTRAL_CONTENT_DIR + "build") + ), createWebBundle(defines, { defaultPreferencesDir: "mozcentral/" }).pipe( gulp.dest(MOZCENTRAL_CONTENT_DIR + "web") ), @@ -1536,6 +1553,9 @@ gulp.task( createWorkerBundle(defines).pipe( gulp.dest(CHROME_BUILD_CONTENT_DIR + "build") ), + createRendererWorkerBundle(defines).pipe( + gulp.dest(CHROME_BUILD_CONTENT_DIR + "build") + ), createSandboxBundle(defines).pipe( gulp.dest(CHROME_BUILD_CONTENT_DIR + "build") ), diff --git a/src/display/api.js b/src/display/api.js index 572cfed9355c7..54135d76e6e85 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -2400,9 +2400,14 @@ class RendererWorker { #handler; constructor(channelPort, enableHWA) { - this.#worker = new Worker("../src/display/renderer_worker.js", { - type: "module", - }); + const src = + // eslint-disable-next-line no-nested-ternary + typeof PDFJSDev === "undefined" + ? "../src/pdf.worker.js" + : PDFJSDev.test("MOZCENTRAL") + ? "resource://pdf.js/build/pdf.worker.mjs" + : "../build/pdf.worker.mjs"; + this.#worker = new Worker(src, { type: "module" }); this.#handler = new MessageHandler("main", "renderer", this.#worker); this.#handler.send("configure", { channelPort, enableHWA }, [channelPort]); this.#handler.on("ready", () => { diff --git a/src/pdf.renderer.js b/src/pdf.renderer.js new file mode 100644 index 0000000000000..5f16c287ce09b --- /dev/null +++ b/src/pdf.renderer.js @@ -0,0 +1,18 @@ +/* Copyright 2025 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RendererMessageHandler } from "./display/renderer_worker.js"; + +export { RendererMessageHandler }; From b928461d5fddbfe59749002408c48a373876f609 Mon Sep 17 00:00:00 2001 From: Ujjwal Sharma Date: Wed, 16 Jul 2025 12:58:54 +0200 Subject: [PATCH 4/5] move common handler code to a shared file --- src/core/document.js | 15 ++--- src/core/evaluator.js | 31 +++------- src/core/worker.js | 4 ++ src/display/api.js | 110 +++------------------------------ src/display/renderer_worker.js | 109 +++----------------------------- src/shared/handle_objs.js | 108 ++++++++++++++++++++++++++++++++ 6 files changed, 144 insertions(+), 233 deletions(-) create mode 100644 src/shared/handle_objs.js diff --git a/src/core/document.js b/src/core/document.js index 239ae333f9116..f05101d5d089c 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -129,11 +129,10 @@ class Page { }; } - #createPartialEvaluator(handler, rendererHandler) { + #createPartialEvaluator(handler) { return new PartialEvaluator({ xref: this.xref, handler, - rendererHandler, pageIndex: this.pageIndex, idFactory: this._localIdFactory, fontCache: this.fontCache, @@ -450,10 +449,7 @@ class Page { const contentStreamPromise = this.getContentStream(); const resourcesPromise = this.loadResources(RESOURCES_KEYS_OPERATOR_LIST); - const partialEvaluator = this.#createPartialEvaluator( - handler, - rendererHandler - ); + const partialEvaluator = this.#createPartialEvaluator(handler); const newAnnotsByPage = !this.xfaFactory ? getNewAnnotationsMap(annotationStorage) @@ -1336,7 +1332,7 @@ class PDFDocument { this.xfaFactory.setImages(xfaImages); } - async #loadXfaFonts(handler, task, rendererHandler) { + async #loadXfaFonts(handler, task) { const acroForm = await this.pdfManager.ensureCatalog("acroForm"); if (!acroForm) { return; @@ -1362,7 +1358,6 @@ class PDFDocument { const partialEvaluator = new PartialEvaluator({ xref: this.xref, handler, - rendererHandler, pageIndex: -1, idFactory: this._globalIdFactory, fontCache, @@ -1475,9 +1470,9 @@ class PDFDocument { this.xfaFactory.appendFonts(pdfFonts, reallyMissingFonts); } - loadXfaResources(handler, task, rendererHandler) { + loadXfaResources(handler, task) { return Promise.all([ - this.#loadXfaFonts(handler, task, rendererHandler).catch(() => { + this.#loadXfaFonts(handler, task).catch(() => { // Ignore errors, to allow the document to load. }), this.#loadXfaImages(), diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 6f369c1a5e226..fcef81106ab7f 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -222,7 +222,6 @@ class PartialEvaluator { constructor({ xref, handler, - rendererHandler, pageIndex, idFactory, fontCache, @@ -235,7 +234,6 @@ class PartialEvaluator { }) { this.xref = xref; this.handler = handler; - this.rendererHandler = rendererHandler; this.pageIndex = pageIndex; this.idFactory = idFactory; this.fontCache = fontCache; @@ -555,19 +553,13 @@ class PartialEvaluator { const transfers = imgData ? [imgData.bitmap || imgData.data.buffer] : null; if (this.parsingType3Font || cacheGlobally) { - this.handler.send("commonobj", [objId, "Image", imgData], transfers); - return this.rendererHandler.send( + return this.handler.send( "commonobj", [objId, "Image", imgData], transfers ); } - this.handler.send( - "obj", - [objId, this.pageIndex, "Image", imgData], - transfers - ); - return this.rendererHandler.send( + return this.handler.send( "obj", [objId, this.pageIndex, "Image", imgData], transfers @@ -795,10 +787,11 @@ 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.rendererHandler.sendWithPromise( - "commonobj", - [objId, "CopyLocalImage", { imageRef }] - ); + const localLength = await this.sendWithPromise("commonobj", [ + objId, + "CopyLocalImage", + { imageRef }, + ]); if (localLength) { this.globalImageCache.setData(imageRef, globalCacheData); @@ -1028,7 +1021,6 @@ class PartialEvaluator { state.font = translated.font; translated.send(this.handler); - translated.send(this.rendererHandler); return translated.loadedName; } @@ -1048,7 +1040,7 @@ class PartialEvaluator { PartialEvaluator.buildFontPaths( font, glyphs, - this.rendererHandler, + this.handler, this.options ); } @@ -1526,15 +1518,8 @@ 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; } diff --git a/src/core/worker.js b/src/core/worker.js index 34e920b24dba2..6e663f394c050 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -873,6 +873,10 @@ 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); }); diff --git a/src/display/api.js b/src/display/api.js index 54135d76e6e85..f6c2de80de164 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -45,7 +45,6 @@ import { RenderingCancelledException, StatTimer, } from "./display_utils.js"; -import { FontFaceObject, FontLoader } from "./font_loader.js"; import { getDataProp, getFactoryUrlProp, @@ -68,7 +67,7 @@ import { DOMCMapReaderFactory } from "display-cmap_reader_factory"; import { DOMFilterFactory } from "./filter_factory.js"; import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory"; import { DOMWasmFactory } from "display-wasm_factory"; -import { FontInfo } from "../shared/obj-bin-transform.js"; +import { FontLoader } from "./font_loader.js"; import { GlobalWorkerOptions } from "./worker_options.js"; import { Metadata } from "./metadata.js"; import { OptionalContentConfig } from "./optional_content_config.js"; @@ -76,6 +75,7 @@ import { PDFDataTransportStream } from "./transport_stream.js"; import { PDFFetchStream } from "display-fetch_stream"; import { PDFNetworkStream } from "display-network"; import { PDFNodeStream } from "display-node_stream"; +import { setupHandler } from "../shared/handle_objs.js"; import { TextLayer } from "./text_layer.js"; import { XfaText } from "./xfa_text.js"; @@ -2828,105 +2828,13 @@ class WorkerTransport { page._startRenderPage(data.transparency, data.cacheKey); }); - messageHandler.on("commonobj", ([id, type, exportedData]) => { - if (this.destroyed) { - return null; // Ignore any pending requests if the worker was terminated. - } - - 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; - } - - const fontData = new FontInfo(exportedData); - const inspectFont = - this._params.pdfBug && globalThis.FontInspector?.enabled - ? (font, url) => globalThis.FontInspector.fontAdded(font, url) - : null; - const font = new FontFaceObject( - fontData, - inspectFont, - exportedData.extra, - exportedData.charProcOperatorList - ); - - this.fontLoader - .bind(font) - .catch(() => messageHandler.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.clearData(); - } - this.commonObjs.resolve(id, font); - }); - break; - case "CopyLocalImage": - const { imageRef } = exportedData; - assert(imageRef, "The imageRef must be defined."); - - for (const pageProxy of this.#pageCache.values()) { - for (const [, data] of pageProxy.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; - }); - - messageHandler.on("obj", ([id, pageIndex, type, imageData]) => { - if (this.destroyed) { - // Ignore any pending requests if the worker was terminated. - return; - } - - const pageProxy = this.#pageCache.get(pageIndex); - if (pageProxy.objs.has(id)) { - return; - } - // Don't store data *after* cleanup has successfully run, see bug 1854145. - if (pageProxy._intentStates.size === 0) { - imageData?.bitmap?.close(); // Release any `ImageBitmap` data. - return; - } - - switch (type) { - case "Image": - case "Pattern": - pageProxy.objs.resolve(id, imageData); - break; - default: - throw new Error(`Got unknown object type ${type}`); - } - }); + setupHandler( + messageHandler, + this.destroyed, + this.commonObjs, + this.#pageCache, + this.fontLoader + ); messageHandler.on("DocProgress", data => { if (this.destroyed) { diff --git a/src/display/renderer_worker.js b/src/display/renderer_worker.js index 6b7e7c96c76b9..bb2464bd436c2 100644 --- a/src/display/renderer_worker.js +++ b/src/display/renderer_worker.js @@ -1,10 +1,11 @@ -import { assert, warn } from "../shared/util.js"; -import { FontFaceObject, FontLoader } from "./font_loader.js"; +import { assert } from "../shared/util.js"; import { CanvasGraphics } from "./canvas.js"; import { DOMFilterFactory } from "./filter_factory.js"; +import { FontLoader } from "./font_loader.js"; import { MessageHandler } from "../shared/message_handler.js"; import { OffscreenCanvasFactory } from "./canvas_factory.js"; import { PDFObjects } from "./display_utils.js"; +import { setupHandler } from "../shared/handle_objs.js"; class RendererMessageHandler { static #commonObjs = new PDFObjects(); @@ -56,19 +57,13 @@ class RendererMessageHandler { }); 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); - }); + setupHandler( + workerHandler, + terminated, + this.#commonObjs, + this.#objsMap, + this.#fontLoader + ); }); mainHandler.on( @@ -154,90 +149,6 @@ class RendererMessageHandler { 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 }; diff --git a/src/shared/handle_objs.js b/src/shared/handle_objs.js new file mode 100644 index 0000000000000..4c45d501c5799 --- /dev/null +++ b/src/shared/handle_objs.js @@ -0,0 +1,108 @@ +import { assert, warn } from "../shared/util.js"; +import { FontFaceObject } from "../display/font_loader.js"; +import { FontInfo } from "../shared/obj-bin-transform.js"; + +function setupHandler(handler, destroyed, commonObjs, pages, fontLoader) { + handler.on("commonobj", ([id, type, exportedData]) => { + if (destroyed) { + return null; // Ignore any pending requests if the worker was terminated. + } + + if (commonObjs.has(id)) { + return null; + } + + switch (type) { + case "Font": + if ("error" in exportedData) { + const exportedError = exportedData.error; + warn(`Error during font loading: ${exportedError}`); + commonObjs.resolve(id, exportedError); + break; + } + + const fontData = new FontInfo(exportedData); + const inspectFont = // TODO: Fix this + // this._params.pdfBug && globalThis.FontInspector?.enabled + // ? (font, url) => globalThis.FontInspector.fontAdded(font, url) + // : null; + null; + const font = new FontFaceObject( + fontData, + inspectFont, + exportedData.extra, + exportedData.charProcOperatorList + ); + + 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.clearData(); + } + commonObjs.resolve(id, font); + }); + break; + case "CopyLocalImage": + const { imageRef } = exportedData; + assert(imageRef, "The imageRef must be defined."); + + for (const page of pages.values()) { + for (const [, data] of page.objs) { + if (data?.ref !== imageRef) { + continue; + } + if (!data.dataLen) { + return null; + } + commonObjs.resolve(id, structuredClone(data)); + return data.dataLen; + } + } + break; + case "FontPath": + case "Image": + case "Pattern": + commonObjs.resolve(id, exportedData); + break; + default: + throw new Error(`Got unknown common object type ${type}`); + } + + return null; + }); + + handler.on("obj", ([id, pageIndex, type, imageData]) => { + if (destroyed) { + // Ignore any pending requests if the worker was terminated. + return; + } + + const page = pages.get(pageIndex); + if (page.objs.has(id)) { + return; + } + // Don't store data *after* cleanup has successfully run, see bug 1854145. + if (page._intentStates.size === 0) { + imageData?.bitmap?.close(); // Release any `ImageBitmap` data. + return; + } + + switch (type) { + case "Image": + case "Pattern": + page.objs.resolve(id, imageData); + break; + default: + throw new Error(`Got unknown object type ${type}`); + } + }); +} + +export { setupHandler }; From 23cfb34bb77d7f876be3f7b9b134279109db8cbe Mon Sep 17 00:00:00 2001 From: Ujjwal Sharma Date: Fri, 3 Oct 2025 16:54:58 +0530 Subject: [PATCH 5/5] Make dependencyTracker work with the renderer --- src/display/api.js | 13 ++++++++++--- src/display/renderer_worker.js | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index f6c2de80de164..d43357931598d 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -3335,9 +3335,16 @@ class InternalRenderTask { this.graphicsReadyCallback ||= this._continueBound; return; } - this.gfx.dependencyTracker?.growOperationsCount( - this.operatorList.fnArray.length - ); + if (!this._renderInWorker) { + this.gfx.dependencyTracker?.growOperationsCount( + this.operatorList.fnArray.length + ); + } else { + this.rendererHandler.send("growOperationsCount", { + taskID: this.taskID, + newOperatorListLength: this.operatorList.fnArray.length, + }); + } this.stepper?.updateOperatorList(this.operatorList); if (this.running) { diff --git a/src/display/renderer_worker.js b/src/display/renderer_worker.js index bb2464bd436c2..c14c81e7e5095 100644 --- a/src/display/renderer_worker.js +++ b/src/display/renderer_worker.js @@ -79,6 +79,7 @@ class RendererMessageHandler { transparency, background, optionalContentConfig, + dependencyTracker, }) => { assert(!this.#tasks.has(taskID), "Task already initialized"); const ctx = canvas.getContext("2d", { @@ -94,7 +95,8 @@ class RendererMessageHandler { this.#filterFactory, { optionalContentConfig }, map, - colors + colors, + dependencyTracker ); gfx.beginDrawing({ transform, viewport, transparency, background }); this.#tasks.set(taskID, { canvas, gfx }); @@ -148,6 +150,18 @@ class RendererMessageHandler { mainHandler.destroy(); mainHandler = null; }); + + 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); + } + ); } }