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
- name: Run types tests
run: npx gulp types
run: npx gulp typestest

View File

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

View File

@ -46,13 +46,19 @@ const PDFViewerApplication = {
* @returns {Promise} - Returns the promise, which is resolved when document
* is opened.
*/
async open(params) {
open(params) {
if (this.pdfLoadingTask) {
// We need to destroy already opened document.
await this.close();
// We need to destroy already opened document
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);
// Loading document.
@ -64,22 +70,24 @@ const PDFViewerApplication = {
});
this.pdfLoadingTask = loadingTask;
loadingTask.onProgress = evt => this.progress(evt.percent);
loadingTask.onProgress = function (progressData) {
self.progress(progressData.loaded / progressData.total);
};
return loadingTask.promise.then(
pdfDocument => {
function (pdfDocument) {
// Document loaded, specifying document for the viewer.
this.pdfDocument = pdfDocument;
this.pdfViewer.setDocument(pdfDocument);
this.pdfLinkService.setDocument(pdfDocument);
this.pdfHistory.initialize({
self.pdfDocument = pdfDocument;
self.pdfViewer.setDocument(pdfDocument);
self.pdfLinkService.setDocument(pdfDocument);
self.pdfHistory.initialize({
fingerprint: pdfDocument.fingerprints[0],
});
this.loadingBar.hide();
this.setTitleUsingMetadata(pdfDocument);
self.loadingBar.hide();
self.setTitleUsingMetadata(pdfDocument);
},
reason => {
function (reason) {
let key = "pdfjs-loading-error";
if (reason instanceof pdfjsLib.InvalidPDFException) {
key = "pdfjs-invalid-file-error";
@ -88,10 +96,10 @@ const PDFViewerApplication = {
? "pdfjs-missing-file-error"
: "pdfjs-unexpected-response-error";
}
this.l10n.get(key).then(msg => {
this.error(msg, { message: reason.message });
self.l10n.get(key).then(msg => {
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
* destruction is completed.
*/
async close() {
close() {
if (!this.pdfLoadingTask) {
return;
return Promise.resolve();
}
const promise = this.pdfLoadingTask.destroy();
@ -120,7 +128,7 @@ const PDFViewerApplication = {
}
}
await promise;
return promise;
},
get loadingBar() {
@ -144,36 +152,48 @@ const PDFViewerApplication = {
this.setTitle(title);
},
async setTitleUsingMetadata(pdfDocument) {
const { info, metadata } = await pdfDocument.getMetadata();
this.documentInfo = info;
this.metadata = metadata;
setTitleUsingMetadata(pdfDocument) {
const self = this;
pdfDocument.getMetadata().then(function (data) {
const info = data.info,
metadata = data.metadata;
self.documentInfo = info;
self.metadata = metadata;
// Provides some basic debug information
console.log(
`PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` +
`${(metadata?.get("pdf:producer") || info.Producer || "-").trim()} / ` +
`${(metadata?.get("xmp:creatortool") || info.Creator || "-").trim()}` +
`] (PDF.js: ${pdfjsLib.version || "?"} [${pdfjsLib.build || "?"}])`
);
// Provides some basic debug information
console.log(
"PDF " +
pdfDocument.fingerprints[0] +
" [" +
info.PDFFormatVersion +
" " +
(info.Producer || "-").trim() +
" / " +
(info.Creator || "-").trim() +
"]" +
" (PDF.js: " +
(pdfjsLib.version || "-") +
")"
);
let pdfTitle;
if (metadata && metadata.has("dc:title")) {
const title = metadata.get("dc:title");
// Ghostscript sometimes returns 'Untitled', so prevent setting the
// title to 'Untitled.
if (title !== "Untitled") {
pdfTitle = title;
let pdfTitle;
if (metadata && metadata.has("dc:title")) {
const title = metadata.get("dc:title");
// Ghostscript sometimes returns 'Untitled', so prevent setting the
// title to 'Untitled.
if (title !== "Untitled") {
pdfTitle = title;
}
}
}
if (!pdfTitle && info && info.Title) {
pdfTitle = info.Title;
}
if (!pdfTitle && info && info.Title) {
pdfTitle = info.Title;
}
if (pdfTitle) {
this.setTitle(pdfTitle + " - " + document.title);
}
if (pdfTitle) {
self.setTitle(pdfTitle + " - " + document.title);
}
});
},
setTitle: function pdfViewSetTitle(title) {
@ -203,7 +223,8 @@ const PDFViewerApplication = {
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.
if (percent > this.loadingBar.percent || isNaN(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 { buildPrefsSchema } from "./external/chromium/prefs.mjs";
import crypto from "crypto";
import { finished } from "stream/promises";
import fs from "fs";
import gulp from "gulp";
import hljs from "highlight.js";
@ -232,7 +231,7 @@ function createWebpackAlias(defines) {
libraryAlias["display-fetch_stream"] = "src/display/fetch_stream.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-null_l10n"] = "web/l10n.js";
viewerAlias["web-preferences"] = "web/chromecom.js";
@ -814,28 +813,6 @@ gulp.task("default", function (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) {
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/qcms/*.js", { base: "qcms/", 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);

View File

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

View File

@ -110,46 +110,6 @@ class DecodeStream extends BaseStream {
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() {
this.pos = 0;
}

View File

@ -163,26 +163,57 @@ class FlateStream extends DecodeStream {
}
async asyncGetBytes() {
const { decompressed, compressed } =
await this.asyncGetBytesFromDecompressionStream("deflate");
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.stream.reset();
const bytes = this.stream.isAsync
? await this.stream.asyncGetBytes()
: this.stream.getBytes();
this.#isAsync = false;
this.stream = new Stream(
compressed,
2 /* = header size (see ctor) */,
compressed.length,
this.stream.dict
);
this.reset();
return null;
try {
const { readable, writable } = new DecompressionStream("deflate");
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 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() {

View File

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

View File

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

View File

@ -13,35 +13,77 @@
* limitations under the License.
*/
import {
BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader,
} from "../shared/base_pdf_stream.js";
import { assert } from "../shared/util.js";
class PDFWorkerStream extends BasePDFStream {
constructor(source) {
super(source, PDFWorkerStreamReader, PDFWorkerStreamRangeReader);
/** @implements {IPDFStream} */
class PDFWorkerStream {
constructor(msgHandler) {
this._msgHandler = msgHandler;
this._contentLength = null;
this._fullRequestReader = null;
this._rangeRequestReaders = [];
}
getFullReader() {
assert(
!this._fullRequestReader,
"PDFWorkerStream.getFullReader can only be called once."
);
this._fullRequestReader = new PDFWorkerStreamReader(this._msgHandler);
return this._fullRequestReader;
}
getRangeReader(begin, end) {
const reader = new PDFWorkerStreamRangeReader(begin, end, this._msgHandler);
this._rangeRequestReaders.push(reader);
return reader;
}
cancelAllRequests(reason) {
this._fullRequestReader?.cancel(reason);
for (const reader of this._rangeRequestReaders.slice(0)) {
reader.cancel(reason);
}
}
}
class PDFWorkerStreamReader extends BasePDFStreamReader {
_reader = null;
/** @implements {IPDFStreamReader} */
class PDFWorkerStreamReader {
constructor(msgHandler) {
this._msgHandler = msgHandler;
this.onProgress = null;
constructor(stream) {
super(stream);
const { msgHandler } = stream._source;
this._contentLength = null;
this._isRangeSupported = false;
this._isStreamingSupported = false;
const readableStream = msgHandler.sendWithStream("GetReader");
const readableStream = this._msgHandler.sendWithStream("GetReader");
this._reader = readableStream.getReader();
msgHandler.sendWithPromise("ReaderHeadersReady").then(data => {
this._contentLength = data.contentLength;
this._isStreamingSupported = data.isStreamingSupported;
this._isRangeSupported = data.isRangeSupported;
this._headersReady = this._msgHandler
.sendWithPromise("ReaderHeadersReady")
.then(data => {
this._isStreamingSupported = data.isStreamingSupported;
this._isRangeSupported = data.isRangeSupported;
this._contentLength = data.contentLength;
});
}
this._headersCapability.resolve();
}, this._headersCapability.reject);
get headersReady() {
return this._headersReady;
}
get contentLength() {
return this._contentLength;
}
get isStreamingSupported() {
return this._isStreamingSupported;
}
get isRangeSupported() {
return this._isRangeSupported;
}
async read() {
@ -59,20 +101,23 @@ class PDFWorkerStreamReader extends BasePDFStreamReader {
}
}
class PDFWorkerStreamRangeReader extends BasePDFStreamRangeReader {
_reader = null;
/** @implements {IPDFStreamRangeReader} */
class PDFWorkerStreamRangeReader {
constructor(begin, end, msgHandler) {
this._msgHandler = msgHandler;
this.onProgress = null;
constructor(stream, begin, end) {
super(stream, begin, end);
const { msgHandler } = stream._source;
const readableStream = msgHandler.sendWithStream("GetRangeReader", {
const readableStream = this._msgHandler.sendWithStream("GetRangeReader", {
begin,
end,
});
this._reader = readableStream.getReader();
}
get isStreamingSupported() {
return false;
}
async read() {
const { value, done } = await this._reader.read();
if (done) {

View File

@ -18,6 +18,9 @@
// eslint-disable-next-line max-len
/** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
// 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 */
// eslint-disable-next-line max-len
/** @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
* @property {Object} data
* @property {HTMLDivElement} layer
* @property {PDFLinkService} linkService
* @property {BaseDownloadManager} [downloadManager]
* @property {IPDFLinkService} linkService
* @property {IDownloadManager} [downloadManager]
* @property {AnnotationStorage} [annotationStorage]
* @property {string} [imageResourcesPath] - Path for image resources, mainly
* for annotation icons. Include trailing slash.
@ -3733,8 +3736,8 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
* @property {HTMLDivElement} div
* @property {Array} annotations
* @property {PDFPageProxy} page
* @property {PDFLinkService} linkService
* @property {BaseDownloadManager} [downloadManager]
* @property {IPDFLinkService} linkService
* @property {IDownloadManager} [downloadManager]
* @property {AnnotationStorage} [annotationStorage]
* @property {string} [imageResourcesPath] - Path for image resources, mainly
* for annotation icons. Include trailing slash.
@ -4015,6 +4018,8 @@ class AnnotationLayer {
* Add link annotations to the annotation layer.
*
* @param {Array<Object>} annotations
* @param {IPDFLinkService} linkService
* @memberof AnnotationLayer
*/
async addLinkAnnotations(annotations) {
const elementParams = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

@ -13929,12 +13929,5 @@
"md5": "e515a9abb11ab74332e57e371bfae61e",
"rounds": 1,
"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);
};
const [pdfDoc, progress] = await Promise.all([
loadingTask.promise,
const data = await Promise.all([
progressReportedCapability.promise,
loadingTask.promise,
]);
expect(pdfDoc instanceof PDFDocumentProxy).toEqual(true);
expect(pdfDoc.loadingTask).toBe(loadingTask);
expect(progress.loaded).toBeGreaterThanOrEqual(0);
expect(progress.total).toEqual(basicApiFileLength);
expect(progress.percent).toBeGreaterThanOrEqual(0);
expect(progress.percent).toBeLessThanOrEqual(100);
expect(data[0].loaded / data[0].total >= 0).toEqual(true);
expect(data[1] instanceof PDFDocumentProxy).toEqual(true);
expect(loadingTask).toEqual(data[1].loadingTask);
await loadingTask.destroy();
});
@ -222,17 +218,12 @@ describe("api", function () {
progressReportedCapability.resolve(data);
};
const [pdfDoc, progress] = await Promise.all([
const data = await Promise.all([
loadingTask.promise,
progressReportedCapability.promise,
]);
expect(pdfDoc instanceof PDFDocumentProxy).toEqual(true);
expect(pdfDoc.loadingTask).toBe(loadingTask);
expect(progress.loaded).toEqual(basicApiFileLength);
expect(progress.total).toEqual(basicApiFileLength);
expect(progress.percent).toEqual(100);
expect(data[0] instanceof PDFDocumentProxy).toEqual(true);
expect(data[1].loaded / data[1].total).toEqual(1);
// Check that the TypedArray was transferred.
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 { 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
// - PDFFetchStream by fetch_stream_spec.js
async function testCrossOriginRedirects({

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import {
import {
parseQueryString,
ProgressBar,
RenderingStates,
ScrollMode,
SpreadMode,
} 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 { PDFSinglePageViewer } from "../../web/pdf_single_page_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 { TextLayerBuilder } from "../../web/text_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 */
// eslint-disable-next-line max-len
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
/** @typedef {import("./interfaces").IL10n} IL10n */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/annotation_layer.js").AnnotationLayer} AnnotationLayer */
// eslint-disable-next-line max-len
@ -32,7 +33,7 @@ import { GenericL10n } from "web-null_l10n";
* @typedef {Object} AnnotationEditorLayerBuilderOptions
* @property {AnnotationEditorUIManager} [uiManager]
* @property {PDFPageProxy} pdfPage
* @property {L10n} [l10n]
* @property {IL10n} [l10n]
* @property {StructTreeLayerBuilder} [structTreeLayer]
* @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationLayer} [annotationLayer]

View File

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

View File

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

View File

@ -138,7 +138,7 @@ class Autolinker {
static findLinks(text) {
// Regex can be tested and verified at https://regex101.com/r/rXoLiT/2.
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 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.
*/
import { RenderableView, RenderingStates } from "./renderable_view.js";
import { RenderingCancelledException } from "pdfjs-lib";
import { RenderingStates } from "./ui_utils.js";
class BasePDFPageView extends RenderableView {
class BasePDFPageView {
#loadingId = null;
#minDurationToUpdateCanvas = 0;
@ -48,8 +48,11 @@ class BasePDFPageView extends RenderableView {
renderingQueue = null;
renderTask = null;
resume = null;
constructor(options) {
super();
this.eventBus = options.eventBus;
this.id = options.id;
this.pageColors = options.pageColors || null;

View File

@ -17,7 +17,6 @@
import { AppOptions } from "./app_options.js";
import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js";
import { DownloadManager as GenericDownloadManager } from "./download_manager.js";
import { GenericL10n } from "./genericl10n.js";
import { GenericScripting } from "./generic_scripting.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.
// Note: The background page takes care of migrating values from
// 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.
*/
import { BaseDownloadManager } from "./base_download_manager.js";
import { createValidAbsoluteUrl } from "pdfjs-lib";
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
import { createValidAbsoluteUrl, isPdfFile } from "pdfjs-lib";
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) {
throw new Error(
@ -23,41 +24,101 @@ if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) {
);
}
class DownloadManager extends BaseDownloadManager {
_triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) {
if (!blobUrl && !isAttachment) {
// Fallback to downloading non-attachments by their URL.
if (!createValidAbsoluteUrl(originalUrl, "http://example.com")) {
throw new Error(`_triggerDownload - not a valid URL: ${originalUrl}`);
}
blobUrl = originalUrl + "#pdfjs.action=download";
}
function download(blobUrl, filename) {
const a = document.createElement("a");
if (!a.click) {
throw new Error('DownloadManager: "a.click()" is not supported.');
}
a.href = blobUrl;
a.target = "_parent";
// 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;
a.target = "_parent";
// 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();
/**
* @implements {IDownloadManager}
*/
class DownloadManager {
#openBlobUrls = new WeakMap();
downloadData(data, filename, contentType) {
const blobUrl = URL.createObjectURL(
new Blob([data], { type: contentType })
);
download(blobUrl, filename);
}
_getOpenDataUrl(blobUrl, filename, dest = null) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) {
throw new Error("Opening data is not supported in `COMPONENTS` builds.");
/**
* @returns {boolean} Indicating if the data was opened.
*/
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);
if (dest) {
url += `#${escape(dest)}`;
this.downloadData(data, filename, contentType);
return false;
}
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.
*/
/** @typedef {import("./interfaces.js").IL10n} IL10n */
class BaseExternalServices {
constructor() {
if (
@ -34,7 +36,7 @@ class BaseExternalServices {
reportText(data) {}
/**
* @returns {Promise<L10n>}
* @returns {Promise<IL10n>}
*/
async createL10n() {
throw new Error("Not implemented: createL10n");

View File

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

View File

@ -13,9 +13,8 @@
* limitations under the License.
*/
import { MathClamp, PDFDataRangeTransport } from "pdfjs-lib";
import { isPdfFile, PDFDataRangeTransport } from "pdfjs-lib";
import { AppOptions } from "./app_options.js";
import { BaseDownloadManager } from "./base_download_manager.js";
import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js";
import { DEFAULT_SCALE_VALUE } from "./ui_utils.js";
@ -81,25 +80,69 @@ class FirefoxCom {
}
}
class DownloadManager extends BaseDownloadManager {
_triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) {
class DownloadManager {
#openBlobUrls = new WeakMap();
downloadData(data, filename, contentType) {
const blobUrl = URL.createObjectURL(
new Blob([data], { type: contentType })
);
FirefoxCom.request("download", {
blobUrl,
originalUrl,
originalUrl: blobUrl,
filename,
isAttachment,
isAttachment: true,
});
}
_getOpenDataUrl(blobUrl, filename, dest = null) {
// 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 url = blobUrl + "#filename=" + encodeURIComponent(filename);
if (dest) {
url += `&filedest=${escape(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 = 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();
break;
case "progress":
const percent = MathClamp(
Math.round((args.loaded / args.total) * 100),
0,
100
);
viewerApp.progress(percent);
viewerApp.progress(args.loaded / args.total);
break;
case "complete":
if (!args.data) {

View File

@ -13,6 +13,8 @@
* limitations under the License.
*/
/** @typedef {import("./interfaces").IL10n} IL10n */
import { FeatureTest, fetchData } from "pdfjs-lib";
import { FluentBundle, FluentResource } from "fluent-bundle";
import { DOMLocalization } from "fluent-dom";
@ -47,6 +49,9 @@ function createBundle(lang, text) {
return bundle;
}
/**
* @implements {IL10n}
*/
class GenericL10n extends L10n {
constructor(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.
*/
/** @typedef {import("./interfaces").IL10n} IL10n */
/**
* NOTE: The L10n-implementations should use lowercase language-codes
* internally.
* @implements {IL10n}
*/
class L10n {
#dir;

View File

@ -14,6 +14,7 @@
*/
/** @typedef {import("./event_utils.js").EventBus} EventBus */
/** @typedef {import("./interfaces.js").IL10n} IL10n */
/** @typedef {import("./overlay_manager.js").OverlayManager} OverlayManager */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
@ -57,7 +58,7 @@ class PDFDocumentProperties {
* @param {PDFDocumentPropertiesOptions} options
* @param {OverlayManager} overlayManager - Manager for the viewer overlays.
* @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
* the document fileName.
*/

View File

@ -15,6 +15,7 @@
/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { binarySearchFirstItem, scrollIntoView } from "./ui_utils.js";
import { getCharacterType, getNormalizeWithNFKC } from "./pdf_find_utils.js";
@ -404,7 +405,7 @@ function getOriginalIndex(diffs, pos, len) {
/**
* @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 {boolean} [updateMatchesCountOnProgress] - True if the matches
* 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("./interfaces").IPDFLinkService} IPDFLinkService */
import { isValidRotation, parseQueryString } from "./ui_utils.js";
import { updateUrlHash } from "pdfjs-lib";
@ -28,7 +29,7 @@ const UPDATE_VIEWAREA_TIMEOUT = 1000; // milliseconds
/**
* @typedef {Object} PDFHistoryOptions
* @property {PDFLinkService} linkService - The navigation/linking service.
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {EventBus} eventBus - The application event bus.
*/

View File

@ -14,6 +14,7 @@
*/
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { isValidExplicitDest } from "pdfjs-lib";
import { parseQueryString } from "./ui_utils.js";
@ -44,6 +45,7 @@ const LinkTarget = {
/**
* Performs navigation functions inside PDF, such as opening specified page,
* or destination.
* @implements {IPDFLinkService}
*/
class PDFLinkService {
externalLinkEnabled = true;
@ -423,7 +425,7 @@ class PDFLinkService {
}
// Support opening of PDF attachments in the Firefox PDF Viewer,
// 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")) {
return;
}
@ -515,6 +517,9 @@ class PDFLinkService {
}
}
/**
* @implements {IPDFLinkService}
*/
class SimpleLinkService extends PDFLinkService {
setDocument(pdfDocument, baseUrl = null) {}
}

View File

@ -16,6 +16,7 @@
/** @typedef {import("./event_utils.js").EventBus} EventBus */
// eslint-disable-next-line max-len
/** @typedef {import("./download_manager.js").DownloadManager} DownloadManager */
/** @typedef {import("./interfaces.js").IPDFLinkService} IPDFLinkService */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
@ -26,7 +27,7 @@ import { SidebarView } from "./ui_utils.js";
* @typedef {Object} PDFOutlineViewerOptions
* @property {HTMLDivElement} container - The viewer element.
* @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.
*/

View File

@ -15,8 +15,13 @@
import { BasePDFPageView } from "./base_pdf_page_view.js";
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 {
#detailArea = null;
@ -49,9 +54,9 @@ class PDFPageDetailView extends BasePDFPageView {
return super.renderingState;
}
set renderingState(state) {
set renderingState(value) {
this.renderingCancelled = false;
super.renderingState = state;
super.renderingState = value;
}
reset({ keepCanvas = false } = {}) {

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,8 @@
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
// eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
@ -26,8 +28,8 @@ import {
OutputScale,
RenderingCancelledException,
} from "pdfjs-lib";
import { RenderableView, RenderingStates } from "./renderable_view.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 MAX_NUM_SCALING_STEPS = 3;
@ -42,7 +44,7 @@ const THUMBNAIL_WIDTH = 126; // px
* @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] -
* A promise that is resolved with an {@link OptionalContentConfig} instance.
* 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 {number} [maxCanvasPixels] - The maximum supported canvas size in
* 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
*/
@ -96,7 +99,6 @@ class PDFThumbnailView extends RenderableView {
pageColors,
enableSplitMerge = false,
}) {
super();
this.id = id;
this.renderingId = `thumbnail${id}`;
this.pageLabel = null;
@ -114,6 +116,9 @@ class PDFThumbnailView extends RenderableView {
this.linkService = linkService;
this.renderingQueue = renderingQueue;
this.renderTask = null;
this.renderingState = RenderingStates.INITIAL;
this.resume = null;
this.placeholder = null;
const imageContainer = (this.div = document.createElement("div"));
@ -158,14 +163,6 @@ class PDFThumbnailView extends RenderableView {
this.image.style.height = `${canvasHeight}px`;
}
get renderingState() {
return this.#renderingState;
}
set renderingState(state) {
this.#renderingState = state;
}
setPdfPage(pdfPage) {
this.pdfPage = pdfPage;
this.pdfPageRotate = pdfPage.rotate;

View File

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

View File

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

View File

@ -20,6 +20,9 @@
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
/** @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
/** @typedef {import("./pdf_find_controller").PDFFindController} PDFFindController */
// eslint-disable-next-line max-len
@ -52,6 +55,7 @@ import {
MIN_SCALE,
PresentationModeState,
removeNullCharacters,
RenderingStates,
SCROLLBAR_PADDING,
scrollIntoView,
ScrollMode,
@ -64,7 +68,6 @@ import {
import { GenericL10n } from "web-null_l10n";
import { PDFPageView } from "./pdf_page_view.js";
import { PDFRenderingQueue } from "./pdf_rendering_queue.js";
import { RenderingStates } from "./renderable_view.js";
import { SimpleLinkService } from "./pdf_link_service.js";
const DEFAULT_CACHE_SIZE = 10;
@ -87,8 +90,8 @@ function isValidAnnotationEditorMode(mode) {
* @property {HTMLDivElement} container - The container for the viewer element.
* @property {HTMLDivElement} [viewer] - The viewer element.
* @property {EventBus} eventBus - The application event bus.
* @property {PDFLinkService} [linkService] - The navigation/linking service.
* @property {BaseDownloadManager} [downloadManager] - The download manager
* @property {IPDFLinkService} [linkService] - The navigation/linking service.
* @property {IDownloadManager} [downloadManager] - The download manager
* component.
* @property {PDFFindController} [findController] - The find controller
* component.
@ -132,7 +135,7 @@ function isValidAnnotationEditorMode(mode) {
* rendering will keep track of which areas of the page each PDF operation
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
* 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,
* when they exist. The default value is `false`.
* @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 { 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) {
const xfaHtml = pdfDocument.allXfaHtml;
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 VERTICAL_PADDING = 5;
const RenderingStates = {
INITIAL: 0,
RUNNING: 1,
PAUSED: 2,
FINISHED: 3,
};
const PresentationModeState = {
UNKNOWN: 0,
NORMAL: 1,
@ -702,7 +709,7 @@ class ProgressBar {
}
set percent(val) {
this.#percent = val;
this.#percent = MathClamp(val, 0, 100);
if (isNaN(val)) {
this.#classList.add("indeterminate");
@ -907,6 +914,7 @@ export {
PresentationModeState,
ProgressBar,
removeNullCharacters,
RenderingStates,
SCROLLBAR_PADDING,
scrollIntoView,
ScrollMode,

View File

@ -13,11 +13,10 @@
* 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 { LinkTarget } from "./pdf_link_service.js";
import { PDFViewerApplication } from "./app.js";
import { RenderingStates } from "./renderable_view.js";
const AppConstants =
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")

View File

@ -13,11 +13,10 @@
* 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 { LinkTarget } from "./pdf_link_service.js";
import { PDFViewerApplication } from "./app.js";
import { RenderingStates } from "./renderable_view.js";
const AppConstants =
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")

View File

@ -14,6 +14,7 @@
*/
/** @typedef {import("./event_utils.js").EventBus} EventBus */
/** @typedef {import("./interfaces.js").IL10n} IL10n */
import {
docStyle,
@ -33,7 +34,7 @@ const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
* @typedef {Object} PDFSidebarOptions
* @property {PDFSidebarElements} elements - The DOM elements.
* @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 */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { XfaLayer } from "pdfjs-lib";
@ -25,7 +26,7 @@ import { XfaLayer } from "pdfjs-lib";
* @typedef {Object} XfaLayerBuilderOptions
* @property {PDFPageProxy} pdfPage
* @property {AnnotationStorage} [annotationStorage]
* @property {PDFLinkService} linkService
* @property {IPDFLinkService} linkService
* @property {Object} [xfaHtml]
*/