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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

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 { getCrossOriginHostname, TestPdfsServer } from "./test_utils.js";
// Common tests to verify behavior across implementations of the IPDFStream
// interface:
// Common tests to verify behavior across `BasePDFStream` implementations:
// - PDFNetworkStream by network_spec.js
// - PDFFetchStream by fetch_stream_spec.js
async function testCrossOriginRedirects({

View File

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

View File

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

View File

@ -22,7 +22,6 @@ import {
import {
parseQueryString,
ProgressBar,
RenderingStates,
ScrollMode,
SpreadMode,
} 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 { PDFSinglePageViewer } from "../../web/pdf_single_page_viewer.js";
import { PDFViewer } from "../../web/pdf_viewer.js";
import { RenderingStates } from "../../web/renderable_view.js";
import { StructTreeLayerBuilder } from "../../web/struct_tree_layer_builder.js";
import { TextLayerBuilder } from "../../web/text_layer_builder.js";
import { XfaLayerBuilder } from "../../web/xfa_layer_builder.js";

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*/
import { RenderableView, RenderingStates } from "./renderable_view.js";
import { RenderingCancelledException } from "pdfjs-lib";
import { RenderingStates } from "./ui_utils.js";
class BasePDFPageView {
class BasePDFPageView extends RenderableView {
#loadingId = null;
#minDurationToUpdateCanvas = 0;
@ -48,11 +48,8 @@ class BasePDFPageView {
renderingQueue = null;
renderTask = null;
resume = null;
constructor(options) {
super();
this.eventBus = options.eventBus;
this.id = options.id;
this.pageColors = options.pageColors || null;

View File

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

View File

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

View File

@ -19,7 +19,10 @@ import {
RenderingCancelledException,
shadow,
} 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.
function composePage(
@ -194,10 +197,7 @@ class FirefoxPrintService {
}
}
/**
* @implements {IPDFPrintServiceFactory}
*/
class PDFPrintServiceFactory {
class PDFPrintServiceFactory extends BasePrintServiceFactory {
static get supportsPrinting() {
const canvas = document.createElement("canvas");
return shadow(this, "supportsPrinting", "mozPrintCallback" in canvas);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,8 @@
/** @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";
/**

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,24 @@ import { getXfaPageViewport, PixelsPerInch } from "pdfjs-lib";
import { SimpleLinkService } from "./pdf_link_service.js";
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
class BasePrintServiceFactory {
constructor() {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
throw new Error("Cannot initialize BasePrintServiceFactory.");
}
}
static initGlobals(app) {}
static get supportsPrinting() {
throw new Error("Not implemented: supportsPrinting");
}
static createPrintService(params) {
throw new Error("Not implemented: createPrintService");
}
}
function getXfaHtmlForPrinting(printContainer, pdfDocument) {
const xfaHtml = pdfDocument.allXfaHtml;
const linkService = new SimpleLinkService();
@ -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 VERTICAL_PADDING = 5;
const RenderingStates = {
INITIAL: 0,
RUNNING: 1,
PAUSED: 2,
FINISHED: 3,
};
const PresentationModeState = {
UNKNOWN: 0,
NORMAL: 1,
@ -709,7 +702,7 @@ class ProgressBar {
}
set percent(val) {
this.#percent = MathClamp(val, 0, 100);
this.#percent = val;
if (isNaN(val)) {
this.#classList.add("indeterminate");
@ -914,7 +907,6 @@ export {
PresentationModeState,
ProgressBar,
removeNullCharacters,
RenderingStates,
SCROLLBAR_PADDING,
scrollIntoView,
ScrollMode,

View File

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

View File

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

View File

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