diff --git a/examples/mobile-viewer/viewer.mjs b/examples/mobile-viewer/viewer.mjs index bf496c91e..edf7d7079 100644 --- a/examples/mobile-viewer/viewer.mjs +++ b/examples/mobile-viewer/viewer.mjs @@ -46,19 +46,13 @@ const PDFViewerApplication = { * @returns {Promise} - Returns the promise, which is resolved when document * is opened. */ - open(params) { + async open(params) { if (this.pdfLoadingTask) { - // We need to destroy already opened document - return this.close().then( - function () { - // ... and repeat the open() call. - return this.open(params); - }.bind(this) - ); + // We need to destroy already opened document. + await this.close(); } - const url = params.url; - const self = this; + const { url } = params; this.setTitleUsingUrl(url); // Loading document. @@ -70,24 +64,22 @@ const PDFViewerApplication = { }); this.pdfLoadingTask = loadingTask; - loadingTask.onProgress = function (progressData) { - self.progress(progressData.loaded / progressData.total); - }; + loadingTask.onProgress = evt => this.progress(evt.percent); return loadingTask.promise.then( - function (pdfDocument) { + pdfDocument => { // Document loaded, specifying document for the viewer. - self.pdfDocument = pdfDocument; - self.pdfViewer.setDocument(pdfDocument); - self.pdfLinkService.setDocument(pdfDocument); - self.pdfHistory.initialize({ + this.pdfDocument = pdfDocument; + this.pdfViewer.setDocument(pdfDocument); + this.pdfLinkService.setDocument(pdfDocument); + this.pdfHistory.initialize({ fingerprint: pdfDocument.fingerprints[0], }); - self.loadingBar.hide(); - self.setTitleUsingMetadata(pdfDocument); + this.loadingBar.hide(); + this.setTitleUsingMetadata(pdfDocument); }, - function (reason) { + reason => { let key = "pdfjs-loading-error"; if (reason instanceof pdfjsLib.InvalidPDFException) { key = "pdfjs-invalid-file-error"; @@ -96,10 +88,10 @@ const PDFViewerApplication = { ? "pdfjs-missing-file-error" : "pdfjs-unexpected-response-error"; } - self.l10n.get(key).then(msg => { - self.error(msg, { message: reason?.message }); + this.l10n.get(key).then(msg => { + this.error(msg, { message: reason.message }); }); - self.loadingBar.hide(); + this.loadingBar.hide(); } ); }, @@ -109,9 +101,9 @@ const PDFViewerApplication = { * @returns {Promise} - Returns the promise, which is resolved when all * destruction is completed. */ - close() { + async close() { if (!this.pdfLoadingTask) { - return Promise.resolve(); + return; } const promise = this.pdfLoadingTask.destroy(); @@ -128,7 +120,7 @@ const PDFViewerApplication = { } } - return promise; + await promise; }, get loadingBar() { @@ -152,48 +144,36 @@ const PDFViewerApplication = { this.setTitle(title); }, - setTitleUsingMetadata(pdfDocument) { - const self = this; - pdfDocument.getMetadata().then(function (data) { - const info = data.info, - metadata = data.metadata; - self.documentInfo = info; - self.metadata = metadata; + async setTitleUsingMetadata(pdfDocument) { + const { info, metadata } = await pdfDocument.getMetadata(); + this.documentInfo = info; + this.metadata = metadata; - // Provides some basic debug information - console.log( - "PDF " + - pdfDocument.fingerprints[0] + - " [" + - info.PDFFormatVersion + - " " + - (info.Producer || "-").trim() + - " / " + - (info.Creator || "-").trim() + - "]" + - " (PDF.js: " + - (pdfjsLib.version || "-") + - ")" - ); + // Provides some basic debug information + console.log( + `PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` + + `${(metadata?.get("pdf:producer") || info.Producer || "-").trim()} / ` + + `${(metadata?.get("xmp:creatortool") || info.Creator || "-").trim()}` + + `] (PDF.js: ${pdfjsLib.version || "?"} [${pdfjsLib.build || "?"}])` + ); - let pdfTitle; - if (metadata && metadata.has("dc:title")) { - const title = metadata.get("dc:title"); - // Ghostscript sometimes returns 'Untitled', so prevent setting the - // title to 'Untitled. - if (title !== "Untitled") { - pdfTitle = title; - } + let pdfTitle; + if (metadata && metadata.has("dc:title")) { + const title = metadata.get("dc:title"); + // Ghostscript sometimes returns 'Untitled', so prevent setting the + // title to 'Untitled. + if (title !== "Untitled") { + pdfTitle = title; } + } - if (!pdfTitle && info && info.Title) { - pdfTitle = info.Title; - } + if (!pdfTitle && info && info.Title) { + pdfTitle = info.Title; + } - if (pdfTitle) { - self.setTitle(pdfTitle + " - " + document.title); - } - }); + if (pdfTitle) { + this.setTitle(pdfTitle + " - " + document.title); + } }, setTitle: function pdfViewSetTitle(title) { @@ -223,8 +203,7 @@ const PDFViewerApplication = { console.error(`${message}\n\n${moreInfoText.join("\n")}`); }, - progress: function pdfViewProgress(level) { - const percent = Math.round(level * 100); + progress(percent) { // Updating the bar if value increases. if (percent > this.loadingBar.percent || isNaN(percent)) { this.loadingBar.percent = percent; diff --git a/src/core/chunked_stream.js b/src/core/chunked_stream.js index c77f62531..94c01fc03 100644 --- a/src/core/chunked_stream.js +++ b/src/core/chunked_stream.js @@ -18,6 +18,12 @@ import { assert } from "../shared/util.js"; import { Stream } from "./stream.js"; class ChunkedStream extends Stream { + progressiveDataLength = 0; + + _lastSuccessfulEnsureByteChunk = -1; // Single-entry cache + + _loadedChunks = new Set(); + constructor(length, chunkSize, manager) { super( /* arrayBuffer = */ new Uint8Array(length), @@ -27,11 +33,8 @@ class ChunkedStream extends Stream { ); this.chunkSize = chunkSize; - this._loadedChunks = new Set(); this.numChunks = Math.ceil(length / chunkSize); this.manager = manager; - this.progressiveDataLength = 0; - this.lastSuccessfulEnsureByteChunk = -1; // Single-entry cache } // If a particular stream does not implement one or more of these methods, @@ -106,14 +109,14 @@ class ChunkedStream extends Stream { if (chunk > this.numChunks) { return; } - if (chunk === this.lastSuccessfulEnsureByteChunk) { + if (chunk === this._lastSuccessfulEnsureByteChunk) { return; } if (!this._loadedChunks.has(chunk)) { throw new MissingDataException(pos, pos + 1); } - this.lastSuccessfulEnsureByteChunk = chunk; + this._lastSuccessfulEnsureByteChunk = chunk; } ensureRange(begin, end) { @@ -257,40 +260,37 @@ class ChunkedStream extends Stream { } class ChunkedStreamManager { - constructor(pdfNetworkStream, args) { + aborted = false; + + currRequestId = 0; + + _chunksNeededByRequest = new Map(); + + _loadedStreamCapability = Promise.withResolvers(); + + _promisesByRequest = new Map(); + + _requestsByChunk = new Map(); + + constructor(pdfStream, args) { this.length = args.length; this.chunkSize = args.rangeChunkSize; this.stream = new ChunkedStream(this.length, this.chunkSize, this); - this.pdfNetworkStream = pdfNetworkStream; + this.pdfStream = pdfStream; this.disableAutoFetch = args.disableAutoFetch; this.msgHandler = args.msgHandler; - - this.currRequestId = 0; - - this._chunksNeededByRequest = new Map(); - this._requestsByChunk = new Map(); - this._promisesByRequest = new Map(); - this.progressiveDataLength = 0; - this.aborted = false; - - this._loadedStreamCapability = Promise.withResolvers(); } sendRequest(begin, end) { - const rangeReader = this.pdfNetworkStream.getRangeReader(begin, end); - if (!rangeReader.isStreamingSupported) { - rangeReader.onProgress = this.onProgress.bind(this); - } + const rangeReader = this.pdfStream.getRangeReader(begin, end); - let chunks = [], - loaded = 0; + let chunks = []; return new Promise((resolve, reject) => { const readChunk = ({ value, done }) => { try { if (done) { - const chunkData = arrayBuffersToBytes(chunks); + resolve(arrayBuffersToBytes(chunks)); chunks = null; - resolve(chunkData); return; } if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { @@ -299,12 +299,6 @@ class ChunkedStreamManager { "readChunk (sendRequest) - expected an ArrayBuffer." ); } - loaded += value.byteLength; - - if (rangeReader.isStreamingSupported) { - this.onProgress({ loaded }); - } - chunks.push(value); rangeReader.read().then(readChunk, reject); } catch (e) { @@ -446,34 +440,26 @@ class ChunkedStreamManager { return groupedChunks; } - onProgress(args) { - this.msgHandler.send("DocProgress", { - loaded: this.stream.numChunksLoaded * this.chunkSize + args.loaded, - total: this.length, - }); - } - onReceiveData(args) { + const { chunkSize, length, stream } = this; + const chunk = args.chunk; const isProgressive = args.begin === undefined; - const begin = isProgressive ? this.progressiveDataLength : args.begin; + const begin = isProgressive ? stream.progressiveDataLength : args.begin; const end = begin + chunk.byteLength; - const beginChunk = Math.floor(begin / this.chunkSize); + const beginChunk = Math.floor(begin / chunkSize); const endChunk = - end < this.length - ? Math.floor(end / this.chunkSize) - : Math.ceil(end / this.chunkSize); + end < length ? Math.floor(end / chunkSize) : Math.ceil(end / chunkSize); if (isProgressive) { - this.stream.onReceiveProgressiveData(chunk); - this.progressiveDataLength = end; + stream.onReceiveProgressiveData(chunk); } else { - this.stream.onReceiveData(begin, chunk); + stream.onReceiveData(begin, chunk); } - if (this.stream.isDataLoaded) { - this._loadedStreamCapability.resolve(this.stream); + if (stream.isDataLoaded) { + this._loadedStreamCapability.resolve(stream); } const loadedRequests = []; @@ -502,16 +488,16 @@ class ChunkedStreamManager { // unfetched chunk of the PDF file. if (!this.disableAutoFetch && this._requestsByChunk.size === 0) { let nextEmptyChunk; - if (this.stream.numChunksLoaded === 1) { + if (stream.numChunksLoaded === 1) { // This is a special optimization so that after fetching the first // chunk, rather than fetching the second chunk, we fetch the last // chunk. - const lastChunk = this.stream.numChunks - 1; - if (!this.stream.hasChunk(lastChunk)) { + const lastChunk = stream.numChunks - 1; + if (!stream.hasChunk(lastChunk)) { nextEmptyChunk = lastChunk; } } else { - nextEmptyChunk = this.stream.nextEmptyChunk(endChunk); + nextEmptyChunk = stream.nextEmptyChunk(endChunk); } if (Number.isInteger(nextEmptyChunk)) { this._requestChunks([nextEmptyChunk]); @@ -525,8 +511,8 @@ class ChunkedStreamManager { } this.msgHandler.send("DocProgress", { - loaded: this.stream.numChunksLoaded * this.chunkSize, - total: this.length, + loaded: stream.numChunksLoaded * chunkSize, + total: length, }); } @@ -544,7 +530,7 @@ class ChunkedStreamManager { abort(reason) { this.aborted = true; - this.pdfNetworkStream?.cancelAllRequests(reason); + this.pdfStream?.cancelAllRequests(reason); for (const capability of this._promisesByRequest.values()) { capability.reject(reason); diff --git a/src/core/worker.js b/src/core/worker.js index a167730f9..ae3c48be9 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -219,23 +219,23 @@ class WorkerMessageHandler { return new LocalPdfManager(pdfManagerArgs); } - const pdfStream = new PDFWorkerStream(handler), - fullRequest = pdfStream.getFullReader(); + const pdfStream = new PDFWorkerStream({ msgHandler: handler }), + fullReader = pdfStream.getFullReader(); const pdfManagerCapability = Promise.withResolvers(); let newPdfManager, cachedChunks = [], loaded = 0; - fullRequest.headersReady + fullReader.headersReady .then(function () { - if (!fullRequest.isRangeSupported) { + if (!fullReader.isRangeSupported) { return; } pdfManagerArgs.source = pdfStream; - pdfManagerArgs.length = fullRequest.contentLength; + pdfManagerArgs.length = fullReader.contentLength; // We don't need auto-fetch when streaming is enabled. - pdfManagerArgs.disableAutoFetch ||= fullRequest.isStreamingSupported; + pdfManagerArgs.disableAutoFetch ||= fullReader.isStreamingSupported; newPdfManager = new NetworkPdfManager(pdfManagerArgs); // There may be a chance that `newPdfManager` is not initialized for @@ -282,10 +282,10 @@ class WorkerMessageHandler { } loaded += value.byteLength; - if (!fullRequest.isStreamingSupported) { + if (!fullReader.isStreamingSupported) { handler.send("DocProgress", { loaded, - total: Math.max(loaded, fullRequest.contentLength || 0), + total: Math.max(loaded, fullReader.contentLength || 0), }); } @@ -294,12 +294,12 @@ class WorkerMessageHandler { } else { cachedChunks.push(value); } - fullRequest.read().then(readChunk, reject); + fullReader.read().then(readChunk, reject); } catch (e) { reject(e); } }; - fullRequest.read().then(readChunk, reject); + fullReader.read().then(readChunk, reject); }).catch(function (e) { pdfManagerCapability.reject(e); cancelXHRs = null; diff --git a/src/core/worker_stream.js b/src/core/worker_stream.js index 047935316..66db66322 100644 --- a/src/core/worker_stream.js +++ b/src/core/worker_stream.js @@ -13,77 +13,35 @@ * limitations under the License. */ -import { assert } from "../shared/util.js"; +import { + BasePDFStream, + BasePDFStreamRangeReader, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; -/** @implements {IPDFStream} */ -class PDFWorkerStream { - constructor(msgHandler) { - this._msgHandler = msgHandler; - this._contentLength = null; - this._fullRequestReader = null; - this._rangeRequestReaders = []; - } - - getFullReader() { - assert( - !this._fullRequestReader, - "PDFWorkerStream.getFullReader can only be called once." - ); - this._fullRequestReader = new PDFWorkerStreamReader(this._msgHandler); - return this._fullRequestReader; - } - - getRangeReader(begin, end) { - const reader = new PDFWorkerStreamRangeReader(begin, end, this._msgHandler); - this._rangeRequestReaders.push(reader); - return reader; - } - - cancelAllRequests(reason) { - this._fullRequestReader?.cancel(reason); - - for (const reader of this._rangeRequestReaders.slice(0)) { - reader.cancel(reason); - } +class PDFWorkerStream extends BasePDFStream { + constructor(source) { + super(source, PDFWorkerStreamReader, PDFWorkerStreamRangeReader); } } -/** @implements {IPDFStreamReader} */ -class PDFWorkerStreamReader { - constructor(msgHandler) { - this._msgHandler = msgHandler; - this.onProgress = null; +class PDFWorkerStreamReader extends BasePDFStreamReader { + _reader = null; - this._contentLength = null; - this._isRangeSupported = false; - this._isStreamingSupported = false; + constructor(stream) { + super(stream); + const { msgHandler } = stream._source; - const readableStream = this._msgHandler.sendWithStream("GetReader"); + const readableStream = msgHandler.sendWithStream("GetReader"); this._reader = readableStream.getReader(); - this._headersReady = this._msgHandler - .sendWithPromise("ReaderHeadersReady") - .then(data => { - this._isStreamingSupported = data.isStreamingSupported; - this._isRangeSupported = data.isRangeSupported; - this._contentLength = data.contentLength; - }); - } + msgHandler.sendWithPromise("ReaderHeadersReady").then(data => { + this._contentLength = data.contentLength; + this._isStreamingSupported = data.isStreamingSupported; + this._isRangeSupported = data.isRangeSupported; - get headersReady() { - return this._headersReady; - } - - get contentLength() { - return this._contentLength; - } - - get isStreamingSupported() { - return this._isStreamingSupported; - } - - get isRangeSupported() { - return this._isRangeSupported; + this._headersCapability.resolve(); + }, this._headersCapability.reject); } async read() { @@ -101,23 +59,20 @@ class PDFWorkerStreamReader { } } -/** @implements {IPDFStreamRangeReader} */ -class PDFWorkerStreamRangeReader { - constructor(begin, end, msgHandler) { - this._msgHandler = msgHandler; - this.onProgress = null; +class PDFWorkerStreamRangeReader extends BasePDFStreamRangeReader { + _reader = null; - const readableStream = this._msgHandler.sendWithStream("GetRangeReader", { + constructor(stream, begin, end) { + super(stream, begin, end); + const { msgHandler } = stream._source; + + const readableStream = msgHandler.sendWithStream("GetRangeReader", { begin, end, }); this._reader = readableStream.getReader(); } - get isStreamingSupported() { - return false; - } - async read() { const { value, done } = await this._reader.read(); if (done) { diff --git a/src/display/api.js b/src/display/api.js index 5695c3f1d..f088b9dc1 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -25,6 +25,7 @@ import { getVerbosityLevel, info, isNodeJS, + MathClamp, RenderingIntentFlag, setVerbosityLevel, shadow, @@ -440,6 +441,7 @@ function getDocument(src = {}) { ownerDocument, pdfBug, styleElement, + enableHWA, loadingParams: { disableAutoFetch, enableXfa, @@ -463,7 +465,8 @@ function getDocument(src = {}) { let networkStream; if (rangeTransport) { - networkStream = new PDFDataTransportStream(rangeTransport, { + networkStream = new PDFDataTransportStream({ + pdfDataRangeTransport: rangeTransport, disableRange, disableStream, }); @@ -508,8 +511,7 @@ function getDocument(src = {}) { task, networkStream, transportParams, - transportFactory, - enableHWA + transportFactory ); task._transport = transport; messageHandler.send("Ready", null); @@ -524,6 +526,8 @@ function getDocument(src = {}) { * @typedef {Object} OnProgressParameters * @property {number} loaded - Currently loaded number of bytes. * @property {number} total - Total number of bytes in the PDF file. + * @property {number} percent - Currently loaded percentage, as an integer value + * in the [0, 100] range. If `total` is undefined, the percentage is `NaN`. */ /** @@ -2391,8 +2395,14 @@ class PDFWorker { * @ignore */ class WorkerTransport { + downloadInfoCapability = Promise.withResolvers(); + + #fullReader = null; + #methodPromises = new Map(); + #networkStream = null; + #pageCache = new Map(); #pagePromises = new Map(); @@ -2403,21 +2413,17 @@ class WorkerTransport { #pagesMapper = PagesMapper.instance; - constructor( - messageHandler, - loadingTask, - networkStream, - params, - factory, - enableHWA - ) { + constructor(messageHandler, loadingTask, networkStream, params, factory) { this.messageHandler = messageHandler; this.loadingTask = loadingTask; + this.#networkStream = networkStream; + this.commonObjs = new PDFObjects(); this.fontLoader = new FontLoader({ ownerDocument: params.ownerDocument, styleElement: params.styleElement, }); + this.enableHWA = params.enableHWA; this.loadingParams = params.loadingParams; this._params = params; @@ -2430,12 +2436,6 @@ class WorkerTransport { this.destroyed = false; this.destroyCapability = null; - this._networkStream = networkStream; - this._fullReader = null; - this._lastProgress = null; - this.downloadInfoCapability = Promise.withResolvers(); - this.enableHWA = enableHWA; - this.setupMessageHandler(); this.#pagesMapper.addListener(this.#updateCaches.bind(this)); @@ -2493,6 +2493,14 @@ class WorkerTransport { return promise; } + #onProgress({ loaded, total }) { + this.loadingTask.onProgress?.({ + loaded, + total, + percent: MathClamp(Math.round((loaded / total) * 100), 0, 100), + }); + } + get annotationStorage() { return shadow(this, "annotationStorage", new AnnotationStorage()); } @@ -2604,7 +2612,7 @@ class WorkerTransport { this.filterFactory.destroy(); TextLayer.cleanup(); - this._networkStream?.cancelAllRequests( + this.#networkStream?.cancelAllRequests( new AbortException("Worker was terminated.") ); @@ -2621,18 +2629,16 @@ class WorkerTransport { messageHandler.on("GetReader", (data, sink) => { assert( - this._networkStream, - "GetReader - no `IPDFStream` instance available." + this.#networkStream, + "GetReader - no `BasePDFStream` instance available." ); - this._fullReader = this._networkStream.getFullReader(); - this._fullReader.onProgress = evt => { - this._lastProgress = { - loaded: evt.loaded, - total: evt.total, - }; - }; + this.#fullReader = this.#networkStream.getFullReader(); + // If stream or range turn out to be disabled, once `headersReady` is + // resolved, this is our only way to report loading progress. + this.#fullReader.onProgress = evt => this.#onProgress(evt); + sink.onPull = () => { - this._fullReader + this.#fullReader .read() .then(function ({ value, done }) { if (done) { @@ -2653,7 +2659,7 @@ class WorkerTransport { }; sink.onCancel = reason => { - this._fullReader.cancel(reason); + this.#fullReader.cancel(reason); sink.ready.catch(readyReason => { if (this.destroyed) { @@ -2665,40 +2671,29 @@ class WorkerTransport { }); messageHandler.on("ReaderHeadersReady", async data => { - await this._fullReader.headersReady; + await this.#fullReader.headersReady; const { isStreamingSupported, isRangeSupported, contentLength } = - this._fullReader; + this.#fullReader; - // If stream or range are disabled, it's our only way to report - // loading progress. - if (!isStreamingSupported || !isRangeSupported) { - if (this._lastProgress) { - loadingTask.onProgress?.(this._lastProgress); - } - this._fullReader.onProgress = evt => { - loadingTask.onProgress?.({ - loaded: evt.loaded, - total: evt.total, - }); - }; + if (isStreamingSupported && isRangeSupported) { + this.#fullReader.onProgress = null; // See comment in "GetReader" above. } - return { isStreamingSupported, isRangeSupported, contentLength }; }); messageHandler.on("GetRangeReader", (data, sink) => { assert( - this._networkStream, - "GetRangeReader - no `IPDFStream` instance available." + this.#networkStream, + "GetRangeReader - no `BasePDFStream` instance available." ); - const rangeReader = this._networkStream.getRangeReader( + const rangeReader = this.#networkStream.getRangeReader( data.begin, data.end ); // When streaming is enabled, it's possible that the data requested here - // has already been fetched via the `_fullRequestReader` implementation. + // has already been fetched via the `#fullReader` implementation. // However, given that the PDF data is loaded asynchronously on the // main-thread and then sent via `postMessage` to the worker-thread, // it may not have been available during parsing (hence the attempt to @@ -2706,7 +2701,7 @@ class WorkerTransport { // // To avoid wasting time and resources here, we'll thus *not* dispatch // range requests if the data was already loaded but has not been sent to - // the worker-thread yet (which will happen via the `_fullRequestReader`). + // the worker-thread yet (which will happen via the `#fullReader`). if (!rangeReader) { sink.close(); return; @@ -2780,10 +2775,7 @@ class WorkerTransport { messageHandler.on("DataLoaded", data => { // For consistency: Ensure that progress is always reported when the // entire PDF file has been loaded, regardless of how it was fetched. - loadingTask.onProgress?.({ - loaded: data.length, - total: data.length, - }); + this.#onProgress({ loaded: data.length, total: data.length }); this.downloadInfoCapability.resolve(data); }); @@ -2906,10 +2898,7 @@ class WorkerTransport { if (this.destroyed) { return; // Ignore any pending requests if the worker was terminated. } - loadingTask.onProgress?.({ - loaded: data.loaded, - total: data.total, - }); + this.#onProgress(data); }); messageHandler.on("FetchBinaryData", async data => { @@ -2950,7 +2939,7 @@ class WorkerTransport { isPureXfa: !!this._htmlForXfa, numPages: this._numPages, annotationStorage: map, - filename: this._fullReader?.filename ?? null, + filename: this.#fullReader?.filename ?? null, }, transfer ) @@ -3118,8 +3107,8 @@ class WorkerTransport { .then(results => ({ info: results[0], metadata: results[1] ? new Metadata(results[1]) : null, - contentDispositionFilename: this._fullReader?.filename ?? null, - contentLength: this._fullReader?.contentLength ?? null, + contentDispositionFilename: this.#fullReader?.filename ?? null, + contentLength: this.#fullReader?.contentLength ?? null, hasStructTree: results[2], })); this.#methodPromises.set(name, promise); diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js index acbf3c868..c1b1b636d 100644 --- a/src/display/fetch_stream.js +++ b/src/display/fetch_stream.js @@ -13,14 +13,19 @@ * limitations under the License. */ -import { AbortException, assert, warn } from "../shared/util.js"; +import { AbortException, warn } from "../shared/util.js"; +import { + BasePDFStream, + BasePDFStreamRangeReader, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; import { createHeaders, createResponseError, + ensureResponseOrigin, extractFilenameFromHeader, getResponseOrigin, validateRangeRequestCapabilities, - validateResponseStatus, } from "./network_utils.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { @@ -29,15 +34,21 @@ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { ); } -function createFetchOptions(headers, withCredentials, abortController) { - return { +function fetchUrl(url, headers, withCredentials, abortController) { + return fetch(url, { method: "GET", headers, signal: abortController.signal, mode: "cors", credentials: withCredentials ? "include" : "same-origin", redirect: "follow", - }; + }); +} + +function ensureResponseStatus(status, url) { + if (status !== 200 && status !== 206) { + throw createResponseError(status, url); + } } function getArrayBuffer(val) { @@ -51,86 +62,44 @@ function getArrayBuffer(val) { return new Uint8Array(val).buffer; } -/** @implements {IPDFStream} */ -class PDFFetchStream { +class PDFFetchStream extends BasePDFStream { _responseOrigin = null; constructor(source) { - this.source = source; + super(source, PDFFetchStreamReader, PDFFetchStreamRangeReader); this.isHttp = /^https?:/i.test(source.url); this.headers = createHeaders(this.isHttp, source.httpHeaders); - - this._fullRequestReader = null; - this._rangeRequestReaders = []; - } - - get _progressiveDataLength() { - return this._fullRequestReader?._loaded ?? 0; - } - - getFullReader() { - assert( - !this._fullRequestReader, - "PDFFetchStream.getFullReader can only be called once." - ); - this._fullRequestReader = new PDFFetchStreamReader(this); - return this._fullRequestReader; - } - - getRangeReader(begin, end) { - if (end <= this._progressiveDataLength) { - return null; - } - const reader = new PDFFetchStreamRangeReader(this, begin, end); - this._rangeRequestReaders.push(reader); - return reader; - } - - cancelAllRequests(reason) { - this._fullRequestReader?.cancel(reason); - - for (const reader of this._rangeRequestReaders.slice(0)) { - reader.cancel(reason); - } } } -/** @implements {IPDFStreamReader} */ -class PDFFetchStreamReader { - constructor(stream) { - this._stream = stream; - this._reader = null; - this._loaded = 0; - this._filename = null; - const source = stream.source; - this._withCredentials = source.withCredentials || false; - this._contentLength = source.length; - this._headersCapability = Promise.withResolvers(); - this._disableRange = source.disableRange || false; - this._rangeChunkSize = source.rangeChunkSize; - if (!this._rangeChunkSize && !this._disableRange) { - this._disableRange = true; - } +class PDFFetchStreamReader extends BasePDFStreamReader { + _abortController = new AbortController(); - this._abortController = new AbortController(); - this._isStreamingSupported = !source.disableStream; - this._isRangeSupported = !source.disableRange; + _reader = null; + + constructor(stream) { + super(stream); + const { + disableRange, + disableStream, + length, + rangeChunkSize, + url, + withCredentials, + } = stream._source; + + this._contentLength = length; + this._isStreamingSupported = !disableStream; + this._isRangeSupported = !disableRange; // Always create a copy of the headers. const headers = new Headers(stream.headers); - const url = source.url; - fetch( - url, - createFetchOptions(headers, this._withCredentials, this._abortController) - ) + fetchUrl(url, headers, withCredentials, this._abortController) .then(response => { stream._responseOrigin = getResponseOrigin(response.url); - if (!validateResponseStatus(response.status)) { - throw createResponseError(response.status, url); - } + ensureResponseStatus(response.status, url); this._reader = response.body.getReader(); - this._headersCapability.resolve(); const responseHeaders = response.headers; @@ -138,8 +107,8 @@ class PDFFetchStreamReader { validateRangeRequestCapabilities({ responseHeaders, isHttp: stream.isHttp, - rangeChunkSize: this._rangeChunkSize, - disableRange: this._disableRange, + rangeChunkSize, + disableRange, }); this._isRangeSupported = allowRangeRequests; @@ -153,30 +122,10 @@ class PDFFetchStreamReader { if (!this._isStreamingSupported && this._isRangeSupported) { this.cancel(new AbortException("Streaming is disabled.")); } + + this._headersCapability.resolve(); }) .catch(this._headersCapability.reject); - - this.onProgress = null; - } - - get headersReady() { - return this._headersCapability.promise; - } - - get filename() { - return this._filename; - } - - get contentLength() { - return this._contentLength; - } - - get isRangeSupported() { - return this._isRangeSupported; - } - - get isStreamingSupported() { - return this._isStreamingSupported; } async read() { @@ -200,48 +149,32 @@ class PDFFetchStreamReader { } } -/** @implements {IPDFStreamRangeReader} */ -class PDFFetchStreamRangeReader { - constructor(stream, begin, end) { - this._stream = stream; - this._reader = null; - this._loaded = 0; - const source = stream.source; - this._withCredentials = source.withCredentials || false; - this._readCapability = Promise.withResolvers(); - this._isStreamingSupported = !source.disableStream; +class PDFFetchStreamRangeReader extends BasePDFStreamRangeReader { + _abortController = new AbortController(); + + _readCapability = Promise.withResolvers(); + + _reader = null; + + constructor(stream, begin, end) { + super(stream, begin, end); + const { url, withCredentials } = stream._source; - this._abortController = new AbortController(); // Always create a copy of the headers. const headers = new Headers(stream.headers); headers.append("Range", `bytes=${begin}-${end - 1}`); - const url = source.url; - fetch( - url, - createFetchOptions(headers, this._withCredentials, this._abortController) - ) + fetchUrl(url, headers, withCredentials, this._abortController) .then(response => { const responseOrigin = getResponseOrigin(response.url); - if (responseOrigin !== stream._responseOrigin) { - throw new Error( - `Expected range response-origin "${responseOrigin}" to match "${stream._responseOrigin}".` - ); - } - if (!validateResponseStatus(response.status)) { - throw createResponseError(response.status, url); - } - this._readCapability.resolve(); + ensureResponseOrigin(responseOrigin, stream._responseOrigin); + ensureResponseStatus(response.status, url); this._reader = response.body.getReader(); + + this._readCapability.resolve(); }) .catch(this._readCapability.reject); - - this.onProgress = null; - } - - get isStreamingSupported() { - return this._isStreamingSupported; } async read() { @@ -250,9 +183,6 @@ class PDFFetchStreamRangeReader { if (done) { return { value, done }; } - this._loaded += value.byteLength; - this.onProgress?.({ loaded: this._loaded }); - return { value: getArrayBuffer(value), done: false }; } diff --git a/src/display/network.js b/src/display/network.js index f4a83bfc2..018df2024 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -14,9 +14,15 @@ */ import { assert, stringToBytes, warn } from "../shared/util.js"; +import { + BasePDFStream, + BasePDFStreamRangeReader, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; import { createHeaders, createResponseError, + ensureResponseOrigin, extractFilenameFromHeader, getResponseOrigin, validateRangeRequestCapabilities, @@ -35,18 +41,13 @@ function getArrayBuffer(val) { return typeof val !== "string" ? val : stringToBytes(val).buffer; } -/** @implements {IPDFStream} */ -class PDFNetworkStream { +class PDFNetworkStream extends BasePDFStream { #pendingRequests = new WeakMap(); - _fullRequestReader = null; - - _rangeRequestReaders = []; - _responseOrigin = null; constructor(source) { - this._source = source; + super(source, PDFNetworkStreamReader, PDFNetworkStreamRangeReader); this.url = source.url; this.isHttp = /^https?:/i.test(this.url); this.headers = createHeaders(this.isHttp, source.httpHeaders); @@ -160,70 +161,44 @@ class PDFNetworkStream { } } - getFullReader() { - assert( - !this._fullRequestReader, - "PDFNetworkStream.getFullReader can only be called once." - ); - this._fullRequestReader = new PDFNetworkStreamFullRequestReader(this); - return this._fullRequestReader; - } - getRangeReader(begin, end) { - const reader = new PDFNetworkStreamRangeRequestReader(this, begin, end); - reader.onClosed = () => { - const i = this._rangeRequestReaders.indexOf(reader); - if (i >= 0) { - this._rangeRequestReaders.splice(i, 1); - } - }; - this._rangeRequestReaders.push(reader); - return reader; - } + const reader = super.getRangeReader(begin, end); - cancelAllRequests(reason) { - this._fullRequestReader?.cancel(reason); - - for (const reader of this._rangeRequestReaders.slice(0)) { - reader.cancel(reason); + if (reader) { + reader.onClosed = () => this._rangeReaders.delete(reader); } + return reader; } } -/** @implements {IPDFStreamReader} */ -class PDFNetworkStreamFullRequestReader { +class PDFNetworkStreamReader extends BasePDFStreamReader { + _cachedChunks = []; + + _done = false; + + _requests = []; + + _storedError = null; + constructor(stream) { - this._stream = stream; - const { disableRange, length, rangeChunkSize } = stream._source; + super(stream); + const { length } = stream._source; + + this._contentLength = length; + // Note that `XMLHttpRequest` doesn't support streaming, and range requests + // will be enabled (if supported) in `this.#onHeadersReceived` below. this._fullRequestXhr = stream._request({ - onHeadersReceived: this._onHeadersReceived.bind(this), - onDone: this._onDone.bind(this), - onError: this._onError.bind(this), - onProgress: this._onProgress.bind(this), + onHeadersReceived: this.#onHeadersReceived.bind(this), + onDone: this.#onDone.bind(this), + onError: this.#onError.bind(this), + onProgress: this.#onProgress.bind(this), }); - this._headersCapability = Promise.withResolvers(); - this._disableRange = disableRange || false; - this._contentLength = length; // Optional - this._rangeChunkSize = rangeChunkSize; - if (!this._rangeChunkSize && !this._disableRange) { - this._disableRange = true; - } - - this._isStreamingSupported = false; - this._isRangeSupported = false; - - this._cachedChunks = []; - this._requests = []; - this._done = false; - this._storedError = undefined; - this._filename = null; - - this.onProgress = null; } - _onHeadersReceived() { + #onHeadersReceived() { const stream = this._stream; + const { disableRange, rangeChunkSize } = stream._source; const fullRequestXhr = this._fullRequestXhr; stream._responseOrigin = getResponseOrigin(fullRequestXhr.responseURL); @@ -246,8 +221,8 @@ class PDFNetworkStreamFullRequestReader { validateRangeRequestCapabilities({ responseHeaders, isHttp: stream.isHttp, - rangeChunkSize: this._rangeChunkSize, - disableRange: this._disableRange, + rangeChunkSize, + disableRange, }); if (allowRangeRequests) { @@ -269,10 +244,10 @@ class PDFNetworkStreamFullRequestReader { this._headersCapability.resolve(); } - _onDone(chunk) { + #onDone(chunk) { if (this._requests.length > 0) { - const requestCapability = this._requests.shift(); - requestCapability.resolve({ value: chunk, done: false }); + const capability = this._requests.shift(); + capability.resolve({ value: chunk, done: false }); } else { this._cachedChunks.push(chunk); } @@ -280,49 +255,29 @@ class PDFNetworkStreamFullRequestReader { if (this._cachedChunks.length > 0) { return; } - for (const requestCapability of this._requests) { - requestCapability.resolve({ value: undefined, done: true }); + for (const capability of this._requests) { + capability.resolve({ value: undefined, done: true }); } this._requests.length = 0; } - _onError(status) { + #onError(status) { this._storedError = createResponseError(status, this._stream.url); this._headersCapability.reject(this._storedError); - for (const requestCapability of this._requests) { - requestCapability.reject(this._storedError); + for (const capability of this._requests) { + capability.reject(this._storedError); } this._requests.length = 0; this._cachedChunks.length = 0; } - _onProgress(evt) { + #onProgress(evt) { this.onProgress?.({ loaded: evt.loaded, total: evt.lengthComputable ? evt.total : this._contentLength, }); } - get filename() { - return this._filename; - } - - get isRangeSupported() { - return this._isRangeSupported; - } - - get isStreamingSupported() { - return this._isStreamingSupported; - } - - get contentLength() { - return this._contentLength; - } - - get headersReady() { - return this._headersCapability.promise; - } - async read() { await this._headersCapability.promise; @@ -336,16 +291,16 @@ class PDFNetworkStreamFullRequestReader { if (this._done) { return { value: undefined, done: true }; } - const requestCapability = Promise.withResolvers(); - this._requests.push(requestCapability); - return requestCapability.promise; + const capability = Promise.withResolvers(); + this._requests.push(capability); + return capability.promise; } cancel(reason) { this._done = true; this._headersCapability.reject(reason); - for (const requestCapability of this._requests) { - requestCapability.resolve({ value: undefined, done: true }); + for (const capability of this._requests) { + capability.resolve({ value: undefined, done: true }); } this._requests.length = 0; @@ -354,74 +309,64 @@ class PDFNetworkStreamFullRequestReader { } } -/** @implements {IPDFStreamRangeReader} */ -class PDFNetworkStreamRangeRequestReader { +class PDFNetworkStreamRangeReader extends BasePDFStreamRangeReader { onClosed = null; + _done = false; + + _queuedChunk = null; + + _requests = []; + + _storedError = null; + constructor(stream, begin, end) { - this._stream = stream; + super(stream, begin, end); this._requestXhr = stream._request({ begin, end, - onHeadersReceived: this._onHeadersReceived.bind(this), - onDone: this._onDone.bind(this), - onError: this._onError.bind(this), - onProgress: this._onProgress.bind(this), + onHeadersReceived: this.#onHeadersReceived.bind(this), + onDone: this.#onDone.bind(this), + onError: this.#onError.bind(this), + onProgress: null, }); - this._requests = []; - this._queuedChunk = null; - this._done = false; - this._storedError = undefined; - - this.onProgress = null; } - _onHeadersReceived() { + #onHeadersReceived() { const responseOrigin = getResponseOrigin(this._requestXhr?.responseURL); - - if (responseOrigin !== this._stream._responseOrigin) { - this._storedError = new Error( - `Expected range response-origin "${responseOrigin}" to match "${this._stream._responseOrigin}".` - ); - this._onError(0); + try { + ensureResponseOrigin(responseOrigin, this._stream._responseOrigin); + } catch (ex) { + this._storedError = ex; + this.#onError(0); } } - _onDone(chunk) { + #onDone(chunk) { if (this._requests.length > 0) { - const requestCapability = this._requests.shift(); - requestCapability.resolve({ value: chunk, done: false }); + const capability = this._requests.shift(); + capability.resolve({ value: chunk, done: false }); } else { this._queuedChunk = chunk; } this._done = true; - for (const requestCapability of this._requests) { - requestCapability.resolve({ value: undefined, done: true }); + for (const capability of this._requests) { + capability.resolve({ value: undefined, done: true }); } this._requests.length = 0; - this.onClosed?.(this); + this.onClosed?.(); } - _onError(status) { + #onError(status) { this._storedError ??= createResponseError(status, this._stream.url); - for (const requestCapability of this._requests) { - requestCapability.reject(this._storedError); + for (const capability of this._requests) { + capability.reject(this._storedError); } this._requests.length = 0; this._queuedChunk = null; } - _onProgress(evt) { - if (!this.isStreamingSupported) { - this.onProgress?.({ loaded: evt.loaded }); - } - } - - get isStreamingSupported() { - return false; - } - async read() { if (this._storedError) { throw this._storedError; @@ -434,20 +379,20 @@ class PDFNetworkStreamRangeRequestReader { if (this._done) { return { value: undefined, done: true }; } - const requestCapability = Promise.withResolvers(); - this._requests.push(requestCapability); - return requestCapability.promise; + const capability = Promise.withResolvers(); + this._requests.push(capability); + return capability.promise; } cancel(reason) { this._done = true; - for (const requestCapability of this._requests) { - requestCapability.resolve({ value: undefined, done: true }); + for (const capability of this._requests) { + capability.resolve({ value: undefined, done: true }); } this._requests.length = 0; this._stream._abortRequest(this._requestXhr); - this.onClosed?.(this); + this.onClosed?.(); } } diff --git a/src/display/network_utils.js b/src/display/network_utils.js index 5af942a9f..0e8a7cffc 100644 --- a/src/display/network_utils.js +++ b/src/display/network_utils.js @@ -107,15 +107,19 @@ function createResponseError(status, url) { ); } -function validateResponseStatus(status) { - return status === 200 || status === 206; +function ensureResponseOrigin(rangeOrigin, origin) { + if (rangeOrigin !== origin) { + throw new Error( + `Expected range response-origin "${rangeOrigin}" to match "${origin}".` + ); + } } export { createHeaders, createResponseError, + ensureResponseOrigin, extractFilenameFromHeader, getResponseOrigin, validateRangeRequestCapabilities, - validateResponseStatus, }; diff --git a/src/display/node_stream.js b/src/display/node_stream.js index e2f3b9cc6..35dfaefdf 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -15,6 +15,11 @@ /* globals process */ import { AbortException, assert, warn } from "../shared/util.js"; +import { + BasePDFStream, + BasePDFStreamRangeReader, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; import { createResponseError } from "./network_utils.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { @@ -60,70 +65,28 @@ function getArrayBuffer(val) { return new Uint8Array(val).buffer; } -class PDFNodeStream { +class PDFNodeStream extends BasePDFStream { constructor(source) { - this.source = source; + super(source, PDFNodeStreamReader, PDFNodeStreamRangeReader); this.url = parseUrlOrPath(source.url); assert( this.url.protocol === "file:", "PDFNodeStream only supports file:// URLs." ); - - this._fullRequestReader = null; - this._rangeRequestReaders = []; - } - - get _progressiveDataLength() { - return this._fullRequestReader?._loaded ?? 0; - } - - getFullReader() { - assert( - !this._fullRequestReader, - "PDFNodeStream.getFullReader can only be called once." - ); - this._fullRequestReader = new PDFNodeStreamFsFullReader(this); - return this._fullRequestReader; - } - - getRangeReader(begin, end) { - if (end <= this._progressiveDataLength) { - return null; - } - const rangeReader = new PDFNodeStreamFsRangeReader(this, begin, end); - this._rangeRequestReaders.push(rangeReader); - return rangeReader; - } - - cancelAllRequests(reason) { - this._fullRequestReader?.cancel(reason); - - for (const reader of this._rangeRequestReaders.slice(0)) { - reader.cancel(reason); - } } } -class PDFNodeStreamFsFullReader { - _headersCapability = Promise.withResolvers(); - +class PDFNodeStreamReader extends BasePDFStreamReader { _reader = null; constructor(stream) { - this.onProgress = null; - const source = stream.source; - this._contentLength = source.length; // optional - this._loaded = 0; - this._filename = null; + super(stream); + const { disableRange, disableStream, length, rangeChunkSize } = + stream._source; - this._disableRange = source.disableRange || false; - this._rangeChunkSize = source.rangeChunkSize; - if (!this._rangeChunkSize && !this._disableRange) { - this._disableRange = true; - } - - this._isStreamingSupported = !source.disableStream; - this._isRangeSupported = !source.disableRange; + this._contentLength = length; + this._isStreamingSupported = !disableStream; + this._isRangeSupported = !disableRange; const url = stream.url; const fs = process.getBuiltinModule("fs"); @@ -136,7 +99,7 @@ class PDFNodeStreamFsFullReader { this._reader = readableStream.getReader(); const { size } = stat; - if (size <= 2 * this._rangeChunkSize) { + if (size <= 2 * rangeChunkSize) { // The file size is smaller than the size of two chunks, so it doesn't // make any sense to abort the request and retry with a range request. this._isRangeSupported = false; @@ -160,26 +123,6 @@ class PDFNodeStreamFsFullReader { }); } - get headersReady() { - return this._headersCapability.promise; - } - - get filename() { - return this._filename; - } - - get contentLength() { - return this._contentLength; - } - - get isRangeSupported() { - return this._isRangeSupported; - } - - get isStreamingSupported() { - return this._isStreamingSupported; - } - async read() { await this._headersCapability.promise; const { value, done } = await this._reader.read(); @@ -200,16 +143,13 @@ class PDFNodeStreamFsFullReader { } } -class PDFNodeStreamFsRangeReader { +class PDFNodeStreamRangeReader extends BasePDFStreamRangeReader { _readCapability = Promise.withResolvers(); _reader = null; constructor(stream, begin, end) { - this.onProgress = null; - this._loaded = 0; - const source = stream.source; - this._isStreamingSupported = !source.disableStream; + super(stream, begin, end); const url = stream.url; const fs = process.getBuiltinModule("fs"); @@ -228,19 +168,12 @@ class PDFNodeStreamFsRangeReader { } } - get isStreamingSupported() { - return this._isStreamingSupported; - } - async read() { await this._readCapability.promise; const { value, done } = await this._reader.read(); if (done) { return { value, done }; } - this._loaded += value.length; - this.onProgress?.({ loaded: this._loaded }); - return { value: getArrayBuffer(value), done: false }; } diff --git a/src/display/transport_stream.js b/src/display/transport_stream.js index 36bef6dc1..57423932b 100644 --- a/src/display/transport_stream.js +++ b/src/display/transport_stream.js @@ -13,185 +13,137 @@ * limitations under the License. */ -/** @typedef {import("../interfaces").IPDFStream} IPDFStream */ -/** @typedef {import("../interfaces").IPDFStreamReader} IPDFStreamReader */ -// eslint-disable-next-line max-len -/** @typedef {import("../interfaces").IPDFStreamRangeReader} IPDFStreamRangeReader */ - +import { + BasePDFStream, + BasePDFStreamRangeReader, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; import { assert } from "../shared/util.js"; import { isPdfFile } from "./display_utils.js"; -/** @implements {IPDFStream} */ -class PDFDataTransportStream { - constructor( - pdfDataRangeTransport, - { disableRange = false, disableStream = false } - ) { - assert( - pdfDataRangeTransport, - 'PDFDataTransportStream - missing required "pdfDataRangeTransport" argument.' - ); - const { length, initialData, progressiveDone, contentDispositionFilename } = - pdfDataRangeTransport; +function getArrayBuffer(val) { + // Prevent any possible issues by only transferring a Uint8Array that + // completely "utilizes" its underlying ArrayBuffer. + return val instanceof Uint8Array && val.byteLength === val.buffer.byteLength + ? val.buffer + : new Uint8Array(val).buffer; +} - this._queuedChunks = []; - this._progressiveDone = progressiveDone; - this._contentDispositionFilename = contentDispositionFilename; +class PDFDataTransportStream extends BasePDFStream { + _progressiveDone = false; + + _queuedChunks = []; + + constructor(source) { + super( + source, + PDFDataTransportStreamReader, + PDFDataTransportStreamRangeReader + ); + const { pdfDataRangeTransport } = source; + const { initialData, progressiveDone } = pdfDataRangeTransport; if (initialData?.length > 0) { - // Prevent any possible issues by only transferring a Uint8Array that - // completely "utilizes" its underlying ArrayBuffer. - const buffer = - initialData instanceof Uint8Array && - initialData.byteLength === initialData.buffer.byteLength - ? initialData.buffer - : new Uint8Array(initialData).buffer; + const buffer = getArrayBuffer(initialData); this._queuedChunks.push(buffer); } - - this._pdfDataRangeTransport = pdfDataRangeTransport; - this._isStreamingSupported = !disableStream; - this._isRangeSupported = !disableRange; - this._contentLength = length; - - this._fullRequestReader = null; - this._rangeReaders = []; + this._progressiveDone = progressiveDone; pdfDataRangeTransport.addRangeListener((begin, chunk) => { - this._onReceiveData({ begin, chunk }); + this.#onReceiveData(begin, chunk); }); pdfDataRangeTransport.addProgressListener((loaded, total) => { - this._onProgress({ loaded, total }); + if (total !== undefined) { + this._fullReader?.onProgress?.({ loaded, total }); + } }); pdfDataRangeTransport.addProgressiveReadListener(chunk => { - this._onReceiveData({ chunk }); + this.#onReceiveData(/* begin = */ undefined, chunk); }); pdfDataRangeTransport.addProgressiveDoneListener(() => { - this._onProgressiveDone(); + this._fullReader?.progressiveDone(); + this._progressiveDone = true; }); pdfDataRangeTransport.transportReady(); } - _onReceiveData({ begin, chunk }) { - // Prevent any possible issues by only transferring a Uint8Array that - // completely "utilizes" its underlying ArrayBuffer. - const buffer = - chunk instanceof Uint8Array && - chunk.byteLength === chunk.buffer.byteLength - ? chunk.buffer - : new Uint8Array(chunk).buffer; + #onReceiveData(begin, chunk) { + const buffer = getArrayBuffer(chunk); if (begin === undefined) { - if (this._fullRequestReader) { - this._fullRequestReader._enqueue(buffer); + if (this._fullReader) { + this._fullReader._enqueue(buffer); } else { this._queuedChunks.push(buffer); } } else { - const found = this._rangeReaders.some(function (rangeReader) { - if (rangeReader._begin !== begin) { - return false; - } - rangeReader._enqueue(buffer); - return true; - }); + const rangeReader = this._rangeReaders + .keys() + .find(r => r._begin === begin); + assert( - found, - "_onReceiveData - no `PDFDataTransportStreamRangeReader` instance found." + rangeReader, + "#onReceiveData - no `PDFDataTransportStreamRangeReader` instance found." ); - } - } - - get _progressiveDataLength() { - return this._fullRequestReader?._loaded ?? 0; - } - - _onProgress(evt) { - if (evt.total === undefined) { - // Reporting to first range reader, if it exists. - this._rangeReaders[0]?.onProgress?.({ loaded: evt.loaded }); - } else { - this._fullRequestReader?.onProgress?.({ - loaded: evt.loaded, - total: evt.total, - }); - } - } - - _onProgressiveDone() { - this._fullRequestReader?.progressiveDone(); - this._progressiveDone = true; - } - - _removeRangeReader(reader) { - const i = this._rangeReaders.indexOf(reader); - if (i >= 0) { - this._rangeReaders.splice(i, 1); + rangeReader._enqueue(buffer); } } getFullReader() { - assert( - !this._fullRequestReader, - "PDFDataTransportStream.getFullReader can only be called once." - ); - const queuedChunks = this._queuedChunks; + const reader = super.getFullReader(); this._queuedChunks = null; - return new PDFDataTransportStreamReader( - this, - queuedChunks, - this._progressiveDone, - this._contentDispositionFilename - ); + return reader; } getRangeReader(begin, end) { - if (end <= this._progressiveDataLength) { - return null; + const reader = super.getRangeReader(begin, end); + + if (reader) { + reader.onDone = () => this._rangeReaders.delete(reader); + + this._source.pdfDataRangeTransport.requestDataRange(begin, end); } - const reader = new PDFDataTransportStreamRangeReader(this, begin, end); - this._pdfDataRangeTransport.requestDataRange(begin, end); - this._rangeReaders.push(reader); return reader; } cancelAllRequests(reason) { - this._fullRequestReader?.cancel(reason); + super.cancelAllRequests(reason); - for (const reader of this._rangeReaders.slice(0)) { - reader.cancel(reason); - } - this._pdfDataRangeTransport.abort(); + this._source.pdfDataRangeTransport.abort(); } } -/** @implements {IPDFStreamReader} */ -class PDFDataTransportStreamReader { - constructor( - stream, - queuedChunks, - progressiveDone = false, - contentDispositionFilename = null - ) { - this._stream = stream; - this._done = progressiveDone || false; - this._filename = isPdfFile(contentDispositionFilename) - ? contentDispositionFilename - : null; - this._queuedChunks = queuedChunks || []; - this._loaded = 0; +class PDFDataTransportStreamReader extends BasePDFStreamReader { + _done = false; + + _queuedChunks = null; + + _requests = []; + + constructor(stream) { + super(stream); + const { pdfDataRangeTransport, disableRange, disableStream } = + stream._source; + const { length, contentDispositionFilename } = pdfDataRangeTransport; + + this._queuedChunks = stream._queuedChunks || []; for (const chunk of this._queuedChunks) { this._loaded += chunk.byteLength; } - this._requests = []; - this._headersReady = Promise.resolve(); - stream._fullRequestReader = this; + this._done = stream._progressiveDone; - this.onProgress = null; + this._contentLength = length; + this._isStreamingSupported = !disableStream; + this._isRangeSupported = !disableRange; + + if (isPdfFile(contentDispositionFilename)) { + this._filename = contentDispositionFilename; + } + this._headersCapability.resolve(); } _enqueue(chunk) { @@ -199,34 +151,14 @@ class PDFDataTransportStreamReader { return; // Ignore new data. } if (this._requests.length > 0) { - const requestCapability = this._requests.shift(); - requestCapability.resolve({ value: chunk, done: false }); + const capability = this._requests.shift(); + capability.resolve({ value: chunk, done: false }); } else { this._queuedChunks.push(chunk); } this._loaded += chunk.byteLength; } - get headersReady() { - return this._headersReady; - } - - get filename() { - return this._filename; - } - - get isRangeSupported() { - return this._stream._isRangeSupported; - } - - get isStreamingSupported() { - return this._stream._isStreamingSupported; - } - - get contentLength() { - return this._stream._contentLength; - } - async read() { if (this._queuedChunks.length > 0) { const chunk = this._queuedChunks.shift(); @@ -235,38 +167,38 @@ class PDFDataTransportStreamReader { if (this._done) { return { value: undefined, done: true }; } - const requestCapability = Promise.withResolvers(); - this._requests.push(requestCapability); - return requestCapability.promise; + const capability = Promise.withResolvers(); + this._requests.push(capability); + return capability.promise; } cancel(reason) { this._done = true; - for (const requestCapability of this._requests) { - requestCapability.resolve({ value: undefined, done: true }); + for (const capability of this._requests) { + capability.resolve({ value: undefined, done: true }); } this._requests.length = 0; } progressiveDone() { - if (this._done) { - return; - } - this._done = true; + this._done ||= true; } } -/** @implements {IPDFStreamRangeReader} */ -class PDFDataTransportStreamRangeReader { - constructor(stream, begin, end) { - this._stream = stream; - this._begin = begin; - this._end = end; - this._queuedChunk = null; - this._requests = []; - this._done = false; +class PDFDataTransportStreamRangeReader extends BasePDFStreamRangeReader { + onDone = null; - this.onProgress = null; + _begin = -1; + + _done = false; + + _queuedChunk = null; + + _requests = []; + + constructor(stream, begin, end) { + super(stream, begin, end); + this._begin = begin; } _enqueue(chunk) { @@ -276,19 +208,16 @@ class PDFDataTransportStreamRangeReader { if (this._requests.length === 0) { this._queuedChunk = chunk; } else { - const requestsCapability = this._requests.shift(); - requestsCapability.resolve({ value: chunk, done: false }); - for (const requestCapability of this._requests) { - requestCapability.resolve({ value: undefined, done: true }); + const firstCapability = this._requests.shift(); + firstCapability.resolve({ value: chunk, done: false }); + + for (const capability of this._requests) { + capability.resolve({ value: undefined, done: true }); } this._requests.length = 0; } this._done = true; - this._stream._removeRangeReader(this); - } - - get isStreamingSupported() { - return false; + this.onDone?.(); } async read() { @@ -300,18 +229,18 @@ class PDFDataTransportStreamRangeReader { if (this._done) { return { value: undefined, done: true }; } - const requestCapability = Promise.withResolvers(); - this._requests.push(requestCapability); - return requestCapability.promise; + const capability = Promise.withResolvers(); + this._requests.push(capability); + return capability.promise; } cancel(reason) { this._done = true; - for (const requestCapability of this._requests) { - requestCapability.resolve({ value: undefined, done: true }); + for (const capability of this._requests) { + capability.resolve({ value: undefined, done: true }); } this._requests.length = 0; - this._stream._removeRangeReader(this); + this.onDone?.(); } } diff --git a/src/interfaces.js b/src/shared/base_pdf_stream.js similarity index 53% rename from src/interfaces.js rename to src/shared/base_pdf_stream.js index 15493bdb8..4f3ee4dfe 100644 --- a/src/interfaces.js +++ b/src/shared/base_pdf_stream.js @@ -13,56 +13,119 @@ * limitations under the License. */ +import { assert, unreachable } from "./util.js"; + /** * Interface that represents PDF data transport. If possible, it allows * progressively load entire or fragment of the PDF binary data. - * - * @interface */ -class IPDFStream { +class BasePDFStream { + #PDFStreamReader = null; + + #PDFStreamRangeReader = null; + + _fullReader = null; + + _rangeReaders = new Set(); + + _source = null; + + constructor(source, PDFStreamReader, PDFStreamRangeReader) { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BasePDFStream + ) { + unreachable("Cannot initialize BasePDFStream."); + } + this._source = source; + + this.#PDFStreamReader = PDFStreamReader; + this.#PDFStreamRangeReader = PDFStreamRangeReader; + } + + get _progressiveDataLength() { + return this._fullReader?._loaded ?? 0; + } + /** * Gets a reader for the entire PDF data. - * @returns {IPDFStreamReader} + * @returns {BasePDFStreamReader} */ getFullReader() { - return null; + assert( + !this._fullReader, + "BasePDFStream.getFullReader can only be called once." + ); + return (this._fullReader = new this.#PDFStreamReader(this)); } /** * Gets a reader for the range of the PDF data. * * NOTE: Currently this method is only expected to be invoked *after* - * the `IPDFStreamReader.prototype.headersReady` promise has resolved. + * the `BasePDFStreamReader.prototype.headersReady` promise has resolved. * * @param {number} begin - the start offset of the data. * @param {number} end - the end offset of the data. - * @returns {IPDFStreamRangeReader} + * @returns {BasePDFStreamRangeReader} */ getRangeReader(begin, end) { - return null; + if (end <= this._progressiveDataLength) { + return null; + } + const reader = new this.#PDFStreamRangeReader(this, begin, end); + this._rangeReaders.add(reader); + return reader; } /** * Cancels all opened reader and closes all their opened requests. * @param {Object} reason - the reason for cancelling */ - cancelAllRequests(reason) {} + cancelAllRequests(reason) { + this._fullReader?.cancel(reason); + + // Always create a copy of the rangeReaders. + for (const reader of new Set(this._rangeReaders)) { + reader.cancel(reason); + } + } } /** * Interface for a PDF binary data reader. - * - * @interface */ -class IPDFStreamReader { - constructor() { - /** - * Sets or gets the progress callback. The callback can be useful when the - * isStreamingSupported property of the object is defined as false. - * The callback is called with one parameter: an object with the loaded and - * total properties. - */ - this.onProgress = null; +class BasePDFStreamReader { + /** + * Sets or gets the progress callback. The callback can be useful when the + * isStreamingSupported property of the object is defined as false. + * The callback is called with one parameter: an object with the loaded and + * total properties. + */ + onProgress = null; + + _contentLength = 0; + + _filename = null; + + _headersCapability = Promise.withResolvers(); + + _isRangeSupported = false; + + _isStreamingSupported = false; + + _loaded = 0; + + _stream = null; + + constructor(stream) { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BasePDFStreamReader + ) { + unreachable("Cannot initialize BasePDFStreamReader."); + } + this._stream = stream; } /** @@ -71,7 +134,7 @@ class IPDFStreamReader { * @type {Promise} */ get headersReady() { - return Promise.resolve(); + return this._headersCapability.promise; } /** @@ -81,7 +144,7 @@ class IPDFStreamReader { * header is missing/invalid. */ get filename() { - return null; + return this._filename; } /** @@ -90,7 +153,7 @@ class IPDFStreamReader { * @type {number} The data length (or 0 if unknown). */ get contentLength() { - return 0; + return this._contentLength; } /** @@ -100,7 +163,7 @@ class IPDFStreamReader { * @type {boolean} */ get isRangeSupported() { - return false; + return this._isRangeSupported; } /** @@ -109,7 +172,7 @@ class IPDFStreamReader { * @type {boolean} */ get isStreamingSupported() { - return false; + return this._isStreamingSupported; } /** @@ -120,37 +183,33 @@ class IPDFStreamReader { * set to true. * @returns {Promise} */ - async read() {} + async read() { + unreachable("Abstract method `read` called"); + } /** * Cancels all pending read requests and closes the stream. * @param {Object} reason */ - cancel(reason) {} + cancel(reason) { + unreachable("Abstract method `cancel` called"); + } } /** * Interface for a PDF binary data fragment reader. - * - * @interface */ -class IPDFStreamRangeReader { - constructor() { - /** - * Sets or gets the progress callback. The callback can be useful when the - * isStreamingSupported property of the object is defined as false. - * The callback is called with one parameter: an object with the loaded - * property. - */ - this.onProgress = null; - } +class BasePDFStreamRangeReader { + _stream = null; - /** - * Gets ability of the stream to progressively load binary data. - * @type {boolean} - */ - get isStreamingSupported() { - return false; + constructor(stream, begin, end) { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BasePDFStreamRangeReader + ) { + unreachable("Cannot initialize BasePDFStreamRangeReader."); + } + this._stream = stream; } /** @@ -161,13 +220,17 @@ class IPDFStreamRangeReader { * set to true. * @returns {Promise} */ - async read() {} + async read() { + unreachable("Abstract method `read` called"); + } /** * Cancels all pending read requests and closes the stream. * @param {Object} reason */ - cancel(reason) {} + cancel(reason) { + unreachable("Abstract method `cancel` called"); + } } -export { IPDFStream, IPDFStreamRangeReader, IPDFStreamReader }; +export { BasePDFStream, BasePDFStreamRangeReader, BasePDFStreamReader }; diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index c3f6a7d2d..801577518 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -160,14 +160,18 @@ describe("api", function () { progressReportedCapability.resolve(progressData); }; - const data = await Promise.all([ - progressReportedCapability.promise, + const [pdfDoc, progress] = await Promise.all([ loadingTask.promise, + progressReportedCapability.promise, ]); - expect(data[0].loaded / data[0].total >= 0).toEqual(true); - expect(data[1] instanceof PDFDocumentProxy).toEqual(true); - expect(loadingTask).toEqual(data[1].loadingTask); + expect(pdfDoc instanceof PDFDocumentProxy).toEqual(true); + expect(pdfDoc.loadingTask).toBe(loadingTask); + + expect(progress.loaded).toBeGreaterThanOrEqual(0); + expect(progress.total).toEqual(basicApiFileLength); + expect(progress.percent).toBeGreaterThanOrEqual(0); + expect(progress.percent).toBeLessThanOrEqual(100); await loadingTask.destroy(); }); @@ -218,12 +222,17 @@ describe("api", function () { progressReportedCapability.resolve(data); }; - const data = await Promise.all([ + const [pdfDoc, progress] = await Promise.all([ loadingTask.promise, progressReportedCapability.promise, ]); - expect(data[0] instanceof PDFDocumentProxy).toEqual(true); - expect(data[1].loaded / data[1].total).toEqual(1); + + expect(pdfDoc instanceof PDFDocumentProxy).toEqual(true); + expect(pdfDoc.loadingTask).toBe(loadingTask); + + expect(progress.loaded).toEqual(basicApiFileLength); + expect(progress.total).toEqual(basicApiFileLength); + expect(progress.percent).toEqual(100); // Check that the TypedArray was transferred. expect(typedArrayPdf.length).toEqual(0); diff --git a/test/unit/common_pdfstream_tests.js b/test/unit/common_pdfstream_tests.js index b96620df9..c0f958851 100644 --- a/test/unit/common_pdfstream_tests.js +++ b/test/unit/common_pdfstream_tests.js @@ -16,8 +16,7 @@ import { AbortException, isNodeJS } from "../../src/shared/util.js"; import { getCrossOriginHostname, TestPdfsServer } from "./test_utils.js"; -// Common tests to verify behavior across implementations of the IPDFStream -// interface: +// Common tests to verify behavior across `BasePDFStream` implementations: // - PDFNetworkStream by network_spec.js // - PDFFetchStream by fetch_stream_spec.js async function testCrossOriginRedirects({ diff --git a/test/unit/fetch_stream_spec.js b/test/unit/fetch_stream_spec.js index 181818a59..82696fb22 100644 --- a/test/unit/fetch_stream_spec.js +++ b/test/unit/fetch_stream_spec.js @@ -35,6 +35,7 @@ describe("fetch_stream", function () { it("read with streaming", async function () { const stream = new PDFFetchStream({ url: getPdfUrl(), + rangeChunkSize: 32768, disableStream: false, disableRange: true, }); diff --git a/test/unit/network_utils_spec.js b/test/unit/network_utils_spec.js index 2b68c4dbf..ae84d9c27 100644 --- a/test/unit/network_utils_spec.js +++ b/test/unit/network_utils_spec.js @@ -18,7 +18,6 @@ import { createResponseError, extractFilenameFromHeader, validateRangeRequestCapabilities, - validateResponseStatus, } from "../../src/display/network_utils.js"; import { ResponseException } from "../../src/shared/util.js"; @@ -391,18 +390,4 @@ describe("network_utils", function () { testCreateResponseError("https://foo.com/bar.pdf", 0, false); }); }); - - describe("validateResponseStatus", function () { - it("accepts valid response statuses", function () { - expect(validateResponseStatus(200)).toEqual(true); - expect(validateResponseStatus(206)).toEqual(true); - }); - - it("rejects invalid response statuses", function () { - expect(validateResponseStatus(302)).toEqual(false); - expect(validateResponseStatus(404)).toEqual(false); - expect(validateResponseStatus(null)).toEqual(false); - expect(validateResponseStatus(undefined)).toEqual(false); - }); - }); }); diff --git a/web/app.js b/web/app.js index e882ad150..a06ba8c12 100644 --- a/web/app.js +++ b/web/app.js @@ -1236,9 +1236,7 @@ const PDFViewerApplication = { this.passwordPrompt.open(); }; - loadingTask.onProgress = ({ loaded, total }) => { - this.progress(loaded / total); - }; + loadingTask.onProgress = evt => this.progress(evt.percent); return loadingTask.promise.then( pdfDocument => { @@ -1374,8 +1372,7 @@ const PDFViewerApplication = { return message; }, - progress(level) { - const percent = Math.round(level * 100); + progress(percent) { // When we transition from full request to range requests, it's possible // that we discard some of the loaded data. This can cause the loading // bar to move backwards. So prevent this by only updating the bar if it diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 253965f16..8db4d8965 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { isPdfFile, PDFDataRangeTransport } from "pdfjs-lib"; +import { isPdfFile, MathClamp, PDFDataRangeTransport } from "pdfjs-lib"; import { AppOptions } from "./app_options.js"; import { BaseExternalServices } from "./external_services.js"; import { BasePreferences } from "./preferences.js"; @@ -627,7 +627,13 @@ class ExternalServices extends BaseExternalServices { pdfDataRangeTransport?.onDataProgressiveDone(); break; case "progress": - viewerApp.progress(args.loaded / args.total); + const percent = MathClamp( + Math.round((args.loaded / args.total) * 100), + 0, + 100 + ); + + viewerApp.progress(percent); break; case "complete": if (!args.data) { diff --git a/web/ui_utils.js b/web/ui_utils.js index 0251c37b6..a4199e611 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -709,7 +709,7 @@ class ProgressBar { } set percent(val) { - this.#percent = MathClamp(val, 0, 100); + this.#percent = val; if (isNaN(val)) { this.#classList.add("indeterminate");