From 62d5408cf0d7b14d796d1fe16de02c31adc7e07d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Jan 2026 07:55:27 +0100 Subject: [PATCH 1/8] Stop tracking `progressiveDataLength` in the `ChunkedStreamManager` class Currently this property is essentially "duplicated", so let's instead use the identical one that's availble on the `ChunkedStream` instance. --- src/core/chunked_stream.js | 66 ++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/core/chunked_stream.js b/src/core/chunked_stream.js index c77f62531..eaa13e29c 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,6 +260,18 @@ class ChunkedStream extends Stream { } class ChunkedStreamManager { + aborted = false; + + currRequestId = 0; + + _chunksNeededByRequest = new Map(); + + _loadedStreamCapability = Promise.withResolvers(); + + _promisesByRequest = new Map(); + + _requestsByChunk = new Map(); + constructor(pdfNetworkStream, args) { this.length = args.length; this.chunkSize = args.rangeChunkSize; @@ -264,16 +279,6 @@ class ChunkedStreamManager { this.pdfNetworkStream = pdfNetworkStream; 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) { @@ -454,26 +459,25 @@ class ChunkedStreamManager { } 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 +506,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 +529,8 @@ class ChunkedStreamManager { } this.msgHandler.send("DocProgress", { - loaded: this.stream.numChunksLoaded * this.chunkSize, - total: this.length, + loaded: stream.numChunksLoaded * chunkSize, + total: length, }); } From 987265720ed339534effedb846e5c8d422b518b2 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Jan 2026 07:55:53 +0100 Subject: [PATCH 2/8] Remove the unused `IPDFStreamRangeReader.prototype.isStreamingSupported` getter This getter was only invoked from `src/display/network.js` and `src/core/chunked_stream.js`, however in both cases it's hardcoded to `false` and thus isn't actually needed. This originated in PR 6879, close to a decade ago, for a potential TODO which was never implemented and it ought to be OK to just simplify this now. --- src/core/chunked_stream.js | 16 +++------------- src/core/worker_stream.js | 4 ---- src/display/fetch_stream.js | 5 ----- src/display/network.js | 8 +------- src/display/node_stream.js | 6 ------ src/display/transport_stream.js | 4 ---- src/interfaces.js | 8 -------- 7 files changed, 4 insertions(+), 47 deletions(-) diff --git a/src/core/chunked_stream.js b/src/core/chunked_stream.js index eaa13e29c..680ac85fc 100644 --- a/src/core/chunked_stream.js +++ b/src/core/chunked_stream.js @@ -283,19 +283,15 @@ class ChunkedStreamManager { sendRequest(begin, end) { const rangeReader = this.pdfNetworkStream.getRangeReader(begin, end); - if (!rangeReader.isStreamingSupported) { - rangeReader.onProgress = this.onProgress.bind(this); - } + rangeReader.onProgress = this.onProgress.bind(this); - 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")) { @@ -304,12 +300,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) { diff --git a/src/core/worker_stream.js b/src/core/worker_stream.js index 047935316..eb8f97f3c 100644 --- a/src/core/worker_stream.js +++ b/src/core/worker_stream.js @@ -114,10 +114,6 @@ class PDFWorkerStreamRangeReader { this._reader = readableStream.getReader(); } - get isStreamingSupported() { - return false; - } - async read() { const { value, done } = await this._reader.read(); if (done) { diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js index acbf3c868..dc08637cc 100644 --- a/src/display/fetch_stream.js +++ b/src/display/fetch_stream.js @@ -209,7 +209,6 @@ class PDFFetchStreamRangeReader { const source = stream.source; this._withCredentials = source.withCredentials || false; this._readCapability = Promise.withResolvers(); - this._isStreamingSupported = !source.disableStream; this._abortController = new AbortController(); // Always create a copy of the headers. @@ -240,10 +239,6 @@ class PDFFetchStreamRangeReader { this.onProgress = null; } - get isStreamingSupported() { - return this._isStreamingSupported; - } - async read() { await this._readCapability.promise; const { value, done } = await this._reader.read(); diff --git a/src/display/network.js b/src/display/network.js index f4a83bfc2..48bf60e2a 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -413,13 +413,7 @@ class PDFNetworkStreamRangeRequestReader { } _onProgress(evt) { - if (!this.isStreamingSupported) { - this.onProgress?.({ loaded: evt.loaded }); - } - } - - get isStreamingSupported() { - return false; + this.onProgress?.({ loaded: evt.loaded }); } async read() { diff --git a/src/display/node_stream.js b/src/display/node_stream.js index e2f3b9cc6..4c6276505 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -208,8 +208,6 @@ class PDFNodeStreamFsRangeReader { constructor(stream, begin, end) { this.onProgress = null; this._loaded = 0; - const source = stream.source; - this._isStreamingSupported = !source.disableStream; const url = stream.url; const fs = process.getBuiltinModule("fs"); @@ -228,10 +226,6 @@ class PDFNodeStreamFsRangeReader { } } - get isStreamingSupported() { - return this._isStreamingSupported; - } - async read() { await this._readCapability.promise; const { value, done } = await this._reader.read(); diff --git a/src/display/transport_stream.js b/src/display/transport_stream.js index 36bef6dc1..8c1a21f16 100644 --- a/src/display/transport_stream.js +++ b/src/display/transport_stream.js @@ -287,10 +287,6 @@ class PDFDataTransportStreamRangeReader { this._stream._removeRangeReader(this); } - get isStreamingSupported() { - return false; - } - async read() { if (this._queuedChunk) { const chunk = this._queuedChunk; diff --git a/src/interfaces.js b/src/interfaces.js index 15493bdb8..587727327 100644 --- a/src/interfaces.js +++ b/src/interfaces.js @@ -145,14 +145,6 @@ class IPDFStreamRangeReader { this.onProgress = null; } - /** - * Gets ability of the stream to progressively load binary data. - * @type {boolean} - */ - get isStreamingSupported() { - return false; - } - /** * Requests a chunk of the binary data. The method returns the promise, which * is resolved into object with properties "value" and "done". If the done From 05b78ce03cb5392eb7664f5100a62351a5cac03d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Jan 2026 07:56:05 +0100 Subject: [PATCH 3/8] Stop registering an `onProgress` callback on the `PDFWorkerStreamRangeReader`-instance, in the `ChunkedStreamManager` class Given that nothing in the `PDFWorkerStreamRangeReader` class attempts to invoke the `onProgress` callback, this is effectively dead code now. Looking briefly at the history of this code it's not clear, at least to me, when this became unused however it's probably close to a decade ago. Finally, note also how progress is already being reported through the `ChunkedStreamManager.prototype.onReceiveData` method. --- src/core/chunked_stream.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/core/chunked_stream.js b/src/core/chunked_stream.js index 680ac85fc..055bcfcde 100644 --- a/src/core/chunked_stream.js +++ b/src/core/chunked_stream.js @@ -283,7 +283,6 @@ class ChunkedStreamManager { sendRequest(begin, end) { const rangeReader = this.pdfNetworkStream.getRangeReader(begin, end); - rangeReader.onProgress = this.onProgress.bind(this); let chunks = []; return new Promise((resolve, reject) => { @@ -441,13 +440,6 @@ 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; From a80f10ff1af47f89c2af7bcf2f1b8fef9b3ad743 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Jan 2026 07:56:22 +0100 Subject: [PATCH 4/8] Remove the `onProgress` callback from the `IPDFStreamRangeReader` interface Note how there's *nowhere* in the code-base where the `IPDFStreamRangeReader.prototype.onProgress` callback is actually being set and used, however the loadingBar (in the viewer) still works just fine since loading progress is already reported via: - The `ChunkedStreamManager` instance respectively the `getPdfManager` function, through the use of a "DocProgress" message, on the worker-thread. - A `IPDFStreamReader.prototype.onProgress` callback, on the main-thread. Furthermore, it would definitely *not* be a good idea to add any `IPDFStreamRangeReader.prototype.onProgress` callbacks since they only include the `loaded`-property which would trigger the "indeterminate" loadingBar (in the viewer). Looking briefly at the history of this code it's not clear, at least to me, when this became unused however it's probably close to a decade ago. --- src/core/worker_stream.js | 1 - src/display/fetch_stream.js | 6 ------ src/display/network.js | 8 +------- src/display/node_stream.js | 6 ------ src/display/transport_stream.js | 7 +------ src/interfaces.js | 10 ---------- 6 files changed, 2 insertions(+), 36 deletions(-) diff --git a/src/core/worker_stream.js b/src/core/worker_stream.js index eb8f97f3c..6dfc3962a 100644 --- a/src/core/worker_stream.js +++ b/src/core/worker_stream.js @@ -105,7 +105,6 @@ class PDFWorkerStreamReader { class PDFWorkerStreamRangeReader { constructor(begin, end, msgHandler) { this._msgHandler = msgHandler; - this.onProgress = null; const readableStream = this._msgHandler.sendWithStream("GetRangeReader", { begin, diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js index dc08637cc..4f603f9b6 100644 --- a/src/display/fetch_stream.js +++ b/src/display/fetch_stream.js @@ -205,7 +205,6 @@ 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(); @@ -235,8 +234,6 @@ class PDFFetchStreamRangeReader { this._reader = response.body.getReader(); }) .catch(this._readCapability.reject); - - this.onProgress = null; } async read() { @@ -245,9 +242,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 48bf60e2a..6f8a1f356 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -367,14 +367,12 @@ class PDFNetworkStreamRangeRequestReader { onHeadersReceived: this._onHeadersReceived.bind(this), onDone: this._onDone.bind(this), onError: this._onError.bind(this), - onProgress: this._onProgress.bind(this), + onProgress: null, }); this._requests = []; this._queuedChunk = null; this._done = false; this._storedError = undefined; - - this.onProgress = null; } _onHeadersReceived() { @@ -412,10 +410,6 @@ class PDFNetworkStreamRangeRequestReader { this._queuedChunk = null; } - _onProgress(evt) { - this.onProgress?.({ loaded: evt.loaded }); - } - async read() { if (this._storedError) { throw this._storedError; diff --git a/src/display/node_stream.js b/src/display/node_stream.js index 4c6276505..03266b0fe 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -206,9 +206,6 @@ class PDFNodeStreamFsRangeReader { _reader = null; constructor(stream, begin, end) { - this.onProgress = null; - this._loaded = 0; - const url = stream.url; const fs = process.getBuiltinModule("fs"); try { @@ -232,9 +229,6 @@ class PDFNodeStreamFsRangeReader { 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 8c1a21f16..1fb74f521 100644 --- a/src/display/transport_stream.js +++ b/src/display/transport_stream.js @@ -111,10 +111,7 @@ class PDFDataTransportStream { } _onProgress(evt) { - if (evt.total === undefined) { - // Reporting to first range reader, if it exists. - this._rangeReaders[0]?.onProgress?.({ loaded: evt.loaded }); - } else { + if (evt.total !== undefined) { this._fullRequestReader?.onProgress?.({ loaded: evt.loaded, total: evt.total, @@ -265,8 +262,6 @@ class PDFDataTransportStreamRangeReader { this._queuedChunk = null; this._requests = []; this._done = false; - - this.onProgress = null; } _enqueue(chunk) { diff --git a/src/interfaces.js b/src/interfaces.js index 587727327..34c5f95bf 100644 --- a/src/interfaces.js +++ b/src/interfaces.js @@ -135,16 +135,6 @@ class IPDFStreamReader { * @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; - } - /** * Requests a chunk of the binary data. The method returns the promise, which * is resolved into object with properties "value" and "done". If the done From 4a8fb4dde16dc76d6ba35a4c0210848e322dfa2b Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Jan 2026 08:01:40 +0100 Subject: [PATCH 5/8] Add an abstract `BasePDFStream` class, that all the old `IPDFStream` implementations inherit from Given that there's no less than *five* different, but very similar, implementations this helps reduce code duplication and simplifies maintenance. Also, spotted during rebasing, pass the `enableHWA` option "correctly" (i.e. as part of the existing `transportParams`) to the `WorkerTransport`-class to keep the constructor simpler. --- src/core/chunked_stream.js | 8 +- src/core/worker.js | 20 +-- src/core/worker_stream.js | 49 ++---- src/display/api.js | 75 ++++----- src/display/fetch_stream.js | 45 +---- src/display/network.js | 46 ++--- src/display/node_stream.js | 40 +---- src/display/transport_stream.js | 159 +++++++----------- .../base_pdf_stream.js} | 63 +++++-- test/unit/common_pdfstream_tests.js | 3 +- 10 files changed, 194 insertions(+), 314 deletions(-) rename src/{interfaces.js => shared/base_pdf_stream.js} (73%) diff --git a/src/core/chunked_stream.js b/src/core/chunked_stream.js index 055bcfcde..94c01fc03 100644 --- a/src/core/chunked_stream.js +++ b/src/core/chunked_stream.js @@ -272,17 +272,17 @@ class ChunkedStreamManager { _requestsByChunk = new Map(); - constructor(pdfNetworkStream, args) { + 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; } sendRequest(begin, end) { - const rangeReader = this.pdfNetworkStream.getRangeReader(begin, end); + const rangeReader = this.pdfStream.getRangeReader(begin, end); let chunks = []; return new Promise((resolve, reject) => { @@ -530,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 6dfc3962a..355d068e4 100644 --- a/src/core/worker_stream.js +++ b/src/core/worker_stream.js @@ -13,55 +13,28 @@ * limitations under the License. */ -import { assert } from "../shared/util.js"; +import { BasePDFStream } 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; + constructor(stream) { + const { msgHandler } = stream._source; this.onProgress = null; this._contentLength = null; this._isRangeSupported = false; this._isStreamingSupported = false; - const readableStream = this._msgHandler.sendWithStream("GetReader"); + const readableStream = msgHandler.sendWithStream("GetReader"); this._reader = readableStream.getReader(); - this._headersReady = this._msgHandler + this._headersReady = msgHandler .sendWithPromise("ReaderHeadersReady") .then(data => { this._isStreamingSupported = data.isStreamingSupported; @@ -103,10 +76,10 @@ class PDFWorkerStreamReader { /** @implements {IPDFStreamRangeReader} */ class PDFWorkerStreamRangeReader { - constructor(begin, end, msgHandler) { - this._msgHandler = msgHandler; + constructor(stream, begin, end) { + const { msgHandler } = stream._source; - const readableStream = this._msgHandler.sendWithStream("GetRangeReader", { + const readableStream = msgHandler.sendWithStream("GetRangeReader", { begin, end, }); diff --git a/src/display/api.js b/src/display/api.js index 5695c3f1d..4f1ad871c 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -440,6 +440,7 @@ function getDocument(src = {}) { ownerDocument, pdfBug, styleElement, + enableHWA, loadingParams: { disableAutoFetch, enableXfa, @@ -463,7 +464,8 @@ function getDocument(src = {}) { let networkStream; if (rangeTransport) { - networkStream = new PDFDataTransportStream(rangeTransport, { + networkStream = new PDFDataTransportStream({ + pdfDataRangeTransport: rangeTransport, disableRange, disableStream, }); @@ -508,8 +510,7 @@ function getDocument(src = {}) { task, networkStream, transportParams, - transportFactory, - enableHWA + transportFactory ); task._transport = transport; messageHandler.send("Ready", null); @@ -2391,8 +2392,16 @@ class PDFWorker { * @ignore */ class WorkerTransport { + downloadInfoCapability = Promise.withResolvers(); + + #fullReader = null; + + #lastProgress = null; + #methodPromises = new Map(); + #networkStream = null; + #pageCache = new Map(); #pagePromises = new Map(); @@ -2403,21 +2412,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 +2435,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)); @@ -2604,7 +2603,7 @@ class WorkerTransport { this.filterFactory.destroy(); TextLayer.cleanup(); - this._networkStream?.cancelAllRequests( + this.#networkStream?.cancelAllRequests( new AbortException("Worker was terminated.") ); @@ -2621,18 +2620,18 @@ 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 = { + this.#fullReader = this.#networkStream.getFullReader(); + this.#fullReader.onProgress = evt => { + this.#lastProgress = { loaded: evt.loaded, total: evt.total, }; }; sink.onPull = () => { - this._fullReader + this.#fullReader .read() .then(function ({ value, done }) { if (done) { @@ -2653,7 +2652,7 @@ class WorkerTransport { }; sink.onCancel = reason => { - this._fullReader.cancel(reason); + this.#fullReader.cancel(reason); sink.ready.catch(readyReason => { if (this.destroyed) { @@ -2665,18 +2664,18 @@ 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); + if (this.#lastProgress) { + loadingTask.onProgress?.(this.#lastProgress); } - this._fullReader.onProgress = evt => { + this.#fullReader.onProgress = evt => { loadingTask.onProgress?.({ loaded: evt.loaded, total: evt.total, @@ -2689,16 +2688,16 @@ class WorkerTransport { 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 +2705,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; @@ -2950,7 +2949,7 @@ class WorkerTransport { isPureXfa: !!this._htmlForXfa, numPages: this._numPages, annotationStorage: map, - filename: this._fullReader?.filename ?? null, + filename: this.#fullReader?.filename ?? null, }, transfer ) @@ -3118,8 +3117,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 4f603f9b6..259a3e5f7 100644 --- a/src/display/fetch_stream.js +++ b/src/display/fetch_stream.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { AbortException, assert, warn } from "../shared/util.js"; +import { AbortException, warn } from "../shared/util.js"; import { createHeaders, createResponseError, @@ -22,6 +22,7 @@ import { validateRangeRequestCapabilities, validateResponseStatus, } from "./network_utils.js"; +import { BasePDFStream } from "../shared/base_pdf_stream.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { throw new Error( @@ -51,47 +52,13 @@ 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); - } } } @@ -102,7 +69,7 @@ class PDFFetchStreamReader { this._reader = null; this._loaded = 0; this._filename = null; - const source = stream.source; + const source = stream._source; this._withCredentials = source.withCredentials || false; this._contentLength = source.length; this._headersCapability = Promise.withResolvers(); @@ -205,7 +172,7 @@ class PDFFetchStreamRangeReader { constructor(stream, begin, end) { this._stream = stream; this._reader = null; - const source = stream.source; + const source = stream._source; this._withCredentials = source.withCredentials || false; this._readCapability = Promise.withResolvers(); diff --git a/src/display/network.js b/src/display/network.js index 6f8a1f356..43b39916a 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -21,6 +21,7 @@ import { getResponseOrigin, validateRangeRequestCapabilities, } from "./network_utils.js"; +import { BasePDFStream } from "../shared/base_pdf_stream.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { throw new Error( @@ -35,18 +36,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, PDFNetworkStreamFullReader, PDFNetworkStreamRangeReader); this.url = source.url; this.isHttp = /^https?:/i.test(this.url); this.headers = createHeaders(this.isHttp, source.httpHeaders); @@ -160,38 +156,18 @@ 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 PDFNetworkStreamFullReader { constructor(stream) { this._stream = stream; const { disableRange, length, rangeChunkSize } = stream._source; @@ -355,7 +331,7 @@ class PDFNetworkStreamFullRequestReader { } /** @implements {IPDFStreamRangeReader} */ -class PDFNetworkStreamRangeRequestReader { +class PDFNetworkStreamRangeReader { onClosed = null; constructor(stream, begin, end) { @@ -398,7 +374,7 @@ class PDFNetworkStreamRangeRequestReader { requestCapability.resolve({ value: undefined, done: true }); } this._requests.length = 0; - this.onClosed?.(this); + this.onClosed?.(); } _onError(status) { @@ -435,7 +411,7 @@ class PDFNetworkStreamRangeRequestReader { this._requests.length = 0; this._stream._abortRequest(this._requestXhr); - this.onClosed?.(this); + this.onClosed?.(); } } diff --git a/src/display/node_stream.js b/src/display/node_stream.js index 03266b0fe..c103f5171 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -15,6 +15,7 @@ /* globals process */ import { AbortException, assert, warn } from "../shared/util.js"; +import { BasePDFStream } from "../shared/base_pdf_stream.js"; import { createResponseError } from "./network_utils.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { @@ -60,47 +61,14 @@ function getArrayBuffer(val) { return new Uint8Array(val).buffer; } -class PDFNodeStream { +class PDFNodeStream extends BasePDFStream { constructor(source) { - this.source = source; + super(source, PDFNodeStreamFsFullReader, PDFNodeStreamFsRangeReader); 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); - } } } @@ -111,7 +79,7 @@ class PDFNodeStreamFsFullReader { constructor(stream) { this.onProgress = null; - const source = stream.source; + const source = stream._source; this._contentLength = source.length; // optional this._loaded = 0; this._filename = null; diff --git a/src/display/transport_stream.js b/src/display/transport_stream.js index 1fb74f521..573391533 100644 --- a/src/display/transport_stream.js +++ b/src/display/transport_stream.js @@ -13,39 +13,42 @@ * 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 { assert } from "../shared/util.js"; +import { BasePDFStream } from "../shared/base_pdf_stream.js"; import { isPdfFile } from "./display_utils.js"; -/** @implements {IPDFStream} */ -class PDFDataTransportStream { - constructor( - pdfDataRangeTransport, - { disableRange = false, disableStream = false } - ) { - assert( - pdfDataRangeTransport, - 'PDFDataTransportStream - missing required "pdfDataRangeTransport" argument.' +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; +} + +class PDFDataTransportStream extends BasePDFStream { + _pdfDataRangeTransport = null; + + _queuedChunks = []; + + constructor(source) { + super( + source, + PDFDataTransportStreamReader, + PDFDataTransportStreamRangeReader ); + const { pdfDataRangeTransport, disableRange, disableStream } = source; const { length, initialData, progressiveDone, contentDispositionFilename } = pdfDataRangeTransport; - this._queuedChunks = []; this._progressiveDone = progressiveDone; this._contentDispositionFilename = contentDispositionFilename; 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); } @@ -54,139 +57,89 @@ class PDFDataTransportStream { this._isRangeSupported = !disableRange; this._contentLength = length; - this._fullRequestReader = null; - this._rangeReaders = []; - 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) { - 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._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(); } } /** @implements {IPDFStreamReader} */ class PDFDataTransportStreamReader { - constructor( - stream, - queuedChunks, - progressiveDone = false, - contentDispositionFilename = null - ) { + constructor(stream) { this._stream = stream; - this._done = progressiveDone || false; - this._filename = isPdfFile(contentDispositionFilename) - ? contentDispositionFilename + this._done = stream._progressiveDone || false; + this._filename = isPdfFile(stream._contentDispositionFilename) + ? stream._contentDispositionFilename : null; - this._queuedChunks = queuedChunks || []; + this._queuedChunks = stream._queuedChunks || []; this._loaded = 0; for (const chunk of this._queuedChunks) { this._loaded += chunk.byteLength; } this._requests = []; this._headersReady = Promise.resolve(); - stream._fullRequestReader = this; this.onProgress = null; } @@ -255,6 +208,8 @@ class PDFDataTransportStreamReader { /** @implements {IPDFStreamRangeReader} */ class PDFDataTransportStreamRangeReader { + onDone = null; + constructor(stream, begin, end) { this._stream = stream; this._begin = begin; @@ -279,7 +234,7 @@ class PDFDataTransportStreamRangeReader { this._requests.length = 0; } this._done = true; - this._stream._removeRangeReader(this); + this.onDone?.(); } async read() { @@ -302,7 +257,7 @@ class PDFDataTransportStreamRangeReader { requestCapability.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 73% rename from src/interfaces.js rename to src/shared/base_pdf_stream.js index 34c5f95bf..efdc0861d 100644 --- a/src/interfaces.js +++ b/src/shared/base_pdf_stream.js @@ -13,40 +13,83 @@ * 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); + } + } } /** @@ -152,4 +195,4 @@ class IPDFStreamRangeReader { cancel(reason) {} } -export { IPDFStream, IPDFStreamRangeReader, IPDFStreamReader }; +export { BasePDFStream, IPDFStreamRangeReader, IPDFStreamReader }; 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({ From 54d8c5e7b48491f268da79c18423e0653d518097 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Jan 2026 08:01:55 +0100 Subject: [PATCH 6/8] Add an abstract `BasePDFStreamReader` class, that all the old `IPDFStreamReader` implementations inherit from Given that there's no less than *five* different, but very similar, implementations this helps reduce code duplication and simplifies maintenance. Also, remove the `rangeChunkSize` not defined checks in all the relevant stream-constructor implementations. Note how the API, since some time, always validates *and* provides that parameter when creating a `BasePDFStreamReader`-instance. --- src/core/worker_stream.js | 46 +++++--------- src/display/fetch_stream.js | 88 +++++++++----------------- src/display/network.js | 107 +++++++++++++------------------- src/display/node_stream.js | 52 ++++------------ src/display/transport_stream.js | 97 ++++++++++++----------------- src/shared/base_pdf_stream.js | 62 ++++++++++++------ test/unit/fetch_stream_spec.js | 1 + 7 files changed, 184 insertions(+), 269 deletions(-) diff --git a/src/core/worker_stream.js b/src/core/worker_stream.js index 355d068e4..becc0de6f 100644 --- a/src/core/worker_stream.js +++ b/src/core/worker_stream.js @@ -13,7 +13,10 @@ * limitations under the License. */ -import { BasePDFStream } from "../shared/base_pdf_stream.js"; +import { + BasePDFStream, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; class PDFWorkerStream extends BasePDFStream { constructor(source) { @@ -21,42 +24,23 @@ class PDFWorkerStream extends BasePDFStream { } } -/** @implements {IPDFStreamReader} */ -class PDFWorkerStreamReader { - constructor(stream) { - const { msgHandler } = stream._source; - 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 = msgHandler.sendWithStream("GetReader"); this._reader = readableStream.getReader(); - this._headersReady = 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() { diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js index 259a3e5f7..3972eb595 100644 --- a/src/display/fetch_stream.js +++ b/src/display/fetch_stream.js @@ -14,6 +14,10 @@ */ import { AbortException, warn } from "../shared/util.js"; +import { + BasePDFStream, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; import { createHeaders, createResponseError, @@ -22,7 +26,6 @@ import { validateRangeRequestCapabilities, validateResponseStatus, } from "./network_utils.js"; -import { BasePDFStream } from "../shared/base_pdf_stream.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { throw new Error( @@ -30,15 +33,15 @@ 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 getArrayBuffer(val) { @@ -62,34 +65,29 @@ class PDFFetchStream extends BasePDFStream { } } -/** @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); @@ -97,7 +95,6 @@ class PDFFetchStreamReader { throw createResponseError(response.status, url); } this._reader = response.body.getReader(); - this._headersCapability.resolve(); const responseHeaders = response.headers; @@ -105,8 +102,8 @@ class PDFFetchStreamReader { validateRangeRequestCapabilities({ responseHeaders, isHttp: stream.isHttp, - rangeChunkSize: this._rangeChunkSize, - disableRange: this._disableRange, + rangeChunkSize, + disableRange, }); this._isRangeSupported = allowRangeRequests; @@ -120,30 +117,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() { @@ -182,10 +159,7 @@ class PDFFetchStreamRangeReader { headers.append("Range", `bytes=${begin}-${end - 1}`); const url = source.url; - fetch( - url, - createFetchOptions(headers, this._withCredentials, this._abortController) - ) + fetchUrl(url, headers, this._withCredentials, this._abortController) .then(response => { const responseOrigin = getResponseOrigin(response.url); diff --git a/src/display/network.js b/src/display/network.js index 43b39916a..5ba4fb1af 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -14,6 +14,10 @@ */ import { assert, stringToBytes, warn } from "../shared/util.js"; +import { + BasePDFStream, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; import { createHeaders, createResponseError, @@ -21,7 +25,6 @@ import { getResponseOrigin, validateRangeRequestCapabilities, } from "./network_utils.js"; -import { BasePDFStream } from "../shared/base_pdf_stream.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { throw new Error( @@ -42,7 +45,7 @@ class PDFNetworkStream extends BasePDFStream { _responseOrigin = null; constructor(source) { - super(source, PDFNetworkStreamFullReader, PDFNetworkStreamRangeReader); + super(source, PDFNetworkStreamReader, PDFNetworkStreamRangeReader); this.url = source.url; this.isHttp = /^https?:/i.test(this.url); this.headers = createHeaders(this.isHttp, source.httpHeaders); @@ -166,40 +169,34 @@ class PDFNetworkStream extends BasePDFStream { } } -/** @implements {IPDFStreamReader} */ -class PDFNetworkStreamFullReader { +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); @@ -222,8 +219,8 @@ class PDFNetworkStreamFullReader { validateRangeRequestCapabilities({ responseHeaders, isHttp: stream.isHttp, - rangeChunkSize: this._rangeChunkSize, - disableRange: this._disableRange, + rangeChunkSize, + disableRange, }); if (allowRangeRequests) { @@ -245,10 +242,10 @@ class PDFNetworkStreamFullReader { 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); } @@ -256,49 +253,29 @@ class PDFNetworkStreamFullReader { 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; @@ -312,16 +289,16 @@ class PDFNetworkStreamFullReader { 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; diff --git a/src/display/node_stream.js b/src/display/node_stream.js index c103f5171..188481f4e 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -15,7 +15,10 @@ /* globals process */ import { AbortException, assert, warn } from "../shared/util.js"; -import { BasePDFStream } from "../shared/base_pdf_stream.js"; +import { + BasePDFStream, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; import { createResponseError } from "./network_utils.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { @@ -63,7 +66,7 @@ function getArrayBuffer(val) { class PDFNodeStream extends BasePDFStream { constructor(source) { - super(source, PDFNodeStreamFsFullReader, PDFNodeStreamFsRangeReader); + super(source, PDFNodeStreamReader, PDFNodeStreamFsRangeReader); this.url = parseUrlOrPath(source.url); assert( this.url.protocol === "file:", @@ -72,26 +75,17 @@ class PDFNodeStream extends BasePDFStream { } } -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"); @@ -104,7 +98,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; @@ -128,26 +122,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(); diff --git a/src/display/transport_stream.js b/src/display/transport_stream.js index 573391533..46c3cf897 100644 --- a/src/display/transport_stream.js +++ b/src/display/transport_stream.js @@ -13,12 +13,14 @@ * limitations under the License. */ -/** @typedef {import("../interfaces").IPDFStreamReader} IPDFStreamReader */ // eslint-disable-next-line max-len /** @typedef {import("../interfaces").IPDFStreamRangeReader} IPDFStreamRangeReader */ +import { + BasePDFStream, + BasePDFStreamReader, +} from "../shared/base_pdf_stream.js"; import { assert } from "../shared/util.js"; -import { BasePDFStream } from "../shared/base_pdf_stream.js"; import { isPdfFile } from "./display_utils.js"; function getArrayBuffer(val) { @@ -30,7 +32,7 @@ function getArrayBuffer(val) { } class PDFDataTransportStream extends BasePDFStream { - _pdfDataRangeTransport = null; + _progressiveDone = false; _queuedChunks = []; @@ -40,22 +42,14 @@ class PDFDataTransportStream extends BasePDFStream { PDFDataTransportStreamReader, PDFDataTransportStreamRangeReader ); - const { pdfDataRangeTransport, disableRange, disableStream } = source; - const { length, initialData, progressiveDone, contentDispositionFilename } = - pdfDataRangeTransport; - - this._progressiveDone = progressiveDone; - this._contentDispositionFilename = contentDispositionFilename; + const { pdfDataRangeTransport } = source; + const { initialData, progressiveDone } = pdfDataRangeTransport; if (initialData?.length > 0) { const buffer = getArrayBuffer(initialData); this._queuedChunks.push(buffer); } - - this._pdfDataRangeTransport = pdfDataRangeTransport; - this._isStreamingSupported = !disableStream; - this._isRangeSupported = !disableRange; - this._contentLength = length; + this._progressiveDone = progressiveDone; pdfDataRangeTransport.addRangeListener((begin, chunk) => { this.#onReceiveData(begin, chunk); @@ -113,7 +107,7 @@ class PDFDataTransportStream extends BasePDFStream { if (reader) { reader.onDone = () => this._rangeReaders.delete(reader); - this._pdfDataRangeTransport.requestDataRange(begin, end); + this._source.pdfDataRangeTransport.requestDataRange(begin, end); } return reader; } @@ -121,27 +115,37 @@ class PDFDataTransportStream extends BasePDFStream { cancelAllRequests(reason) { super.cancelAllRequests(reason); - this._pdfDataRangeTransport.abort(); + this._source.pdfDataRangeTransport.abort(); } } -/** @implements {IPDFStreamReader} */ -class PDFDataTransportStreamReader { +class PDFDataTransportStreamReader extends BasePDFStreamReader { + _done = false; + + _queuedChunks = null; + + _requests = []; + constructor(stream) { - this._stream = stream; - this._done = stream._progressiveDone || false; - this._filename = isPdfFile(stream._contentDispositionFilename) - ? stream._contentDispositionFilename - : null; + super(stream); + const { pdfDataRangeTransport, disableRange, disableStream } = + stream._source; + const { length, contentDispositionFilename } = pdfDataRangeTransport; + this._queuedChunks = stream._queuedChunks || []; - this._loaded = 0; for (const chunk of this._queuedChunks) { this._loaded += chunk.byteLength; } - this._requests = []; - this._headersReady = Promise.resolve(); + 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) { @@ -149,34 +153,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(); @@ -185,24 +169,21 @@ 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; } } diff --git a/src/shared/base_pdf_stream.js b/src/shared/base_pdf_stream.js index efdc0861d..c003a6640 100644 --- a/src/shared/base_pdf_stream.js +++ b/src/shared/base_pdf_stream.js @@ -94,18 +94,38 @@ class BasePDFStream { /** * 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; } /** @@ -114,7 +134,7 @@ class IPDFStreamReader { * @type {Promise} */ get headersReady() { - return Promise.resolve(); + return this._headersCapability.promise; } /** @@ -124,7 +144,7 @@ class IPDFStreamReader { * header is missing/invalid. */ get filename() { - return null; + return this._filename; } /** @@ -133,7 +153,7 @@ class IPDFStreamReader { * @type {number} The data length (or 0 if unknown). */ get contentLength() { - return 0; + return this._contentLength; } /** @@ -143,7 +163,7 @@ class IPDFStreamReader { * @type {boolean} */ get isRangeSupported() { - return false; + return this._isRangeSupported; } /** @@ -152,7 +172,7 @@ class IPDFStreamReader { * @type {boolean} */ get isStreamingSupported() { - return false; + return this._isStreamingSupported; } /** @@ -163,13 +183,17 @@ 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"); + } } /** @@ -195,4 +219,4 @@ class IPDFStreamRangeReader { cancel(reason) {} } -export { BasePDFStream, IPDFStreamRangeReader, IPDFStreamReader }; +export { BasePDFStream, BasePDFStreamReader, IPDFStreamRangeReader }; 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, }); From 4ca205bac3bec369ae88818e7271ae019b2c0cd9 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Jan 2026 08:02:07 +0100 Subject: [PATCH 7/8] Add an abstract `BasePDFStreamRangeReader` class, that all the old `IPDFStreamRangeReader` implementations inherit from Given that there's no less than *five* different, but very similar, implementations this helps reduce code duplication and simplifies maintenance. --- src/core/worker_stream.js | 7 ++-- src/display/fetch_stream.js | 48 ++++++++++++------------- src/display/network.js | 64 +++++++++++++++++---------------- src/display/network_utils.js | 10 ++++-- src/display/node_stream.js | 7 ++-- src/display/transport_stream.js | 40 +++++++++++---------- src/shared/base_pdf_stream.js | 26 ++++++++++---- test/unit/network_utils_spec.js | 15 -------- 8 files changed, 116 insertions(+), 101 deletions(-) diff --git a/src/core/worker_stream.js b/src/core/worker_stream.js index becc0de6f..66db66322 100644 --- a/src/core/worker_stream.js +++ b/src/core/worker_stream.js @@ -15,6 +15,7 @@ import { BasePDFStream, + BasePDFStreamRangeReader, BasePDFStreamReader, } from "../shared/base_pdf_stream.js"; @@ -58,9 +59,11 @@ class PDFWorkerStreamReader extends BasePDFStreamReader { } } -/** @implements {IPDFStreamRangeReader} */ -class PDFWorkerStreamRangeReader { +class PDFWorkerStreamRangeReader extends BasePDFStreamRangeReader { + _reader = null; + constructor(stream, begin, end) { + super(stream, begin, end); const { msgHandler } = stream._source; const readableStream = msgHandler.sendWithStream("GetRangeReader", { diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js index 3972eb595..c1b1b636d 100644 --- a/src/display/fetch_stream.js +++ b/src/display/fetch_stream.js @@ -16,15 +16,16 @@ 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")) { @@ -44,6 +45,12 @@ function fetchUrl(url, headers, withCredentials, abortController) { }); } +function ensureResponseStatus(status, url) { + if (status !== 200 && status !== 206) { + throw createResponseError(status, url); + } +} + function getArrayBuffer(val) { if (val instanceof Uint8Array) { return val.buffer; @@ -91,9 +98,7 @@ class PDFFetchStreamReader extends BasePDFStreamReader { .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(); const responseHeaders = response.headers; @@ -144,35 +149,30 @@ class PDFFetchStreamReader extends BasePDFStreamReader { } } -/** @implements {IPDFStreamRangeReader} */ -class PDFFetchStreamRangeReader { - constructor(stream, begin, end) { - this._stream = stream; - this._reader = null; - const source = stream._source; - this._withCredentials = source.withCredentials || false; - this._readCapability = Promise.withResolvers(); +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; - fetchUrl(url, 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); } diff --git a/src/display/network.js b/src/display/network.js index 5ba4fb1af..018df2024 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -16,11 +16,13 @@ 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, @@ -307,57 +309,59 @@ class PDFNetworkStreamReader extends BasePDFStreamReader { } } -/** @implements {IPDFStreamRangeReader} */ -class PDFNetworkStreamRangeReader { +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), + 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; } - _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?.(); } - _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; @@ -375,15 +379,15 @@ class PDFNetworkStreamRangeReader { 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; 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 188481f4e..35dfaefdf 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -17,6 +17,7 @@ import { AbortException, assert, warn } from "../shared/util.js"; import { BasePDFStream, + BasePDFStreamRangeReader, BasePDFStreamReader, } from "../shared/base_pdf_stream.js"; import { createResponseError } from "./network_utils.js"; @@ -66,7 +67,7 @@ function getArrayBuffer(val) { class PDFNodeStream extends BasePDFStream { constructor(source) { - super(source, PDFNodeStreamReader, PDFNodeStreamFsRangeReader); + super(source, PDFNodeStreamReader, PDFNodeStreamRangeReader); this.url = parseUrlOrPath(source.url); assert( this.url.protocol === "file:", @@ -142,12 +143,14 @@ class PDFNodeStreamReader extends BasePDFStreamReader { } } -class PDFNodeStreamFsRangeReader { +class PDFNodeStreamRangeReader extends BasePDFStreamRangeReader { _readCapability = Promise.withResolvers(); _reader = null; constructor(stream, begin, end) { + super(stream, begin, end); + const url = stream.url; const fs = process.getBuiltinModule("fs"); try { diff --git a/src/display/transport_stream.js b/src/display/transport_stream.js index 46c3cf897..57423932b 100644 --- a/src/display/transport_stream.js +++ b/src/display/transport_stream.js @@ -13,11 +13,9 @@ * limitations under the License. */ -// 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"; @@ -187,17 +185,20 @@ class PDFDataTransportStreamReader extends BasePDFStreamReader { } } -/** @implements {IPDFStreamRangeReader} */ -class PDFDataTransportStreamRangeReader { +class PDFDataTransportStreamRangeReader extends BasePDFStreamRangeReader { onDone = null; + _begin = -1; + + _done = false; + + _queuedChunk = null; + + _requests = []; + constructor(stream, begin, end) { - this._stream = stream; + super(stream, begin, end); this._begin = begin; - this._end = end; - this._queuedChunk = null; - this._requests = []; - this._done = false; } _enqueue(chunk) { @@ -207,10 +208,11 @@ 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; } @@ -227,15 +229,15 @@ 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.onDone?.(); diff --git a/src/shared/base_pdf_stream.js b/src/shared/base_pdf_stream.js index c003a6640..4f3ee4dfe 100644 --- a/src/shared/base_pdf_stream.js +++ b/src/shared/base_pdf_stream.js @@ -198,10 +198,20 @@ class BasePDFStreamReader { /** * Interface for a PDF binary data fragment reader. - * - * @interface */ -class IPDFStreamRangeReader { +class BasePDFStreamRangeReader { + _stream = null; + + constructor(stream, begin, end) { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BasePDFStreamRangeReader + ) { + unreachable("Cannot initialize BasePDFStreamRangeReader."); + } + this._stream = stream; + } + /** * Requests a chunk of the binary data. The method returns the promise, which * is resolved into object with properties "value" and "done". If the done @@ -210,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 { BasePDFStream, BasePDFStreamReader, IPDFStreamRangeReader }; +export { BasePDFStream, BasePDFStreamRangeReader, BasePDFStreamReader }; 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); - }); - }); }); From ecb09d62fc1fe5d52964ee498a54b2d97df4c017 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Jan 2026 08:02:24 +0100 Subject: [PATCH 8/8] Add the current loading percentage to the `onPassword` callback The percentage calculation is currently "spread out" across various viewer functionality, which we can avoid by having the API handle that instead. Also, remove the `this.#lastProgress` special-case[1] and just register a "normal" `fullReader.onProgress` callback unconditionally. Once `headersReady` is resolved the callback can simply be removed when not needed, since the "worst" thing that could theoretically happen is that the loadingBar (in the viewer) updates sooner this way. In practice though, since `fullReader.read` cannot return data until `headersReady` is resolved, this change is not actually observable in the API. --- [1] This was added in PR 8617, close to a decade ago, but it's not obvious to me that it was ever necessary to implement it that way. --- examples/mobile-viewer/viewer.mjs | 111 ++++++++++++------------------ src/display/api.js | 48 +++++-------- test/unit/api_spec.js | 25 ++++--- web/app.js | 7 +- web/firefoxcom.js | 10 ++- web/ui_utils.js | 2 +- 6 files changed, 92 insertions(+), 111 deletions(-) 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/display/api.js b/src/display/api.js index 4f1ad871c..f088b9dc1 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -25,6 +25,7 @@ import { getVerbosityLevel, info, isNodeJS, + MathClamp, RenderingIntentFlag, setVerbosityLevel, shadow, @@ -525,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`. */ /** @@ -2396,8 +2399,6 @@ class WorkerTransport { #fullReader = null; - #lastProgress = null; - #methodPromises = new Map(); #networkStream = null; @@ -2492,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()); } @@ -2624,12 +2633,10 @@ class WorkerTransport { "GetReader - no `BasePDFStream` instance available." ); this.#fullReader = this.#networkStream.getFullReader(); - this.#fullReader.onProgress = evt => { - this.#lastProgress = { - loaded: evt.loaded, - total: evt.total, - }; - }; + // 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 .read() @@ -2669,20 +2676,9 @@ class WorkerTransport { const { isStreamingSupported, isRangeSupported, contentLength } = 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 }; }); @@ -2779,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); }); @@ -2905,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 => { 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/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");