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({