mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-02-08 00:21:11 +01:00
Merge pull request #20602 from Snuffleupagus/BasePDFStream-2
Replace the `IPDFStream`, `IPDFStreamReader`, and `IPDFStreamRangeReader` interfaces with proper base classes
This commit is contained in:
commit
e4cd3176ab
@ -46,19 +46,13 @@ const PDFViewerApplication = {
|
||||
* @returns {Promise} - Returns the promise, which is resolved when document
|
||||
* is opened.
|
||||
*/
|
||||
open(params) {
|
||||
async open(params) {
|
||||
if (this.pdfLoadingTask) {
|
||||
// We need to destroy already opened document
|
||||
return this.close().then(
|
||||
function () {
|
||||
// ... and repeat the open() call.
|
||||
return this.open(params);
|
||||
}.bind(this)
|
||||
);
|
||||
// We need to destroy already opened document.
|
||||
await this.close();
|
||||
}
|
||||
|
||||
const url = params.url;
|
||||
const self = this;
|
||||
const { url } = params;
|
||||
this.setTitleUsingUrl(url);
|
||||
|
||||
// Loading document.
|
||||
@ -70,24 +64,22 @@ const PDFViewerApplication = {
|
||||
});
|
||||
this.pdfLoadingTask = loadingTask;
|
||||
|
||||
loadingTask.onProgress = function (progressData) {
|
||||
self.progress(progressData.loaded / progressData.total);
|
||||
};
|
||||
loadingTask.onProgress = evt => this.progress(evt.percent);
|
||||
|
||||
return loadingTask.promise.then(
|
||||
function (pdfDocument) {
|
||||
pdfDocument => {
|
||||
// Document loaded, specifying document for the viewer.
|
||||
self.pdfDocument = pdfDocument;
|
||||
self.pdfViewer.setDocument(pdfDocument);
|
||||
self.pdfLinkService.setDocument(pdfDocument);
|
||||
self.pdfHistory.initialize({
|
||||
this.pdfDocument = pdfDocument;
|
||||
this.pdfViewer.setDocument(pdfDocument);
|
||||
this.pdfLinkService.setDocument(pdfDocument);
|
||||
this.pdfHistory.initialize({
|
||||
fingerprint: pdfDocument.fingerprints[0],
|
||||
});
|
||||
|
||||
self.loadingBar.hide();
|
||||
self.setTitleUsingMetadata(pdfDocument);
|
||||
this.loadingBar.hide();
|
||||
this.setTitleUsingMetadata(pdfDocument);
|
||||
},
|
||||
function (reason) {
|
||||
reason => {
|
||||
let key = "pdfjs-loading-error";
|
||||
if (reason instanceof pdfjsLib.InvalidPDFException) {
|
||||
key = "pdfjs-invalid-file-error";
|
||||
@ -96,10 +88,10 @@ const PDFViewerApplication = {
|
||||
? "pdfjs-missing-file-error"
|
||||
: "pdfjs-unexpected-response-error";
|
||||
}
|
||||
self.l10n.get(key).then(msg => {
|
||||
self.error(msg, { message: reason?.message });
|
||||
this.l10n.get(key).then(msg => {
|
||||
this.error(msg, { message: reason.message });
|
||||
});
|
||||
self.loadingBar.hide();
|
||||
this.loadingBar.hide();
|
||||
}
|
||||
);
|
||||
},
|
||||
@ -109,9 +101,9 @@ const PDFViewerApplication = {
|
||||
* @returns {Promise} - Returns the promise, which is resolved when all
|
||||
* destruction is completed.
|
||||
*/
|
||||
close() {
|
||||
async close() {
|
||||
if (!this.pdfLoadingTask) {
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = this.pdfLoadingTask.destroy();
|
||||
@ -128,7 +120,7 @@ const PDFViewerApplication = {
|
||||
}
|
||||
}
|
||||
|
||||
return promise;
|
||||
await promise;
|
||||
},
|
||||
|
||||
get loadingBar() {
|
||||
@ -152,48 +144,36 @@ const PDFViewerApplication = {
|
||||
this.setTitle(title);
|
||||
},
|
||||
|
||||
setTitleUsingMetadata(pdfDocument) {
|
||||
const self = this;
|
||||
pdfDocument.getMetadata().then(function (data) {
|
||||
const info = data.info,
|
||||
metadata = data.metadata;
|
||||
self.documentInfo = info;
|
||||
self.metadata = metadata;
|
||||
async setTitleUsingMetadata(pdfDocument) {
|
||||
const { info, metadata } = await pdfDocument.getMetadata();
|
||||
this.documentInfo = info;
|
||||
this.metadata = metadata;
|
||||
|
||||
// Provides some basic debug information
|
||||
console.log(
|
||||
"PDF " +
|
||||
pdfDocument.fingerprints[0] +
|
||||
" [" +
|
||||
info.PDFFormatVersion +
|
||||
" " +
|
||||
(info.Producer || "-").trim() +
|
||||
" / " +
|
||||
(info.Creator || "-").trim() +
|
||||
"]" +
|
||||
" (PDF.js: " +
|
||||
(pdfjsLib.version || "-") +
|
||||
")"
|
||||
);
|
||||
// Provides some basic debug information
|
||||
console.log(
|
||||
`PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` +
|
||||
`${(metadata?.get("pdf:producer") || info.Producer || "-").trim()} / ` +
|
||||
`${(metadata?.get("xmp:creatortool") || info.Creator || "-").trim()}` +
|
||||
`] (PDF.js: ${pdfjsLib.version || "?"} [${pdfjsLib.build || "?"}])`
|
||||
);
|
||||
|
||||
let pdfTitle;
|
||||
if (metadata && metadata.has("dc:title")) {
|
||||
const title = metadata.get("dc:title");
|
||||
// Ghostscript sometimes returns 'Untitled', so prevent setting the
|
||||
// title to 'Untitled.
|
||||
if (title !== "Untitled") {
|
||||
pdfTitle = title;
|
||||
}
|
||||
let pdfTitle;
|
||||
if (metadata && metadata.has("dc:title")) {
|
||||
const title = metadata.get("dc:title");
|
||||
// Ghostscript sometimes returns 'Untitled', so prevent setting the
|
||||
// title to 'Untitled.
|
||||
if (title !== "Untitled") {
|
||||
pdfTitle = title;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pdfTitle && info && info.Title) {
|
||||
pdfTitle = info.Title;
|
||||
}
|
||||
if (!pdfTitle && info && info.Title) {
|
||||
pdfTitle = info.Title;
|
||||
}
|
||||
|
||||
if (pdfTitle) {
|
||||
self.setTitle(pdfTitle + " - " + document.title);
|
||||
}
|
||||
});
|
||||
if (pdfTitle) {
|
||||
this.setTitle(pdfTitle + " - " + document.title);
|
||||
}
|
||||
},
|
||||
|
||||
setTitle: function pdfViewSetTitle(title) {
|
||||
@ -223,8 +203,7 @@ const PDFViewerApplication = {
|
||||
console.error(`${message}\n\n${moreInfoText.join("\n")}`);
|
||||
},
|
||||
|
||||
progress: function pdfViewProgress(level) {
|
||||
const percent = Math.round(level * 100);
|
||||
progress(percent) {
|
||||
// Updating the bar if value increases.
|
||||
if (percent > this.loadingBar.percent || isNaN(percent)) {
|
||||
this.loadingBar.percent = percent;
|
||||
|
||||
@ -18,6 +18,12 @@ import { assert } from "../shared/util.js";
|
||||
import { Stream } from "./stream.js";
|
||||
|
||||
class ChunkedStream extends Stream {
|
||||
progressiveDataLength = 0;
|
||||
|
||||
_lastSuccessfulEnsureByteChunk = -1; // Single-entry cache
|
||||
|
||||
_loadedChunks = new Set();
|
||||
|
||||
constructor(length, chunkSize, manager) {
|
||||
super(
|
||||
/* arrayBuffer = */ new Uint8Array(length),
|
||||
@ -27,11 +33,8 @@ class ChunkedStream extends Stream {
|
||||
);
|
||||
|
||||
this.chunkSize = chunkSize;
|
||||
this._loadedChunks = new Set();
|
||||
this.numChunks = Math.ceil(length / chunkSize);
|
||||
this.manager = manager;
|
||||
this.progressiveDataLength = 0;
|
||||
this.lastSuccessfulEnsureByteChunk = -1; // Single-entry cache
|
||||
}
|
||||
|
||||
// If a particular stream does not implement one or more of these methods,
|
||||
@ -106,14 +109,14 @@ class ChunkedStream extends Stream {
|
||||
if (chunk > this.numChunks) {
|
||||
return;
|
||||
}
|
||||
if (chunk === this.lastSuccessfulEnsureByteChunk) {
|
||||
if (chunk === this._lastSuccessfulEnsureByteChunk) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._loadedChunks.has(chunk)) {
|
||||
throw new MissingDataException(pos, pos + 1);
|
||||
}
|
||||
this.lastSuccessfulEnsureByteChunk = chunk;
|
||||
this._lastSuccessfulEnsureByteChunk = chunk;
|
||||
}
|
||||
|
||||
ensureRange(begin, end) {
|
||||
@ -257,40 +260,37 @@ class ChunkedStream extends Stream {
|
||||
}
|
||||
|
||||
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.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;
|
||||
|
||||
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) {
|
||||
const rangeReader = this.pdfNetworkStream.getRangeReader(begin, end);
|
||||
if (!rangeReader.isStreamingSupported) {
|
||||
rangeReader.onProgress = this.onProgress.bind(this);
|
||||
}
|
||||
const rangeReader = this.pdfStream.getRangeReader(begin, end);
|
||||
|
||||
let chunks = [],
|
||||
loaded = 0;
|
||||
let chunks = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
const readChunk = ({ value, done }) => {
|
||||
try {
|
||||
if (done) {
|
||||
const chunkData = arrayBuffersToBytes(chunks);
|
||||
resolve(arrayBuffersToBytes(chunks));
|
||||
chunks = null;
|
||||
resolve(chunkData);
|
||||
return;
|
||||
}
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
@ -299,12 +299,6 @@ class ChunkedStreamManager {
|
||||
"readChunk (sendRequest) - expected an ArrayBuffer."
|
||||
);
|
||||
}
|
||||
loaded += value.byteLength;
|
||||
|
||||
if (rangeReader.isStreamingSupported) {
|
||||
this.onProgress({ loaded });
|
||||
}
|
||||
|
||||
chunks.push(value);
|
||||
rangeReader.read().then(readChunk, reject);
|
||||
} catch (e) {
|
||||
@ -446,34 +440,26 @@ class ChunkedStreamManager {
|
||||
return groupedChunks;
|
||||
}
|
||||
|
||||
onProgress(args) {
|
||||
this.msgHandler.send("DocProgress", {
|
||||
loaded: this.stream.numChunksLoaded * this.chunkSize + args.loaded,
|
||||
total: this.length,
|
||||
});
|
||||
}
|
||||
|
||||
onReceiveData(args) {
|
||||
const { chunkSize, length, stream } = this;
|
||||
|
||||
const chunk = args.chunk;
|
||||
const isProgressive = args.begin === undefined;
|
||||
const begin = isProgressive ? this.progressiveDataLength : args.begin;
|
||||
const begin = isProgressive ? stream.progressiveDataLength : args.begin;
|
||||
const end = begin + chunk.byteLength;
|
||||
|
||||
const beginChunk = Math.floor(begin / this.chunkSize);
|
||||
const beginChunk = Math.floor(begin / chunkSize);
|
||||
const endChunk =
|
||||
end < this.length
|
||||
? Math.floor(end / this.chunkSize)
|
||||
: Math.ceil(end / this.chunkSize);
|
||||
end < length ? Math.floor(end / chunkSize) : Math.ceil(end / chunkSize);
|
||||
|
||||
if (isProgressive) {
|
||||
this.stream.onReceiveProgressiveData(chunk);
|
||||
this.progressiveDataLength = end;
|
||||
stream.onReceiveProgressiveData(chunk);
|
||||
} else {
|
||||
this.stream.onReceiveData(begin, chunk);
|
||||
stream.onReceiveData(begin, chunk);
|
||||
}
|
||||
|
||||
if (this.stream.isDataLoaded) {
|
||||
this._loadedStreamCapability.resolve(this.stream);
|
||||
if (stream.isDataLoaded) {
|
||||
this._loadedStreamCapability.resolve(stream);
|
||||
}
|
||||
|
||||
const loadedRequests = [];
|
||||
@ -502,16 +488,16 @@ class ChunkedStreamManager {
|
||||
// unfetched chunk of the PDF file.
|
||||
if (!this.disableAutoFetch && this._requestsByChunk.size === 0) {
|
||||
let nextEmptyChunk;
|
||||
if (this.stream.numChunksLoaded === 1) {
|
||||
if (stream.numChunksLoaded === 1) {
|
||||
// This is a special optimization so that after fetching the first
|
||||
// chunk, rather than fetching the second chunk, we fetch the last
|
||||
// chunk.
|
||||
const lastChunk = this.stream.numChunks - 1;
|
||||
if (!this.stream.hasChunk(lastChunk)) {
|
||||
const lastChunk = stream.numChunks - 1;
|
||||
if (!stream.hasChunk(lastChunk)) {
|
||||
nextEmptyChunk = lastChunk;
|
||||
}
|
||||
} else {
|
||||
nextEmptyChunk = this.stream.nextEmptyChunk(endChunk);
|
||||
nextEmptyChunk = stream.nextEmptyChunk(endChunk);
|
||||
}
|
||||
if (Number.isInteger(nextEmptyChunk)) {
|
||||
this._requestChunks([nextEmptyChunk]);
|
||||
@ -525,8 +511,8 @@ class ChunkedStreamManager {
|
||||
}
|
||||
|
||||
this.msgHandler.send("DocProgress", {
|
||||
loaded: this.stream.numChunksLoaded * this.chunkSize,
|
||||
total: this.length,
|
||||
loaded: stream.numChunksLoaded * chunkSize,
|
||||
total: length,
|
||||
});
|
||||
}
|
||||
|
||||
@ -544,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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -13,77 +13,35 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { assert } from "../shared/util.js";
|
||||
import {
|
||||
BasePDFStream,
|
||||
BasePDFStreamRangeReader,
|
||||
BasePDFStreamReader,
|
||||
} 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;
|
||||
this.onProgress = null;
|
||||
class PDFWorkerStreamReader extends BasePDFStreamReader {
|
||||
_reader = null;
|
||||
|
||||
this._contentLength = null;
|
||||
this._isRangeSupported = false;
|
||||
this._isStreamingSupported = false;
|
||||
constructor(stream) {
|
||||
super(stream);
|
||||
const { msgHandler } = stream._source;
|
||||
|
||||
const readableStream = this._msgHandler.sendWithStream("GetReader");
|
||||
const readableStream = msgHandler.sendWithStream("GetReader");
|
||||
this._reader = readableStream.getReader();
|
||||
|
||||
this._headersReady = this._msgHandler
|
||||
.sendWithPromise("ReaderHeadersReady")
|
||||
.then(data => {
|
||||
this._isStreamingSupported = data.isStreamingSupported;
|
||||
this._isRangeSupported = data.isRangeSupported;
|
||||
this._contentLength = data.contentLength;
|
||||
});
|
||||
}
|
||||
msgHandler.sendWithPromise("ReaderHeadersReady").then(data => {
|
||||
this._contentLength = data.contentLength;
|
||||
this._isStreamingSupported = data.isStreamingSupported;
|
||||
this._isRangeSupported = data.isRangeSupported;
|
||||
|
||||
get headersReady() {
|
||||
return this._headersReady;
|
||||
}
|
||||
|
||||
get contentLength() {
|
||||
return this._contentLength;
|
||||
}
|
||||
|
||||
get isStreamingSupported() {
|
||||
return this._isStreamingSupported;
|
||||
}
|
||||
|
||||
get isRangeSupported() {
|
||||
return this._isRangeSupported;
|
||||
this._headersCapability.resolve();
|
||||
}, this._headersCapability.reject);
|
||||
}
|
||||
|
||||
async read() {
|
||||
@ -101,23 +59,20 @@ class PDFWorkerStreamReader {
|
||||
}
|
||||
}
|
||||
|
||||
/** @implements {IPDFStreamRangeReader} */
|
||||
class PDFWorkerStreamRangeReader {
|
||||
constructor(begin, end, msgHandler) {
|
||||
this._msgHandler = msgHandler;
|
||||
this.onProgress = null;
|
||||
class PDFWorkerStreamRangeReader extends BasePDFStreamRangeReader {
|
||||
_reader = 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,
|
||||
end,
|
||||
});
|
||||
this._reader = readableStream.getReader();
|
||||
}
|
||||
|
||||
get isStreamingSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async read() {
|
||||
const { value, done } = await this._reader.read();
|
||||
if (done) {
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
getVerbosityLevel,
|
||||
info,
|
||||
isNodeJS,
|
||||
MathClamp,
|
||||
RenderingIntentFlag,
|
||||
setVerbosityLevel,
|
||||
shadow,
|
||||
@ -440,6 +441,7 @@ function getDocument(src = {}) {
|
||||
ownerDocument,
|
||||
pdfBug,
|
||||
styleElement,
|
||||
enableHWA,
|
||||
loadingParams: {
|
||||
disableAutoFetch,
|
||||
enableXfa,
|
||||
@ -463,7 +465,8 @@ function getDocument(src = {}) {
|
||||
|
||||
let networkStream;
|
||||
if (rangeTransport) {
|
||||
networkStream = new PDFDataTransportStream(rangeTransport, {
|
||||
networkStream = new PDFDataTransportStream({
|
||||
pdfDataRangeTransport: rangeTransport,
|
||||
disableRange,
|
||||
disableStream,
|
||||
});
|
||||
@ -508,8 +511,7 @@ function getDocument(src = {}) {
|
||||
task,
|
||||
networkStream,
|
||||
transportParams,
|
||||
transportFactory,
|
||||
enableHWA
|
||||
transportFactory
|
||||
);
|
||||
task._transport = transport;
|
||||
messageHandler.send("Ready", null);
|
||||
@ -524,6 +526,8 @@ function getDocument(src = {}) {
|
||||
* @typedef {Object} OnProgressParameters
|
||||
* @property {number} loaded - Currently loaded number of bytes.
|
||||
* @property {number} total - Total number of bytes in the PDF file.
|
||||
* @property {number} percent - Currently loaded percentage, as an integer value
|
||||
* in the [0, 100] range. If `total` is undefined, the percentage is `NaN`.
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -2391,8 +2395,14 @@ class PDFWorker {
|
||||
* @ignore
|
||||
*/
|
||||
class WorkerTransport {
|
||||
downloadInfoCapability = Promise.withResolvers();
|
||||
|
||||
#fullReader = null;
|
||||
|
||||
#methodPromises = new Map();
|
||||
|
||||
#networkStream = null;
|
||||
|
||||
#pageCache = new Map();
|
||||
|
||||
#pagePromises = new Map();
|
||||
@ -2403,21 +2413,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 +2436,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));
|
||||
@ -2493,6 +2493,14 @@ class WorkerTransport {
|
||||
return promise;
|
||||
}
|
||||
|
||||
#onProgress({ loaded, total }) {
|
||||
this.loadingTask.onProgress?.({
|
||||
loaded,
|
||||
total,
|
||||
percent: MathClamp(Math.round((loaded / total) * 100), 0, 100),
|
||||
});
|
||||
}
|
||||
|
||||
get annotationStorage() {
|
||||
return shadow(this, "annotationStorage", new AnnotationStorage());
|
||||
}
|
||||
@ -2604,7 +2612,7 @@ class WorkerTransport {
|
||||
this.filterFactory.destroy();
|
||||
TextLayer.cleanup();
|
||||
|
||||
this._networkStream?.cancelAllRequests(
|
||||
this.#networkStream?.cancelAllRequests(
|
||||
new AbortException("Worker was terminated.")
|
||||
);
|
||||
|
||||
@ -2621,18 +2629,16 @@ 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 = {
|
||||
loaded: evt.loaded,
|
||||
total: evt.total,
|
||||
};
|
||||
};
|
||||
this.#fullReader = this.#networkStream.getFullReader();
|
||||
// If stream or range turn out to be disabled, once `headersReady` is
|
||||
// resolved, this is our only way to report loading progress.
|
||||
this.#fullReader.onProgress = evt => this.#onProgress(evt);
|
||||
|
||||
sink.onPull = () => {
|
||||
this._fullReader
|
||||
this.#fullReader
|
||||
.read()
|
||||
.then(function ({ value, done }) {
|
||||
if (done) {
|
||||
@ -2653,7 +2659,7 @@ class WorkerTransport {
|
||||
};
|
||||
|
||||
sink.onCancel = reason => {
|
||||
this._fullReader.cancel(reason);
|
||||
this.#fullReader.cancel(reason);
|
||||
|
||||
sink.ready.catch(readyReason => {
|
||||
if (this.destroyed) {
|
||||
@ -2665,40 +2671,29 @@ 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);
|
||||
}
|
||||
this._fullReader.onProgress = evt => {
|
||||
loadingTask.onProgress?.({
|
||||
loaded: evt.loaded,
|
||||
total: evt.total,
|
||||
});
|
||||
};
|
||||
if (isStreamingSupported && isRangeSupported) {
|
||||
this.#fullReader.onProgress = null; // See comment in "GetReader" above.
|
||||
}
|
||||
|
||||
return { isStreamingSupported, isRangeSupported, contentLength };
|
||||
});
|
||||
|
||||
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 +2701,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;
|
||||
@ -2780,10 +2775,7 @@ class WorkerTransport {
|
||||
messageHandler.on("DataLoaded", data => {
|
||||
// For consistency: Ensure that progress is always reported when the
|
||||
// entire PDF file has been loaded, regardless of how it was fetched.
|
||||
loadingTask.onProgress?.({
|
||||
loaded: data.length,
|
||||
total: data.length,
|
||||
});
|
||||
this.#onProgress({ loaded: data.length, total: data.length });
|
||||
|
||||
this.downloadInfoCapability.resolve(data);
|
||||
});
|
||||
@ -2906,10 +2898,7 @@ class WorkerTransport {
|
||||
if (this.destroyed) {
|
||||
return; // Ignore any pending requests if the worker was terminated.
|
||||
}
|
||||
loadingTask.onProgress?.({
|
||||
loaded: data.loaded,
|
||||
total: data.total,
|
||||
});
|
||||
this.#onProgress(data);
|
||||
});
|
||||
|
||||
messageHandler.on("FetchBinaryData", async data => {
|
||||
@ -2950,7 +2939,7 @@ class WorkerTransport {
|
||||
isPureXfa: !!this._htmlForXfa,
|
||||
numPages: this._numPages,
|
||||
annotationStorage: map,
|
||||
filename: this._fullReader?.filename ?? null,
|
||||
filename: this.#fullReader?.filename ?? null,
|
||||
},
|
||||
transfer
|
||||
)
|
||||
@ -3118,8 +3107,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);
|
||||
|
||||
@ -13,14 +13,19 @@
|
||||
* 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 {
|
||||
createHeaders,
|
||||
createResponseError,
|
||||
ensureResponseOrigin,
|
||||
extractFilenameFromHeader,
|
||||
getResponseOrigin,
|
||||
validateRangeRequestCapabilities,
|
||||
validateResponseStatus,
|
||||
} from "./network_utils.js";
|
||||
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
|
||||
@ -29,15 +34,21 @@ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
|
||||
);
|
||||
}
|
||||
|
||||
function createFetchOptions(headers, withCredentials, abortController) {
|
||||
return {
|
||||
function fetchUrl(url, headers, withCredentials, abortController) {
|
||||
return fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: abortController.signal,
|
||||
mode: "cors",
|
||||
credentials: withCredentials ? "include" : "same-origin",
|
||||
redirect: "follow",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function ensureResponseStatus(status, url) {
|
||||
if (status !== 200 && status !== 206) {
|
||||
throw createResponseError(status, url);
|
||||
}
|
||||
}
|
||||
|
||||
function getArrayBuffer(val) {
|
||||
@ -51,86 +62,44 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @implements {IPDFStreamReader} */
|
||||
class PDFFetchStreamReader {
|
||||
constructor(stream) {
|
||||
this._stream = stream;
|
||||
this._reader = null;
|
||||
this._loaded = 0;
|
||||
this._filename = null;
|
||||
const source = stream.source;
|
||||
this._withCredentials = source.withCredentials || false;
|
||||
this._contentLength = source.length;
|
||||
this._headersCapability = Promise.withResolvers();
|
||||
this._disableRange = source.disableRange || false;
|
||||
this._rangeChunkSize = source.rangeChunkSize;
|
||||
if (!this._rangeChunkSize && !this._disableRange) {
|
||||
this._disableRange = true;
|
||||
}
|
||||
class PDFFetchStreamReader extends BasePDFStreamReader {
|
||||
_abortController = new AbortController();
|
||||
|
||||
this._abortController = new AbortController();
|
||||
this._isStreamingSupported = !source.disableStream;
|
||||
this._isRangeSupported = !source.disableRange;
|
||||
_reader = null;
|
||||
|
||||
constructor(stream) {
|
||||
super(stream);
|
||||
const {
|
||||
disableRange,
|
||||
disableStream,
|
||||
length,
|
||||
rangeChunkSize,
|
||||
url,
|
||||
withCredentials,
|
||||
} = stream._source;
|
||||
|
||||
this._contentLength = length;
|
||||
this._isStreamingSupported = !disableStream;
|
||||
this._isRangeSupported = !disableRange;
|
||||
// Always create a copy of the headers.
|
||||
const headers = new Headers(stream.headers);
|
||||
|
||||
const url = source.url;
|
||||
fetch(
|
||||
url,
|
||||
createFetchOptions(headers, this._withCredentials, this._abortController)
|
||||
)
|
||||
fetchUrl(url, headers, withCredentials, this._abortController)
|
||||
.then(response => {
|
||||
stream._responseOrigin = getResponseOrigin(response.url);
|
||||
|
||||
if (!validateResponseStatus(response.status)) {
|
||||
throw createResponseError(response.status, url);
|
||||
}
|
||||
ensureResponseStatus(response.status, url);
|
||||
this._reader = response.body.getReader();
|
||||
this._headersCapability.resolve();
|
||||
|
||||
const responseHeaders = response.headers;
|
||||
|
||||
@ -138,8 +107,8 @@ class PDFFetchStreamReader {
|
||||
validateRangeRequestCapabilities({
|
||||
responseHeaders,
|
||||
isHttp: stream.isHttp,
|
||||
rangeChunkSize: this._rangeChunkSize,
|
||||
disableRange: this._disableRange,
|
||||
rangeChunkSize,
|
||||
disableRange,
|
||||
});
|
||||
|
||||
this._isRangeSupported = allowRangeRequests;
|
||||
@ -153,30 +122,10 @@ class PDFFetchStreamReader {
|
||||
if (!this._isStreamingSupported && this._isRangeSupported) {
|
||||
this.cancel(new AbortException("Streaming is disabled."));
|
||||
}
|
||||
|
||||
this._headersCapability.resolve();
|
||||
})
|
||||
.catch(this._headersCapability.reject);
|
||||
|
||||
this.onProgress = null;
|
||||
}
|
||||
|
||||
get headersReady() {
|
||||
return this._headersCapability.promise;
|
||||
}
|
||||
|
||||
get filename() {
|
||||
return this._filename;
|
||||
}
|
||||
|
||||
get contentLength() {
|
||||
return this._contentLength;
|
||||
}
|
||||
|
||||
get isRangeSupported() {
|
||||
return this._isRangeSupported;
|
||||
}
|
||||
|
||||
get isStreamingSupported() {
|
||||
return this._isStreamingSupported;
|
||||
}
|
||||
|
||||
async read() {
|
||||
@ -200,48 +149,32 @@ class PDFFetchStreamReader {
|
||||
}
|
||||
}
|
||||
|
||||
/** @implements {IPDFStreamRangeReader} */
|
||||
class PDFFetchStreamRangeReader {
|
||||
constructor(stream, begin, end) {
|
||||
this._stream = stream;
|
||||
this._reader = null;
|
||||
this._loaded = 0;
|
||||
const source = stream.source;
|
||||
this._withCredentials = source.withCredentials || false;
|
||||
this._readCapability = Promise.withResolvers();
|
||||
this._isStreamingSupported = !source.disableStream;
|
||||
class PDFFetchStreamRangeReader extends BasePDFStreamRangeReader {
|
||||
_abortController = new AbortController();
|
||||
|
||||
_readCapability = Promise.withResolvers();
|
||||
|
||||
_reader = null;
|
||||
|
||||
constructor(stream, begin, end) {
|
||||
super(stream, begin, end);
|
||||
const { url, withCredentials } = stream._source;
|
||||
|
||||
this._abortController = new AbortController();
|
||||
// Always create a copy of the headers.
|
||||
const headers = new Headers(stream.headers);
|
||||
headers.append("Range", `bytes=${begin}-${end - 1}`);
|
||||
|
||||
const url = source.url;
|
||||
fetch(
|
||||
url,
|
||||
createFetchOptions(headers, this._withCredentials, this._abortController)
|
||||
)
|
||||
fetchUrl(url, headers, withCredentials, this._abortController)
|
||||
.then(response => {
|
||||
const responseOrigin = getResponseOrigin(response.url);
|
||||
|
||||
if (responseOrigin !== stream._responseOrigin) {
|
||||
throw new Error(
|
||||
`Expected range response-origin "${responseOrigin}" to match "${stream._responseOrigin}".`
|
||||
);
|
||||
}
|
||||
if (!validateResponseStatus(response.status)) {
|
||||
throw createResponseError(response.status, url);
|
||||
}
|
||||
this._readCapability.resolve();
|
||||
ensureResponseOrigin(responseOrigin, stream._responseOrigin);
|
||||
ensureResponseStatus(response.status, url);
|
||||
this._reader = response.body.getReader();
|
||||
|
||||
this._readCapability.resolve();
|
||||
})
|
||||
.catch(this._readCapability.reject);
|
||||
|
||||
this.onProgress = null;
|
||||
}
|
||||
|
||||
get isStreamingSupported() {
|
||||
return this._isStreamingSupported;
|
||||
}
|
||||
|
||||
async read() {
|
||||
@ -250,9 +183,6 @@ class PDFFetchStreamRangeReader {
|
||||
if (done) {
|
||||
return { value, done };
|
||||
}
|
||||
this._loaded += value.byteLength;
|
||||
this.onProgress?.({ loaded: this._loaded });
|
||||
|
||||
return { value: getArrayBuffer(value), done: false };
|
||||
}
|
||||
|
||||
|
||||
@ -14,9 +14,15 @@
|
||||
*/
|
||||
|
||||
import { assert, stringToBytes, warn } from "../shared/util.js";
|
||||
import {
|
||||
BasePDFStream,
|
||||
BasePDFStreamRangeReader,
|
||||
BasePDFStreamReader,
|
||||
} from "../shared/base_pdf_stream.js";
|
||||
import {
|
||||
createHeaders,
|
||||
createResponseError,
|
||||
ensureResponseOrigin,
|
||||
extractFilenameFromHeader,
|
||||
getResponseOrigin,
|
||||
validateRangeRequestCapabilities,
|
||||
@ -35,18 +41,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, PDFNetworkStreamReader, PDFNetworkStreamRangeReader);
|
||||
this.url = source.url;
|
||||
this.isHttp = /^https?:/i.test(this.url);
|
||||
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) {
|
||||
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 PDFNetworkStreamReader extends BasePDFStreamReader {
|
||||
_cachedChunks = [];
|
||||
|
||||
_done = false;
|
||||
|
||||
_requests = [];
|
||||
|
||||
_storedError = null;
|
||||
|
||||
constructor(stream) {
|
||||
this._stream = stream;
|
||||
const { disableRange, length, rangeChunkSize } = stream._source;
|
||||
super(stream);
|
||||
const { length } = stream._source;
|
||||
|
||||
this._contentLength = length;
|
||||
// Note that `XMLHttpRequest` doesn't support streaming, and range requests
|
||||
// will be enabled (if supported) in `this.#onHeadersReceived` below.
|
||||
|
||||
this._fullRequestXhr = stream._request({
|
||||
onHeadersReceived: this._onHeadersReceived.bind(this),
|
||||
onDone: this._onDone.bind(this),
|
||||
onError: this._onError.bind(this),
|
||||
onProgress: this._onProgress.bind(this),
|
||||
onHeadersReceived: this.#onHeadersReceived.bind(this),
|
||||
onDone: this.#onDone.bind(this),
|
||||
onError: this.#onError.bind(this),
|
||||
onProgress: this.#onProgress.bind(this),
|
||||
});
|
||||
this._headersCapability = Promise.withResolvers();
|
||||
this._disableRange = disableRange || false;
|
||||
this._contentLength = length; // Optional
|
||||
this._rangeChunkSize = rangeChunkSize;
|
||||
if (!this._rangeChunkSize && !this._disableRange) {
|
||||
this._disableRange = true;
|
||||
}
|
||||
|
||||
this._isStreamingSupported = false;
|
||||
this._isRangeSupported = false;
|
||||
|
||||
this._cachedChunks = [];
|
||||
this._requests = [];
|
||||
this._done = false;
|
||||
this._storedError = undefined;
|
||||
this._filename = null;
|
||||
|
||||
this.onProgress = null;
|
||||
}
|
||||
|
||||
_onHeadersReceived() {
|
||||
#onHeadersReceived() {
|
||||
const stream = this._stream;
|
||||
const { disableRange, rangeChunkSize } = stream._source;
|
||||
const fullRequestXhr = this._fullRequestXhr;
|
||||
|
||||
stream._responseOrigin = getResponseOrigin(fullRequestXhr.responseURL);
|
||||
@ -246,8 +221,8 @@ class PDFNetworkStreamFullRequestReader {
|
||||
validateRangeRequestCapabilities({
|
||||
responseHeaders,
|
||||
isHttp: stream.isHttp,
|
||||
rangeChunkSize: this._rangeChunkSize,
|
||||
disableRange: this._disableRange,
|
||||
rangeChunkSize,
|
||||
disableRange,
|
||||
});
|
||||
|
||||
if (allowRangeRequests) {
|
||||
@ -269,10 +244,10 @@ class PDFNetworkStreamFullRequestReader {
|
||||
this._headersCapability.resolve();
|
||||
}
|
||||
|
||||
_onDone(chunk) {
|
||||
#onDone(chunk) {
|
||||
if (this._requests.length > 0) {
|
||||
const requestCapability = this._requests.shift();
|
||||
requestCapability.resolve({ value: chunk, done: false });
|
||||
const capability = this._requests.shift();
|
||||
capability.resolve({ value: chunk, done: false });
|
||||
} else {
|
||||
this._cachedChunks.push(chunk);
|
||||
}
|
||||
@ -280,49 +255,29 @@ class PDFNetworkStreamFullRequestReader {
|
||||
if (this._cachedChunks.length > 0) {
|
||||
return;
|
||||
}
|
||||
for (const requestCapability of this._requests) {
|
||||
requestCapability.resolve({ value: undefined, done: true });
|
||||
for (const capability of this._requests) {
|
||||
capability.resolve({ value: undefined, done: true });
|
||||
}
|
||||
this._requests.length = 0;
|
||||
}
|
||||
|
||||
_onError(status) {
|
||||
#onError(status) {
|
||||
this._storedError = createResponseError(status, this._stream.url);
|
||||
this._headersCapability.reject(this._storedError);
|
||||
for (const requestCapability of this._requests) {
|
||||
requestCapability.reject(this._storedError);
|
||||
for (const capability of this._requests) {
|
||||
capability.reject(this._storedError);
|
||||
}
|
||||
this._requests.length = 0;
|
||||
this._cachedChunks.length = 0;
|
||||
}
|
||||
|
||||
_onProgress(evt) {
|
||||
#onProgress(evt) {
|
||||
this.onProgress?.({
|
||||
loaded: evt.loaded,
|
||||
total: evt.lengthComputable ? evt.total : this._contentLength,
|
||||
});
|
||||
}
|
||||
|
||||
get filename() {
|
||||
return this._filename;
|
||||
}
|
||||
|
||||
get isRangeSupported() {
|
||||
return this._isRangeSupported;
|
||||
}
|
||||
|
||||
get isStreamingSupported() {
|
||||
return this._isStreamingSupported;
|
||||
}
|
||||
|
||||
get contentLength() {
|
||||
return this._contentLength;
|
||||
}
|
||||
|
||||
get headersReady() {
|
||||
return this._headersCapability.promise;
|
||||
}
|
||||
|
||||
async read() {
|
||||
await this._headersCapability.promise;
|
||||
|
||||
@ -336,16 +291,16 @@ class PDFNetworkStreamFullRequestReader {
|
||||
if (this._done) {
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
const requestCapability = Promise.withResolvers();
|
||||
this._requests.push(requestCapability);
|
||||
return requestCapability.promise;
|
||||
const capability = Promise.withResolvers();
|
||||
this._requests.push(capability);
|
||||
return capability.promise;
|
||||
}
|
||||
|
||||
cancel(reason) {
|
||||
this._done = true;
|
||||
this._headersCapability.reject(reason);
|
||||
for (const requestCapability of this._requests) {
|
||||
requestCapability.resolve({ value: undefined, done: true });
|
||||
for (const capability of this._requests) {
|
||||
capability.resolve({ value: undefined, done: true });
|
||||
}
|
||||
this._requests.length = 0;
|
||||
|
||||
@ -354,74 +309,64 @@ class PDFNetworkStreamFullRequestReader {
|
||||
}
|
||||
}
|
||||
|
||||
/** @implements {IPDFStreamRangeReader} */
|
||||
class PDFNetworkStreamRangeRequestReader {
|
||||
class PDFNetworkStreamRangeReader extends BasePDFStreamRangeReader {
|
||||
onClosed = null;
|
||||
|
||||
_done = false;
|
||||
|
||||
_queuedChunk = null;
|
||||
|
||||
_requests = [];
|
||||
|
||||
_storedError = null;
|
||||
|
||||
constructor(stream, begin, end) {
|
||||
this._stream = stream;
|
||||
super(stream, begin, end);
|
||||
|
||||
this._requestXhr = stream._request({
|
||||
begin,
|
||||
end,
|
||||
onHeadersReceived: this._onHeadersReceived.bind(this),
|
||||
onDone: this._onDone.bind(this),
|
||||
onError: this._onError.bind(this),
|
||||
onProgress: this._onProgress.bind(this),
|
||||
onHeadersReceived: this.#onHeadersReceived.bind(this),
|
||||
onDone: this.#onDone.bind(this),
|
||||
onError: this.#onError.bind(this),
|
||||
onProgress: null,
|
||||
});
|
||||
this._requests = [];
|
||||
this._queuedChunk = null;
|
||||
this._done = false;
|
||||
this._storedError = undefined;
|
||||
|
||||
this.onProgress = null;
|
||||
}
|
||||
|
||||
_onHeadersReceived() {
|
||||
#onHeadersReceived() {
|
||||
const responseOrigin = getResponseOrigin(this._requestXhr?.responseURL);
|
||||
|
||||
if (responseOrigin !== this._stream._responseOrigin) {
|
||||
this._storedError = new Error(
|
||||
`Expected range response-origin "${responseOrigin}" to match "${this._stream._responseOrigin}".`
|
||||
);
|
||||
this._onError(0);
|
||||
try {
|
||||
ensureResponseOrigin(responseOrigin, this._stream._responseOrigin);
|
||||
} catch (ex) {
|
||||
this._storedError = ex;
|
||||
this.#onError(0);
|
||||
}
|
||||
}
|
||||
|
||||
_onDone(chunk) {
|
||||
#onDone(chunk) {
|
||||
if (this._requests.length > 0) {
|
||||
const requestCapability = this._requests.shift();
|
||||
requestCapability.resolve({ value: chunk, done: false });
|
||||
const capability = this._requests.shift();
|
||||
capability.resolve({ value: chunk, done: false });
|
||||
} else {
|
||||
this._queuedChunk = chunk;
|
||||
}
|
||||
this._done = true;
|
||||
for (const requestCapability of this._requests) {
|
||||
requestCapability.resolve({ value: undefined, done: true });
|
||||
for (const capability of this._requests) {
|
||||
capability.resolve({ value: undefined, done: true });
|
||||
}
|
||||
this._requests.length = 0;
|
||||
this.onClosed?.(this);
|
||||
this.onClosed?.();
|
||||
}
|
||||
|
||||
_onError(status) {
|
||||
#onError(status) {
|
||||
this._storedError ??= createResponseError(status, this._stream.url);
|
||||
for (const requestCapability of this._requests) {
|
||||
requestCapability.reject(this._storedError);
|
||||
for (const capability of this._requests) {
|
||||
capability.reject(this._storedError);
|
||||
}
|
||||
this._requests.length = 0;
|
||||
this._queuedChunk = null;
|
||||
}
|
||||
|
||||
_onProgress(evt) {
|
||||
if (!this.isStreamingSupported) {
|
||||
this.onProgress?.({ loaded: evt.loaded });
|
||||
}
|
||||
}
|
||||
|
||||
get isStreamingSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async read() {
|
||||
if (this._storedError) {
|
||||
throw this._storedError;
|
||||
@ -434,20 +379,20 @@ class PDFNetworkStreamRangeRequestReader {
|
||||
if (this._done) {
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
const requestCapability = Promise.withResolvers();
|
||||
this._requests.push(requestCapability);
|
||||
return requestCapability.promise;
|
||||
const capability = Promise.withResolvers();
|
||||
this._requests.push(capability);
|
||||
return capability.promise;
|
||||
}
|
||||
|
||||
cancel(reason) {
|
||||
this._done = true;
|
||||
for (const requestCapability of this._requests) {
|
||||
requestCapability.resolve({ value: undefined, done: true });
|
||||
for (const capability of this._requests) {
|
||||
capability.resolve({ value: undefined, done: true });
|
||||
}
|
||||
this._requests.length = 0;
|
||||
|
||||
this._stream._abortRequest(this._requestXhr);
|
||||
this.onClosed?.(this);
|
||||
this.onClosed?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -107,15 +107,19 @@ function createResponseError(status, url) {
|
||||
);
|
||||
}
|
||||
|
||||
function validateResponseStatus(status) {
|
||||
return status === 200 || status === 206;
|
||||
function ensureResponseOrigin(rangeOrigin, origin) {
|
||||
if (rangeOrigin !== origin) {
|
||||
throw new Error(
|
||||
`Expected range response-origin "${rangeOrigin}" to match "${origin}".`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
createHeaders,
|
||||
createResponseError,
|
||||
ensureResponseOrigin,
|
||||
extractFilenameFromHeader,
|
||||
getResponseOrigin,
|
||||
validateRangeRequestCapabilities,
|
||||
validateResponseStatus,
|
||||
};
|
||||
|
||||
@ -15,6 +15,11 @@
|
||||
/* globals process */
|
||||
|
||||
import { AbortException, assert, warn } from "../shared/util.js";
|
||||
import {
|
||||
BasePDFStream,
|
||||
BasePDFStreamRangeReader,
|
||||
BasePDFStreamReader,
|
||||
} from "../shared/base_pdf_stream.js";
|
||||
import { createResponseError } from "./network_utils.js";
|
||||
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
|
||||
@ -60,70 +65,28 @@ function getArrayBuffer(val) {
|
||||
return new Uint8Array(val).buffer;
|
||||
}
|
||||
|
||||
class PDFNodeStream {
|
||||
class PDFNodeStream extends BasePDFStream {
|
||||
constructor(source) {
|
||||
this.source = source;
|
||||
super(source, PDFNodeStreamReader, PDFNodeStreamRangeReader);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PDFNodeStreamFsFullReader {
|
||||
_headersCapability = Promise.withResolvers();
|
||||
|
||||
class PDFNodeStreamReader extends BasePDFStreamReader {
|
||||
_reader = null;
|
||||
|
||||
constructor(stream) {
|
||||
this.onProgress = null;
|
||||
const source = stream.source;
|
||||
this._contentLength = source.length; // optional
|
||||
this._loaded = 0;
|
||||
this._filename = null;
|
||||
super(stream);
|
||||
const { disableRange, disableStream, length, rangeChunkSize } =
|
||||
stream._source;
|
||||
|
||||
this._disableRange = source.disableRange || false;
|
||||
this._rangeChunkSize = source.rangeChunkSize;
|
||||
if (!this._rangeChunkSize && !this._disableRange) {
|
||||
this._disableRange = true;
|
||||
}
|
||||
|
||||
this._isStreamingSupported = !source.disableStream;
|
||||
this._isRangeSupported = !source.disableRange;
|
||||
this._contentLength = length;
|
||||
this._isStreamingSupported = !disableStream;
|
||||
this._isRangeSupported = !disableRange;
|
||||
|
||||
const url = stream.url;
|
||||
const fs = process.getBuiltinModule("fs");
|
||||
@ -136,7 +99,7 @@ class PDFNodeStreamFsFullReader {
|
||||
this._reader = readableStream.getReader();
|
||||
|
||||
const { size } = stat;
|
||||
if (size <= 2 * this._rangeChunkSize) {
|
||||
if (size <= 2 * rangeChunkSize) {
|
||||
// The file size is smaller than the size of two chunks, so it doesn't
|
||||
// make any sense to abort the request and retry with a range request.
|
||||
this._isRangeSupported = false;
|
||||
@ -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() {
|
||||
await this._headersCapability.promise;
|
||||
const { value, done } = await this._reader.read();
|
||||
@ -200,16 +143,13 @@ class PDFNodeStreamFsFullReader {
|
||||
}
|
||||
}
|
||||
|
||||
class PDFNodeStreamFsRangeReader {
|
||||
class PDFNodeStreamRangeReader extends BasePDFStreamRangeReader {
|
||||
_readCapability = Promise.withResolvers();
|
||||
|
||||
_reader = null;
|
||||
|
||||
constructor(stream, begin, end) {
|
||||
this.onProgress = null;
|
||||
this._loaded = 0;
|
||||
const source = stream.source;
|
||||
this._isStreamingSupported = !source.disableStream;
|
||||
super(stream, begin, end);
|
||||
|
||||
const url = stream.url;
|
||||
const fs = process.getBuiltinModule("fs");
|
||||
@ -228,19 +168,12 @@ class PDFNodeStreamFsRangeReader {
|
||||
}
|
||||
}
|
||||
|
||||
get isStreamingSupported() {
|
||||
return this._isStreamingSupported;
|
||||
}
|
||||
|
||||
async read() {
|
||||
await this._readCapability.promise;
|
||||
const { value, done } = await this._reader.read();
|
||||
if (done) {
|
||||
return { value, done };
|
||||
}
|
||||
this._loaded += value.length;
|
||||
this.onProgress?.({ loaded: this._loaded });
|
||||
|
||||
return { value: getArrayBuffer(value), done: false };
|
||||
}
|
||||
|
||||
|
||||
@ -13,185 +13,137 @@
|
||||
* 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 {
|
||||
BasePDFStream,
|
||||
BasePDFStreamRangeReader,
|
||||
BasePDFStreamReader,
|
||||
} from "../shared/base_pdf_stream.js";
|
||||
import { assert } from "../shared/util.js";
|
||||
import { isPdfFile } from "./display_utils.js";
|
||||
|
||||
/** @implements {IPDFStream} */
|
||||
class PDFDataTransportStream {
|
||||
constructor(
|
||||
pdfDataRangeTransport,
|
||||
{ disableRange = false, disableStream = false }
|
||||
) {
|
||||
assert(
|
||||
pdfDataRangeTransport,
|
||||
'PDFDataTransportStream - missing required "pdfDataRangeTransport" argument.'
|
||||
);
|
||||
const { length, initialData, progressiveDone, contentDispositionFilename } =
|
||||
pdfDataRangeTransport;
|
||||
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;
|
||||
}
|
||||
|
||||
this._queuedChunks = [];
|
||||
this._progressiveDone = progressiveDone;
|
||||
this._contentDispositionFilename = contentDispositionFilename;
|
||||
class PDFDataTransportStream extends BasePDFStream {
|
||||
_progressiveDone = false;
|
||||
|
||||
_queuedChunks = [];
|
||||
|
||||
constructor(source) {
|
||||
super(
|
||||
source,
|
||||
PDFDataTransportStreamReader,
|
||||
PDFDataTransportStreamRangeReader
|
||||
);
|
||||
const { pdfDataRangeTransport } = source;
|
||||
const { initialData, progressiveDone } = pdfDataRangeTransport;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
this._pdfDataRangeTransport = pdfDataRangeTransport;
|
||||
this._isStreamingSupported = !disableStream;
|
||||
this._isRangeSupported = !disableRange;
|
||||
this._contentLength = length;
|
||||
|
||||
this._fullRequestReader = null;
|
||||
this._rangeReaders = [];
|
||||
this._progressiveDone = progressiveDone;
|
||||
|
||||
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) {
|
||||
// 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);
|
||||
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._source.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();
|
||||
this._source.pdfDataRangeTransport.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/** @implements {IPDFStreamReader} */
|
||||
class PDFDataTransportStreamReader {
|
||||
constructor(
|
||||
stream,
|
||||
queuedChunks,
|
||||
progressiveDone = false,
|
||||
contentDispositionFilename = null
|
||||
) {
|
||||
this._stream = stream;
|
||||
this._done = progressiveDone || false;
|
||||
this._filename = isPdfFile(contentDispositionFilename)
|
||||
? contentDispositionFilename
|
||||
: null;
|
||||
this._queuedChunks = queuedChunks || [];
|
||||
this._loaded = 0;
|
||||
class PDFDataTransportStreamReader extends BasePDFStreamReader {
|
||||
_done = false;
|
||||
|
||||
_queuedChunks = null;
|
||||
|
||||
_requests = [];
|
||||
|
||||
constructor(stream) {
|
||||
super(stream);
|
||||
const { pdfDataRangeTransport, disableRange, disableStream } =
|
||||
stream._source;
|
||||
const { length, contentDispositionFilename } = pdfDataRangeTransport;
|
||||
|
||||
this._queuedChunks = stream._queuedChunks || [];
|
||||
for (const chunk of this._queuedChunks) {
|
||||
this._loaded += chunk.byteLength;
|
||||
}
|
||||
this._requests = [];
|
||||
this._headersReady = Promise.resolve();
|
||||
stream._fullRequestReader = this;
|
||||
this._done = stream._progressiveDone;
|
||||
|
||||
this.onProgress = null;
|
||||
this._contentLength = length;
|
||||
this._isStreamingSupported = !disableStream;
|
||||
this._isRangeSupported = !disableRange;
|
||||
|
||||
if (isPdfFile(contentDispositionFilename)) {
|
||||
this._filename = contentDispositionFilename;
|
||||
}
|
||||
this._headersCapability.resolve();
|
||||
}
|
||||
|
||||
_enqueue(chunk) {
|
||||
@ -199,34 +151,14 @@ class PDFDataTransportStreamReader {
|
||||
return; // Ignore new data.
|
||||
}
|
||||
if (this._requests.length > 0) {
|
||||
const requestCapability = this._requests.shift();
|
||||
requestCapability.resolve({ value: chunk, done: false });
|
||||
const capability = this._requests.shift();
|
||||
capability.resolve({ value: chunk, done: false });
|
||||
} else {
|
||||
this._queuedChunks.push(chunk);
|
||||
}
|
||||
this._loaded += chunk.byteLength;
|
||||
}
|
||||
|
||||
get headersReady() {
|
||||
return this._headersReady;
|
||||
}
|
||||
|
||||
get filename() {
|
||||
return this._filename;
|
||||
}
|
||||
|
||||
get isRangeSupported() {
|
||||
return this._stream._isRangeSupported;
|
||||
}
|
||||
|
||||
get isStreamingSupported() {
|
||||
return this._stream._isStreamingSupported;
|
||||
}
|
||||
|
||||
get contentLength() {
|
||||
return this._stream._contentLength;
|
||||
}
|
||||
|
||||
async read() {
|
||||
if (this._queuedChunks.length > 0) {
|
||||
const chunk = this._queuedChunks.shift();
|
||||
@ -235,38 +167,38 @@ class PDFDataTransportStreamReader {
|
||||
if (this._done) {
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
const requestCapability = Promise.withResolvers();
|
||||
this._requests.push(requestCapability);
|
||||
return requestCapability.promise;
|
||||
const capability = Promise.withResolvers();
|
||||
this._requests.push(capability);
|
||||
return capability.promise;
|
||||
}
|
||||
|
||||
cancel(reason) {
|
||||
this._done = true;
|
||||
for (const requestCapability of this._requests) {
|
||||
requestCapability.resolve({ value: undefined, done: true });
|
||||
for (const capability of this._requests) {
|
||||
capability.resolve({ value: undefined, done: true });
|
||||
}
|
||||
this._requests.length = 0;
|
||||
}
|
||||
|
||||
progressiveDone() {
|
||||
if (this._done) {
|
||||
return;
|
||||
}
|
||||
this._done = true;
|
||||
this._done ||= true;
|
||||
}
|
||||
}
|
||||
|
||||
/** @implements {IPDFStreamRangeReader} */
|
||||
class PDFDataTransportStreamRangeReader {
|
||||
constructor(stream, begin, end) {
|
||||
this._stream = stream;
|
||||
this._begin = begin;
|
||||
this._end = end;
|
||||
this._queuedChunk = null;
|
||||
this._requests = [];
|
||||
this._done = false;
|
||||
class PDFDataTransportStreamRangeReader extends BasePDFStreamRangeReader {
|
||||
onDone = null;
|
||||
|
||||
this.onProgress = null;
|
||||
_begin = -1;
|
||||
|
||||
_done = false;
|
||||
|
||||
_queuedChunk = null;
|
||||
|
||||
_requests = [];
|
||||
|
||||
constructor(stream, begin, end) {
|
||||
super(stream, begin, end);
|
||||
this._begin = begin;
|
||||
}
|
||||
|
||||
_enqueue(chunk) {
|
||||
@ -276,19 +208,16 @@ class PDFDataTransportStreamRangeReader {
|
||||
if (this._requests.length === 0) {
|
||||
this._queuedChunk = chunk;
|
||||
} else {
|
||||
const requestsCapability = this._requests.shift();
|
||||
requestsCapability.resolve({ value: chunk, done: false });
|
||||
for (const requestCapability of this._requests) {
|
||||
requestCapability.resolve({ value: undefined, done: true });
|
||||
const firstCapability = this._requests.shift();
|
||||
firstCapability.resolve({ value: chunk, done: false });
|
||||
|
||||
for (const capability of this._requests) {
|
||||
capability.resolve({ value: undefined, done: true });
|
||||
}
|
||||
this._requests.length = 0;
|
||||
}
|
||||
this._done = true;
|
||||
this._stream._removeRangeReader(this);
|
||||
}
|
||||
|
||||
get isStreamingSupported() {
|
||||
return false;
|
||||
this.onDone?.();
|
||||
}
|
||||
|
||||
async read() {
|
||||
@ -300,18 +229,18 @@ class PDFDataTransportStreamRangeReader {
|
||||
if (this._done) {
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
const requestCapability = Promise.withResolvers();
|
||||
this._requests.push(requestCapability);
|
||||
return requestCapability.promise;
|
||||
const capability = Promise.withResolvers();
|
||||
this._requests.push(capability);
|
||||
return capability.promise;
|
||||
}
|
||||
|
||||
cancel(reason) {
|
||||
this._done = true;
|
||||
for (const requestCapability of this._requests) {
|
||||
requestCapability.resolve({ value: undefined, done: true });
|
||||
for (const capability of this._requests) {
|
||||
capability.resolve({ value: undefined, done: true });
|
||||
}
|
||||
this._requests.length = 0;
|
||||
this._stream._removeRangeReader(this);
|
||||
this.onDone?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,56 +13,119 @@
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a PDF binary data reader.
|
||||
*
|
||||
* @interface
|
||||
*/
|
||||
class IPDFStreamReader {
|
||||
constructor() {
|
||||
/**
|
||||
* Sets or gets the progress callback. The callback can be useful when the
|
||||
* isStreamingSupported property of the object is defined as false.
|
||||
* The callback is called with one parameter: an object with the loaded and
|
||||
* total properties.
|
||||
*/
|
||||
this.onProgress = null;
|
||||
class BasePDFStreamReader {
|
||||
/**
|
||||
* Sets or gets the progress callback. The callback can be useful when the
|
||||
* isStreamingSupported property of the object is defined as false.
|
||||
* The callback is called with one parameter: an object with the loaded and
|
||||
* total properties.
|
||||
*/
|
||||
onProgress = null;
|
||||
|
||||
_contentLength = 0;
|
||||
|
||||
_filename = null;
|
||||
|
||||
_headersCapability = Promise.withResolvers();
|
||||
|
||||
_isRangeSupported = false;
|
||||
|
||||
_isStreamingSupported = false;
|
||||
|
||||
_loaded = 0;
|
||||
|
||||
_stream = null;
|
||||
|
||||
constructor(stream) {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
|
||||
this.constructor === BasePDFStreamReader
|
||||
) {
|
||||
unreachable("Cannot initialize BasePDFStreamReader.");
|
||||
}
|
||||
this._stream = stream;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,7 +134,7 @@ class IPDFStreamReader {
|
||||
* @type {Promise}
|
||||
*/
|
||||
get headersReady() {
|
||||
return Promise.resolve();
|
||||
return this._headersCapability.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,7 +144,7 @@ class IPDFStreamReader {
|
||||
* header is missing/invalid.
|
||||
*/
|
||||
get filename() {
|
||||
return null;
|
||||
return this._filename;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,7 +153,7 @@ class IPDFStreamReader {
|
||||
* @type {number} The data length (or 0 if unknown).
|
||||
*/
|
||||
get contentLength() {
|
||||
return 0;
|
||||
return this._contentLength;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,7 +163,7 @@ class IPDFStreamReader {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isRangeSupported() {
|
||||
return false;
|
||||
return this._isRangeSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,7 +172,7 @@ class IPDFStreamReader {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isStreamingSupported() {
|
||||
return false;
|
||||
return this._isStreamingSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -120,37 +183,33 @@ class IPDFStreamReader {
|
||||
* set to true.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async read() {}
|
||||
async read() {
|
||||
unreachable("Abstract method `read` called");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all pending read requests and closes the stream.
|
||||
* @param {Object} reason
|
||||
*/
|
||||
cancel(reason) {}
|
||||
cancel(reason) {
|
||||
unreachable("Abstract method `cancel` called");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a PDF binary data fragment reader.
|
||||
*
|
||||
* @interface
|
||||
*/
|
||||
class IPDFStreamRangeReader {
|
||||
constructor() {
|
||||
/**
|
||||
* Sets or gets the progress callback. The callback can be useful when the
|
||||
* isStreamingSupported property of the object is defined as false.
|
||||
* The callback is called with one parameter: an object with the loaded
|
||||
* property.
|
||||
*/
|
||||
this.onProgress = null;
|
||||
}
|
||||
class BasePDFStreamRangeReader {
|
||||
_stream = null;
|
||||
|
||||
/**
|
||||
* Gets ability of the stream to progressively load binary data.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isStreamingSupported() {
|
||||
return false;
|
||||
constructor(stream, begin, end) {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
|
||||
this.constructor === BasePDFStreamRangeReader
|
||||
) {
|
||||
unreachable("Cannot initialize BasePDFStreamRangeReader.");
|
||||
}
|
||||
this._stream = stream;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,13 +220,17 @@ class IPDFStreamRangeReader {
|
||||
* set to true.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async read() {}
|
||||
async read() {
|
||||
unreachable("Abstract method `read` called");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all pending read requests and closes the stream.
|
||||
* @param {Object} reason
|
||||
*/
|
||||
cancel(reason) {}
|
||||
cancel(reason) {
|
||||
unreachable("Abstract method `cancel` called");
|
||||
}
|
||||
}
|
||||
|
||||
export { IPDFStream, IPDFStreamRangeReader, IPDFStreamReader };
|
||||
export { BasePDFStream, BasePDFStreamRangeReader, BasePDFStreamReader };
|
||||
@ -160,14 +160,18 @@ describe("api", function () {
|
||||
progressReportedCapability.resolve(progressData);
|
||||
};
|
||||
|
||||
const data = await Promise.all([
|
||||
progressReportedCapability.promise,
|
||||
const [pdfDoc, progress] = await Promise.all([
|
||||
loadingTask.promise,
|
||||
progressReportedCapability.promise,
|
||||
]);
|
||||
|
||||
expect(data[0].loaded / data[0].total >= 0).toEqual(true);
|
||||
expect(data[1] instanceof PDFDocumentProxy).toEqual(true);
|
||||
expect(loadingTask).toEqual(data[1].loadingTask);
|
||||
expect(pdfDoc instanceof PDFDocumentProxy).toEqual(true);
|
||||
expect(pdfDoc.loadingTask).toBe(loadingTask);
|
||||
|
||||
expect(progress.loaded).toBeGreaterThanOrEqual(0);
|
||||
expect(progress.total).toEqual(basicApiFileLength);
|
||||
expect(progress.percent).toBeGreaterThanOrEqual(0);
|
||||
expect(progress.percent).toBeLessThanOrEqual(100);
|
||||
|
||||
await loadingTask.destroy();
|
||||
});
|
||||
@ -218,12 +222,17 @@ describe("api", function () {
|
||||
progressReportedCapability.resolve(data);
|
||||
};
|
||||
|
||||
const data = await Promise.all([
|
||||
const [pdfDoc, progress] = await Promise.all([
|
||||
loadingTask.promise,
|
||||
progressReportedCapability.promise,
|
||||
]);
|
||||
expect(data[0] instanceof PDFDocumentProxy).toEqual(true);
|
||||
expect(data[1].loaded / data[1].total).toEqual(1);
|
||||
|
||||
expect(pdfDoc instanceof PDFDocumentProxy).toEqual(true);
|
||||
expect(pdfDoc.loadingTask).toBe(loadingTask);
|
||||
|
||||
expect(progress.loaded).toEqual(basicApiFileLength);
|
||||
expect(progress.total).toEqual(basicApiFileLength);
|
||||
expect(progress.percent).toEqual(100);
|
||||
|
||||
// Check that the TypedArray was transferred.
|
||||
expect(typedArrayPdf.length).toEqual(0);
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -35,6 +35,7 @@ describe("fetch_stream", function () {
|
||||
it("read with streaming", async function () {
|
||||
const stream = new PDFFetchStream({
|
||||
url: getPdfUrl(),
|
||||
rangeChunkSize: 32768,
|
||||
disableStream: false,
|
||||
disableRange: true,
|
||||
});
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
createResponseError,
|
||||
extractFilenameFromHeader,
|
||||
validateRangeRequestCapabilities,
|
||||
validateResponseStatus,
|
||||
} from "../../src/display/network_utils.js";
|
||||
import { ResponseException } from "../../src/shared/util.js";
|
||||
|
||||
@ -391,18 +390,4 @@ describe("network_utils", function () {
|
||||
testCreateResponseError("https://foo.com/bar.pdf", 0, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateResponseStatus", function () {
|
||||
it("accepts valid response statuses", function () {
|
||||
expect(validateResponseStatus(200)).toEqual(true);
|
||||
expect(validateResponseStatus(206)).toEqual(true);
|
||||
});
|
||||
|
||||
it("rejects invalid response statuses", function () {
|
||||
expect(validateResponseStatus(302)).toEqual(false);
|
||||
expect(validateResponseStatus(404)).toEqual(false);
|
||||
expect(validateResponseStatus(null)).toEqual(false);
|
||||
expect(validateResponseStatus(undefined)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1236,9 +1236,7 @@ const PDFViewerApplication = {
|
||||
this.passwordPrompt.open();
|
||||
};
|
||||
|
||||
loadingTask.onProgress = ({ loaded, total }) => {
|
||||
this.progress(loaded / total);
|
||||
};
|
||||
loadingTask.onProgress = evt => this.progress(evt.percent);
|
||||
|
||||
return loadingTask.promise.then(
|
||||
pdfDocument => {
|
||||
@ -1374,8 +1372,7 @@ const PDFViewerApplication = {
|
||||
return message;
|
||||
},
|
||||
|
||||
progress(level) {
|
||||
const percent = Math.round(level * 100);
|
||||
progress(percent) {
|
||||
// When we transition from full request to range requests, it's possible
|
||||
// that we discard some of the loaded data. This can cause the loading
|
||||
// bar to move backwards. So prevent this by only updating the bar if it
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isPdfFile, PDFDataRangeTransport } from "pdfjs-lib";
|
||||
import { isPdfFile, MathClamp, PDFDataRangeTransport } from "pdfjs-lib";
|
||||
import { AppOptions } from "./app_options.js";
|
||||
import { BaseExternalServices } from "./external_services.js";
|
||||
import { BasePreferences } from "./preferences.js";
|
||||
@ -627,7 +627,13 @@ class ExternalServices extends BaseExternalServices {
|
||||
pdfDataRangeTransport?.onDataProgressiveDone();
|
||||
break;
|
||||
case "progress":
|
||||
viewerApp.progress(args.loaded / args.total);
|
||||
const percent = MathClamp(
|
||||
Math.round((args.loaded / args.total) * 100),
|
||||
0,
|
||||
100
|
||||
);
|
||||
|
||||
viewerApp.progress(percent);
|
||||
break;
|
||||
case "complete":
|
||||
if (!args.data) {
|
||||
|
||||
@ -709,7 +709,7 @@ class ProgressBar {
|
||||
}
|
||||
|
||||
set percent(val) {
|
||||
this.#percent = MathClamp(val, 0, 100);
|
||||
this.#percent = val;
|
||||
|
||||
if (isNaN(val)) {
|
||||
this.#classList.add("indeterminate");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user