Merge pull request #20602 from Snuffleupagus/BasePDFStream-2

Replace the `IPDFStream`, `IPDFStreamReader`, and `IPDFStreamRangeReader` interfaces with proper base classes
This commit is contained in:
Tim van der Meij 2026-02-01 16:53:17 +01:00 committed by GitHub
commit e4cd3176ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 589 additions and 879 deletions

View File

@ -46,19 +46,13 @@ const PDFViewerApplication = {
* @returns {Promise} - Returns the promise, which is resolved when document * @returns {Promise} - Returns the promise, which is resolved when document
* is opened. * is opened.
*/ */
open(params) { async open(params) {
if (this.pdfLoadingTask) { if (this.pdfLoadingTask) {
// We need to destroy already opened document // We need to destroy already opened document.
return this.close().then( await this.close();
function () {
// ... and repeat the open() call.
return this.open(params);
}.bind(this)
);
} }
const url = params.url; const { url } = params;
const self = this;
this.setTitleUsingUrl(url); this.setTitleUsingUrl(url);
// Loading document. // Loading document.
@ -70,24 +64,22 @@ const PDFViewerApplication = {
}); });
this.pdfLoadingTask = loadingTask; this.pdfLoadingTask = loadingTask;
loadingTask.onProgress = function (progressData) { loadingTask.onProgress = evt => this.progress(evt.percent);
self.progress(progressData.loaded / progressData.total);
};
return loadingTask.promise.then( return loadingTask.promise.then(
function (pdfDocument) { pdfDocument => {
// Document loaded, specifying document for the viewer. // Document loaded, specifying document for the viewer.
self.pdfDocument = pdfDocument; this.pdfDocument = pdfDocument;
self.pdfViewer.setDocument(pdfDocument); this.pdfViewer.setDocument(pdfDocument);
self.pdfLinkService.setDocument(pdfDocument); this.pdfLinkService.setDocument(pdfDocument);
self.pdfHistory.initialize({ this.pdfHistory.initialize({
fingerprint: pdfDocument.fingerprints[0], fingerprint: pdfDocument.fingerprints[0],
}); });
self.loadingBar.hide(); this.loadingBar.hide();
self.setTitleUsingMetadata(pdfDocument); this.setTitleUsingMetadata(pdfDocument);
}, },
function (reason) { reason => {
let key = "pdfjs-loading-error"; let key = "pdfjs-loading-error";
if (reason instanceof pdfjsLib.InvalidPDFException) { if (reason instanceof pdfjsLib.InvalidPDFException) {
key = "pdfjs-invalid-file-error"; key = "pdfjs-invalid-file-error";
@ -96,10 +88,10 @@ const PDFViewerApplication = {
? "pdfjs-missing-file-error" ? "pdfjs-missing-file-error"
: "pdfjs-unexpected-response-error"; : "pdfjs-unexpected-response-error";
} }
self.l10n.get(key).then(msg => { this.l10n.get(key).then(msg => {
self.error(msg, { message: reason?.message }); 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 * @returns {Promise} - Returns the promise, which is resolved when all
* destruction is completed. * destruction is completed.
*/ */
close() { async close() {
if (!this.pdfLoadingTask) { if (!this.pdfLoadingTask) {
return Promise.resolve(); return;
} }
const promise = this.pdfLoadingTask.destroy(); const promise = this.pdfLoadingTask.destroy();
@ -128,7 +120,7 @@ const PDFViewerApplication = {
} }
} }
return promise; await promise;
}, },
get loadingBar() { get loadingBar() {
@ -152,28 +144,17 @@ const PDFViewerApplication = {
this.setTitle(title); this.setTitle(title);
}, },
setTitleUsingMetadata(pdfDocument) { async setTitleUsingMetadata(pdfDocument) {
const self = this; const { info, metadata } = await pdfDocument.getMetadata();
pdfDocument.getMetadata().then(function (data) { this.documentInfo = info;
const info = data.info, this.metadata = metadata;
metadata = data.metadata;
self.documentInfo = info;
self.metadata = metadata;
// Provides some basic debug information // Provides some basic debug information
console.log( console.log(
"PDF " + `PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` +
pdfDocument.fingerprints[0] + `${(metadata?.get("pdf:producer") || info.Producer || "-").trim()} / ` +
" [" + `${(metadata?.get("xmp:creatortool") || info.Creator || "-").trim()}` +
info.PDFFormatVersion + `] (PDF.js: ${pdfjsLib.version || "?"} [${pdfjsLib.build || "?"}])`
" " +
(info.Producer || "-").trim() +
" / " +
(info.Creator || "-").trim() +
"]" +
" (PDF.js: " +
(pdfjsLib.version || "-") +
")"
); );
let pdfTitle; let pdfTitle;
@ -191,9 +172,8 @@ const PDFViewerApplication = {
} }
if (pdfTitle) { if (pdfTitle) {
self.setTitle(pdfTitle + " - " + document.title); this.setTitle(pdfTitle + " - " + document.title);
} }
});
}, },
setTitle: function pdfViewSetTitle(title) { setTitle: function pdfViewSetTitle(title) {
@ -223,8 +203,7 @@ const PDFViewerApplication = {
console.error(`${message}\n\n${moreInfoText.join("\n")}`); console.error(`${message}\n\n${moreInfoText.join("\n")}`);
}, },
progress: function pdfViewProgress(level) { progress(percent) {
const percent = Math.round(level * 100);
// Updating the bar if value increases. // Updating the bar if value increases.
if (percent > this.loadingBar.percent || isNaN(percent)) { if (percent > this.loadingBar.percent || isNaN(percent)) {
this.loadingBar.percent = percent; this.loadingBar.percent = percent;

View File

@ -18,6 +18,12 @@ import { assert } from "../shared/util.js";
import { Stream } from "./stream.js"; import { Stream } from "./stream.js";
class ChunkedStream extends Stream { class ChunkedStream extends Stream {
progressiveDataLength = 0;
_lastSuccessfulEnsureByteChunk = -1; // Single-entry cache
_loadedChunks = new Set();
constructor(length, chunkSize, manager) { constructor(length, chunkSize, manager) {
super( super(
/* arrayBuffer = */ new Uint8Array(length), /* arrayBuffer = */ new Uint8Array(length),
@ -27,11 +33,8 @@ class ChunkedStream extends Stream {
); );
this.chunkSize = chunkSize; this.chunkSize = chunkSize;
this._loadedChunks = new Set();
this.numChunks = Math.ceil(length / chunkSize); this.numChunks = Math.ceil(length / chunkSize);
this.manager = manager; 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, // 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) { if (chunk > this.numChunks) {
return; return;
} }
if (chunk === this.lastSuccessfulEnsureByteChunk) { if (chunk === this._lastSuccessfulEnsureByteChunk) {
return; return;
} }
if (!this._loadedChunks.has(chunk)) { if (!this._loadedChunks.has(chunk)) {
throw new MissingDataException(pos, pos + 1); throw new MissingDataException(pos, pos + 1);
} }
this.lastSuccessfulEnsureByteChunk = chunk; this._lastSuccessfulEnsureByteChunk = chunk;
} }
ensureRange(begin, end) { ensureRange(begin, end) {
@ -257,40 +260,37 @@ class ChunkedStream extends Stream {
} }
class ChunkedStreamManager { class ChunkedStreamManager {
constructor(pdfNetworkStream, args) { aborted = false;
currRequestId = 0;
_chunksNeededByRequest = new Map();
_loadedStreamCapability = Promise.withResolvers();
_promisesByRequest = new Map();
_requestsByChunk = new Map();
constructor(pdfStream, args) {
this.length = args.length; this.length = args.length;
this.chunkSize = args.rangeChunkSize; this.chunkSize = args.rangeChunkSize;
this.stream = new ChunkedStream(this.length, this.chunkSize, this); this.stream = new ChunkedStream(this.length, this.chunkSize, this);
this.pdfNetworkStream = pdfNetworkStream; this.pdfStream = pdfStream;
this.disableAutoFetch = args.disableAutoFetch; this.disableAutoFetch = args.disableAutoFetch;
this.msgHandler = args.msgHandler; 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) { sendRequest(begin, end) {
const rangeReader = this.pdfNetworkStream.getRangeReader(begin, end); const rangeReader = this.pdfStream.getRangeReader(begin, end);
if (!rangeReader.isStreamingSupported) {
rangeReader.onProgress = this.onProgress.bind(this);
}
let chunks = [], let chunks = [];
loaded = 0;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const readChunk = ({ value, done }) => { const readChunk = ({ value, done }) => {
try { try {
if (done) { if (done) {
const chunkData = arrayBuffersToBytes(chunks); resolve(arrayBuffersToBytes(chunks));
chunks = null; chunks = null;
resolve(chunkData);
return; return;
} }
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
@ -299,12 +299,6 @@ class ChunkedStreamManager {
"readChunk (sendRequest) - expected an ArrayBuffer." "readChunk (sendRequest) - expected an ArrayBuffer."
); );
} }
loaded += value.byteLength;
if (rangeReader.isStreamingSupported) {
this.onProgress({ loaded });
}
chunks.push(value); chunks.push(value);
rangeReader.read().then(readChunk, reject); rangeReader.read().then(readChunk, reject);
} catch (e) { } catch (e) {
@ -446,34 +440,26 @@ class ChunkedStreamManager {
return groupedChunks; return groupedChunks;
} }
onProgress(args) {
this.msgHandler.send("DocProgress", {
loaded: this.stream.numChunksLoaded * this.chunkSize + args.loaded,
total: this.length,
});
}
onReceiveData(args) { onReceiveData(args) {
const { chunkSize, length, stream } = this;
const chunk = args.chunk; const chunk = args.chunk;
const isProgressive = args.begin === undefined; 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 end = begin + chunk.byteLength;
const beginChunk = Math.floor(begin / this.chunkSize); const beginChunk = Math.floor(begin / chunkSize);
const endChunk = const endChunk =
end < this.length end < length ? Math.floor(end / chunkSize) : Math.ceil(end / chunkSize);
? Math.floor(end / this.chunkSize)
: Math.ceil(end / this.chunkSize);
if (isProgressive) { if (isProgressive) {
this.stream.onReceiveProgressiveData(chunk); stream.onReceiveProgressiveData(chunk);
this.progressiveDataLength = end;
} else { } else {
this.stream.onReceiveData(begin, chunk); stream.onReceiveData(begin, chunk);
} }
if (this.stream.isDataLoaded) { if (stream.isDataLoaded) {
this._loadedStreamCapability.resolve(this.stream); this._loadedStreamCapability.resolve(stream);
} }
const loadedRequests = []; const loadedRequests = [];
@ -502,16 +488,16 @@ class ChunkedStreamManager {
// unfetched chunk of the PDF file. // unfetched chunk of the PDF file.
if (!this.disableAutoFetch && this._requestsByChunk.size === 0) { if (!this.disableAutoFetch && this._requestsByChunk.size === 0) {
let nextEmptyChunk; let nextEmptyChunk;
if (this.stream.numChunksLoaded === 1) { if (stream.numChunksLoaded === 1) {
// This is a special optimization so that after fetching the first // This is a special optimization so that after fetching the first
// chunk, rather than fetching the second chunk, we fetch the last // chunk, rather than fetching the second chunk, we fetch the last
// chunk. // chunk.
const lastChunk = this.stream.numChunks - 1; const lastChunk = stream.numChunks - 1;
if (!this.stream.hasChunk(lastChunk)) { if (!stream.hasChunk(lastChunk)) {
nextEmptyChunk = lastChunk; nextEmptyChunk = lastChunk;
} }
} else { } else {
nextEmptyChunk = this.stream.nextEmptyChunk(endChunk); nextEmptyChunk = stream.nextEmptyChunk(endChunk);
} }
if (Number.isInteger(nextEmptyChunk)) { if (Number.isInteger(nextEmptyChunk)) {
this._requestChunks([nextEmptyChunk]); this._requestChunks([nextEmptyChunk]);
@ -525,8 +511,8 @@ class ChunkedStreamManager {
} }
this.msgHandler.send("DocProgress", { this.msgHandler.send("DocProgress", {
loaded: this.stream.numChunksLoaded * this.chunkSize, loaded: stream.numChunksLoaded * chunkSize,
total: this.length, total: length,
}); });
} }
@ -544,7 +530,7 @@ class ChunkedStreamManager {
abort(reason) { abort(reason) {
this.aborted = true; this.aborted = true;
this.pdfNetworkStream?.cancelAllRequests(reason); this.pdfStream?.cancelAllRequests(reason);
for (const capability of this._promisesByRequest.values()) { for (const capability of this._promisesByRequest.values()) {
capability.reject(reason); capability.reject(reason);

View File

@ -219,23 +219,23 @@ class WorkerMessageHandler {
return new LocalPdfManager(pdfManagerArgs); return new LocalPdfManager(pdfManagerArgs);
} }
const pdfStream = new PDFWorkerStream(handler), const pdfStream = new PDFWorkerStream({ msgHandler: handler }),
fullRequest = pdfStream.getFullReader(); fullReader = pdfStream.getFullReader();
const pdfManagerCapability = Promise.withResolvers(); const pdfManagerCapability = Promise.withResolvers();
let newPdfManager, let newPdfManager,
cachedChunks = [], cachedChunks = [],
loaded = 0; loaded = 0;
fullRequest.headersReady fullReader.headersReady
.then(function () { .then(function () {
if (!fullRequest.isRangeSupported) { if (!fullReader.isRangeSupported) {
return; return;
} }
pdfManagerArgs.source = pdfStream; pdfManagerArgs.source = pdfStream;
pdfManagerArgs.length = fullRequest.contentLength; pdfManagerArgs.length = fullReader.contentLength;
// We don't need auto-fetch when streaming is enabled. // We don't need auto-fetch when streaming is enabled.
pdfManagerArgs.disableAutoFetch ||= fullRequest.isStreamingSupported; pdfManagerArgs.disableAutoFetch ||= fullReader.isStreamingSupported;
newPdfManager = new NetworkPdfManager(pdfManagerArgs); newPdfManager = new NetworkPdfManager(pdfManagerArgs);
// There may be a chance that `newPdfManager` is not initialized for // There may be a chance that `newPdfManager` is not initialized for
@ -282,10 +282,10 @@ class WorkerMessageHandler {
} }
loaded += value.byteLength; loaded += value.byteLength;
if (!fullRequest.isStreamingSupported) { if (!fullReader.isStreamingSupported) {
handler.send("DocProgress", { handler.send("DocProgress", {
loaded, loaded,
total: Math.max(loaded, fullRequest.contentLength || 0), total: Math.max(loaded, fullReader.contentLength || 0),
}); });
} }
@ -294,12 +294,12 @@ class WorkerMessageHandler {
} else { } else {
cachedChunks.push(value); cachedChunks.push(value);
} }
fullRequest.read().then(readChunk, reject); fullReader.read().then(readChunk, reject);
} catch (e) { } catch (e) {
reject(e); reject(e);
} }
}; };
fullRequest.read().then(readChunk, reject); fullReader.read().then(readChunk, reject);
}).catch(function (e) { }).catch(function (e) {
pdfManagerCapability.reject(e); pdfManagerCapability.reject(e);
cancelXHRs = null; cancelXHRs = null;

View File

@ -13,77 +13,35 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert } from "../shared/util.js"; import {
BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader,
} from "../shared/base_pdf_stream.js";
/** @implements {IPDFStream} */ class PDFWorkerStream extends BasePDFStream {
class PDFWorkerStream { constructor(source) {
constructor(msgHandler) { super(source, PDFWorkerStreamReader, PDFWorkerStreamRangeReader);
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);
}
} }
} }
/** @implements {IPDFStreamReader} */ class PDFWorkerStreamReader extends BasePDFStreamReader {
class PDFWorkerStreamReader { _reader = null;
constructor(msgHandler) {
this._msgHandler = msgHandler;
this.onProgress = null;
this._contentLength = null; constructor(stream) {
this._isRangeSupported = false; super(stream);
this._isStreamingSupported = false; const { msgHandler } = stream._source;
const readableStream = this._msgHandler.sendWithStream("GetReader"); const readableStream = msgHandler.sendWithStream("GetReader");
this._reader = readableStream.getReader(); this._reader = readableStream.getReader();
this._headersReady = this._msgHandler msgHandler.sendWithPromise("ReaderHeadersReady").then(data => {
.sendWithPromise("ReaderHeadersReady") this._contentLength = data.contentLength;
.then(data => {
this._isStreamingSupported = data.isStreamingSupported; this._isStreamingSupported = data.isStreamingSupported;
this._isRangeSupported = data.isRangeSupported; this._isRangeSupported = data.isRangeSupported;
this._contentLength = data.contentLength;
});
}
get headersReady() { this._headersCapability.resolve();
return this._headersReady; }, this._headersCapability.reject);
}
get contentLength() {
return this._contentLength;
}
get isStreamingSupported() {
return this._isStreamingSupported;
}
get isRangeSupported() {
return this._isRangeSupported;
} }
async read() { async read() {
@ -101,23 +59,20 @@ class PDFWorkerStreamReader {
} }
} }
/** @implements {IPDFStreamRangeReader} */ class PDFWorkerStreamRangeReader extends BasePDFStreamRangeReader {
class PDFWorkerStreamRangeReader { _reader = null;
constructor(begin, end, msgHandler) {
this._msgHandler = msgHandler;
this.onProgress = null;
const readableStream = this._msgHandler.sendWithStream("GetRangeReader", { constructor(stream, begin, end) {
super(stream, begin, end);
const { msgHandler } = stream._source;
const readableStream = msgHandler.sendWithStream("GetRangeReader", {
begin, begin,
end, end,
}); });
this._reader = readableStream.getReader(); this._reader = readableStream.getReader();
} }
get isStreamingSupported() {
return false;
}
async read() { async read() {
const { value, done } = await this._reader.read(); const { value, done } = await this._reader.read();
if (done) { if (done) {

View File

@ -25,6 +25,7 @@ import {
getVerbosityLevel, getVerbosityLevel,
info, info,
isNodeJS, isNodeJS,
MathClamp,
RenderingIntentFlag, RenderingIntentFlag,
setVerbosityLevel, setVerbosityLevel,
shadow, shadow,
@ -440,6 +441,7 @@ function getDocument(src = {}) {
ownerDocument, ownerDocument,
pdfBug, pdfBug,
styleElement, styleElement,
enableHWA,
loadingParams: { loadingParams: {
disableAutoFetch, disableAutoFetch,
enableXfa, enableXfa,
@ -463,7 +465,8 @@ function getDocument(src = {}) {
let networkStream; let networkStream;
if (rangeTransport) { if (rangeTransport) {
networkStream = new PDFDataTransportStream(rangeTransport, { networkStream = new PDFDataTransportStream({
pdfDataRangeTransport: rangeTransport,
disableRange, disableRange,
disableStream, disableStream,
}); });
@ -508,8 +511,7 @@ function getDocument(src = {}) {
task, task,
networkStream, networkStream,
transportParams, transportParams,
transportFactory, transportFactory
enableHWA
); );
task._transport = transport; task._transport = transport;
messageHandler.send("Ready", null); messageHandler.send("Ready", null);
@ -524,6 +526,8 @@ function getDocument(src = {}) {
* @typedef {Object} OnProgressParameters * @typedef {Object} OnProgressParameters
* @property {number} loaded - Currently loaded number of bytes. * @property {number} loaded - Currently loaded number of bytes.
* @property {number} total - Total number of bytes in the PDF file. * @property {number} total - Total number of bytes in the PDF file.
* @property {number} percent - Currently loaded percentage, as an integer value
* in the [0, 100] range. If `total` is undefined, the percentage is `NaN`.
*/ */
/** /**
@ -2391,8 +2395,14 @@ class PDFWorker {
* @ignore * @ignore
*/ */
class WorkerTransport { class WorkerTransport {
downloadInfoCapability = Promise.withResolvers();
#fullReader = null;
#methodPromises = new Map(); #methodPromises = new Map();
#networkStream = null;
#pageCache = new Map(); #pageCache = new Map();
#pagePromises = new Map(); #pagePromises = new Map();
@ -2403,21 +2413,17 @@ class WorkerTransport {
#pagesMapper = PagesMapper.instance; #pagesMapper = PagesMapper.instance;
constructor( constructor(messageHandler, loadingTask, networkStream, params, factory) {
messageHandler,
loadingTask,
networkStream,
params,
factory,
enableHWA
) {
this.messageHandler = messageHandler; this.messageHandler = messageHandler;
this.loadingTask = loadingTask; this.loadingTask = loadingTask;
this.#networkStream = networkStream;
this.commonObjs = new PDFObjects(); this.commonObjs = new PDFObjects();
this.fontLoader = new FontLoader({ this.fontLoader = new FontLoader({
ownerDocument: params.ownerDocument, ownerDocument: params.ownerDocument,
styleElement: params.styleElement, styleElement: params.styleElement,
}); });
this.enableHWA = params.enableHWA;
this.loadingParams = params.loadingParams; this.loadingParams = params.loadingParams;
this._params = params; this._params = params;
@ -2430,12 +2436,6 @@ class WorkerTransport {
this.destroyed = false; this.destroyed = false;
this.destroyCapability = null; this.destroyCapability = null;
this._networkStream = networkStream;
this._fullReader = null;
this._lastProgress = null;
this.downloadInfoCapability = Promise.withResolvers();
this.enableHWA = enableHWA;
this.setupMessageHandler(); this.setupMessageHandler();
this.#pagesMapper.addListener(this.#updateCaches.bind(this)); this.#pagesMapper.addListener(this.#updateCaches.bind(this));
@ -2493,6 +2493,14 @@ class WorkerTransport {
return promise; return promise;
} }
#onProgress({ loaded, total }) {
this.loadingTask.onProgress?.({
loaded,
total,
percent: MathClamp(Math.round((loaded / total) * 100), 0, 100),
});
}
get annotationStorage() { get annotationStorage() {
return shadow(this, "annotationStorage", new AnnotationStorage()); return shadow(this, "annotationStorage", new AnnotationStorage());
} }
@ -2604,7 +2612,7 @@ class WorkerTransport {
this.filterFactory.destroy(); this.filterFactory.destroy();
TextLayer.cleanup(); TextLayer.cleanup();
this._networkStream?.cancelAllRequests( this.#networkStream?.cancelAllRequests(
new AbortException("Worker was terminated.") new AbortException("Worker was terminated.")
); );
@ -2621,18 +2629,16 @@ class WorkerTransport {
messageHandler.on("GetReader", (data, sink) => { messageHandler.on("GetReader", (data, sink) => {
assert( assert(
this._networkStream, this.#networkStream,
"GetReader - no `IPDFStream` instance available." "GetReader - no `BasePDFStream` instance available."
); );
this._fullReader = this._networkStream.getFullReader(); this.#fullReader = this.#networkStream.getFullReader();
this._fullReader.onProgress = evt => { // If stream or range turn out to be disabled, once `headersReady` is
this._lastProgress = { // resolved, this is our only way to report loading progress.
loaded: evt.loaded, this.#fullReader.onProgress = evt => this.#onProgress(evt);
total: evt.total,
};
};
sink.onPull = () => { sink.onPull = () => {
this._fullReader this.#fullReader
.read() .read()
.then(function ({ value, done }) { .then(function ({ value, done }) {
if (done) { if (done) {
@ -2653,7 +2659,7 @@ class WorkerTransport {
}; };
sink.onCancel = reason => { sink.onCancel = reason => {
this._fullReader.cancel(reason); this.#fullReader.cancel(reason);
sink.ready.catch(readyReason => { sink.ready.catch(readyReason => {
if (this.destroyed) { if (this.destroyed) {
@ -2665,40 +2671,29 @@ class WorkerTransport {
}); });
messageHandler.on("ReaderHeadersReady", async data => { messageHandler.on("ReaderHeadersReady", async data => {
await this._fullReader.headersReady; await this.#fullReader.headersReady;
const { isStreamingSupported, isRangeSupported, contentLength } = const { isStreamingSupported, isRangeSupported, contentLength } =
this._fullReader; this.#fullReader;
// If stream or range are disabled, it's our only way to report if (isStreamingSupported && isRangeSupported) {
// loading progress. this.#fullReader.onProgress = null; // See comment in "GetReader" above.
if (!isStreamingSupported || !isRangeSupported) {
if (this._lastProgress) {
loadingTask.onProgress?.(this._lastProgress);
} }
this._fullReader.onProgress = evt => {
loadingTask.onProgress?.({
loaded: evt.loaded,
total: evt.total,
});
};
}
return { isStreamingSupported, isRangeSupported, contentLength }; return { isStreamingSupported, isRangeSupported, contentLength };
}); });
messageHandler.on("GetRangeReader", (data, sink) => { messageHandler.on("GetRangeReader", (data, sink) => {
assert( assert(
this._networkStream, this.#networkStream,
"GetRangeReader - no `IPDFStream` instance available." "GetRangeReader - no `BasePDFStream` instance available."
); );
const rangeReader = this._networkStream.getRangeReader( const rangeReader = this.#networkStream.getRangeReader(
data.begin, data.begin,
data.end data.end
); );
// When streaming is enabled, it's possible that the data requested here // 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 // However, given that the PDF data is loaded asynchronously on the
// main-thread and then sent via `postMessage` to the worker-thread, // main-thread and then sent via `postMessage` to the worker-thread,
// it may not have been available during parsing (hence the attempt to // it may not have been available during parsing (hence the attempt to
@ -2706,7 +2701,7 @@ class WorkerTransport {
// //
// To avoid wasting time and resources here, we'll thus *not* dispatch // 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 // 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) { if (!rangeReader) {
sink.close(); sink.close();
return; return;
@ -2780,10 +2775,7 @@ class WorkerTransport {
messageHandler.on("DataLoaded", data => { messageHandler.on("DataLoaded", data => {
// For consistency: Ensure that progress is always reported when the // For consistency: Ensure that progress is always reported when the
// entire PDF file has been loaded, regardless of how it was fetched. // entire PDF file has been loaded, regardless of how it was fetched.
loadingTask.onProgress?.({ this.#onProgress({ loaded: data.length, total: data.length });
loaded: data.length,
total: data.length,
});
this.downloadInfoCapability.resolve(data); this.downloadInfoCapability.resolve(data);
}); });
@ -2906,10 +2898,7 @@ class WorkerTransport {
if (this.destroyed) { if (this.destroyed) {
return; // Ignore any pending requests if the worker was terminated. return; // Ignore any pending requests if the worker was terminated.
} }
loadingTask.onProgress?.({ this.#onProgress(data);
loaded: data.loaded,
total: data.total,
});
}); });
messageHandler.on("FetchBinaryData", async data => { messageHandler.on("FetchBinaryData", async data => {
@ -2950,7 +2939,7 @@ class WorkerTransport {
isPureXfa: !!this._htmlForXfa, isPureXfa: !!this._htmlForXfa,
numPages: this._numPages, numPages: this._numPages,
annotationStorage: map, annotationStorage: map,
filename: this._fullReader?.filename ?? null, filename: this.#fullReader?.filename ?? null,
}, },
transfer transfer
) )
@ -3118,8 +3107,8 @@ class WorkerTransport {
.then(results => ({ .then(results => ({
info: results[0], info: results[0],
metadata: results[1] ? new Metadata(results[1]) : null, metadata: results[1] ? new Metadata(results[1]) : null,
contentDispositionFilename: this._fullReader?.filename ?? null, contentDispositionFilename: this.#fullReader?.filename ?? null,
contentLength: this._fullReader?.contentLength ?? null, contentLength: this.#fullReader?.contentLength ?? null,
hasStructTree: results[2], hasStructTree: results[2],
})); }));
this.#methodPromises.set(name, promise); this.#methodPromises.set(name, promise);

View File

@ -13,14 +13,19 @@
* limitations under the License. * limitations under the License.
*/ */
import { AbortException, assert, warn } from "../shared/util.js"; import { AbortException, warn } from "../shared/util.js";
import {
BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader,
} from "../shared/base_pdf_stream.js";
import { import {
createHeaders, createHeaders,
createResponseError, createResponseError,
ensureResponseOrigin,
extractFilenameFromHeader, extractFilenameFromHeader,
getResponseOrigin, getResponseOrigin,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
validateResponseStatus,
} from "./network_utils.js"; } from "./network_utils.js";
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
@ -29,15 +34,21 @@ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
); );
} }
function createFetchOptions(headers, withCredentials, abortController) { function fetchUrl(url, headers, withCredentials, abortController) {
return { return fetch(url, {
method: "GET", method: "GET",
headers, headers,
signal: abortController.signal, signal: abortController.signal,
mode: "cors", mode: "cors",
credentials: withCredentials ? "include" : "same-origin", credentials: withCredentials ? "include" : "same-origin",
redirect: "follow", redirect: "follow",
}; });
}
function ensureResponseStatus(status, url) {
if (status !== 200 && status !== 206) {
throw createResponseError(status, url);
}
} }
function getArrayBuffer(val) { function getArrayBuffer(val) {
@ -51,86 +62,44 @@ function getArrayBuffer(val) {
return new Uint8Array(val).buffer; return new Uint8Array(val).buffer;
} }
/** @implements {IPDFStream} */ class PDFFetchStream extends BasePDFStream {
class PDFFetchStream {
_responseOrigin = null; _responseOrigin = null;
constructor(source) { constructor(source) {
this.source = source; super(source, PDFFetchStreamReader, PDFFetchStreamRangeReader);
this.isHttp = /^https?:/i.test(source.url); this.isHttp = /^https?:/i.test(source.url);
this.headers = createHeaders(this.isHttp, source.httpHeaders); this.headers = createHeaders(this.isHttp, source.httpHeaders);
this._fullRequestReader = null;
this._rangeRequestReaders = [];
}
get _progressiveDataLength() {
return this._fullRequestReader?._loaded ?? 0;
}
getFullReader() {
assert(
!this._fullRequestReader,
"PDFFetchStream.getFullReader can only be called once."
);
this._fullRequestReader = new PDFFetchStreamReader(this);
return this._fullRequestReader;
}
getRangeReader(begin, end) {
if (end <= this._progressiveDataLength) {
return null;
}
const reader = new PDFFetchStreamRangeReader(this, begin, end);
this._rangeRequestReaders.push(reader);
return reader;
}
cancelAllRequests(reason) {
this._fullRequestReader?.cancel(reason);
for (const reader of this._rangeRequestReaders.slice(0)) {
reader.cancel(reason);
}
} }
} }
/** @implements {IPDFStreamReader} */ class PDFFetchStreamReader extends BasePDFStreamReader {
class PDFFetchStreamReader { _abortController = new AbortController();
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;
}
this._abortController = new AbortController(); _reader = null;
this._isStreamingSupported = !source.disableStream;
this._isRangeSupported = !source.disableRange; 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. // Always create a copy of the headers.
const headers = new Headers(stream.headers); const headers = new Headers(stream.headers);
const url = source.url; fetchUrl(url, headers, withCredentials, this._abortController)
fetch(
url,
createFetchOptions(headers, this._withCredentials, this._abortController)
)
.then(response => { .then(response => {
stream._responseOrigin = getResponseOrigin(response.url); stream._responseOrigin = getResponseOrigin(response.url);
if (!validateResponseStatus(response.status)) { ensureResponseStatus(response.status, url);
throw createResponseError(response.status, url);
}
this._reader = response.body.getReader(); this._reader = response.body.getReader();
this._headersCapability.resolve();
const responseHeaders = response.headers; const responseHeaders = response.headers;
@ -138,8 +107,8 @@ class PDFFetchStreamReader {
validateRangeRequestCapabilities({ validateRangeRequestCapabilities({
responseHeaders, responseHeaders,
isHttp: stream.isHttp, isHttp: stream.isHttp,
rangeChunkSize: this._rangeChunkSize, rangeChunkSize,
disableRange: this._disableRange, disableRange,
}); });
this._isRangeSupported = allowRangeRequests; this._isRangeSupported = allowRangeRequests;
@ -153,30 +122,10 @@ class PDFFetchStreamReader {
if (!this._isStreamingSupported && this._isRangeSupported) { if (!this._isStreamingSupported && this._isRangeSupported) {
this.cancel(new AbortException("Streaming is disabled.")); this.cancel(new AbortException("Streaming is disabled."));
} }
this._headersCapability.resolve();
}) })
.catch(this._headersCapability.reject); .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() { async read() {
@ -200,48 +149,32 @@ class PDFFetchStreamReader {
} }
} }
/** @implements {IPDFStreamRangeReader} */ class PDFFetchStreamRangeReader extends BasePDFStreamRangeReader {
class PDFFetchStreamRangeReader { _abortController = new AbortController();
constructor(stream, begin, end) {
this._stream = stream; _readCapability = Promise.withResolvers();
this._reader = null;
this._loaded = 0; _reader = null;
const source = stream.source;
this._withCredentials = source.withCredentials || false; constructor(stream, begin, end) {
this._readCapability = Promise.withResolvers(); super(stream, begin, end);
this._isStreamingSupported = !source.disableStream; const { url, withCredentials } = stream._source;
this._abortController = new AbortController();
// Always create a copy of the headers. // Always create a copy of the headers.
const headers = new Headers(stream.headers); const headers = new Headers(stream.headers);
headers.append("Range", `bytes=${begin}-${end - 1}`); headers.append("Range", `bytes=${begin}-${end - 1}`);
const url = source.url; fetchUrl(url, headers, withCredentials, this._abortController)
fetch(
url,
createFetchOptions(headers, this._withCredentials, this._abortController)
)
.then(response => { .then(response => {
const responseOrigin = getResponseOrigin(response.url); const responseOrigin = getResponseOrigin(response.url);
if (responseOrigin !== stream._responseOrigin) { ensureResponseOrigin(responseOrigin, stream._responseOrigin);
throw new Error( ensureResponseStatus(response.status, url);
`Expected range response-origin "${responseOrigin}" to match "${stream._responseOrigin}".`
);
}
if (!validateResponseStatus(response.status)) {
throw createResponseError(response.status, url);
}
this._readCapability.resolve();
this._reader = response.body.getReader(); this._reader = response.body.getReader();
this._readCapability.resolve();
}) })
.catch(this._readCapability.reject); .catch(this._readCapability.reject);
this.onProgress = null;
}
get isStreamingSupported() {
return this._isStreamingSupported;
} }
async read() { async read() {
@ -250,9 +183,6 @@ class PDFFetchStreamRangeReader {
if (done) { if (done) {
return { value, done }; return { value, done };
} }
this._loaded += value.byteLength;
this.onProgress?.({ loaded: this._loaded });
return { value: getArrayBuffer(value), done: false }; return { value: getArrayBuffer(value), done: false };
} }

View File

@ -14,9 +14,15 @@
*/ */
import { assert, stringToBytes, warn } from "../shared/util.js"; import { assert, stringToBytes, warn } from "../shared/util.js";
import {
BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader,
} from "../shared/base_pdf_stream.js";
import { import {
createHeaders, createHeaders,
createResponseError, createResponseError,
ensureResponseOrigin,
extractFilenameFromHeader, extractFilenameFromHeader,
getResponseOrigin, getResponseOrigin,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
@ -35,18 +41,13 @@ function getArrayBuffer(val) {
return typeof val !== "string" ? val : stringToBytes(val).buffer; return typeof val !== "string" ? val : stringToBytes(val).buffer;
} }
/** @implements {IPDFStream} */ class PDFNetworkStream extends BasePDFStream {
class PDFNetworkStream {
#pendingRequests = new WeakMap(); #pendingRequests = new WeakMap();
_fullRequestReader = null;
_rangeRequestReaders = [];
_responseOrigin = null; _responseOrigin = null;
constructor(source) { constructor(source) {
this._source = source; super(source, PDFNetworkStreamReader, PDFNetworkStreamRangeReader);
this.url = source.url; this.url = source.url;
this.isHttp = /^https?:/i.test(this.url); this.isHttp = /^https?:/i.test(this.url);
this.headers = createHeaders(this.isHttp, source.httpHeaders); this.headers = createHeaders(this.isHttp, source.httpHeaders);
@ -160,70 +161,44 @@ class PDFNetworkStream {
} }
} }
getFullReader() {
assert(
!this._fullRequestReader,
"PDFNetworkStream.getFullReader can only be called once."
);
this._fullRequestReader = new PDFNetworkStreamFullRequestReader(this);
return this._fullRequestReader;
}
getRangeReader(begin, end) { getRangeReader(begin, end) {
const reader = new PDFNetworkStreamRangeRequestReader(this, begin, end); const reader = super.getRangeReader(begin, end);
reader.onClosed = () => {
const i = this._rangeRequestReaders.indexOf(reader); if (reader) {
if (i >= 0) { reader.onClosed = () => this._rangeReaders.delete(reader);
this._rangeRequestReaders.splice(i, 1);
} }
};
this._rangeRequestReaders.push(reader);
return reader; return reader;
} }
cancelAllRequests(reason) {
this._fullRequestReader?.cancel(reason);
for (const reader of this._rangeRequestReaders.slice(0)) {
reader.cancel(reason);
}
}
} }
/** @implements {IPDFStreamReader} */ class PDFNetworkStreamReader extends BasePDFStreamReader {
class PDFNetworkStreamFullRequestReader { _cachedChunks = [];
_done = false;
_requests = [];
_storedError = null;
constructor(stream) { constructor(stream) {
this._stream = stream; super(stream);
const { disableRange, length, rangeChunkSize } = stream._source; 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({ this._fullRequestXhr = stream._request({
onHeadersReceived: this._onHeadersReceived.bind(this), onHeadersReceived: this.#onHeadersReceived.bind(this),
onDone: this._onDone.bind(this), onDone: this.#onDone.bind(this),
onError: this._onError.bind(this), onError: this.#onError.bind(this),
onProgress: this._onProgress.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; #onHeadersReceived() {
this._isRangeSupported = false;
this._cachedChunks = [];
this._requests = [];
this._done = false;
this._storedError = undefined;
this._filename = null;
this.onProgress = null;
}
_onHeadersReceived() {
const stream = this._stream; const stream = this._stream;
const { disableRange, rangeChunkSize } = stream._source;
const fullRequestXhr = this._fullRequestXhr; const fullRequestXhr = this._fullRequestXhr;
stream._responseOrigin = getResponseOrigin(fullRequestXhr.responseURL); stream._responseOrigin = getResponseOrigin(fullRequestXhr.responseURL);
@ -246,8 +221,8 @@ class PDFNetworkStreamFullRequestReader {
validateRangeRequestCapabilities({ validateRangeRequestCapabilities({
responseHeaders, responseHeaders,
isHttp: stream.isHttp, isHttp: stream.isHttp,
rangeChunkSize: this._rangeChunkSize, rangeChunkSize,
disableRange: this._disableRange, disableRange,
}); });
if (allowRangeRequests) { if (allowRangeRequests) {
@ -269,10 +244,10 @@ class PDFNetworkStreamFullRequestReader {
this._headersCapability.resolve(); this._headersCapability.resolve();
} }
_onDone(chunk) { #onDone(chunk) {
if (this._requests.length > 0) { if (this._requests.length > 0) {
const requestCapability = this._requests.shift(); const capability = this._requests.shift();
requestCapability.resolve({ value: chunk, done: false }); capability.resolve({ value: chunk, done: false });
} else { } else {
this._cachedChunks.push(chunk); this._cachedChunks.push(chunk);
} }
@ -280,49 +255,29 @@ class PDFNetworkStreamFullRequestReader {
if (this._cachedChunks.length > 0) { if (this._cachedChunks.length > 0) {
return; return;
} }
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
} }
_onError(status) { #onError(status) {
this._storedError = createResponseError(status, this._stream.url); this._storedError = createResponseError(status, this._stream.url);
this._headersCapability.reject(this._storedError); this._headersCapability.reject(this._storedError);
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.reject(this._storedError); capability.reject(this._storedError);
} }
this._requests.length = 0; this._requests.length = 0;
this._cachedChunks.length = 0; this._cachedChunks.length = 0;
} }
_onProgress(evt) { #onProgress(evt) {
this.onProgress?.({ this.onProgress?.({
loaded: evt.loaded, loaded: evt.loaded,
total: evt.lengthComputable ? evt.total : this._contentLength, 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() { async read() {
await this._headersCapability.promise; await this._headersCapability.promise;
@ -336,16 +291,16 @@ class PDFNetworkStreamFullRequestReader {
if (this._done) { if (this._done) {
return { value: undefined, done: true }; return { value: undefined, done: true };
} }
const requestCapability = Promise.withResolvers(); const capability = Promise.withResolvers();
this._requests.push(requestCapability); this._requests.push(capability);
return requestCapability.promise; return capability.promise;
} }
cancel(reason) { cancel(reason) {
this._done = true; this._done = true;
this._headersCapability.reject(reason); this._headersCapability.reject(reason);
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
@ -354,74 +309,64 @@ class PDFNetworkStreamFullRequestReader {
} }
} }
/** @implements {IPDFStreamRangeReader} */ class PDFNetworkStreamRangeReader extends BasePDFStreamRangeReader {
class PDFNetworkStreamRangeRequestReader {
onClosed = null; onClosed = null;
_done = false;
_queuedChunk = null;
_requests = [];
_storedError = null;
constructor(stream, begin, end) { constructor(stream, begin, end) {
this._stream = stream; super(stream, begin, end);
this._requestXhr = stream._request({ this._requestXhr = stream._request({
begin, begin,
end, end,
onHeadersReceived: this._onHeadersReceived.bind(this), onHeadersReceived: this.#onHeadersReceived.bind(this),
onDone: this._onDone.bind(this), onDone: this.#onDone.bind(this),
onError: this._onError.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() { #onHeadersReceived() {
const responseOrigin = getResponseOrigin(this._requestXhr?.responseURL); const responseOrigin = getResponseOrigin(this._requestXhr?.responseURL);
try {
if (responseOrigin !== this._stream._responseOrigin) { ensureResponseOrigin(responseOrigin, this._stream._responseOrigin);
this._storedError = new Error( } catch (ex) {
`Expected range response-origin "${responseOrigin}" to match "${this._stream._responseOrigin}".` this._storedError = ex;
); this.#onError(0);
this._onError(0);
} }
} }
_onDone(chunk) { #onDone(chunk) {
if (this._requests.length > 0) { if (this._requests.length > 0) {
const requestCapability = this._requests.shift(); const capability = this._requests.shift();
requestCapability.resolve({ value: chunk, done: false }); capability.resolve({ value: chunk, done: false });
} else { } else {
this._queuedChunk = chunk; this._queuedChunk = chunk;
} }
this._done = true; this._done = true;
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
this.onClosed?.(this); this.onClosed?.();
} }
_onError(status) { #onError(status) {
this._storedError ??= createResponseError(status, this._stream.url); this._storedError ??= createResponseError(status, this._stream.url);
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.reject(this._storedError); capability.reject(this._storedError);
} }
this._requests.length = 0; this._requests.length = 0;
this._queuedChunk = null; this._queuedChunk = null;
} }
_onProgress(evt) {
if (!this.isStreamingSupported) {
this.onProgress?.({ loaded: evt.loaded });
}
}
get isStreamingSupported() {
return false;
}
async read() { async read() {
if (this._storedError) { if (this._storedError) {
throw this._storedError; throw this._storedError;
@ -434,20 +379,20 @@ class PDFNetworkStreamRangeRequestReader {
if (this._done) { if (this._done) {
return { value: undefined, done: true }; return { value: undefined, done: true };
} }
const requestCapability = Promise.withResolvers(); const capability = Promise.withResolvers();
this._requests.push(requestCapability); this._requests.push(capability);
return requestCapability.promise; return capability.promise;
} }
cancel(reason) { cancel(reason) {
this._done = true; this._done = true;
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
this._stream._abortRequest(this._requestXhr); this._stream._abortRequest(this._requestXhr);
this.onClosed?.(this); this.onClosed?.();
} }
} }

View File

@ -107,15 +107,19 @@ function createResponseError(status, url) {
); );
} }
function validateResponseStatus(status) { function ensureResponseOrigin(rangeOrigin, origin) {
return status === 200 || status === 206; if (rangeOrigin !== origin) {
throw new Error(
`Expected range response-origin "${rangeOrigin}" to match "${origin}".`
);
}
} }
export { export {
createHeaders, createHeaders,
createResponseError, createResponseError,
ensureResponseOrigin,
extractFilenameFromHeader, extractFilenameFromHeader,
getResponseOrigin, getResponseOrigin,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
validateResponseStatus,
}; };

View File

@ -15,6 +15,11 @@
/* globals process */ /* globals process */
import { AbortException, assert, warn } from "../shared/util.js"; import { AbortException, assert, warn } from "../shared/util.js";
import {
BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader,
} from "../shared/base_pdf_stream.js";
import { createResponseError } from "./network_utils.js"; import { createResponseError } from "./network_utils.js";
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
@ -60,70 +65,28 @@ function getArrayBuffer(val) {
return new Uint8Array(val).buffer; return new Uint8Array(val).buffer;
} }
class PDFNodeStream { class PDFNodeStream extends BasePDFStream {
constructor(source) { constructor(source) {
this.source = source; super(source, PDFNodeStreamReader, PDFNodeStreamRangeReader);
this.url = parseUrlOrPath(source.url); this.url = parseUrlOrPath(source.url);
assert( assert(
this.url.protocol === "file:", this.url.protocol === "file:",
"PDFNodeStream only supports file:// URLs." "PDFNodeStream only supports file:// URLs."
); );
this._fullRequestReader = null;
this._rangeRequestReaders = [];
}
get _progressiveDataLength() {
return this._fullRequestReader?._loaded ?? 0;
}
getFullReader() {
assert(
!this._fullRequestReader,
"PDFNodeStream.getFullReader can only be called once."
);
this._fullRequestReader = new PDFNodeStreamFsFullReader(this);
return this._fullRequestReader;
}
getRangeReader(begin, end) {
if (end <= this._progressiveDataLength) {
return null;
}
const rangeReader = new PDFNodeStreamFsRangeReader(this, begin, end);
this._rangeRequestReaders.push(rangeReader);
return rangeReader;
}
cancelAllRequests(reason) {
this._fullRequestReader?.cancel(reason);
for (const reader of this._rangeRequestReaders.slice(0)) {
reader.cancel(reason);
}
} }
} }
class PDFNodeStreamFsFullReader { class PDFNodeStreamReader extends BasePDFStreamReader {
_headersCapability = Promise.withResolvers();
_reader = null; _reader = null;
constructor(stream) { constructor(stream) {
this.onProgress = null; super(stream);
const source = stream.source; const { disableRange, disableStream, length, rangeChunkSize } =
this._contentLength = source.length; // optional stream._source;
this._loaded = 0;
this._filename = null;
this._disableRange = source.disableRange || false; this._contentLength = length;
this._rangeChunkSize = source.rangeChunkSize; this._isStreamingSupported = !disableStream;
if (!this._rangeChunkSize && !this._disableRange) { this._isRangeSupported = !disableRange;
this._disableRange = true;
}
this._isStreamingSupported = !source.disableStream;
this._isRangeSupported = !source.disableRange;
const url = stream.url; const url = stream.url;
const fs = process.getBuiltinModule("fs"); const fs = process.getBuiltinModule("fs");
@ -136,7 +99,7 @@ class PDFNodeStreamFsFullReader {
this._reader = readableStream.getReader(); this._reader = readableStream.getReader();
const { size } = stat; 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 // 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. // make any sense to abort the request and retry with a range request.
this._isRangeSupported = false; this._isRangeSupported = false;
@ -160,26 +123,6 @@ class PDFNodeStreamFsFullReader {
}); });
} }
get headersReady() {
return this._headersCapability.promise;
}
get filename() {
return this._filename;
}
get contentLength() {
return this._contentLength;
}
get isRangeSupported() {
return this._isRangeSupported;
}
get isStreamingSupported() {
return this._isStreamingSupported;
}
async read() { async read() {
await this._headersCapability.promise; await this._headersCapability.promise;
const { value, done } = await this._reader.read(); const { value, done } = await this._reader.read();
@ -200,16 +143,13 @@ class PDFNodeStreamFsFullReader {
} }
} }
class PDFNodeStreamFsRangeReader { class PDFNodeStreamRangeReader extends BasePDFStreamRangeReader {
_readCapability = Promise.withResolvers(); _readCapability = Promise.withResolvers();
_reader = null; _reader = null;
constructor(stream, begin, end) { constructor(stream, begin, end) {
this.onProgress = null; super(stream, begin, end);
this._loaded = 0;
const source = stream.source;
this._isStreamingSupported = !source.disableStream;
const url = stream.url; const url = stream.url;
const fs = process.getBuiltinModule("fs"); const fs = process.getBuiltinModule("fs");
@ -228,19 +168,12 @@ class PDFNodeStreamFsRangeReader {
} }
} }
get isStreamingSupported() {
return this._isStreamingSupported;
}
async read() { async read() {
await this._readCapability.promise; await this._readCapability.promise;
const { value, done } = await this._reader.read(); const { value, done } = await this._reader.read();
if (done) { if (done) {
return { value, done }; return { value, done };
} }
this._loaded += value.length;
this.onProgress?.({ loaded: this._loaded });
return { value: getArrayBuffer(value), done: false }; return { value: getArrayBuffer(value), done: false };
} }

View File

@ -13,185 +13,137 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("../interfaces").IPDFStream} IPDFStream */ import {
/** @typedef {import("../interfaces").IPDFStreamReader} IPDFStreamReader */ BasePDFStream,
// eslint-disable-next-line max-len BasePDFStreamRangeReader,
/** @typedef {import("../interfaces").IPDFStreamRangeReader} IPDFStreamRangeReader */ BasePDFStreamReader,
} from "../shared/base_pdf_stream.js";
import { assert } from "../shared/util.js"; import { assert } from "../shared/util.js";
import { isPdfFile } from "./display_utils.js"; import { isPdfFile } from "./display_utils.js";
/** @implements {IPDFStream} */ function getArrayBuffer(val) {
class PDFDataTransportStream {
constructor(
pdfDataRangeTransport,
{ disableRange = false, disableStream = false }
) {
assert(
pdfDataRangeTransport,
'PDFDataTransportStream - missing required "pdfDataRangeTransport" argument.'
);
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 // Prevent any possible issues by only transferring a Uint8Array that
// completely "utilizes" its underlying ArrayBuffer. // completely "utilizes" its underlying ArrayBuffer.
const buffer = return val instanceof Uint8Array && val.byteLength === val.buffer.byteLength
initialData instanceof Uint8Array && ? val.buffer
initialData.byteLength === initialData.buffer.byteLength : new Uint8Array(val).buffer;
? initialData.buffer }
: new Uint8Array(initialData).buffer;
class PDFDataTransportStream extends BasePDFStream {
_progressiveDone = false;
_queuedChunks = [];
constructor(source) {
super(
source,
PDFDataTransportStreamReader,
PDFDataTransportStreamRangeReader
);
const { pdfDataRangeTransport } = source;
const { initialData, progressiveDone } = pdfDataRangeTransport;
if (initialData?.length > 0) {
const buffer = getArrayBuffer(initialData);
this._queuedChunks.push(buffer); this._queuedChunks.push(buffer);
} }
this._progressiveDone = progressiveDone;
this._pdfDataRangeTransport = pdfDataRangeTransport;
this._isStreamingSupported = !disableStream;
this._isRangeSupported = !disableRange;
this._contentLength = length;
this._fullRequestReader = null;
this._rangeReaders = [];
pdfDataRangeTransport.addRangeListener((begin, chunk) => { pdfDataRangeTransport.addRangeListener((begin, chunk) => {
this._onReceiveData({ begin, chunk }); this.#onReceiveData(begin, chunk);
}); });
pdfDataRangeTransport.addProgressListener((loaded, total) => { pdfDataRangeTransport.addProgressListener((loaded, total) => {
this._onProgress({ loaded, total }); if (total !== undefined) {
this._fullReader?.onProgress?.({ loaded, total });
}
}); });
pdfDataRangeTransport.addProgressiveReadListener(chunk => { pdfDataRangeTransport.addProgressiveReadListener(chunk => {
this._onReceiveData({ chunk }); this.#onReceiveData(/* begin = */ undefined, chunk);
}); });
pdfDataRangeTransport.addProgressiveDoneListener(() => { pdfDataRangeTransport.addProgressiveDoneListener(() => {
this._onProgressiveDone(); this._fullReader?.progressiveDone();
this._progressiveDone = true;
}); });
pdfDataRangeTransport.transportReady(); pdfDataRangeTransport.transportReady();
} }
_onReceiveData({ begin, chunk }) { #onReceiveData(begin, chunk) {
// Prevent any possible issues by only transferring a Uint8Array that const buffer = getArrayBuffer(chunk);
// completely "utilizes" its underlying ArrayBuffer.
const buffer =
chunk instanceof Uint8Array &&
chunk.byteLength === chunk.buffer.byteLength
? chunk.buffer
: new Uint8Array(chunk).buffer;
if (begin === undefined) { if (begin === undefined) {
if (this._fullRequestReader) { if (this._fullReader) {
this._fullRequestReader._enqueue(buffer); this._fullReader._enqueue(buffer);
} else { } else {
this._queuedChunks.push(buffer); this._queuedChunks.push(buffer);
} }
} else { } else {
const found = this._rangeReaders.some(function (rangeReader) { const rangeReader = this._rangeReaders
if (rangeReader._begin !== begin) { .keys()
return false; .find(r => r._begin === begin);
}
rangeReader._enqueue(buffer);
return true;
});
assert( assert(
found, rangeReader,
"_onReceiveData - no `PDFDataTransportStreamRangeReader` instance found." "#onReceiveData - no `PDFDataTransportStreamRangeReader` instance found."
); );
} rangeReader._enqueue(buffer);
}
get _progressiveDataLength() {
return this._fullRequestReader?._loaded ?? 0;
}
_onProgress(evt) {
if (evt.total === undefined) {
// Reporting to first range reader, if it exists.
this._rangeReaders[0]?.onProgress?.({ loaded: evt.loaded });
} else {
this._fullRequestReader?.onProgress?.({
loaded: evt.loaded,
total: evt.total,
});
}
}
_onProgressiveDone() {
this._fullRequestReader?.progressiveDone();
this._progressiveDone = true;
}
_removeRangeReader(reader) {
const i = this._rangeReaders.indexOf(reader);
if (i >= 0) {
this._rangeReaders.splice(i, 1);
} }
} }
getFullReader() { getFullReader() {
assert( const reader = super.getFullReader();
!this._fullRequestReader,
"PDFDataTransportStream.getFullReader can only be called once."
);
const queuedChunks = this._queuedChunks;
this._queuedChunks = null; this._queuedChunks = null;
return new PDFDataTransportStreamReader( return reader;
this,
queuedChunks,
this._progressiveDone,
this._contentDispositionFilename
);
} }
getRangeReader(begin, end) { getRangeReader(begin, end) {
if (end <= this._progressiveDataLength) { const reader = super.getRangeReader(begin, end);
return null;
if (reader) {
reader.onDone = () => this._rangeReaders.delete(reader);
this._source.pdfDataRangeTransport.requestDataRange(begin, end);
} }
const reader = new PDFDataTransportStreamRangeReader(this, begin, end);
this._pdfDataRangeTransport.requestDataRange(begin, end);
this._rangeReaders.push(reader);
return reader; return reader;
} }
cancelAllRequests(reason) { cancelAllRequests(reason) {
this._fullRequestReader?.cancel(reason); super.cancelAllRequests(reason);
for (const reader of this._rangeReaders.slice(0)) { this._source.pdfDataRangeTransport.abort();
reader.cancel(reason);
}
this._pdfDataRangeTransport.abort();
} }
} }
/** @implements {IPDFStreamReader} */ class PDFDataTransportStreamReader extends BasePDFStreamReader {
class PDFDataTransportStreamReader { _done = false;
constructor(
stream, _queuedChunks = null;
queuedChunks,
progressiveDone = false, _requests = [];
contentDispositionFilename = null
) { constructor(stream) {
this._stream = stream; super(stream);
this._done = progressiveDone || false; const { pdfDataRangeTransport, disableRange, disableStream } =
this._filename = isPdfFile(contentDispositionFilename) stream._source;
? contentDispositionFilename const { length, contentDispositionFilename } = pdfDataRangeTransport;
: null;
this._queuedChunks = queuedChunks || []; this._queuedChunks = stream._queuedChunks || [];
this._loaded = 0;
for (const chunk of this._queuedChunks) { for (const chunk of this._queuedChunks) {
this._loaded += chunk.byteLength; this._loaded += chunk.byteLength;
} }
this._requests = []; this._done = stream._progressiveDone;
this._headersReady = Promise.resolve();
stream._fullRequestReader = this;
this.onProgress = null; this._contentLength = length;
this._isStreamingSupported = !disableStream;
this._isRangeSupported = !disableRange;
if (isPdfFile(contentDispositionFilename)) {
this._filename = contentDispositionFilename;
}
this._headersCapability.resolve();
} }
_enqueue(chunk) { _enqueue(chunk) {
@ -199,34 +151,14 @@ class PDFDataTransportStreamReader {
return; // Ignore new data. return; // Ignore new data.
} }
if (this._requests.length > 0) { if (this._requests.length > 0) {
const requestCapability = this._requests.shift(); const capability = this._requests.shift();
requestCapability.resolve({ value: chunk, done: false }); capability.resolve({ value: chunk, done: false });
} else { } else {
this._queuedChunks.push(chunk); this._queuedChunks.push(chunk);
} }
this._loaded += chunk.byteLength; 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() { async read() {
if (this._queuedChunks.length > 0) { if (this._queuedChunks.length > 0) {
const chunk = this._queuedChunks.shift(); const chunk = this._queuedChunks.shift();
@ -235,38 +167,38 @@ class PDFDataTransportStreamReader {
if (this._done) { if (this._done) {
return { value: undefined, done: true }; return { value: undefined, done: true };
} }
const requestCapability = Promise.withResolvers(); const capability = Promise.withResolvers();
this._requests.push(requestCapability); this._requests.push(capability);
return requestCapability.promise; return capability.promise;
} }
cancel(reason) { cancel(reason) {
this._done = true; this._done = true;
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
} }
progressiveDone() { progressiveDone() {
if (this._done) { this._done ||= true;
return;
}
this._done = true;
} }
} }
/** @implements {IPDFStreamRangeReader} */ class PDFDataTransportStreamRangeReader extends BasePDFStreamRangeReader {
class PDFDataTransportStreamRangeReader { onDone = null;
constructor(stream, begin, end) {
this._stream = stream;
this._begin = begin;
this._end = end;
this._queuedChunk = null;
this._requests = [];
this._done = false;
this.onProgress = null; _begin = -1;
_done = false;
_queuedChunk = null;
_requests = [];
constructor(stream, begin, end) {
super(stream, begin, end);
this._begin = begin;
} }
_enqueue(chunk) { _enqueue(chunk) {
@ -276,19 +208,16 @@ class PDFDataTransportStreamRangeReader {
if (this._requests.length === 0) { if (this._requests.length === 0) {
this._queuedChunk = chunk; this._queuedChunk = chunk;
} else { } else {
const requestsCapability = this._requests.shift(); const firstCapability = this._requests.shift();
requestsCapability.resolve({ value: chunk, done: false }); firstCapability.resolve({ value: chunk, done: false });
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._requests.length = 0;
} }
this._done = true; this._done = true;
this._stream._removeRangeReader(this); this.onDone?.();
}
get isStreamingSupported() {
return false;
} }
async read() { async read() {
@ -300,18 +229,18 @@ class PDFDataTransportStreamRangeReader {
if (this._done) { if (this._done) {
return { value: undefined, done: true }; return { value: undefined, done: true };
} }
const requestCapability = Promise.withResolvers(); const capability = Promise.withResolvers();
this._requests.push(requestCapability); this._requests.push(capability);
return requestCapability.promise; return capability.promise;
} }
cancel(reason) { cancel(reason) {
this._done = true; this._done = true;
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
this._stream._removeRangeReader(this); this.onDone?.();
} }
} }

View File

@ -13,56 +13,119 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert, unreachable } from "./util.js";
/** /**
* Interface that represents PDF data transport. If possible, it allows * Interface that represents PDF data transport. If possible, it allows
* progressively load entire or fragment of the PDF binary data. * 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. * Gets a reader for the entire PDF data.
* @returns {IPDFStreamReader} * @returns {BasePDFStreamReader}
*/ */
getFullReader() { 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. * Gets a reader for the range of the PDF data.
* *
* NOTE: Currently this method is only expected to be invoked *after* * 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} begin - the start offset of the data.
* @param {number} end - the end offset of the data. * @param {number} end - the end offset of the data.
* @returns {IPDFStreamRangeReader} * @returns {BasePDFStreamRangeReader}
*/ */
getRangeReader(begin, end) { getRangeReader(begin, end) {
if (end <= this._progressiveDataLength) {
return null; 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. * Cancels all opened reader and closes all their opened requests.
* @param {Object} reason - the reason for cancelling * @param {Object} reason - the reason for cancelling
*/ */
cancelAllRequests(reason) {} cancelAllRequests(reason) {
this._fullReader?.cancel(reason);
// Always create a copy of the rangeReaders.
for (const reader of new Set(this._rangeReaders)) {
reader.cancel(reason);
}
}
} }
/** /**
* Interface for a PDF binary data reader. * Interface for a PDF binary data reader.
*
* @interface
*/ */
class IPDFStreamReader { class BasePDFStreamReader {
constructor() {
/** /**
* Sets or gets the progress callback. The callback can be useful when the * Sets or gets the progress callback. The callback can be useful when the
* isStreamingSupported property of the object is defined as false. * isStreamingSupported property of the object is defined as false.
* The callback is called with one parameter: an object with the loaded and * The callback is called with one parameter: an object with the loaded and
* total properties. * total properties.
*/ */
this.onProgress = null; onProgress = null;
_contentLength = 0;
_filename = null;
_headersCapability = Promise.withResolvers();
_isRangeSupported = false;
_isStreamingSupported = false;
_loaded = 0;
_stream = null;
constructor(stream) {
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
this.constructor === BasePDFStreamReader
) {
unreachable("Cannot initialize BasePDFStreamReader.");
}
this._stream = stream;
} }
/** /**
@ -71,7 +134,7 @@ class IPDFStreamReader {
* @type {Promise} * @type {Promise}
*/ */
get headersReady() { get headersReady() {
return Promise.resolve(); return this._headersCapability.promise;
} }
/** /**
@ -81,7 +144,7 @@ class IPDFStreamReader {
* header is missing/invalid. * header is missing/invalid.
*/ */
get filename() { get filename() {
return null; return this._filename;
} }
/** /**
@ -90,7 +153,7 @@ class IPDFStreamReader {
* @type {number} The data length (or 0 if unknown). * @type {number} The data length (or 0 if unknown).
*/ */
get contentLength() { get contentLength() {
return 0; return this._contentLength;
} }
/** /**
@ -100,7 +163,7 @@ class IPDFStreamReader {
* @type {boolean} * @type {boolean}
*/ */
get isRangeSupported() { get isRangeSupported() {
return false; return this._isRangeSupported;
} }
/** /**
@ -109,7 +172,7 @@ class IPDFStreamReader {
* @type {boolean} * @type {boolean}
*/ */
get isStreamingSupported() { get isStreamingSupported() {
return false; return this._isStreamingSupported;
} }
/** /**
@ -120,37 +183,33 @@ class IPDFStreamReader {
* set to true. * set to true.
* @returns {Promise} * @returns {Promise}
*/ */
async read() {} async read() {
unreachable("Abstract method `read` called");
}
/** /**
* Cancels all pending read requests and closes the stream. * Cancels all pending read requests and closes the stream.
* @param {Object} reason * @param {Object} reason
*/ */
cancel(reason) {} cancel(reason) {
unreachable("Abstract method `cancel` called");
}
} }
/** /**
* Interface for a PDF binary data fragment reader. * Interface for a PDF binary data fragment reader.
*
* @interface
*/ */
class IPDFStreamRangeReader { class BasePDFStreamRangeReader {
constructor() { _stream = null;
/**
* 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;
}
/** constructor(stream, begin, end) {
* Gets ability of the stream to progressively load binary data. if (
* @type {boolean} (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
*/ this.constructor === BasePDFStreamRangeReader
get isStreamingSupported() { ) {
return false; unreachable("Cannot initialize BasePDFStreamRangeReader.");
}
this._stream = stream;
} }
/** /**
@ -161,13 +220,17 @@ class IPDFStreamRangeReader {
* set to true. * set to true.
* @returns {Promise} * @returns {Promise}
*/ */
async read() {} async read() {
unreachable("Abstract method `read` called");
}
/** /**
* Cancels all pending read requests and closes the stream. * Cancels all pending read requests and closes the stream.
* @param {Object} reason * @param {Object} reason
*/ */
cancel(reason) {} cancel(reason) {
unreachable("Abstract method `cancel` called");
}
} }
export { IPDFStream, IPDFStreamRangeReader, IPDFStreamReader }; export { BasePDFStream, BasePDFStreamRangeReader, BasePDFStreamReader };

View File

@ -160,14 +160,18 @@ describe("api", function () {
progressReportedCapability.resolve(progressData); progressReportedCapability.resolve(progressData);
}; };
const data = await Promise.all([ const [pdfDoc, progress] = await Promise.all([
progressReportedCapability.promise,
loadingTask.promise, loadingTask.promise,
progressReportedCapability.promise,
]); ]);
expect(data[0].loaded / data[0].total >= 0).toEqual(true); expect(pdfDoc instanceof PDFDocumentProxy).toEqual(true);
expect(data[1] instanceof PDFDocumentProxy).toEqual(true); expect(pdfDoc.loadingTask).toBe(loadingTask);
expect(loadingTask).toEqual(data[1].loadingTask);
expect(progress.loaded).toBeGreaterThanOrEqual(0);
expect(progress.total).toEqual(basicApiFileLength);
expect(progress.percent).toBeGreaterThanOrEqual(0);
expect(progress.percent).toBeLessThanOrEqual(100);
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -218,12 +222,17 @@ describe("api", function () {
progressReportedCapability.resolve(data); progressReportedCapability.resolve(data);
}; };
const data = await Promise.all([ const [pdfDoc, progress] = await Promise.all([
loadingTask.promise, loadingTask.promise,
progressReportedCapability.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. // Check that the TypedArray was transferred.
expect(typedArrayPdf.length).toEqual(0); expect(typedArrayPdf.length).toEqual(0);

View File

@ -16,8 +16,7 @@
import { AbortException, isNodeJS } from "../../src/shared/util.js"; import { AbortException, isNodeJS } from "../../src/shared/util.js";
import { getCrossOriginHostname, TestPdfsServer } from "./test_utils.js"; import { getCrossOriginHostname, TestPdfsServer } from "./test_utils.js";
// Common tests to verify behavior across implementations of the IPDFStream // Common tests to verify behavior across `BasePDFStream` implementations:
// interface:
// - PDFNetworkStream by network_spec.js // - PDFNetworkStream by network_spec.js
// - PDFFetchStream by fetch_stream_spec.js // - PDFFetchStream by fetch_stream_spec.js
async function testCrossOriginRedirects({ async function testCrossOriginRedirects({

View File

@ -35,6 +35,7 @@ describe("fetch_stream", function () {
it("read with streaming", async function () { it("read with streaming", async function () {
const stream = new PDFFetchStream({ const stream = new PDFFetchStream({
url: getPdfUrl(), url: getPdfUrl(),
rangeChunkSize: 32768,
disableStream: false, disableStream: false,
disableRange: true, disableRange: true,
}); });

View File

@ -18,7 +18,6 @@ import {
createResponseError, createResponseError,
extractFilenameFromHeader, extractFilenameFromHeader,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
validateResponseStatus,
} from "../../src/display/network_utils.js"; } from "../../src/display/network_utils.js";
import { ResponseException } from "../../src/shared/util.js"; import { ResponseException } from "../../src/shared/util.js";
@ -391,18 +390,4 @@ describe("network_utils", function () {
testCreateResponseError("https://foo.com/bar.pdf", 0, false); 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);
});
});
}); });

View File

@ -1236,9 +1236,7 @@ const PDFViewerApplication = {
this.passwordPrompt.open(); this.passwordPrompt.open();
}; };
loadingTask.onProgress = ({ loaded, total }) => { loadingTask.onProgress = evt => this.progress(evt.percent);
this.progress(loaded / total);
};
return loadingTask.promise.then( return loadingTask.promise.then(
pdfDocument => { pdfDocument => {
@ -1374,8 +1372,7 @@ const PDFViewerApplication = {
return message; return message;
}, },
progress(level) { progress(percent) {
const percent = Math.round(level * 100);
// When we transition from full request to range requests, it's possible // 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 // 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 // bar to move backwards. So prevent this by only updating the bar if it

View File

@ -13,7 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { isPdfFile, PDFDataRangeTransport } from "pdfjs-lib"; import { isPdfFile, MathClamp, PDFDataRangeTransport } from "pdfjs-lib";
import { AppOptions } from "./app_options.js"; import { AppOptions } from "./app_options.js";
import { BaseExternalServices } from "./external_services.js"; import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js"; import { BasePreferences } from "./preferences.js";
@ -627,7 +627,13 @@ class ExternalServices extends BaseExternalServices {
pdfDataRangeTransport?.onDataProgressiveDone(); pdfDataRangeTransport?.onDataProgressiveDone();
break; break;
case "progress": case "progress":
viewerApp.progress(args.loaded / args.total); const percent = MathClamp(
Math.round((args.loaded / args.total) * 100),
0,
100
);
viewerApp.progress(percent);
break; break;
case "complete": case "complete":
if (!args.data) { if (!args.data) {

View File

@ -709,7 +709,7 @@ class ProgressBar {
} }
set percent(val) { set percent(val) {
this.#percent = MathClamp(val, 0, 100); this.#percent = val;
if (isNaN(val)) { if (isNaN(val)) {
this.#classList.add("indeterminate"); this.#classList.add("indeterminate");