From 9e8d4e4d4686ce36c783f22b655ec9f0bfc9b8f4 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 9 Mar 2025 14:50:43 +0100 Subject: [PATCH] [api-minor] Attempt to support fetching the raw data of the PDF document from the `PDFDocumentLoadingTask`-instance (issue 15085) The new API-functionality will allow a PDF document to be downloaded in the viewer e.g. while the PasswordPrompt is open, or in cases when document initialization failed. Normally the raw data of the PDF document would be accessed via the `PDFDocumentProxy.prototype.getData` method, however in these cases the `PDFDocumentProxy`-instance isn't available. --- src/display/api.js | 77 +++++++++++++++++++++++++++---------------- test/unit/api_spec.js | 41 +++++++++++++++++++++++ web/app.js | 4 ++- 3 files changed, 92 insertions(+), 30 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 815e7ee23..84f7c2d83 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -629,39 +629,48 @@ const isValidExplicitDest = _isValidExplicitDest.bind( class PDFDocumentLoadingTask { static #docId = 0; - constructor() { - this._capability = Promise.withResolvers(); - this._transport = null; - this._worker = null; + /** + * @private + */ + _capability = Promise.withResolvers(); - /** - * Unique identifier for the document loading task. - * @type {string} - */ - this.docId = `d${PDFDocumentLoadingTask.#docId++}`; + /** + * @private + */ + _transport = null; - /** - * Whether the loading task is destroyed or not. - * @type {boolean} - */ - this.destroyed = false; + /** + * @private + */ + _worker = null; - /** - * Callback to request a password if a wrong or no password was provided. - * The callback receives two parameters: a function that should be called - * with the new password, and a reason (see {@link PasswordResponses}). - * @type {function} - */ - this.onPassword = null; + /** + * Unique identifier for the document loading task. + * @type {string} + */ + docId = `d${PDFDocumentLoadingTask.#docId++}`; - /** - * Callback to be able to monitor the loading progress of the PDF file - * (necessary to implement e.g. a loading bar). - * The callback receives an {@link OnProgressParameters} argument. - * @type {function} - */ - this.onProgress = null; - } + /** + * Whether the loading task is destroyed or not. + * @type {boolean} + */ + destroyed = false; + + /** + * Callback to request a password if a wrong or no password was provided. + * The callback receives two parameters: a function that should be called + * with the new password, and a reason (see {@link PasswordResponses}). + * @type {function} + */ + onPassword = null; + + /** + * Callback to be able to monitor the loading progress of the PDF file + * (necessary to implement e.g. a loading bar). + * The callback receives an {@link OnProgressParameters} argument. + * @type {function} + */ + onProgress = null; /** * Promise for document loading task completion. @@ -699,6 +708,16 @@ class PDFDocumentLoadingTask { this._worker?.destroy(); this._worker = null; } + + /** + * Attempt to fetch the raw data of the PDF document, when e.g. + * - An exception was thrown during document initialization. + * - An `onPassword` callback is delaying initialization. + * @returns {Promise} + */ + async getData() { + return this._transport.getData(); + } } /** diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 3acc4c38f..2f0fdc040 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -828,6 +828,47 @@ describe("api", function () { await loadingTask.destroy(); }); + + it("gets data, on failure, from `PDFDocumentLoadingTask`-instance", async function () { + const typedArrayPdf = await DefaultFileReaderFactory.fetch({ + path: TEST_PDFS_PATH + "issue6010_1.pdf", + }); + + // Sanity check to make sure that we fetched the entire PDF file. + expect(typedArrayPdf instanceof Uint8Array).toEqual(true); + expect(typedArrayPdf.length).toEqual(1116); + + const loadingTask = getDocument(typedArrayPdf.slice()); + expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true); + + let passwordData = null; + // Attach the callback that is used to request a password; + // similarly to how the default viewer handles passwords. + loadingTask.onPassword = async (updatePassword, reason) => { + passwordData = await loadingTask.getData(); + + updatePassword(new Error("Should reject the loadingTask.")); + }; + + try { + await loadingTask.promise; + + // Shouldn't get here. + expect(false).toEqual(true); + } catch (ex) { + expect(ex instanceof PasswordException).toEqual(true); + expect(ex.code).toEqual(PasswordResponses.NEED_PASSWORD); + } + + // Ensure that the raw PDF document can be fetched while + // an `onPassword` callback is delaying initialization... + expect(passwordData).toEqual(typedArrayPdf); + // ... and once an exception has stopped initialization. + const data = await loadingTask.getData(); + expect(data).toEqual(typedArrayPdf); + + await loadingTask.destroy(); + }); }); describe("PDFWorker", function () { diff --git a/web/app.js b/web/app.js index 05cb832f3..eed21ddef 100644 --- a/web/app.js +++ b/web/app.js @@ -1153,7 +1153,9 @@ const PDFViewerApplication = { async download() { let data; try { - data = await this.pdfDocument.getData(); + data = await (this.pdfDocument + ? this.pdfDocument.getData() + : this.pdfLoadingTask.getData()); } catch { // When the PDF document isn't ready, simply download using the URL. }