Compare commits

..

No commits in common. "f4326e17c40760d0251768ca2279141f6ead1b0a" and "814df09e21d13895c25eb3a9499b0570f39ccc9e" have entirely different histories.

69 changed files with 1445 additions and 3604 deletions

View File

@ -28,4 +28,4 @@ jobs:
run: npm ci run: npm ci
- name: Run types tests - name: Run types tests
run: npx gulp types run: npx gulp typestest

View File

@ -31,7 +31,6 @@ export default [
"**/docs/", "**/docs/",
"**/node_modules/", "**/node_modules/",
"external/bcmaps/", "external/bcmaps/",
"external/brotli/",
"external/builder/fixtures/", "external/builder/fixtures/",
"external/builder/fixtures_babel/", "external/builder/fixtures_babel/",
"external/openjpeg/", "external/openjpeg/",

View File

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

View File

@ -1,19 +0,0 @@
Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,8 +0,0 @@
## Release
In order to get the file `decoder.js`:
* `gulp release-brotli --hash` followed by the git hash of the revision.
## Licensing
[brotli](https://github.com/google/brotli/) is under [MIT License](https://github.com/google/brotli/blob/master/LICENSE)

File diff suppressed because one or more lines are too long

View File

@ -22,7 +22,6 @@ import autoprefixer from "autoprefixer";
import babel from "@babel/core"; import babel from "@babel/core";
import { buildPrefsSchema } from "./external/chromium/prefs.mjs"; import { buildPrefsSchema } from "./external/chromium/prefs.mjs";
import crypto from "crypto"; import crypto from "crypto";
import { finished } from "stream/promises";
import fs from "fs"; import fs from "fs";
import gulp from "gulp"; import gulp from "gulp";
import hljs from "highlight.js"; import hljs from "highlight.js";
@ -232,7 +231,7 @@ function createWebpackAlias(defines) {
libraryAlias["display-fetch_stream"] = "src/display/fetch_stream.js"; libraryAlias["display-fetch_stream"] = "src/display/fetch_stream.js";
libraryAlias["display-network"] = "src/display/network.js"; libraryAlias["display-network"] = "src/display/network.js";
viewerAlias["web-download_manager"] = "web/chromecom.js"; viewerAlias["web-download_manager"] = "web/download_manager.js";
viewerAlias["web-external_services"] = "web/chromecom.js"; viewerAlias["web-external_services"] = "web/chromecom.js";
viewerAlias["web-null_l10n"] = "web/l10n.js"; viewerAlias["web-null_l10n"] = "web/l10n.js";
viewerAlias["web-preferences"] = "web/chromecom.js"; viewerAlias["web-preferences"] = "web/chromecom.js";
@ -814,28 +813,6 @@ gulp.task("default", function (done) {
done(); done();
}); });
gulp.task("release-brotli", async function (done) {
const hashIndex = process.argv.indexOf("--hash");
if (hashIndex === -1 || hashIndex + 1 >= process.argv.length) {
throw new Error('Missing "--hash <commit-hash>" argument.');
}
console.log();
console.log("### Getting Brotli js file for release");
const OUTPUT_DIR = "./external/brotli/";
const hash = process.argv[hashIndex + 1];
const url = `https://raw.githubusercontent.com/google/brotli/${hash}/js/decode.js`;
const outputPath = OUTPUT_DIR + "decode.js";
const res = await fetch(url);
const fileStream = fs.createWriteStream(outputPath, { flags: "w" });
await finished(stream.Readable.fromWeb(res.body).pipe(fileStream));
fileStream.end();
console.log(`Brotli js file saved to: ${outputPath}`);
done();
});
function createBuildNumber(done) { function createBuildNumber(done) {
console.log("\n### Getting extension build number"); console.log("\n### Getting extension build number");
@ -1605,7 +1582,6 @@ function buildLib(defines, dir) {
gulp.src("external/openjpeg/*.js", { base: "openjpeg/", encoding: false }), gulp.src("external/openjpeg/*.js", { base: "openjpeg/", encoding: false }),
gulp.src("external/qcms/*.js", { base: "qcms/", encoding: false }), gulp.src("external/qcms/*.js", { base: "qcms/", encoding: false }),
gulp.src("external/jbig2/*.js", { base: "jbig2/", encoding: false }), gulp.src("external/jbig2/*.js", { base: "jbig2/", encoding: false }),
gulp.src("external/brotli/*.js", { base: "brotli/", encoding: false }),
]); ]);
return buildLibHelper(bundleDefines, inputStream, dir); return buildLibHelper(bundleDefines, inputStream, dir);

View File

@ -1,5 +1,5 @@
{ {
"stableVersion": "5.4.624", "stableVersion": "5.4.530",
"baseVersion": "1b427a3af5e0a40c296a3cafb08edbd36d973ff1", "baseVersion": "1b427a3af5e0a40c296a3cafb08edbd36d973ff1",
"versionPrefix": "5.4." "versionPrefix": "5.4."
} }

View File

@ -1,86 +0,0 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BrotliDecode } from "../../external/brotli/decode.js";
import { DecodeStream } from "./decode_stream.js";
import { Stream } from "./stream.js";
class BrotliStream extends DecodeStream {
#isAsync = true;
constructor(stream, maybeLength) {
super(maybeLength);
this.stream = stream;
this.dict = stream.dict;
}
readBlock() {
// TODO: add some telemetry to measure how often we fallback here.
// Get all bytes from the input stream
const bytes = this.stream.getBytes();
const decodedData = BrotliDecode(
new Int8Array(bytes.buffer, bytes.byteOffset, bytes.length)
);
this.buffer = new Uint8Array(
decodedData.buffer,
decodedData.byteOffset,
decodedData.length
);
this.bufferLength = this.buffer.length;
this.eof = true;
}
async getImageData(length, _decoderOptions) {
const data = await this.asyncGetBytes();
if (!data) {
return this.getBytes(length);
}
if (data.length <= length) {
return data;
}
return data.subarray(0, length);
}
async asyncGetBytes() {
const { decompressed, compressed } =
await this.asyncGetBytesFromDecompressionStream("brotli");
if (decompressed) {
return decompressed;
}
// DecompressionStream failed (for example because there are some extra
// bytes after the end of the compressed data), so we fallback to our
// decoder.
// We already get the bytes from the underlying stream, so we just reuse
// them to avoid get them again.
this.#isAsync = false;
this.stream = new Stream(
compressed,
0,
compressed.length,
this.stream.dict
);
this.reset();
return null;
}
get isAsync() {
return this.#isAsync;
}
}
export { BrotliStream };

View File

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

View File

@ -110,46 +110,6 @@ class DecodeStream extends BaseStream {
return this.decodeImage(data, decoderOptions); return this.decodeImage(data, decoderOptions);
} }
async asyncGetBytesFromDecompressionStream(name) {
this.stream.reset();
const bytes = this.stream.isAsync
? await this.stream.asyncGetBytes()
: this.stream.getBytes();
try {
const { readable, writable } = new DecompressionStream(name);
const writer = writable.getWriter();
await writer.ready;
// We can't await writer.write() because it'll block until the reader
// starts which happens few lines below.
writer
.write(bytes)
.then(async () => {
await writer.ready;
await writer.close();
})
.catch(() => {});
const chunks = [];
let totalLength = 0;
for await (const chunk of readable) {
chunks.push(chunk);
totalLength += chunk.byteLength;
}
const data = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
data.set(chunk, offset);
offset += chunk.byteLength;
}
return { decompressed: data, compressed: bytes };
} catch {
return { decompressed: null, compressed: bytes };
}
}
reset() { reset() {
this.pos = 0; this.pos = 0;
} }

View File

@ -163,26 +163,57 @@ class FlateStream extends DecodeStream {
} }
async asyncGetBytes() { async asyncGetBytes() {
const { decompressed, compressed } = this.stream.reset();
await this.asyncGetBytesFromDecompressionStream("deflate"); const bytes = this.stream.isAsync
if (decompressed) { ? await this.stream.asyncGetBytes()
return decompressed; : this.stream.getBytes();
}
// DecompressionStream failed (for example because there are some extra
// bytes after the end of the compressed data), so we fallback to our
// decoder.
// We already get the bytes from the underlying stream, so we just reuse
// them to avoid get them again.
this.#isAsync = false; try {
this.stream = new Stream( const { readable, writable } = new DecompressionStream("deflate");
compressed, const writer = writable.getWriter();
2 /* = header size (see ctor) */, await writer.ready;
compressed.length,
this.stream.dict // We can't await writer.write() because it'll block until the reader
); // starts which happens few lines below.
this.reset(); writer
return null; .write(bytes)
.then(async () => {
await writer.ready;
await writer.close();
})
.catch(() => {});
const chunks = [];
let totalLength = 0;
for await (const chunk of readable) {
chunks.push(chunk);
totalLength += chunk.byteLength;
}
const data = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
data.set(chunk, offset);
offset += chunk.byteLength;
}
return data;
} catch {
// DecompressionStream failed (for example because there are some extra
// bytes after the end of the compressed data), so we fallback to our
// decoder.
// We already get the bytes from the underlying stream, so we just reuse
// them to avoid get them again.
this.#isAsync = false;
this.stream = new Stream(
bytes,
2 /* = header size (see ctor) */,
bytes.length,
this.stream.dict
);
this.reset();
return null;
}
} }
get isAsync() { get isAsync() {

View File

@ -29,7 +29,6 @@ import {
import { NullStream, Stream } from "./stream.js"; import { NullStream, Stream } from "./stream.js";
import { Ascii85Stream } from "./ascii_85_stream.js"; import { Ascii85Stream } from "./ascii_85_stream.js";
import { AsciiHexStream } from "./ascii_hex_stream.js"; import { AsciiHexStream } from "./ascii_hex_stream.js";
import { BrotliStream } from "./brotli_stream.js";
import { CCITTFaxStream } from "./ccitt_stream.js"; import { CCITTFaxStream } from "./ccitt_stream.js";
import { FlateStream } from "./flate_stream.js"; import { FlateStream } from "./flate_stream.js";
import { Jbig2Stream } from "./jbig2_stream.js"; import { Jbig2Stream } from "./jbig2_stream.js";
@ -823,8 +822,6 @@ class Parser {
return new RunLengthStream(stream, maybeLength); return new RunLengthStream(stream, maybeLength);
case "JBIG2Decode": case "JBIG2Decode":
return new Jbig2Stream(stream, maybeLength, params); return new Jbig2Stream(stream, maybeLength, params);
case "BrotliDecode":
return new BrotliStream(stream, maybeLength);
} }
warn(`Filter "${name}" is not supported.`); warn(`Filter "${name}" is not supported.`);
return stream; return stream;

View File

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

View File

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

View File

@ -18,6 +18,9 @@
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ /** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../../web/interfaces").IDownloadManager} IDownloadManager */
/** @typedef {import("../../web/interfaces").IPDFLinkService} IPDFLinkService */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ /** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../../web/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */ /** @typedef {import("../../web/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */
@ -54,8 +57,8 @@ const TIMEZONE_OFFSET = new Date().getTimezoneOffset() * 60 * 1000;
* @typedef {Object} AnnotationElementParameters * @typedef {Object} AnnotationElementParameters
* @property {Object} data * @property {Object} data
* @property {HTMLDivElement} layer * @property {HTMLDivElement} layer
* @property {PDFLinkService} linkService * @property {IPDFLinkService} linkService
* @property {BaseDownloadManager} [downloadManager] * @property {IDownloadManager} [downloadManager]
* @property {AnnotationStorage} [annotationStorage] * @property {AnnotationStorage} [annotationStorage]
* @property {string} [imageResourcesPath] - Path for image resources, mainly * @property {string} [imageResourcesPath] - Path for image resources, mainly
* for annotation icons. Include trailing slash. * for annotation icons. Include trailing slash.
@ -3733,8 +3736,8 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
* @property {HTMLDivElement} div * @property {HTMLDivElement} div
* @property {Array} annotations * @property {Array} annotations
* @property {PDFPageProxy} page * @property {PDFPageProxy} page
* @property {PDFLinkService} linkService * @property {IPDFLinkService} linkService
* @property {BaseDownloadManager} [downloadManager] * @property {IDownloadManager} [downloadManager]
* @property {AnnotationStorage} [annotationStorage] * @property {AnnotationStorage} [annotationStorage]
* @property {string} [imageResourcesPath] - Path for image resources, mainly * @property {string} [imageResourcesPath] - Path for image resources, mainly
* for annotation icons. Include trailing slash. * for annotation icons. Include trailing slash.
@ -4015,6 +4018,8 @@ class AnnotationLayer {
* Add link annotations to the annotation layer. * Add link annotations to the annotation layer.
* *
* @param {Array<Object>} annotations * @param {Array<Object>} annotations
* @param {IPDFLinkService} linkService
* @memberof AnnotationLayer
*/ */
async addLinkAnnotations(annotations) { async addLinkAnnotations(annotations) {
const elementParams = { const elementParams = {

View File

@ -25,7 +25,6 @@ import {
getVerbosityLevel, getVerbosityLevel,
info, info,
isNodeJS, isNodeJS,
MathClamp,
RenderingIntentFlag, RenderingIntentFlag,
setVerbosityLevel, setVerbosityLevel,
shadow, shadow,
@ -441,7 +440,6 @@ function getDocument(src = {}) {
ownerDocument, ownerDocument,
pdfBug, pdfBug,
styleElement, styleElement,
enableHWA,
loadingParams: { loadingParams: {
disableAutoFetch, disableAutoFetch,
enableXfa, enableXfa,
@ -465,8 +463,7 @@ function getDocument(src = {}) {
let networkStream; let networkStream;
if (rangeTransport) { if (rangeTransport) {
networkStream = new PDFDataTransportStream({ networkStream = new PDFDataTransportStream(rangeTransport, {
pdfDataRangeTransport: rangeTransport,
disableRange, disableRange,
disableStream, disableStream,
}); });
@ -511,7 +508,8 @@ 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);
@ -526,8 +524,6 @@ function getDocument(src = {}) {
* @typedef {Object} OnProgressParameters * @typedef {Object} OnProgressParameters
* @property {number} loaded - Currently loaded number of bytes. * @property {number} loaded - Currently loaded number of bytes.
* @property {number} total - Total number of bytes in the PDF file. * @property {number} total - Total number of bytes in the PDF file.
* @property {number} percent - Currently loaded percentage, as an integer value
* in the [0, 100] range. If `total` is undefined, the percentage is `NaN`.
*/ */
/** /**
@ -2395,14 +2391,8 @@ class PDFWorker {
* @ignore * @ignore
*/ */
class WorkerTransport { class WorkerTransport {
downloadInfoCapability = Promise.withResolvers();
#fullReader = null;
#methodPromises = new Map(); #methodPromises = new Map();
#networkStream = null;
#pageCache = new Map(); #pageCache = new Map();
#pagePromises = new Map(); #pagePromises = new Map();
@ -2413,17 +2403,21 @@ class WorkerTransport {
#pagesMapper = PagesMapper.instance; #pagesMapper = PagesMapper.instance;
constructor(messageHandler, loadingTask, networkStream, params, factory) { constructor(
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;
@ -2436,6 +2430,12 @@ class WorkerTransport {
this.destroyed = false; this.destroyed = false;
this.destroyCapability = null; this.destroyCapability = null;
this._networkStream = networkStream;
this._fullReader = null;
this._lastProgress = null;
this.downloadInfoCapability = Promise.withResolvers();
this.enableHWA = enableHWA;
this.setupMessageHandler(); this.setupMessageHandler();
this.#pagesMapper.addListener(this.#updateCaches.bind(this)); this.#pagesMapper.addListener(this.#updateCaches.bind(this));
@ -2493,14 +2493,6 @@ class WorkerTransport {
return promise; return promise;
} }
#onProgress({ loaded, total }) {
this.loadingTask.onProgress?.({
loaded,
total,
percent: MathClamp(Math.round((loaded / total) * 100), 0, 100),
});
}
get annotationStorage() { get annotationStorage() {
return shadow(this, "annotationStorage", new AnnotationStorage()); return shadow(this, "annotationStorage", new AnnotationStorage());
} }
@ -2612,7 +2604,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.")
); );
@ -2629,16 +2621,18 @@ class WorkerTransport {
messageHandler.on("GetReader", (data, sink) => { messageHandler.on("GetReader", (data, sink) => {
assert( assert(
this.#networkStream, this._networkStream,
"GetReader - no `BasePDFStream` instance available." "GetReader - no `IPDFStream` instance available."
); );
this.#fullReader = this.#networkStream.getFullReader(); this._fullReader = this._networkStream.getFullReader();
// If stream or range turn out to be disabled, once `headersReady` is this._fullReader.onProgress = evt => {
// resolved, this is our only way to report loading progress. this._lastProgress = {
this.#fullReader.onProgress = evt => this.#onProgress(evt); loaded: evt.loaded,
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) {
@ -2659,7 +2653,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) {
@ -2671,29 +2665,40 @@ 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 (isStreamingSupported && isRangeSupported) { // If stream or range are disabled, it's our only way to report
this.#fullReader.onProgress = null; // See comment in "GetReader" above. // loading progress.
if (!isStreamingSupported || !isRangeSupported) {
if (this._lastProgress) {
loadingTask.onProgress?.(this._lastProgress);
}
this._fullReader.onProgress = evt => {
loadingTask.onProgress?.({
loaded: evt.loaded,
total: evt.total,
});
};
} }
return { isStreamingSupported, isRangeSupported, contentLength }; return { isStreamingSupported, isRangeSupported, contentLength };
}); });
messageHandler.on("GetRangeReader", (data, sink) => { messageHandler.on("GetRangeReader", (data, sink) => {
assert( assert(
this.#networkStream, this._networkStream,
"GetRangeReader - no `BasePDFStream` instance available." "GetRangeReader - no `IPDFStream` 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 `#fullReader` implementation. // has already been fetched via the `_fullRequestReader` 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
@ -2701,7 +2706,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 `#fullReader`). // the worker-thread yet (which will happen via the `_fullRequestReader`).
if (!rangeReader) { if (!rangeReader) {
sink.close(); sink.close();
return; return;
@ -2775,7 +2780,10 @@ class WorkerTransport {
messageHandler.on("DataLoaded", data => { messageHandler.on("DataLoaded", data => {
// For consistency: Ensure that progress is always reported when the // For consistency: Ensure that progress is always reported when the
// entire PDF file has been loaded, regardless of how it was fetched. // entire PDF file has been loaded, regardless of how it was fetched.
this.#onProgress({ loaded: data.length, total: data.length }); loadingTask.onProgress?.({
loaded: data.length,
total: data.length,
});
this.downloadInfoCapability.resolve(data); this.downloadInfoCapability.resolve(data);
}); });
@ -2898,7 +2906,10 @@ class WorkerTransport {
if (this.destroyed) { if (this.destroyed) {
return; // Ignore any pending requests if the worker was terminated. return; // Ignore any pending requests if the worker was terminated.
} }
this.#onProgress(data); loadingTask.onProgress?.({
loaded: data.loaded,
total: data.total,
});
}); });
messageHandler.on("FetchBinaryData", async data => { messageHandler.on("FetchBinaryData", async data => {
@ -2939,7 +2950,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
) )
@ -3107,8 +3118,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

@ -18,6 +18,7 @@
/** @typedef {import("../display_utils.js").PageViewport} PageViewport */ /** @typedef {import("../display_utils.js").PageViewport} PageViewport */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ /** @typedef {import("../../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
/** @typedef {import("../../../web/interfaces").IL10n} IL10n */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../annotation_layer.js").AnnotationLayer} AnnotationLayer */ /** @typedef {import("../annotation_layer.js").AnnotationLayer} AnnotationLayer */
/** @typedef {import("../draw_layer.js").DrawLayer} DrawLayer */ /** @typedef {import("../draw_layer.js").DrawLayer} DrawLayer */
@ -46,7 +47,7 @@ import { StampEditor } from "./stamp.js";
* @property {boolean} enabled * @property {boolean} enabled
* @property {TextAccessibilityManager} [accessibilityManager] * @property {TextAccessibilityManager} [accessibilityManager]
* @property {number} pageIndex * @property {number} pageIndex
* @property {L10n} l10n * @property {IL10n} l10n
* @property {AnnotationLayer} [annotationLayer] * @property {AnnotationLayer} [annotationLayer]
* @property {HTMLDivElement} [textLayer] * @property {HTMLDivElement} [textLayer]
* @property {DrawLayer} drawLayer * @property {DrawLayer} drawLayer

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./annotation_storage").AnnotationStorage} AnnotationStorage */ /** @typedef {import("./annotation_storage").AnnotationStorage} AnnotationStorage */
/** @typedef {import("./display_utils").PageViewport} PageViewport */ /** @typedef {import("./display_utils").PageViewport} PageViewport */
/** @typedef {import("../../web/interfaces").IPDFLinkService} IPDFLinkService */
import { XfaText } from "./xfa_text.js"; import { XfaText } from "./xfa_text.js";
@ -25,7 +26,7 @@ import { XfaText } from "./xfa_text.js";
* @property {HTMLDivElement} div * @property {HTMLDivElement} div
* @property {Object} xfaHtml * @property {Object} xfaHtml
* @property {AnnotationStorage} [annotationStorage] * @property {AnnotationStorage} [annotationStorage]
* @property {PDFLinkService} linkService * @property {IPDFLinkService} linkService
* @property {string} [intent] - (default value is 'display'). * @property {string} [intent] - (default value is 'display').
*/ */

View File

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

View File

@ -870,4 +870,3 @@
!bug2009627.pdf !bug2009627.pdf
!page_with_number.pdf !page_with_number.pdf
!page_with_number_and_link.pdf !page_with_number_and_link.pdf
!Brotli-Prototype-FileA.pdf

Binary file not shown.

View File

@ -13929,12 +13929,5 @@
"md5": "e515a9abb11ab74332e57e371bfae61e", "md5": "e515a9abb11ab74332e57e371bfae61e",
"rounds": 1, "rounds": 1,
"type": "eq" "type": "eq"
},
{
"id": "Brotli-Prototype-FileA",
"file": "pdfs/Brotli-Prototype-FileA.pdf",
"md5": "9113370932798776ba91c807ce95082e",
"rounds": 1,
"type": "eq"
} }
] ]

View File

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

View File

@ -209,14 +209,4 @@ describe("autolinker", function () {
], ],
]); ]);
}); });
it("should correctly find emails with hyphens in domain (bug 20557)", function () {
testLinks([
[
"john.doe@faculity.uni-cityname.tld",
"mailto:john.doe@faculity.uni-cityname.tld",
],
["john.doe@uni-cityname.tld", "mailto:john.doe@uni-cityname.tld"],
]);
});
}); });

View File

@ -16,7 +16,8 @@
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 `BasePDFStream` implementations: // Common tests to verify behavior across implementations of the IPDFStream
// interface:
// - PDFNetworkStream by network_spec.js // - PDFNetworkStream by network_spec.js
// - PDFFetchStream by fetch_stream_spec.js // - PDFFetchStream by fetch_stream_spec.js
async function testCrossOriginRedirects({ async function testCrossOriginRedirects({

View File

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

View File

@ -18,6 +18,7 @@ import {
createResponseError, createResponseError,
extractFilenameFromHeader, extractFilenameFromHeader,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
validateResponseStatus,
} from "../../src/display/network_utils.js"; } from "../../src/display/network_utils.js";
import { ResponseException } from "../../src/shared/util.js"; import { ResponseException } from "../../src/shared/util.js";
@ -390,4 +391,18 @@ describe("network_utils", function () {
testCreateResponseError("https://foo.com/bar.pdf", 0, false); testCreateResponseError("https://foo.com/bar.pdf", 0, false);
}); });
}); });
describe("validateResponseStatus", function () {
it("accepts valid response statuses", function () {
expect(validateResponseStatus(200)).toEqual(true);
expect(validateResponseStatus(206)).toEqual(true);
});
it("rejects invalid response statuses", function () {
expect(validateResponseStatus(302)).toEqual(false);
expect(validateResponseStatus(404)).toEqual(false);
expect(validateResponseStatus(null)).toEqual(false);
expect(validateResponseStatus(undefined)).toEqual(false);
});
});
}); });

View File

@ -22,6 +22,7 @@ import {
import { import {
parseQueryString, parseQueryString,
ProgressBar, ProgressBar,
RenderingStates,
ScrollMode, ScrollMode,
SpreadMode, SpreadMode,
} from "../../web/ui_utils.js"; } from "../../web/ui_utils.js";
@ -34,7 +35,6 @@ import { PDFPageView } from "../../web/pdf_page_view.js";
import { PDFScriptingManager } from "../../web/pdf_scripting_manager.component.js"; import { PDFScriptingManager } from "../../web/pdf_scripting_manager.component.js";
import { PDFSinglePageViewer } from "../../web/pdf_single_page_viewer.js"; import { PDFSinglePageViewer } from "../../web/pdf_single_page_viewer.js";
import { PDFViewer } from "../../web/pdf_viewer.js"; import { PDFViewer } from "../../web/pdf_viewer.js";
import { RenderingStates } from "../../web/renderable_view.js";
import { StructTreeLayerBuilder } from "../../web/struct_tree_layer_builder.js"; import { StructTreeLayerBuilder } from "../../web/struct_tree_layer_builder.js";
import { TextLayerBuilder } from "../../web/text_layer_builder.js"; import { TextLayerBuilder } from "../../web/text_layer_builder.js";
import { XfaLayerBuilder } from "../../web/xfa_layer_builder.js"; import { XfaLayerBuilder } from "../../web/xfa_layer_builder.js";

View File

@ -20,6 +20,7 @@
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ /** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ /** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
/** @typedef {import("./interfaces").IL10n} IL10n */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/annotation_layer.js").AnnotationLayer} AnnotationLayer */ /** @typedef {import("../src/display/annotation_layer.js").AnnotationLayer} AnnotationLayer */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
@ -32,7 +33,7 @@ import { GenericL10n } from "web-null_l10n";
* @typedef {Object} AnnotationEditorLayerBuilderOptions * @typedef {Object} AnnotationEditorLayerBuilderOptions
* @property {AnnotationEditorUIManager} [uiManager] * @property {AnnotationEditorUIManager} [uiManager]
* @property {PDFPageProxy} pdfPage * @property {PDFPageProxy} pdfPage
* @property {L10n} [l10n] * @property {IL10n} [l10n]
* @property {StructTreeLayerBuilder} [structTreeLayer] * @property {StructTreeLayerBuilder} [structTreeLayer]
* @property {TextAccessibilityManager} [accessibilityManager] * @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationLayer} [annotationLayer] * @property {AnnotationLayer} [annotationLayer]

View File

@ -18,6 +18,8 @@
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */ /** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */ /** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */ /** @typedef {import("./struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
@ -41,8 +43,8 @@ import { PresentationModeState } from "./ui_utils.js";
* @property {string} [imageResourcesPath] - Path for image resources, mainly * @property {string} [imageResourcesPath] - Path for image resources, mainly
* for annotation icons. Include trailing slash. * for annotation icons. Include trailing slash.
* @property {boolean} renderForms * @property {boolean} renderForms
* @property {PDFLinkService} linkService * @property {IPDFLinkService} linkService
* @property {BaseDownloadManager} [downloadManager] * @property {IDownloadManager} [downloadManager]
* @property {boolean} [enableComment] * @property {boolean} [enableComment]
* @property {boolean} [enableScripting] * @property {boolean} [enableScripting]
* @property {Promise<boolean>} [hasJSActionsPromise] * @property {Promise<boolean>} [hasJSActionsPromise]

View File

@ -13,6 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./interfaces.js").IL10n} IL10n */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */ /** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
@ -33,6 +34,7 @@ import {
normalizeWheelEventDirection, normalizeWheelEventDirection,
parseQueryString, parseQueryString,
ProgressBar, ProgressBar,
RenderingStates,
ScrollMode, ScrollMode,
SidebarView, SidebarView,
SpreadMode, SpreadMode,
@ -90,7 +92,6 @@ import { PdfTextExtractor } from "./pdf_text_extractor.js";
import { PDFThumbnailViewer } from "web-pdf_thumbnail_viewer"; import { PDFThumbnailViewer } from "web-pdf_thumbnail_viewer";
import { PDFViewer } from "./pdf_viewer.js"; import { PDFViewer } from "./pdf_viewer.js";
import { Preferences } from "web-preferences"; import { Preferences } from "web-preferences";
import { RenderingStates } from "./renderable_view.js";
import { SecondaryToolbar } from "web-secondary_toolbar"; import { SecondaryToolbar } from "web-secondary_toolbar";
import { SignatureManager } from "web-signature_manager"; import { SignatureManager } from "web-signature_manager";
import { Toolbar } from "web-toolbar"; import { Toolbar } from "web-toolbar";
@ -159,7 +160,7 @@ const PDFViewerApplication = {
secondaryToolbar: null, secondaryToolbar: null,
/** @type {EventBus} */ /** @type {EventBus} */
eventBus: null, eventBus: null,
/** @type {L10n} */ /** @type {IL10n} */
l10n: null, l10n: null,
/** @type {AnnotationEditorParams} */ /** @type {AnnotationEditorParams} */
annotationEditorParams: null, annotationEditorParams: null,
@ -1235,7 +1236,9 @@ const PDFViewerApplication = {
this.passwordPrompt.open(); this.passwordPrompt.open();
}; };
loadingTask.onProgress = evt => this.progress(evt.percent); loadingTask.onProgress = ({ loaded, total }) => {
this.progress(loaded / total);
};
return loadingTask.promise.then( return loadingTask.promise.then(
pdfDocument => { pdfDocument => {
@ -1371,7 +1374,8 @@ const PDFViewerApplication = {
return message; return message;
}, },
progress(percent) { progress(level) {
const percent = Math.round(level * 100);
// When we transition from full request to range requests, it's possible // When we transition from full request to range requests, it's possible
// that we discard some of the loaded data. This can cause the loading // that we discard some of the loaded data. This can cause the loading
// bar to move backwards. So prevent this by only updating the bar if it // bar to move backwards. So prevent this by only updating the bar if it
@ -2456,7 +2460,10 @@ const PDFViewerApplication = {
}; };
initCom(PDFViewerApplication); initCom(PDFViewerApplication);
PDFPrintServiceFactory.initGlobals(PDFViewerApplication);
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
PDFPrintServiceFactory.initGlobals(PDFViewerApplication);
}
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
const HOSTED_VIEWER_ORIGINS = new Set([ const HOSTED_VIEWER_ORIGINS = new Set([

View File

@ -138,7 +138,7 @@ class Autolinker {
static findLinks(text) { static findLinks(text) {
// Regex can be tested and verified at https://regex101.com/r/rXoLiT/2. // Regex can be tested and verified at https://regex101.com/r/rXoLiT/2.
this.#regex ??= this.#regex ??=
/\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|(?=\p{L})[\S--[@\p{Ps}\p{Pe}<>]]+@([\S--[[\p{P}--\-]<>]]+(?:\.[\S--[[\p{P}--\-]<>]]+)+)/gmv; /\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|(?=\p{L})[\S--[@\p{Ps}\p{Pe}<>]]+@([\S--[\p{P}<>]]+(?:\.[\S--[\p{P}<>]]+)+)/gmv;
const [normalizedText, diffs] = normalize(text, { ignoreDashEOL: true }); const [normalizedText, diffs] = normalize(text, { ignoreDashEOL: true });
const matches = normalizedText.matchAll(this.#regex); const matches = normalizedText.matchAll(this.#regex);

View File

@ -1,103 +0,0 @@
/* Copyright 2013 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isPdfFile } from "pdfjs-lib";
class BaseDownloadManager {
#openBlobUrls = new WeakMap();
constructor() {
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
this.constructor === BaseDownloadManager
) {
throw new Error("Cannot initialize BaseDownloadManager.");
}
}
_triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) {
throw new Error("Not implemented: _triggerDownload");
}
_getOpenDataUrl(blobUrl, filename, dest = null) {
throw new Error("Not implemented: _getOpenDataUrl");
}
/**
* @param {Uint8Array} data
* @param {string} filename
* @param {string} [contentType]
*/
downloadData(data, filename, contentType) {
const blobUrl = URL.createObjectURL(
new Blob([data], { type: contentType })
);
this._triggerDownload(
blobUrl,
/* originalUrl = */ blobUrl,
filename,
/* isAttachment = */ true
);
}
/**
* @param {Uint8Array} data
* @param {string} filename
* @param {string | null} [dest]
* @returns {boolean} Indicating if the data was opened.
*/
openOrDownloadData(data, filename, dest = null) {
const isPdfData = isPdfFile(filename);
const contentType = isPdfData ? "application/pdf" : "";
if (isPdfData) {
let blobUrl;
try {
blobUrl = this.#openBlobUrls.getOrInsertComputed(data, () =>
URL.createObjectURL(new Blob([data], { type: contentType }))
);
const viewerUrl = this._getOpenDataUrl(blobUrl, filename, dest);
window.open(viewerUrl);
return true;
} catch (ex) {
console.error("openOrDownloadData:", ex);
// Release the `blobUrl`, since opening it failed, and fallback to
// downloading the PDF file.
URL.revokeObjectURL(blobUrl);
this.#openBlobUrls.delete(data);
}
}
this.downloadData(data, filename, contentType);
return false;
}
/**
* @param {Uint8Array} data
* @param {string} url
* @param {string} filename
*/
download(data, url, filename) {
const blobUrl = data
? URL.createObjectURL(new Blob([data], { type: "application/pdf" }))
: null;
this._triggerDownload(blobUrl, /* originalUrl = */ url, filename);
}
}
export { BaseDownloadManager };

View File

@ -13,10 +13,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { RenderableView, RenderingStates } from "./renderable_view.js";
import { RenderingCancelledException } from "pdfjs-lib"; import { RenderingCancelledException } from "pdfjs-lib";
import { RenderingStates } from "./ui_utils.js";
class BasePDFPageView extends RenderableView { class BasePDFPageView {
#loadingId = null; #loadingId = null;
#minDurationToUpdateCanvas = 0; #minDurationToUpdateCanvas = 0;
@ -48,8 +48,11 @@ class BasePDFPageView extends RenderableView {
renderingQueue = null; renderingQueue = null;
renderTask = null;
resume = null;
constructor(options) { constructor(options) {
super();
this.eventBus = options.eventBus; this.eventBus = options.eventBus;
this.id = options.id; this.id = options.id;
this.pageColors = options.pageColors || null; this.pageColors = options.pageColors || null;

View File

@ -17,7 +17,6 @@
import { AppOptions } from "./app_options.js"; import { AppOptions } from "./app_options.js";
import { BaseExternalServices } from "./external_services.js"; import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js"; import { BasePreferences } from "./preferences.js";
import { DownloadManager as GenericDownloadManager } from "./download_manager.js";
import { GenericL10n } from "./genericl10n.js"; import { GenericL10n } from "./genericl10n.js";
import { GenericScripting } from "./generic_scripting.js"; import { GenericScripting } from "./generic_scripting.js";
import { SignatureStorage } from "./generic_signature_storage.js"; import { SignatureStorage } from "./generic_signature_storage.js";
@ -311,25 +310,6 @@ function setReferer(url, callback) {
} }
} }
/**
* This "should" really extend the `BaseDownloadManager` class,
* however doing it this way instead reduces code duplication.
*/
class DownloadManager extends GenericDownloadManager {
_getOpenDataUrl(blobUrl, filename, dest = null) {
// In the Chrome extension, the URL is rewritten using the history API
// in viewer.js, so an absolute URL must be generated.
let url =
chrome.runtime.getURL("/content/web/viewer.html") +
"?file=" +
encodeURIComponent(blobUrl + "#" + filename);
if (dest) {
url += `#${escape(dest)}`;
}
return url;
}
}
// chrome.storage.sync is not supported in every Chromium-derivate. // chrome.storage.sync is not supported in every Chromium-derivate.
// Note: The background page takes care of migrating values from // Note: The background page takes care of migrating values from
// chrome.storage.local to chrome.storage.sync when needed. // chrome.storage.local to chrome.storage.sync when needed.
@ -457,4 +437,4 @@ class MLManager {
} }
} }
export { DownloadManager, ExternalServices, initCom, MLManager, Preferences }; export { ExternalServices, initCom, MLManager, Preferences };

View File

@ -13,8 +13,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { BaseDownloadManager } from "./base_download_manager.js"; /** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
import { createValidAbsoluteUrl } from "pdfjs-lib";
import { createValidAbsoluteUrl, isPdfFile } from "pdfjs-lib";
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) { if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) {
throw new Error( throw new Error(
@ -23,41 +24,101 @@ if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) {
); );
} }
class DownloadManager extends BaseDownloadManager { function download(blobUrl, filename) {
_triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) { const a = document.createElement("a");
if (!blobUrl && !isAttachment) { if (!a.click) {
// Fallback to downloading non-attachments by their URL. throw new Error('DownloadManager: "a.click()" is not supported.');
if (!createValidAbsoluteUrl(originalUrl, "http://example.com")) { }
throw new Error(`_triggerDownload - not a valid URL: ${originalUrl}`); a.href = blobUrl;
} a.target = "_parent";
blobUrl = originalUrl + "#pdfjs.action=download"; // Use a.download if available. This increases the likelihood that
} // the file is downloaded instead of opened by another PDF plugin.
if ("download" in a) {
a.download = filename;
}
// <a> must be in the document for recent Firefox versions,
// otherwise .click() is ignored.
(document.body || document.documentElement).append(a);
a.click();
a.remove();
}
const a = document.createElement("a"); /**
a.href = blobUrl; * @implements {IDownloadManager}
a.target = "_parent"; */
// Use a.download if available. This increases the likelihood that class DownloadManager {
// the file is downloaded instead of opened by another PDF plugin. #openBlobUrls = new WeakMap();
if ("download" in a) {
a.download = filename; downloadData(data, filename, contentType) {
} const blobUrl = URL.createObjectURL(
// <a> must be in the document for recent Firefox versions, new Blob([data], { type: contentType })
// otherwise .click() is ignored. );
(document.body || document.documentElement).append(a); download(blobUrl, filename);
a.click();
a.remove();
} }
_getOpenDataUrl(blobUrl, filename, dest = null) { /**
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) { * @returns {boolean} Indicating if the data was opened.
throw new Error("Opening data is not supported in `COMPONENTS` builds."); */
openOrDownloadData(data, filename, dest = null) {
const isPdfData = isPdfFile(filename);
const contentType = isPdfData ? "application/pdf" : "";
if (
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("COMPONENTS")) &&
isPdfData
) {
let blobUrl = this.#openBlobUrls.get(data);
if (!blobUrl) {
blobUrl = URL.createObjectURL(new Blob([data], { type: contentType }));
this.#openBlobUrls.set(data, blobUrl);
}
let viewerUrl;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
// The current URL is the viewer, let's use it and append the file.
viewerUrl = "?file=" + encodeURIComponent(blobUrl + "#" + filename);
} else if (PDFJSDev.test("CHROME")) {
// In the Chrome extension, the URL is rewritten using the history API
// in viewer.js, so an absolute URL must be generated.
viewerUrl =
// eslint-disable-next-line no-undef
chrome.runtime.getURL("/content/web/viewer.html") +
"?file=" +
encodeURIComponent(blobUrl + "#" + filename);
}
if (dest) {
viewerUrl += `#${escape(dest)}`;
}
try {
window.open(viewerUrl);
return true;
} catch (ex) {
console.error("openOrDownloadData:", ex);
// Release the `blobUrl`, since opening it failed, and fallback to
// downloading the PDF file.
URL.revokeObjectURL(blobUrl);
this.#openBlobUrls.delete(data);
}
} }
// The current URL is the viewer, let's use it and append the file.
let url = "?file=" + encodeURIComponent(blobUrl + "#" + filename); this.downloadData(data, filename, contentType);
if (dest) { return false;
url += `#${escape(dest)}`; }
download(data, url, filename) {
let blobUrl;
if (data) {
blobUrl = URL.createObjectURL(
new Blob([data], { type: "application/pdf" })
);
} else {
if (!createValidAbsoluteUrl(url, "http://example.com")) {
console.error(`download - not a valid URL: ${url}`);
return;
}
blobUrl = url + "#pdfjs.action=download";
} }
return url; download(blobUrl, filename);
} }
} }

View File

@ -13,6 +13,8 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./interfaces.js").IL10n} IL10n */
class BaseExternalServices { class BaseExternalServices {
constructor() { constructor() {
if ( if (
@ -34,7 +36,7 @@ class BaseExternalServices {
reportText(data) {} reportText(data) {}
/** /**
* @returns {Promise<L10n>} * @returns {Promise<IL10n>}
*/ */
async createL10n() { async createL10n() {
throw new Error("Not implemented: createL10n"); throw new Error("Not implemented: createL10n");

View File

@ -19,10 +19,7 @@ import {
RenderingCancelledException, RenderingCancelledException,
shadow, shadow,
} from "pdfjs-lib"; } from "pdfjs-lib";
import { import { getXfaHtmlForPrinting } from "./print_utils.js";
BasePrintServiceFactory,
getXfaHtmlForPrinting,
} from "./print_utils.js";
// Creates a placeholder with div and canvas with right size for the page. // Creates a placeholder with div and canvas with right size for the page.
function composePage( function composePage(
@ -197,7 +194,10 @@ class FirefoxPrintService {
} }
} }
class PDFPrintServiceFactory extends BasePrintServiceFactory { /**
* @implements {IPDFPrintServiceFactory}
*/
class PDFPrintServiceFactory {
static get supportsPrinting() { static get supportsPrinting() {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
return shadow(this, "supportsPrinting", "mozPrintCallback" in canvas); return shadow(this, "supportsPrinting", "mozPrintCallback" in canvas);

View File

@ -13,9 +13,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { MathClamp, PDFDataRangeTransport } from "pdfjs-lib"; import { isPdfFile, PDFDataRangeTransport } from "pdfjs-lib";
import { AppOptions } from "./app_options.js"; import { AppOptions } from "./app_options.js";
import { BaseDownloadManager } from "./base_download_manager.js";
import { BaseExternalServices } from "./external_services.js"; import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js"; import { BasePreferences } from "./preferences.js";
import { DEFAULT_SCALE_VALUE } from "./ui_utils.js"; import { DEFAULT_SCALE_VALUE } from "./ui_utils.js";
@ -81,25 +80,69 @@ class FirefoxCom {
} }
} }
class DownloadManager extends BaseDownloadManager { class DownloadManager {
_triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) { #openBlobUrls = new WeakMap();
downloadData(data, filename, contentType) {
const blobUrl = URL.createObjectURL(
new Blob([data], { type: contentType })
);
FirefoxCom.request("download", { FirefoxCom.request("download", {
blobUrl, blobUrl,
originalUrl, originalUrl: blobUrl,
filename, filename,
isAttachment, isAttachment: true,
}); });
} }
_getOpenDataUrl(blobUrl, filename, dest = null) { /**
// Let Firefox's content handler catch the URL and display the PDF. * @returns {boolean} Indicating if the data was opened.
// NOTE: This cannot use a query string for the filename, see */
// https://bugzilla.mozilla.org/show_bug.cgi?id=1632644#c5 openOrDownloadData(data, filename, dest = null) {
let url = blobUrl + "#filename=" + encodeURIComponent(filename); const isPdfData = isPdfFile(filename);
if (dest) { const contentType = isPdfData ? "application/pdf" : "";
url += `&filedest=${escape(dest)}`;
if (isPdfData) {
let blobUrl = this.#openBlobUrls.get(data);
if (!blobUrl) {
blobUrl = URL.createObjectURL(new Blob([data], { type: contentType }));
this.#openBlobUrls.set(data, blobUrl);
}
// Let Firefox's content handler catch the URL and display the PDF.
// NOTE: This cannot use a query string for the filename, see
// https://bugzilla.mozilla.org/show_bug.cgi?id=1632644#c5
let viewerUrl = blobUrl + "#filename=" + encodeURIComponent(filename);
if (dest) {
viewerUrl += `&filedest=${escape(dest)}`;
}
try {
window.open(viewerUrl);
return true;
} catch (ex) {
console.error("openOrDownloadData:", ex);
// Release the `blobUrl`, since opening it failed, and fallback to
// downloading the PDF file.
URL.revokeObjectURL(blobUrl);
this.#openBlobUrls.delete(data);
}
} }
return url;
this.downloadData(data, filename, contentType);
return false;
}
download(data, url, filename) {
const blobUrl = data
? URL.createObjectURL(new Blob([data], { type: "application/pdf" }))
: null;
FirefoxCom.request("download", {
blobUrl,
originalUrl: url,
filename,
});
} }
} }
@ -584,13 +627,7 @@ class ExternalServices extends BaseExternalServices {
pdfDataRangeTransport?.onDataProgressiveDone(); pdfDataRangeTransport?.onDataProgressiveDone();
break; break;
case "progress": case "progress":
const percent = MathClamp( viewerApp.progress(args.loaded / args.total);
Math.round((args.loaded / args.total) * 100),
0,
100
);
viewerApp.progress(percent);
break; break;
case "complete": case "complete":
if (!args.data) { if (!args.data) {

View File

@ -13,6 +13,8 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./interfaces").IL10n} IL10n */
import { FeatureTest, fetchData } from "pdfjs-lib"; import { FeatureTest, fetchData } from "pdfjs-lib";
import { FluentBundle, FluentResource } from "fluent-bundle"; import { FluentBundle, FluentResource } from "fluent-bundle";
import { DOMLocalization } from "fluent-dom"; import { DOMLocalization } from "fluent-dom";
@ -47,6 +49,9 @@ function createBundle(lang, text) {
return bundle; return bundle;
} }
/**
* @implements {IL10n}
*/
class GenericL10n extends L10n { class GenericL10n extends L10n {
constructor(lang) { constructor(lang) {
super({ lang }); super({ lang });

235
web/interfaces.js Normal file
View File

@ -0,0 +1,235 @@
/* Copyright 2018 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable getter-return */
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
/** @typedef {import("./ui_utils").RenderingStates} RenderingStates */
/**
* @interface
*/
class IPDFLinkService {
/**
* @type {number}
*/
get pagesCount() {}
/**
* @type {number}
*/
get page() {}
/**
* @param {number} value
*/
set page(value) {}
/**
* @type {number}
*/
get rotation() {}
/**
* @param {number} value
*/
set rotation(value) {}
/**
* @type {boolean}
*/
get isInPresentationMode() {}
/**
* @type {boolean}
*/
get externalLinkEnabled() {}
/**
* @param {boolean} value
*/
set externalLinkEnabled(value) {}
/**
* @param {string|Array} dest - The named, or explicit, PDF destination.
*/
async goToDestination(dest) {}
/**
* @param {number|string} val - The page number, or page label.
*/
goToPage(val) {}
/**
* Scrolls to a specific location in the PDF document.
* @param {number} pageNumber - The page number to scroll to.
* @param {number} x - The x-coordinate to scroll to in page coordinates.
* @param {number} y - The y-coordinate to scroll to in page coordinates.
*/
goToXY(pageNumber, x, y) {}
/**
* @param {HTMLAnchorElement} link
* @param {string} url
* @param {boolean} [newWindow]
*/
addLinkAttributes(link, url, newWindow = false) {}
/**
* @param dest - The PDF destination object.
* @returns {string} The hyperlink to the PDF object.
*/
getDestinationHash(dest) {}
/**
* @param hash - The PDF parameters/hash.
* @returns {string} The hyperlink to the PDF object.
*/
getAnchorUrl(hash) {}
/**
* @param {string} hash
*/
setHash(hash) {}
/**
* @param {string} action
*/
executeNamedAction(action) {}
/**
* @param {Object} action
*/
executeSetOCGState(action) {}
}
/**
* @interface
*/
class IRenderableView {
constructor() {
/** @type {function | null} */
this.resume = null;
}
/**
* @type {string} - Unique ID for rendering queue.
*/
get renderingId() {}
/**
* @type {RenderingStates}
*/
get renderingState() {}
/**
* @returns {Promise} Resolved on draw completion.
*/
async draw() {}
}
/**
* @interface
*/
class IDownloadManager {
/**
* @param {Uint8Array} data
* @param {string} filename
* @param {string} [contentType]
*/
downloadData(data, filename, contentType) {}
/**
* @param {Uint8Array} data
* @param {string} filename
* @param {string | null} [dest]
* @returns {boolean} Indicating if the data was opened.
*/
openOrDownloadData(data, filename, dest = null) {}
/**
* @param {Uint8Array} data
* @param {string} url
* @param {string} filename
*/
download(data, url, filename) {}
}
/**
* @interface
*/
class IL10n {
/**
* @returns {string} - The current locale.
*/
getLanguage() {}
/**
* @returns {string} - 'rtl' or 'ltr'.
*/
getDirection() {}
/**
* Translates text identified by the key and adds/formats data using the args
* property bag. If the key was not found, translation falls back to the
* fallback text.
* @param {Array | string} ids
* @param {Object | null} [args]
* @param {string} [fallback]
* @returns {Promise<string>}
*/
async get(ids, args = null, fallback) {}
/**
* Translates HTML element.
* @param {HTMLElement} element
* @returns {Promise<void>}
*/
async translate(element) {}
/**
* Pause the localization.
*/
pause() {}
/**
* Resume the localization.
*/
resume() {}
}
/**
* @interface
*/
class IPDFPrintServiceFactory {
static initGlobals() {}
static get supportsPrinting() {
return false;
}
static createPrintService() {
throw new Error("Not implemented: createPrintService");
}
}
export {
IDownloadManager,
IL10n,
IPDFLinkService,
IPDFPrintServiceFactory,
IRenderableView,
};

View File

@ -13,9 +13,12 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./interfaces").IL10n} IL10n */
/** /**
* NOTE: The L10n-implementations should use lowercase language-codes * NOTE: The L10n-implementations should use lowercase language-codes
* internally. * internally.
* @implements {IL10n}
*/ */
class L10n { class L10n {
#dir; #dir;

View File

@ -14,6 +14,7 @@
*/ */
/** @typedef {import("./event_utils.js").EventBus} EventBus */ /** @typedef {import("./event_utils.js").EventBus} EventBus */
/** @typedef {import("./interfaces.js").IL10n} IL10n */
/** @typedef {import("./overlay_manager.js").OverlayManager} OverlayManager */ /** @typedef {import("./overlay_manager.js").OverlayManager} OverlayManager */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */ /** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
@ -57,7 +58,7 @@ class PDFDocumentProperties {
* @param {PDFDocumentPropertiesOptions} options * @param {PDFDocumentPropertiesOptions} options
* @param {OverlayManager} overlayManager - Manager for the viewer overlays. * @param {OverlayManager} overlayManager - Manager for the viewer overlays.
* @param {EventBus} eventBus - The application event bus. * @param {EventBus} eventBus - The application event bus.
* @param {L10n} l10n - Localization service. * @param {IL10n} l10n - Localization service.
* @param {function} fileNameLookup - The function that is used to lookup * @param {function} fileNameLookup - The function that is used to lookup
* the document fileName. * the document fileName.
*/ */

View File

@ -15,6 +15,7 @@
/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */ /** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { binarySearchFirstItem, scrollIntoView } from "./ui_utils.js"; import { binarySearchFirstItem, scrollIntoView } from "./ui_utils.js";
import { getCharacterType, getNormalizeWithNFKC } from "./pdf_find_utils.js"; import { getCharacterType, getNormalizeWithNFKC } from "./pdf_find_utils.js";
@ -404,7 +405,7 @@ function getOriginalIndex(diffs, pos, len) {
/** /**
* @typedef {Object} PDFFindControllerOptions * @typedef {Object} PDFFindControllerOptions
* @property {PDFLinkService} linkService - The navigation/linking service. * @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {EventBus} eventBus - The application event bus. * @property {EventBus} eventBus - The application event bus.
* @property {boolean} [updateMatchesCountOnProgress] - True if the matches * @property {boolean} [updateMatchesCountOnProgress] - True if the matches
* count must be updated on progress or only when the last page is reached. * count must be updated on progress or only when the last page is reached.

View File

@ -14,6 +14,7 @@
*/ */
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { isValidRotation, parseQueryString } from "./ui_utils.js"; import { isValidRotation, parseQueryString } from "./ui_utils.js";
import { updateUrlHash } from "pdfjs-lib"; import { updateUrlHash } from "pdfjs-lib";
@ -28,7 +29,7 @@ const UPDATE_VIEWAREA_TIMEOUT = 1000; // milliseconds
/** /**
* @typedef {Object} PDFHistoryOptions * @typedef {Object} PDFHistoryOptions
* @property {PDFLinkService} linkService - The navigation/linking service. * @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {EventBus} eventBus - The application event bus. * @property {EventBus} eventBus - The application event bus.
*/ */

View File

@ -14,6 +14,7 @@
*/ */
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { isValidExplicitDest } from "pdfjs-lib"; import { isValidExplicitDest } from "pdfjs-lib";
import { parseQueryString } from "./ui_utils.js"; import { parseQueryString } from "./ui_utils.js";
@ -44,6 +45,7 @@ const LinkTarget = {
/** /**
* Performs navigation functions inside PDF, such as opening specified page, * Performs navigation functions inside PDF, such as opening specified page,
* or destination. * or destination.
* @implements {IPDFLinkService}
*/ */
class PDFLinkService { class PDFLinkService {
externalLinkEnabled = true; externalLinkEnabled = true;
@ -423,7 +425,7 @@ class PDFLinkService {
} }
// Support opening of PDF attachments in the Firefox PDF Viewer, // Support opening of PDF attachments in the Firefox PDF Viewer,
// which uses a couple of non-standard hash parameters; refer to // which uses a couple of non-standard hash parameters; refer to
// `DownloadManager._getOpenDataUrl` in the firefoxcom.js file. // `DownloadManager.openOrDownloadData` in the firefoxcom.js file.
if (!params.has("filename") || !params.has("filedest")) { if (!params.has("filename") || !params.has("filedest")) {
return; return;
} }
@ -515,6 +517,9 @@ class PDFLinkService {
} }
} }
/**
* @implements {IPDFLinkService}
*/
class SimpleLinkService extends PDFLinkService { class SimpleLinkService extends PDFLinkService {
setDocument(pdfDocument, baseUrl = null) {} setDocument(pdfDocument, baseUrl = null) {}
} }

View File

@ -16,6 +16,7 @@
/** @typedef {import("./event_utils.js").EventBus} EventBus */ /** @typedef {import("./event_utils.js").EventBus} EventBus */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./download_manager.js").DownloadManager} DownloadManager */ /** @typedef {import("./download_manager.js").DownloadManager} DownloadManager */
/** @typedef {import("./interfaces.js").IPDFLinkService} IPDFLinkService */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */ /** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
@ -26,7 +27,7 @@ import { SidebarView } from "./ui_utils.js";
* @typedef {Object} PDFOutlineViewerOptions * @typedef {Object} PDFOutlineViewerOptions
* @property {HTMLDivElement} container - The viewer element. * @property {HTMLDivElement} container - The viewer element.
* @property {EventBus} eventBus - The application event bus. * @property {EventBus} eventBus - The application event bus.
* @property {PDFLinkService} linkService - The navigation/linking service. * @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {DownloadManager} downloadManager - The download manager. * @property {DownloadManager} downloadManager - The download manager.
*/ */

View File

@ -15,8 +15,13 @@
import { BasePDFPageView } from "./base_pdf_page_view.js"; import { BasePDFPageView } from "./base_pdf_page_view.js";
import { OutputScale } from "pdfjs-lib"; import { OutputScale } from "pdfjs-lib";
import { RenderingStates } from "./renderable_view.js"; import { RenderingStates } from "./ui_utils.js";
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
/**
* @implements {IRenderableView}
*/
class PDFPageDetailView extends BasePDFPageView { class PDFPageDetailView extends BasePDFPageView {
#detailArea = null; #detailArea = null;
@ -49,9 +54,9 @@ class PDFPageDetailView extends BasePDFPageView {
return super.renderingState; return super.renderingState;
} }
set renderingState(state) { set renderingState(value) {
this.renderingCancelled = false; this.renderingCancelled = false;
super.renderingState = state; super.renderingState = value;
} }
reset({ keepCanvas = false } = {}) { reset({ keepCanvas = false } = {}) {

View File

@ -18,6 +18,8 @@
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */ /** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IL10n} IL10n */
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */ /** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
/** @typedef {import("./comment_manager.js").CommentManager} CommentManager */ /** @typedef {import("./comment_manager.js").CommentManager} CommentManager */
@ -35,6 +37,7 @@ import {
calcRound, calcRound,
DEFAULT_SCALE, DEFAULT_SCALE,
floorToDivide, floorToDivide,
RenderingStates,
TextLayerMode, TextLayerMode,
} from "./ui_utils.js"; } from "./ui_utils.js";
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js"; import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
@ -45,7 +48,6 @@ import { BasePDFPageView } from "./base_pdf_page_view.js";
import { DrawLayerBuilder } from "./draw_layer_builder.js"; import { DrawLayerBuilder } from "./draw_layer_builder.js";
import { GenericL10n } from "web-null_l10n"; import { GenericL10n } from "web-null_l10n";
import { PDFPageDetailView } from "./pdf_page_detail_view.js"; import { PDFPageDetailView } from "./pdf_page_detail_view.js";
import { RenderingStates } from "./renderable_view.js";
import { SimpleLinkService } from "./pdf_link_service.js"; import { SimpleLinkService } from "./pdf_link_service.js";
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js"; import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
import { TextAccessibilityManager } from "./text_accessibility.js"; import { TextAccessibilityManager } from "./text_accessibility.js";
@ -96,7 +98,7 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
* @property {Object} [pageColors] - Overwrites background and foreground colors * @property {Object} [pageColors] - Overwrites background and foreground colors
* with user defined ones in order to improve readability in high contrast * with user defined ones in order to improve readability in high contrast
* mode. * mode.
* @property {L10n} [l10n] - Localization service. * @property {IL10n} [l10n] - Localization service.
* @property {Object} [layerProperties] - The object that is used to lookup * @property {Object} [layerProperties] - The object that is used to lookup
* the necessary layer-properties. * the necessary layer-properties.
* @property {boolean} [enableAutoLinking] - Enable creation of hyperlinks from * @property {boolean} [enableAutoLinking] - Enable creation of hyperlinks from
@ -128,6 +130,9 @@ const LAYERS_ORDER = new Map([
["xfaLayer", 3], ["xfaLayer", 3],
]); ]);
/**
* @implements {IRenderableView}
*/
class PDFPageView extends BasePDFPageView { class PDFPageView extends BasePDFPageView {
#annotationMode = AnnotationMode.ENABLE_FORMS; #annotationMode = AnnotationMode.ENABLE_FORMS;

View File

@ -13,16 +13,16 @@
* limitations under the License. * limitations under the License.
*/ */
// eslint-disable-next-line max-len
/** @typedef {import("./interfaces.js").IPDFPrintServiceFactory} IPDFPrintServiceFactory */
import { import {
AnnotationMode, AnnotationMode,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
shadow, shadow,
} from "pdfjs-lib"; } from "pdfjs-lib";
import { import { getXfaHtmlForPrinting } from "./print_utils.js";
BasePrintServiceFactory,
getXfaHtmlForPrinting,
} from "./print_utils.js";
let activeService = null; let activeService = null;
let dialog = null; let dialog = null;
@ -371,7 +371,10 @@ function ensureOverlay() {
return overlayPromise; return overlayPromise;
} }
class PDFPrintServiceFactory extends BasePrintServiceFactory { /**
* @implements {IPDFPrintServiceFactory}
*/
class PDFPrintServiceFactory {
static initGlobals(app) { static initGlobals(app) {
viewerApp = app; viewerApp = app;
} }

View File

@ -13,12 +13,13 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
/** @typedef {import("./pdf_viewer").PDFViewer} PDFViewer */ /** @typedef {import("./pdf_viewer").PDFViewer} PDFViewer */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./pdf_thumbnail_viewer").PDFThumbnailViewer} PDFThumbnailViewer */ /** @typedef {import("./pdf_thumbnail_viewer").PDFThumbnailViewer} PDFThumbnailViewer */
import { RenderingCancelledException } from "pdfjs-lib"; import { RenderingCancelledException } from "pdfjs-lib";
import { RenderingStates } from "./renderable_view.js"; import { RenderingStates } from "./ui_utils.js";
const CLEANUP_TIMEOUT = 30000; const CLEANUP_TIMEOUT = 30000;
@ -58,7 +59,7 @@ class PDFRenderingQueue {
} }
/** /**
* @param {RenderableView} view * @param {IRenderableView} view
* @returns {boolean} * @returns {boolean}
*/ */
isHighestPriority(view) { isHighestPriority(view) {
@ -182,7 +183,7 @@ class PDFRenderingQueue {
} }
/** /**
* @param {RenderableView} view * @param {IRenderableView} view
* @returns {boolean} * @returns {boolean}
*/ */
isViewFinished(view) { isViewFinished(view) {
@ -194,7 +195,7 @@ class PDFRenderingQueue {
* based on the views state. If the view is already rendered it will return * based on the views state. If the view is already rendered it will return
* `false`. * `false`.
* *
* @param {RenderableView} view * @param {IRenderableView} view
*/ */
renderView(view) { renderView(view) {
switch (view.renderingState) { switch (view.renderingState) {

View File

@ -15,8 +15,7 @@
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
import { apiPageLayoutToViewerModes } from "./ui_utils.js"; import { apiPageLayoutToViewerModes, RenderingStates } from "./ui_utils.js";
import { RenderingStates } from "./renderable_view.js";
import { shadow } from "pdfjs-lib"; import { shadow } from "pdfjs-lib";
/** /**

View File

@ -18,6 +18,8 @@
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */ /** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */ /** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
@ -26,8 +28,8 @@ import {
OutputScale, OutputScale,
RenderingCancelledException, RenderingCancelledException,
} from "pdfjs-lib"; } from "pdfjs-lib";
import { RenderableView, RenderingStates } from "./renderable_view.js";
import { AppOptions } from "./app_options.js"; import { AppOptions } from "./app_options.js";
import { RenderingStates } from "./ui_utils.js";
const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below. const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below.
const MAX_NUM_SCALING_STEPS = 3; const MAX_NUM_SCALING_STEPS = 3;
@ -42,7 +44,7 @@ const THUMBNAIL_WIDTH = 126; // px
* @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] - * @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] -
* A promise that is resolved with an {@link OptionalContentConfig} instance. * A promise that is resolved with an {@link OptionalContentConfig} instance.
* The default value is `null`. * The default value is `null`.
* @property {PDFLinkService} linkService - The navigation/linking service. * @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in * @property {number} [maxCanvasPixels] - The maximum supported canvas size in
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for * total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
@ -77,9 +79,10 @@ class TempImageFactory {
} }
} }
class PDFThumbnailView extends RenderableView { /**
#renderingState = RenderingStates.INITIAL; * @implements {IRenderableView}
*/
class PDFThumbnailView {
/** /**
* @param {PDFThumbnailViewOptions} options * @param {PDFThumbnailViewOptions} options
*/ */
@ -96,7 +99,6 @@ class PDFThumbnailView extends RenderableView {
pageColors, pageColors,
enableSplitMerge = false, enableSplitMerge = false,
}) { }) {
super();
this.id = id; this.id = id;
this.renderingId = `thumbnail${id}`; this.renderingId = `thumbnail${id}`;
this.pageLabel = null; this.pageLabel = null;
@ -114,6 +116,9 @@ class PDFThumbnailView extends RenderableView {
this.linkService = linkService; this.linkService = linkService;
this.renderingQueue = renderingQueue; this.renderingQueue = renderingQueue;
this.renderTask = null;
this.renderingState = RenderingStates.INITIAL;
this.resume = null;
this.placeholder = null; this.placeholder = null;
const imageContainer = (this.div = document.createElement("div")); const imageContainer = (this.div = document.createElement("div"));
@ -158,14 +163,6 @@ class PDFThumbnailView extends RenderableView {
this.image.style.height = `${canvasHeight}px`; this.image.style.height = `${canvasHeight}px`;
} }
get renderingState() {
return this.#renderingState;
}
set renderingState(state) {
this.#renderingState = state;
}
setPdfPage(pdfPage) { setPdfPage(pdfPage) {
this.pdfPage = pdfPage; this.pdfPage = pdfPage;
this.pdfPageRotate = pdfPage.rotate; this.pdfPageRotate = pdfPage.rotate;

View File

@ -16,6 +16,7 @@
/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */ /** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */ /** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */ /** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
@ -23,12 +24,12 @@ import {
binarySearchFirstItem, binarySearchFirstItem,
getVisibleElements, getVisibleElements,
isValidRotation, isValidRotation,
RenderingStates,
watchScroll, watchScroll,
} from "./ui_utils.js"; } from "./ui_utils.js";
import { MathClamp, noContextMenu, PagesMapper, stopEvent } from "pdfjs-lib"; import { MathClamp, noContextMenu, PagesMapper, stopEvent } from "pdfjs-lib";
import { Menu } from "./menu.js"; import { Menu } from "./menu.js";
import { PDFThumbnailView } from "./pdf_thumbnail_view.js"; import { PDFThumbnailView } from "./pdf_thumbnail_view.js";
import { RenderingStates } from "./renderable_view.js";
const SCROLL_OPTIONS = { const SCROLL_OPTIONS = {
behavior: "instant", behavior: "instant",
@ -50,7 +51,7 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15;
* @property {HTMLDivElement} container - The container for the thumbnail * @property {HTMLDivElement} container - The container for the thumbnail
* elements. * elements.
* @property {EventBus} eventBus - The application event bus. * @property {EventBus} eventBus - The application event bus.
* @property {PDFLinkService} linkService - The navigation/linking service. * @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in * @property {number} [maxCanvasPixels] - The maximum supported canvas size in
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for * total pixels, i.e. width * height. Use `-1` for no limit, or `0` for

View File

@ -22,6 +22,7 @@ import {
import { import {
parseQueryString, parseQueryString,
ProgressBar, ProgressBar,
RenderingStates,
ScrollMode, ScrollMode,
SpreadMode, SpreadMode,
} from "./ui_utils.js"; } from "./ui_utils.js";
@ -34,7 +35,6 @@ import { PDFPageView } from "./pdf_page_view.js";
import { PDFScriptingManager } from "./pdf_scripting_manager.component.js"; import { PDFScriptingManager } from "./pdf_scripting_manager.component.js";
import { PDFSinglePageViewer } from "./pdf_single_page_viewer.js"; import { PDFSinglePageViewer } from "./pdf_single_page_viewer.js";
import { PDFViewer } from "./pdf_viewer.js"; import { PDFViewer } from "./pdf_viewer.js";
import { RenderingStates } from "./renderable_view.js";
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js"; import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
import { TextLayerBuilder } from "./text_layer_builder.js"; import { TextLayerBuilder } from "./text_layer_builder.js";
import { XfaLayerBuilder } from "./xfa_layer_builder.js"; import { XfaLayerBuilder } from "./xfa_layer_builder.js";

View File

@ -20,6 +20,9 @@
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */ /** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
/** @typedef {import("./interfaces").IL10n} IL10n */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./pdf_find_controller").PDFFindController} PDFFindController */ /** @typedef {import("./pdf_find_controller").PDFFindController} PDFFindController */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
@ -52,6 +55,7 @@ import {
MIN_SCALE, MIN_SCALE,
PresentationModeState, PresentationModeState,
removeNullCharacters, removeNullCharacters,
RenderingStates,
SCROLLBAR_PADDING, SCROLLBAR_PADDING,
scrollIntoView, scrollIntoView,
ScrollMode, ScrollMode,
@ -64,7 +68,6 @@ import {
import { GenericL10n } from "web-null_l10n"; import { GenericL10n } from "web-null_l10n";
import { PDFPageView } from "./pdf_page_view.js"; import { PDFPageView } from "./pdf_page_view.js";
import { PDFRenderingQueue } from "./pdf_rendering_queue.js"; import { PDFRenderingQueue } from "./pdf_rendering_queue.js";
import { RenderingStates } from "./renderable_view.js";
import { SimpleLinkService } from "./pdf_link_service.js"; import { SimpleLinkService } from "./pdf_link_service.js";
const DEFAULT_CACHE_SIZE = 10; const DEFAULT_CACHE_SIZE = 10;
@ -87,8 +90,8 @@ function isValidAnnotationEditorMode(mode) {
* @property {HTMLDivElement} container - The container for the viewer element. * @property {HTMLDivElement} container - The container for the viewer element.
* @property {HTMLDivElement} [viewer] - The viewer element. * @property {HTMLDivElement} [viewer] - The viewer element.
* @property {EventBus} eventBus - The application event bus. * @property {EventBus} eventBus - The application event bus.
* @property {PDFLinkService} [linkService] - The navigation/linking service. * @property {IPDFLinkService} [linkService] - The navigation/linking service.
* @property {BaseDownloadManager} [downloadManager] - The download manager * @property {IDownloadManager} [downloadManager] - The download manager
* component. * component.
* @property {PDFFindController} [findController] - The find controller * @property {PDFFindController} [findController] - The find controller
* component. * component.
@ -132,7 +135,7 @@ function isValidAnnotationEditorMode(mode) {
* rendering will keep track of which areas of the page each PDF operation * rendering will keep track of which areas of the page each PDF operation
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is * affects. Then, when rendering a partial page (if `enableDetailCanvas` is
* enabled), it will only run through the operations that affect that portion. * enabled), it will only run through the operations that affect that portion.
* @property {L10n} [l10n] - Localization service. * @property {IL10n} [l10n] - Localization service.
* @property {boolean} [enablePermissions] - Enables PDF document permissions, * @property {boolean} [enablePermissions] - Enables PDF document permissions,
* when they exist. The default value is `false`. * when they exist. The default value is `false`.
* @property {Object} [pageColors] - Overwrites background and foreground colors * @property {Object} [pageColors] - Overwrites background and foreground colors

View File

@ -17,24 +17,6 @@ import { getXfaPageViewport, PixelsPerInch } from "pdfjs-lib";
import { SimpleLinkService } from "./pdf_link_service.js"; import { SimpleLinkService } from "./pdf_link_service.js";
import { XfaLayerBuilder } from "./xfa_layer_builder.js"; import { XfaLayerBuilder } from "./xfa_layer_builder.js";
class BasePrintServiceFactory {
constructor() {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
throw new Error("Cannot initialize BasePrintServiceFactory.");
}
}
static initGlobals(app) {}
static get supportsPrinting() {
throw new Error("Not implemented: supportsPrinting");
}
static createPrintService(params) {
throw new Error("Not implemented: createPrintService");
}
}
function getXfaHtmlForPrinting(printContainer, pdfDocument) { function getXfaHtmlForPrinting(printContainer, pdfDocument) {
const xfaHtml = pdfDocument.allXfaHtml; const xfaHtml = pdfDocument.allXfaHtml;
const linkService = new SimpleLinkService(); const linkService = new SimpleLinkService();
@ -58,4 +40,4 @@ function getXfaHtmlForPrinting(printContainer, pdfDocument) {
} }
} }
export { BasePrintServiceFactory, getXfaHtmlForPrinting }; export { getXfaHtmlForPrinting };

View File

@ -1,71 +0,0 @@
/* Copyright 2018 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const RenderingStates = {
INITIAL: 0,
RUNNING: 1,
PAUSED: 2,
FINISHED: 3,
};
class RenderableView {
/**
* Unique ID for rendering queue.
* @type {string}
*/
renderingId = "";
/**
* @type {RenderTask | null}
*/
renderTask = null;
/**
* @type {function | null}
*/
resume = null;
constructor() {
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
this.constructor === RenderableView
) {
throw new Error("Cannot initialize RenderableView.");
}
}
/**
* @type {RenderingStates}
*/
get renderingState() {
throw new Error("Abstract getter `renderingState` accessed");
}
/**
* @param {RenderingStates}
*/
set renderingState(state) {
throw new Error("Abstract setter `renderingState` accessed");
}
/**
* @returns {Promise} Resolved on draw completion.
*/
async draw() {
throw new Error("Not implemented: draw");
}
}
export { RenderableView, RenderingStates };

View File

@ -25,6 +25,13 @@ const MAX_AUTO_SCALE = 1.25;
const SCROLLBAR_PADDING = 40; const SCROLLBAR_PADDING = 40;
const VERTICAL_PADDING = 5; const VERTICAL_PADDING = 5;
const RenderingStates = {
INITIAL: 0,
RUNNING: 1,
PAUSED: 2,
FINISHED: 3,
};
const PresentationModeState = { const PresentationModeState = {
UNKNOWN: 0, UNKNOWN: 0,
NORMAL: 1, NORMAL: 1,
@ -702,7 +709,7 @@ class ProgressBar {
} }
set percent(val) { set percent(val) {
this.#percent = val; this.#percent = MathClamp(val, 0, 100);
if (isNaN(val)) { if (isNaN(val)) {
this.#classList.add("indeterminate"); this.#classList.add("indeterminate");
@ -907,6 +914,7 @@ export {
PresentationModeState, PresentationModeState,
ProgressBar, ProgressBar,
removeNullCharacters, removeNullCharacters,
RenderingStates,
SCROLLBAR_PADDING, SCROLLBAR_PADDING,
scrollIntoView, scrollIntoView,
ScrollMode, ScrollMode,

View File

@ -13,11 +13,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { ScrollMode, SpreadMode } from "./ui_utils.js"; import { RenderingStates, ScrollMode, SpreadMode } from "./ui_utils.js";
import { AppOptions } from "./app_options.js"; import { AppOptions } from "./app_options.js";
import { LinkTarget } from "./pdf_link_service.js"; import { LinkTarget } from "./pdf_link_service.js";
import { PDFViewerApplication } from "./app.js"; import { PDFViewerApplication } from "./app.js";
import { RenderingStates } from "./renderable_view.js";
const AppConstants = const AppConstants =
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC") typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")

View File

@ -13,11 +13,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { ScrollMode, SpreadMode } from "./ui_utils.js"; import { RenderingStates, ScrollMode, SpreadMode } from "./ui_utils.js";
import { AppOptions } from "./app_options.js"; import { AppOptions } from "./app_options.js";
import { LinkTarget } from "./pdf_link_service.js"; import { LinkTarget } from "./pdf_link_service.js";
import { PDFViewerApplication } from "./app.js"; import { PDFViewerApplication } from "./app.js";
import { RenderingStates } from "./renderable_view.js";
const AppConstants = const AppConstants =
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC") typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")

View File

@ -14,6 +14,7 @@
*/ */
/** @typedef {import("./event_utils.js").EventBus} EventBus */ /** @typedef {import("./event_utils.js").EventBus} EventBus */
/** @typedef {import("./interfaces.js").IL10n} IL10n */
import { import {
docStyle, docStyle,
@ -33,7 +34,7 @@ const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
* @typedef {Object} PDFSidebarOptions * @typedef {Object} PDFSidebarOptions
* @property {PDFSidebarElements} elements - The DOM elements. * @property {PDFSidebarElements} elements - The DOM elements.
* @property {EventBus} eventBus - The application event bus. * @property {EventBus} eventBus - The application event bus.
* @property {L10n} l10n - The localization service. * @property {IL10n} l10n - The localization service.
*/ */
/** /**

View File

@ -18,6 +18,7 @@
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */ /** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */ /** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { XfaLayer } from "pdfjs-lib"; import { XfaLayer } from "pdfjs-lib";
@ -25,7 +26,7 @@ import { XfaLayer } from "pdfjs-lib";
* @typedef {Object} XfaLayerBuilderOptions * @typedef {Object} XfaLayerBuilderOptions
* @property {PDFPageProxy} pdfPage * @property {PDFPageProxy} pdfPage
* @property {AnnotationStorage} [annotationStorage] * @property {AnnotationStorage} [annotationStorage]
* @property {PDFLinkService} linkService * @property {IPDFLinkService} linkService
* @property {Object} [xfaHtml] * @property {Object} [xfaHtml]
*/ */