diff --git a/src/display/api.js b/src/display/api.js index 4efa0a72f72e4..86564b63e5f7d 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -146,9 +146,6 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) { * cross-site Access-Control requests should be made using credentials such * as cookies or authorization headers. The default is `false`. * @property {string} [password] - For decrypting password-protected PDFs. - * @property {TypedArray} [initialData] - A typed array with the first portion - * or all of the pdf data. Used by the extension since some data is already - * loaded before the switch to range requests. * @property {number} [length] - The PDF file length. It's used for progress * reports and range requests operations. * @property {PDFDataRangeTransport} [range] - Allows for using a custom range @@ -192,6 +189,14 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) { * @property {number} [maxImageSize] - The maximum allowed image size in total * pixels, i.e. width * height. Images above this value will not be rendered. * Use -1 for no limit, which is also the default value. + * @property {boolean} [transferPdfData] - Determines if we can transfer + * TypedArrays used for loading the PDF file, utilized together with: + * - The `data`-option, for the `getDocument` function. + * - The `initialData`-option, for the `PDFDataRangeTransport` constructor. + * - The `chunk`-option, for the `PDFDataTransportStream._onReceiveData` + * method. + * This will help reduce main-thread memory usage, however it will take + * ownership of the TypedArrays. The default value is `false`. * @property {boolean} [isEvalSupported] - Determines if we can evaluate strings * as JavaScript. Primarily used to improve performance of font rendering, and * when parsing PDF functions. The default value is `true`. @@ -342,6 +347,7 @@ function getDocument(src) { params.StandardFontDataFactory = params.StandardFontDataFactory || DefaultStandardFontDataFactory; params.ignoreErrors = params.stopAtErrors !== true; + params.transferPdfData = params.transferPdfData === true; params.fontExtraProperties = params.fontExtraProperties === true; params.pdfBug = params.pdfBug === true; params.enableXfa = params.enableXfa === true; @@ -439,6 +445,7 @@ function getDocument(src) { { length: params.length, initialData: params.initialData, + transferPdfData: params.transferPdfData, progressiveDone: params.progressiveDone, contentDispositionFilename: params.contentDispositionFilename, disableRange: params.disableRange, @@ -513,6 +520,9 @@ async function _fetchDocument(worker, source, pdfDataRangeTransport, docId) { source.contentDispositionFilename = pdfDataRangeTransport.contentDispositionFilename; } + const transfers = + source.transferPdfData && source.data ? [source.data.buffer] : null; + const workerId = await worker.messageHandler.sendWithPromise( "GetDocRequest", // Only send the required properties, and *not* the entire `source` object. @@ -542,15 +552,10 @@ async function _fetchDocument(worker, source, pdfDataRangeTransport, docId) { ? source.standardFontDataUrl : null, }, - } + }, + transfers ); - // Release the TypedArray data, when it exists, since it's no longer needed - // on the main-thread *after* it's been sent to the worker-thread. - if (source.data) { - source.data = null; - } - if (worker.destroyed) { throw new Error("Worker was destroyed"); } diff --git a/src/display/transport_stream.js b/src/display/transport_stream.js index 7cdee5407552d..874cb58055ce8 100644 --- a/src/display/transport_stream.js +++ b/src/display/transport_stream.js @@ -18,27 +18,41 @@ import { isPdfFile } from "./display_utils.js"; /** @implements {IPDFStream} */ class PDFDataTransportStream { - constructor(params, pdfDataRangeTransport) { + #transferPdfData = false; + + constructor( + { + length, + initialData, + transferPdfData = false, + progressiveDone = false, + contentDispositionFilename = null, + disableRange = false, + disableStream = false, + }, + pdfDataRangeTransport + ) { assert( pdfDataRangeTransport, 'PDFDataTransportStream - missing required "pdfDataRangeTransport" argument.' ); this._queuedChunks = []; - this._progressiveDone = params.progressiveDone || false; - this._contentDispositionFilename = - params.contentDispositionFilename || null; + this.#transferPdfData = transferPdfData; + this._progressiveDone = progressiveDone; + this._contentDispositionFilename = contentDispositionFilename; - const initialData = params.initialData; if (initialData?.length > 0) { - const buffer = new Uint8Array(initialData).buffer; + const buffer = this.#transferPdfData + ? initialData.buffer + : new Uint8Array(initialData).buffer; this._queuedChunks.push(buffer); } this._pdfDataRangeTransport = pdfDataRangeTransport; - this._isStreamingSupported = !params.disableStream; - this._isRangeSupported = !params.disableRange; - this._contentLength = params.length; + this._isStreamingSupported = !disableStream; + this._isRangeSupported = !disableRange; + this._contentLength = length; this._fullRequestReader = null; this._rangeReaders = []; @@ -62,9 +76,12 @@ class PDFDataTransportStream { this._pdfDataRangeTransport.transportReady(); } - _onReceiveData(args) { - const buffer = new Uint8Array(args.chunk).buffer; - if (args.begin === undefined) { + _onReceiveData({ begin, chunk }) { + const buffer = this.#transferPdfData + ? chunk.buffer + : new Uint8Array(chunk).buffer; + + if (begin === undefined) { if (this._fullRequestReader) { this._fullRequestReader._enqueue(buffer); } else { @@ -72,7 +89,7 @@ class PDFDataTransportStream { } } else { const found = this._rangeReaders.some(function (rangeReader) { - if (rangeReader._begin !== args.begin) { + if (rangeReader._begin !== begin) { return false; } rangeReader._enqueue(buffer); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 933cd6130e3cf..53cb12de9e564 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -193,6 +193,45 @@ describe("api", function () { expect(data[0] instanceof PDFDocumentProxy).toEqual(true); expect(data[1].loaded / data[1].total).toEqual(1); + // Check that the TypedArray wasn't transferred. + expect(typedArrayPdf.length).toEqual(basicApiFileLength); + + await loadingTask.destroy(); + }); + + it("creates pdf doc from TypedArray, with `transferPdfData` set", async function () { + if (isNodeJS) { + pending("Worker is not supported in Node.js."); + } + const typedArrayPdf = await DefaultFileReaderFactory.fetch({ + path: TEST_PDFS_PATH + basicApiFileName, + }); + + // Sanity check to make sure that we fetched the entire PDF file. + expect(typedArrayPdf instanceof Uint8Array).toEqual(true); + expect(typedArrayPdf.length).toEqual(basicApiFileLength); + + const loadingTask = getDocument({ + data: typedArrayPdf, + transferPdfData: true, + }); + expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true); + + const progressReportedCapability = createPromiseCapability(); + loadingTask.onProgress = function (data) { + progressReportedCapability.resolve(data); + }; + + const data = await Promise.all([ + loadingTask.promise, + progressReportedCapability.promise, + ]); + expect(data[0] instanceof PDFDocumentProxy).toEqual(true); + expect(data[1].loaded / data[1].total).toEqual(1); + + // Check that the TypedArray was transferred. + expect(typedArrayPdf.length).toEqual(0); + await loadingTask.destroy(); }); @@ -3257,6 +3296,47 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`) expect(pdfPage.rotate).toEqual(0); expect(fetches).toBeGreaterThan(2); + // Check that the TypedArray wasn't transferred. + expect(initialData.length).toEqual(initialDataLength); + + await loadingTask.destroy(); + }); + + it("should fetch document info and page using ranges, with `transferPdfData` set", async function () { + if (isNodeJS) { + pending("Worker is not supported in Node.js."); + } + const initialDataLength = 4000; + let fetches = 0; + + const data = await dataPromise; + const initialData = new Uint8Array(data.subarray(0, initialDataLength)); + const transport = new PDFDataRangeTransport(data.length, initialData); + transport.requestDataRange = function (begin, end) { + fetches++; + waitSome(function () { + transport.onDataProgress(4000); + transport.onDataRange( + begin, + new Uint8Array(data.subarray(begin, end)) + ); + }); + }; + + const loadingTask = getDocument({ + range: transport, + transferPdfData: true, + }); + const pdfDocument = await loadingTask.promise; + expect(pdfDocument.numPages).toEqual(14); + + const pdfPage = await pdfDocument.getPage(10); + expect(pdfPage.rotate).toEqual(0); + expect(fetches).toBeGreaterThan(2); + + // Check that the TypedArray was transferred. + expect(initialData.length).toEqual(0); + await loadingTask.destroy(); }); diff --git a/web/app_options.js b/web/app_options.js index d0cafe2427538..bc98b00db2a11 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -270,6 +270,11 @@ const defaultOptions = { : "../web/standard_fonts/", kind: OptionKind.API, }, + transferPdfData: { + /** @type {boolean} */ + value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL"), + kind: OptionKind.API, + }, verbosity: { /** @type {number} */ value: 1,