From 01deb085f8acbc4f173f63e2e7c36514cdc81df4 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 5 Feb 2026 16:04:45 +0100 Subject: [PATCH 1/2] Improve progress reporting in the `ChunkedStreamManager` Currently there's two small bugs, which have existed around a decade, in the `loaded` property that's sent via the "DocProgress" message from the `ChunkedStreamManager.prototype.onReceiveData` method. - When the entire PDF has loaded the `loaded` property can become larger than the `total` property, which obviously doesn't make sense. This happens whenever the size of the PDF is *not* a multiple of the `rangeChunkSize` API-option, which is a very common situation. - When streaming is being used, the `loaded` property can become smaller than the actually loaded amount of data. This happens whenever the size of a streamed chunk is *not* a multiple of the `rangeChunkSize` API-option, which is a common situation. --- src/core/chunked_stream.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/chunked_stream.js b/src/core/chunked_stream.js index 94c01fc03..e6ebf065c 100644 --- a/src/core/chunked_stream.js +++ b/src/core/chunked_stream.js @@ -14,7 +14,7 @@ */ import { arrayBuffersToBytes, MissingDataException } from "./core_utils.js"; -import { assert } from "../shared/util.js"; +import { assert, MathClamp } from "../shared/util.js"; import { Stream } from "./stream.js"; class ChunkedStream extends Stream { @@ -511,7 +511,11 @@ class ChunkedStreamManager { } this.msgHandler.send("DocProgress", { - loaded: stream.numChunksLoaded * chunkSize, + loaded: MathClamp( + stream.numChunksLoaded * chunkSize, + stream.progressiveDataLength, + length + ), total: length, }); } From b3cd042ded4b0d0cca4f2469939dc587034b971f Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 5 Feb 2026 16:16:36 +0100 Subject: [PATCH 2/2] Prevent unnecessary data copy in `ChunkedStream.prototype.onReceiveData` This method is only invoked via `ChunkedStreamManager.prototype.sendRequest`, which currently returns data in `Uint8Array` format (since it potentially combines multiple `ArrayBuffer`s). Hence we end up doing a short-lived, but still completely unnecessary, data copy[1] in `ChunkedStream.prototype.onReceiveData` when handling range requests. In practice this is unlikely to be a big problem by default, given that streaming is used and the (low) value of the `rangeChunkSize` API-option. (However, in custom PDF.js deployments it might affect things more.) Given that no data copy is better than a short lived one, let's fix this small oversight and add non-production `assert`s to keep it working as intended. This way we also improve consistency, since all other streaming and range request methods (see e.g. `BasePDFStream` and related code) only return `ArrayBuffer` data. --- [1] Remember that `new Uint8Array(arrayBuffer)` only creates a view of the underlying `arrayBuffer`, whereas `new Uint8Array(typedArray)` actually creates a copy of the `typedArray`. --- src/core/chunked_stream.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/core/chunked_stream.js b/src/core/chunked_stream.js index e6ebf065c..642323fae 100644 --- a/src/core/chunked_stream.js +++ b/src/core/chunked_stream.js @@ -70,6 +70,12 @@ class ChunkedStream extends Stream { throw new Error(`Bad end offset: ${end}`); } + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + assert( + chunk instanceof ArrayBuffer, + "onReceiveData - expected an ArrayBuffer." + ); + } this.bytes.set(new Uint8Array(chunk), begin); const beginChunk = Math.floor(begin / chunkSize); const endChunk = Math.floor((end - 1) / chunkSize) + 1; @@ -85,6 +91,12 @@ class ChunkedStream extends Stream { let position = this.progressiveDataLength; const beginChunk = Math.floor(position / this.chunkSize); + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + assert( + data instanceof ArrayBuffer, + "onReceiveProgressiveData - expected an ArrayBuffer." + ); + } this.bytes.set(new Uint8Array(data), position); position += data.byteLength; this.progressiveDataLength = position; @@ -310,7 +322,7 @@ class ChunkedStreamManager { if (this.aborted) { return; // Ignoring any data after abort. } - this.onReceiveData({ chunk: data, begin }); + this.onReceiveData({ chunk: data.buffer, begin }); }); }