Compare commits

..

22 Commits

Author SHA1 Message Date
Tim van der Meij
f4326e17c4
Merge pull request #20610 from calixteman/brotli
Add support for Brotli decompression
2026-02-01 20:41:06 +01:00
Tim van der Meij
3f21efc942
Merge pull request #20607 from Snuffleupagus/rm-web-interfaces
Replace the various interfaces in `web/interfaces.js` with proper classes
2026-02-01 20:31:13 +01:00
Tim van der Meij
8eb9340fa7
Merge pull request #20617 from timvandermeij/bump
Bump the stable version in `pdfjs.config`
2026-02-01 20:23:40 +01:00
Tim van der Meij
031f633236
Bump the stable version in pdfjs.config 2026-02-01 20:20:49 +01:00
Jonas Jenwald
aa4d0f7c07 Temporarily disable typestest in GitHub Actions 2026-02-01 17:56:07 +01:00
Jonas Jenwald
023af46186 Replace the IRenderableView interface with an abstract RenderableView class
This should help reduce the maintenance burden of the code, since you no longer need to remember to update separate code when touching the different page/thumbnail classes.
2026-02-01 17:56:06 +01:00
Jonas Jenwald
839c257f87 Replace the IDownloadManager interface with an abstract BaseDownloadManager class
This should help reduce the maintenance burden of the code, since you no longer need to remember to update separate code when touching the different `DownloadManager` classes.
2026-02-01 17:56:03 +01:00
Jonas Jenwald
ff7f87fc21 Replace the IPDFPrintServiceFactory interface with an abstract BasePrintServiceFactory class
This should help reduce the maintenance burden of the code, since you no longer need to remember to update separate code when touching the different `PDFPrintServiceFactory` classes.
2026-02-01 17:53:45 +01:00
Jonas Jenwald
50a12e3e67 Remove the IL10n interface
Given that we either use the `L10n` class directly or extend it via `GenericL10n`, it should no longer be necessary to keep the interface-definition.
This should help reduce the maintenance burden of the code, since you no longer need to remember to update separate code when touching the `L10n` class.
2026-02-01 17:53:45 +01:00
Jonas Jenwald
b517b5c597 Remove the IPDFLinkService interface
Given that `SimpleLinkService` now extends the regular `PDFLinkService` class, see PR 18013, it should no longer be necessary to keep the interface-definition.
This should help reduce the maintenance burden of the code, since you no longer need to remember to update separate code when touching the `PDFLinkService` class.
2026-02-01 17:53:45 +01:00
Tim van der Meij
384c6208b2
Merge pull request #20565 from kairosci/fix-bug-20557
fix: Fix mailto links truncated at dash
2026-02-01 17:34:34 +01:00
Tim van der Meij
e4cd3176ab
Merge pull request #20602 from Snuffleupagus/BasePDFStream-2
Replace the `IPDFStream`, `IPDFStreamReader`, and `IPDFStreamRangeReader` interfaces with proper base classes
2026-02-01 16:53:17 +01:00
Jonas Jenwald
ecb09d62fc Add the current loading percentage to the onPassword callback
The percentage calculation is currently "spread out" across various viewer functionality, which we can avoid by having the API handle that instead.

Also, remove the `this.#lastProgress` special-case[1] and just register a "normal" `fullReader.onProgress` callback unconditionally. Once `headersReady` is resolved the callback can simply be removed when not needed, since the "worst" thing that could theoretically happen is that the loadingBar (in the viewer) updates sooner this way. In practice though, since `fullReader.read` cannot return data until `headersReady` is resolved, this change is not actually observable in the API.

---

[1] This was added in PR 8617, close to a decade ago, but it's not obvious to me that it was ever necessary to implement it that way.
2026-01-31 16:33:58 +01:00
calixteman
43273fde27
Add support for Brotli decompression
For now, `BrotliDecode` hasn't been specified but it should be in a
close future.
So when it's possible we use the native `DecompressionStream` API
with "brotli" as argument.
If that fails or if we've to decompress in a sync context, we fallback
to `BrotliStream` which a pure js implementation (see README in external/brotli).
2026-01-31 16:25:53 +01:00
Jonas Jenwald
4ca205bac3 Add an abstract BasePDFStreamRangeReader class, that all the old IPDFStreamRangeReader implementations inherit from
Given that there's no less than *five* different, but very similar, implementations this helps reduce code duplication and simplifies maintenance.
2026-01-30 14:15:39 +01:00
Jonas Jenwald
54d8c5e7b4 Add an abstract BasePDFStreamReader class, that all the old IPDFStreamReader implementations inherit from
Given that there's no less than *five* different, but very similar, implementations this helps reduce code duplication and simplifies maintenance.

Also, remove the `rangeChunkSize` not defined checks in all the relevant stream-constructor implementations.
Note how the API, since some time, always validates *and* provides that parameter when creating a `BasePDFStreamReader`-instance.
2026-01-30 14:15:39 +01:00
Jonas Jenwald
4a8fb4dde1 Add an abstract BasePDFStream class, that all the old IPDFStream implementations inherit from
Given that there's no less than *five* different, but very similar, implementations this helps reduce code duplication and simplifies maintenance.

Also, spotted during rebasing, pass the `enableHWA` option "correctly" (i.e. as part of the existing `transportParams`) to the `WorkerTransport`-class to keep the constructor simpler.
2026-01-30 14:15:39 +01:00
Jonas Jenwald
a80f10ff1a Remove the onProgress callback from the IPDFStreamRangeReader interface
Note how there's *nowhere* in the code-base where the `IPDFStreamRangeReader.prototype.onProgress` callback is actually being set and used, however the loadingBar (in the viewer) still works just fine since loading progress is already reported via:
 - The `ChunkedStreamManager` instance respectively the `getPdfManager` function, through the use of a "DocProgress" message, on the worker-thread.
 - A `IPDFStreamReader.prototype.onProgress` callback, on the main-thread.

Furthermore, it would definitely *not* be a good idea to add any `IPDFStreamRangeReader.prototype.onProgress` callbacks since they only include the `loaded`-property which would trigger the "indeterminate" loadingBar (in the viewer).

Looking briefly at the history of this code it's not clear, at least to me, when this became unused however it's probably close to a decade ago.
2026-01-30 14:15:39 +01:00
Jonas Jenwald
05b78ce03c Stop registering an onProgress callback on the PDFWorkerStreamRangeReader-instance, in the ChunkedStreamManager class
Given that nothing in the `PDFWorkerStreamRangeReader` class attempts to invoke the `onProgress` callback, this is effectively dead code now.
Looking briefly at the history of this code it's not clear, at least to me, when this became unused however it's probably close to a decade ago.

Finally, note also how progress is already being reported through the `ChunkedStreamManager.prototype.onReceiveData` method.
2026-01-30 14:15:38 +01:00
Jonas Jenwald
987265720e Remove the unused IPDFStreamRangeReader.prototype.isStreamingSupported getter
This getter was only invoked from `src/display/network.js` and `src/core/chunked_stream.js`, however in both cases it's hardcoded to `false` and thus isn't actually needed.
This originated in PR 6879, close to a decade ago, for a potential TODO which was never implemented and it ought to be OK to just simplify this now.
2026-01-30 14:15:38 +01:00
Jonas Jenwald
62d5408cf0 Stop tracking progressiveDataLength in the ChunkedStreamManager class
Currently this property is essentially "duplicated", so let's instead use the identical one that's availble on the `ChunkedStream` instance.
2026-01-30 14:15:38 +01:00
Alessio Attilio
50f2d4db65 fix: allow hyphens in mailto link auto-detection (bug 20557)
Modified the regex in web/autolinker.js to explicitly allow hyphens (-) in
the domain part of email addresses, while maintaining the exclusion of
other punctuation. This fixes mailto links like user@uni-city.tld being
truncated at the hyphen.

Fixes #20557
2026-01-25 17:20:14 +01:00
69 changed files with 3605 additions and 1446 deletions

View File

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

View File

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

View File

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

19
external/brotli/LICENSE_BROTLI vendored Normal file
View File

@ -0,0 +1,19 @@
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.

8
external/brotli/README.md vendored Normal file
View File

@ -0,0 +1,8 @@
## 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)

2466
external/brotli/decode.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

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

86
src/core/brotli_stream.js Normal file
View File

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

View File

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

View File

@ -163,58 +163,27 @@ class FlateStream extends DecodeStream {
} }
async asyncGetBytes() { async asyncGetBytes() {
this.stream.reset(); const { decompressed, compressed } =
const bytes = this.stream.isAsync await this.asyncGetBytesFromDecompressionStream("deflate");
? await this.stream.asyncGetBytes() if (decompressed) {
: this.stream.getBytes(); return decompressed;
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 // DecompressionStream failed (for example because there are some extra
// bytes after the end of the compressed data), so we fallback to our // bytes after the end of the compressed data), so we fallback to our
// decoder. // decoder.
// We already get the bytes from the underlying stream, so we just reuse // We already get the bytes from the underlying stream, so we just reuse
// them to avoid get them again. // them to avoid get them again.
this.#isAsync = false; this.#isAsync = false;
this.stream = new Stream( this.stream = new Stream(
bytes, compressed,
2 /* = header size (see ctor) */, 2 /* = header size (see ctor) */,
bytes.length, compressed.length,
this.stream.dict this.stream.dict
); );
this.reset(); this.reset();
return null; return null;
} }
}
get isAsync() { get isAsync() {
return this.#isAsync; return this.#isAsync;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

@ -209,4 +209,14 @@ 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,8 +16,7 @@
import { AbortException, isNodeJS } from "../../src/shared/util.js"; import { AbortException, isNodeJS } from "../../src/shared/util.js";
import { getCrossOriginHostname, TestPdfsServer } from "./test_utils.js"; import { getCrossOriginHostname, TestPdfsServer } from "./test_utils.js";
// Common tests to verify behavior across implementations of the IPDFStream // Common tests to verify behavior across `BasePDFStream` implementations:
// interface:
// - PDFNetworkStream by network_spec.js // - PDFNetworkStream by network_spec.js
// - PDFFetchStream by fetch_stream_spec.js // - PDFFetchStream by fetch_stream_spec.js
async function testCrossOriginRedirects({ async function testCrossOriginRedirects({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,9 +13,8 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */ import { BaseDownloadManager } from "./base_download_manager.js";
import { createValidAbsoluteUrl } from "pdfjs-lib";
import { createValidAbsoluteUrl, isPdfFile } from "pdfjs-lib";
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) { if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) {
throw new Error( throw new Error(
@ -24,11 +23,17 @@ if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) {
); );
} }
function download(blobUrl, filename) { class DownloadManager extends BaseDownloadManager {
const a = document.createElement("a"); _triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) {
if (!a.click) { if (!blobUrl && !isAttachment) {
throw new Error('DownloadManager: "a.click()" is not supported.'); // 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";
}
const a = document.createElement("a");
a.href = blobUrl; a.href = blobUrl;
a.target = "_parent"; a.target = "_parent";
// Use a.download if available. This increases the likelihood that // Use a.download if available. This increases the likelihood that
@ -41,84 +46,18 @@ function download(blobUrl, filename) {
(document.body || document.documentElement).append(a); (document.body || document.documentElement).append(a);
a.click(); a.click();
a.remove(); 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) {
* @returns {boolean} Indicating if the data was opened. if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) {
*/ throw new Error("Opening data is not supported in `COMPONENTS` builds.");
openOrDownloadData(data, filename, dest = null) {
const isPdfData = isPdfFile(filename);
const contentType = isPdfData ? "application/pdf" : "";
if (
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("COMPONENTS")) &&
isPdfData
) {
let blobUrl = this.#openBlobUrls.get(data);
if (!blobUrl) {
blobUrl = URL.createObjectURL(new Blob([data], { type: contentType }));
this.#openBlobUrls.set(data, blobUrl);
} }
let viewerUrl;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
// The current URL is the viewer, let's use it and append the file. // The current URL is the viewer, let's use it and append the file.
viewerUrl = "?file=" + encodeURIComponent(blobUrl + "#" + filename); let url = "?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) { if (dest) {
viewerUrl += `#${escape(dest)}`; url += `#${escape(dest)}`;
} }
return url;
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);
}
}
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";
}
download(blobUrl, filename);
} }
} }

View File

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

View File

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

View File

@ -13,8 +13,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { isPdfFile, PDFDataRangeTransport } from "pdfjs-lib"; import { MathClamp, PDFDataRangeTransport } from "pdfjs-lib";
import { AppOptions } from "./app_options.js"; import { AppOptions } from "./app_options.js";
import { BaseDownloadManager } from "./base_download_manager.js";
import { BaseExternalServices } from "./external_services.js"; import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js"; import { BasePreferences } from "./preferences.js";
import { DEFAULT_SCALE_VALUE } from "./ui_utils.js"; import { DEFAULT_SCALE_VALUE } from "./ui_utils.js";
@ -80,69 +81,25 @@ class FirefoxCom {
} }
} }
class DownloadManager { class DownloadManager extends BaseDownloadManager {
#openBlobUrls = new WeakMap(); _triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) {
downloadData(data, filename, contentType) {
const blobUrl = URL.createObjectURL(
new Blob([data], { type: contentType })
);
FirefoxCom.request("download", { FirefoxCom.request("download", {
blobUrl, blobUrl,
originalUrl: blobUrl, originalUrl,
filename, filename,
isAttachment: true, isAttachment,
}); });
} }
/** _getOpenDataUrl(blobUrl, filename, dest = null) {
* @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. // Let Firefox's content handler catch the URL and display the PDF.
// NOTE: This cannot use a query string for the filename, see // NOTE: This cannot use a query string for the filename, see
// https://bugzilla.mozilla.org/show_bug.cgi?id=1632644#c5 // https://bugzilla.mozilla.org/show_bug.cgi?id=1632644#c5
let viewerUrl = blobUrl + "#filename=" + encodeURIComponent(filename); let url = blobUrl + "#filename=" + encodeURIComponent(filename);
if (dest) { if (dest) {
viewerUrl += `&filedest=${escape(dest)}`; url += `&filedest=${escape(dest)}`;
} }
return url;
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);
}
}
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,
});
} }
} }
@ -627,7 +584,13 @@ class ExternalServices extends BaseExternalServices {
pdfDataRangeTransport?.onDataProgressiveDone(); pdfDataRangeTransport?.onDataProgressiveDone();
break; break;
case "progress": case "progress":
viewerApp.progress(args.loaded / args.total); const percent = MathClamp(
Math.round((args.loaded / args.total) * 100),
0,
100
);
viewerApp.progress(percent);
break; break;
case "complete": case "complete":
if (!args.data) { if (!args.data) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

71
web/renderable_view.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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