Add an abstract BasePDFStream class, that all the old IPDFStream implementations inherit from

Given that there's no less than *five* different, but very similar, implementations this helps reduce code duplication and simplifies maintenance.

Also, spotted during rebasing, pass the `enableHWA` option "correctly" (i.e. as part of the existing `transportParams`) to the `WorkerTransport`-class to keep the constructor simpler.
This commit is contained in:
Jonas Jenwald 2026-01-30 08:01:40 +01:00
parent a80f10ff1a
commit 4a8fb4dde1
10 changed files with 194 additions and 314 deletions

View File

@ -272,17 +272,17 @@ class ChunkedStreamManager {
_requestsByChunk = new Map(); _requestsByChunk = new Map();
constructor(pdfNetworkStream, args) { 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;
} }
sendRequest(begin, end) { sendRequest(begin, end) {
const rangeReader = this.pdfNetworkStream.getRangeReader(begin, end); const rangeReader = this.pdfStream.getRangeReader(begin, end);
let chunks = []; let chunks = [];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -530,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,55 +13,28 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert } from "../shared/util.js"; import { BasePDFStream } 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} */ /** @implements {IPDFStreamReader} */
class PDFWorkerStreamReader { class PDFWorkerStreamReader {
constructor(msgHandler) { constructor(stream) {
this._msgHandler = msgHandler; const { msgHandler } = stream._source;
this.onProgress = null; this.onProgress = null;
this._contentLength = null; this._contentLength = null;
this._isRangeSupported = false; this._isRangeSupported = false;
this._isStreamingSupported = false; this._isStreamingSupported = false;
const readableStream = this._msgHandler.sendWithStream("GetReader"); const readableStream = msgHandler.sendWithStream("GetReader");
this._reader = readableStream.getReader(); this._reader = readableStream.getReader();
this._headersReady = this._msgHandler this._headersReady = msgHandler
.sendWithPromise("ReaderHeadersReady") .sendWithPromise("ReaderHeadersReady")
.then(data => { .then(data => {
this._isStreamingSupported = data.isStreamingSupported; this._isStreamingSupported = data.isStreamingSupported;
@ -103,10 +76,10 @@ class PDFWorkerStreamReader {
/** @implements {IPDFStreamRangeReader} */ /** @implements {IPDFStreamRangeReader} */
class PDFWorkerStreamRangeReader { class PDFWorkerStreamRangeReader {
constructor(begin, end, msgHandler) { constructor(stream, begin, end) {
this._msgHandler = msgHandler; const { msgHandler } = stream._source;
const readableStream = this._msgHandler.sendWithStream("GetRangeReader", { const readableStream = msgHandler.sendWithStream("GetRangeReader", {
begin, begin,
end, end,
}); });

View File

@ -440,6 +440,7 @@ function getDocument(src = {}) {
ownerDocument, ownerDocument,
pdfBug, pdfBug,
styleElement, styleElement,
enableHWA,
loadingParams: { loadingParams: {
disableAutoFetch, disableAutoFetch,
enableXfa, enableXfa,
@ -463,7 +464,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 +510,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);
@ -2391,8 +2392,16 @@ class PDFWorker {
* @ignore * @ignore
*/ */
class WorkerTransport { class WorkerTransport {
downloadInfoCapability = Promise.withResolvers();
#fullReader = null;
#lastProgress = null;
#methodPromises = new Map(); #methodPromises = new Map();
#networkStream = null;
#pageCache = new Map(); #pageCache = new Map();
#pagePromises = new Map(); #pagePromises = new Map();
@ -2403,21 +2412,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 +2435,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));
@ -2604,7 +2603,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 +2620,18 @@ 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 => { this.#fullReader.onProgress = evt => {
this._lastProgress = { this.#lastProgress = {
loaded: evt.loaded, loaded: evt.loaded,
total: evt.total, 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 +2652,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,18 +2664,18 @@ 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 stream or range are disabled, it's our only way to report
// loading progress. // loading progress.
if (!isStreamingSupported || !isRangeSupported) { if (!isStreamingSupported || !isRangeSupported) {
if (this._lastProgress) { if (this.#lastProgress) {
loadingTask.onProgress?.(this._lastProgress); loadingTask.onProgress?.(this.#lastProgress);
} }
this._fullReader.onProgress = evt => { this.#fullReader.onProgress = evt => {
loadingTask.onProgress?.({ loadingTask.onProgress?.({
loaded: evt.loaded, loaded: evt.loaded,
total: evt.total, total: evt.total,
@ -2689,16 +2688,16 @@ class WorkerTransport {
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 +2705,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;
@ -2950,7 +2949,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 +3117,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,7 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { AbortException, assert, warn } from "../shared/util.js"; import { AbortException, warn } from "../shared/util.js";
import { import {
createHeaders, createHeaders,
createResponseError, createResponseError,
@ -22,6 +22,7 @@ import {
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
validateResponseStatus, validateResponseStatus,
} from "./network_utils.js"; } from "./network_utils.js";
import { BasePDFStream } from "../shared/base_pdf_stream.js";
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
throw new Error( throw new Error(
@ -51,47 +52,13 @@ 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);
}
} }
} }
@ -102,7 +69,7 @@ class PDFFetchStreamReader {
this._reader = null; this._reader = null;
this._loaded = 0; this._loaded = 0;
this._filename = null; this._filename = null;
const source = stream.source; const source = stream._source;
this._withCredentials = source.withCredentials || false; this._withCredentials = source.withCredentials || false;
this._contentLength = source.length; this._contentLength = source.length;
this._headersCapability = Promise.withResolvers(); this._headersCapability = Promise.withResolvers();
@ -205,7 +172,7 @@ class PDFFetchStreamRangeReader {
constructor(stream, begin, end) { constructor(stream, begin, end) {
this._stream = stream; this._stream = stream;
this._reader = null; this._reader = null;
const source = stream.source; const source = stream._source;
this._withCredentials = source.withCredentials || false; this._withCredentials = source.withCredentials || false;
this._readCapability = Promise.withResolvers(); this._readCapability = Promise.withResolvers();

View File

@ -21,6 +21,7 @@ import {
getResponseOrigin, getResponseOrigin,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
} from "./network_utils.js"; } from "./network_utils.js";
import { BasePDFStream } from "../shared/base_pdf_stream.js";
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
throw new Error( throw new Error(
@ -35,18 +36,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, PDFNetworkStreamFullReader, 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,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) { 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} */ /** @implements {IPDFStreamReader} */
class PDFNetworkStreamFullRequestReader { class PDFNetworkStreamFullReader {
constructor(stream) { constructor(stream) {
this._stream = stream; this._stream = stream;
const { disableRange, length, rangeChunkSize } = stream._source; const { disableRange, length, rangeChunkSize } = stream._source;
@ -355,7 +331,7 @@ class PDFNetworkStreamFullRequestReader {
} }
/** @implements {IPDFStreamRangeReader} */ /** @implements {IPDFStreamRangeReader} */
class PDFNetworkStreamRangeRequestReader { class PDFNetworkStreamRangeReader {
onClosed = null; onClosed = null;
constructor(stream, begin, end) { constructor(stream, begin, end) {
@ -398,7 +374,7 @@ class PDFNetworkStreamRangeRequestReader {
requestCapability.resolve({ value: undefined, done: true }); requestCapability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
this.onClosed?.(this); this.onClosed?.();
} }
_onError(status) { _onError(status) {
@ -435,7 +411,7 @@ class PDFNetworkStreamRangeRequestReader {
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

@ -15,6 +15,7 @@
/* globals process */ /* globals process */
import { AbortException, assert, warn } from "../shared/util.js"; import { AbortException, assert, warn } from "../shared/util.js";
import { BasePDFStream } 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,47 +61,14 @@ 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, PDFNodeStreamFsFullReader, PDFNodeStreamFsRangeReader);
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);
}
} }
} }
@ -111,7 +79,7 @@ class PDFNodeStreamFsFullReader {
constructor(stream) { constructor(stream) {
this.onProgress = null; this.onProgress = null;
const source = stream.source; const source = stream._source;
this._contentLength = source.length; // optional this._contentLength = source.length; // optional
this._loaded = 0; this._loaded = 0;
this._filename = null; this._filename = null;

View File

@ -13,39 +13,42 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("../interfaces").IPDFStream} IPDFStream */
/** @typedef {import("../interfaces").IPDFStreamReader} IPDFStreamReader */ /** @typedef {import("../interfaces").IPDFStreamReader} IPDFStreamReader */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../interfaces").IPDFStreamRangeReader} IPDFStreamRangeReader */ /** @typedef {import("../interfaces").IPDFStreamRangeReader} IPDFStreamRangeReader */
import { assert } from "../shared/util.js"; import { assert } from "../shared/util.js";
import { BasePDFStream } from "../shared/base_pdf_stream.js";
import { isPdfFile } from "./display_utils.js"; import { isPdfFile } from "./display_utils.js";
/** @implements {IPDFStream} */ function getArrayBuffer(val) {
class PDFDataTransportStream { // Prevent any possible issues by only transferring a Uint8Array that
constructor( // completely "utilizes" its underlying ArrayBuffer.
pdfDataRangeTransport, return val instanceof Uint8Array && val.byteLength === val.buffer.byteLength
{ disableRange = false, disableStream = false } ? val.buffer
) { : new Uint8Array(val).buffer;
assert( }
pdfDataRangeTransport,
'PDFDataTransportStream - missing required "pdfDataRangeTransport" argument.' class PDFDataTransportStream extends BasePDFStream {
_pdfDataRangeTransport = null;
_queuedChunks = [];
constructor(source) {
super(
source,
PDFDataTransportStreamReader,
PDFDataTransportStreamRangeReader
); );
const { pdfDataRangeTransport, disableRange, disableStream } = source;
const { length, initialData, progressiveDone, contentDispositionFilename } = const { length, initialData, progressiveDone, contentDispositionFilename } =
pdfDataRangeTransport; pdfDataRangeTransport;
this._queuedChunks = [];
this._progressiveDone = progressiveDone; this._progressiveDone = progressiveDone;
this._contentDispositionFilename = contentDispositionFilename; this._contentDispositionFilename = contentDispositionFilename;
if (initialData?.length > 0) { if (initialData?.length > 0) {
// Prevent any possible issues by only transferring a Uint8Array that const buffer = getArrayBuffer(initialData);
// completely "utilizes" its underlying ArrayBuffer.
const buffer =
initialData instanceof Uint8Array &&
initialData.byteLength === initialData.buffer.byteLength
? initialData.buffer
: new Uint8Array(initialData).buffer;
this._queuedChunks.push(buffer); this._queuedChunks.push(buffer);
} }
@ -54,139 +57,89 @@ class PDFDataTransportStream {
this._isRangeSupported = !disableRange; this._isRangeSupported = !disableRange;
this._contentLength = length; 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) {
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) {
const reader = new PDFDataTransportStreamRangeReader(this, begin, end); reader.onDone = () => this._rangeReaders.delete(reader);
this._pdfDataRangeTransport.requestDataRange(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)) {
reader.cancel(reason);
}
this._pdfDataRangeTransport.abort(); this._pdfDataRangeTransport.abort();
} }
} }
/** @implements {IPDFStreamReader} */ /** @implements {IPDFStreamReader} */
class PDFDataTransportStreamReader { class PDFDataTransportStreamReader {
constructor( constructor(stream) {
stream,
queuedChunks,
progressiveDone = false,
contentDispositionFilename = null
) {
this._stream = stream; this._stream = stream;
this._done = progressiveDone || false; this._done = stream._progressiveDone || false;
this._filename = isPdfFile(contentDispositionFilename) this._filename = isPdfFile(stream._contentDispositionFilename)
? contentDispositionFilename ? stream._contentDispositionFilename
: null; : null;
this._queuedChunks = queuedChunks || []; this._queuedChunks = stream._queuedChunks || [];
this._loaded = 0; 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._requests = [];
this._headersReady = Promise.resolve(); this._headersReady = Promise.resolve();
stream._fullRequestReader = this;
this.onProgress = null; this.onProgress = null;
} }
@ -255,6 +208,8 @@ class PDFDataTransportStreamReader {
/** @implements {IPDFStreamRangeReader} */ /** @implements {IPDFStreamRangeReader} */
class PDFDataTransportStreamRangeReader { class PDFDataTransportStreamRangeReader {
onDone = null;
constructor(stream, begin, end) { constructor(stream, begin, end) {
this._stream = stream; this._stream = stream;
this._begin = begin; this._begin = begin;
@ -279,7 +234,7 @@ class PDFDataTransportStreamRangeReader {
this._requests.length = 0; this._requests.length = 0;
} }
this._done = true; this._done = true;
this._stream._removeRangeReader(this); this.onDone?.();
} }
async read() { async read() {
@ -302,7 +257,7 @@ class PDFDataTransportStreamRangeReader {
requestCapability.resolve({ value: undefined, done: true }); requestCapability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
this._stream._removeRangeReader(this); this.onDone?.();
} }
} }

View File

@ -13,40 +13,83 @@
* 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);
}
}
} }
/** /**
@ -152,4 +195,4 @@ class IPDFStreamRangeReader {
cancel(reason) {} cancel(reason) {}
} }
export { IPDFStream, IPDFStreamRangeReader, IPDFStreamReader }; export { BasePDFStream, IPDFStreamRangeReader, IPDFStreamReader };

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