[api-minor] Move the viewer scripting initialization/handling into a new PDFScriptingManager class

The *main* purpose of this patch is to allow scripting to be used together with the viewer components, note the updated "simpleviewer"/"singlepageviewer" examples, in addition to the full default viewer.
Given how the scripting functionality is currently implemented in the default viewer, trying to re-use this with the standalone viewer components would be *very* hard and ideally you'd want it to work out-of-the-box.

For an initial implementation, in the default viewer, of the scripting functionality it probably made sense to simply dump all of the code in the `app.js` file, however that cannot be used with the viewer components.
To address this, the functionality is moved into a new `PDFScriptingManager` class which can thus be handled in the same way as all other viewer components (and e.g. be passed to the `BaseViewer`-implementations).

Obviously the scripting functionality needs quite a lot of data, during its initialization, and for the default viewer we want to maintain the current way of doing the lookups since that helps avoid a number of redundant API-calls.
To that end, the `PDFScriptingManager` implementation accepts (optional) factories/functions such that we can maintain the current behaviour for the default viewer. For the viewer components specifically, fallback code-paths are provided to ensure that scripting will "just work"[1].

Besides moving the viewer handling of the scripting code to its own file/class, this patch also takes the opportunity to re-factor the functionality into a number of helper methods to improve overall readability[2].
Note that it's definitely possible that the `PDFScriptingManager` class could be improved even further (e.g. for general re-use), since it's still heavily tailored to the default viewer use-case, however I believe that this patch is still a good step forward overall.

---

[1] Obviously *all* the relevant document properties might not be available in the viewer components use-case (e.g. the various URLs), but most things should work just fine.

[2] The old `PDFViewerApplication._initializeJavaScript` method, where everything was simply inlined, have over time (in my opinion) become quite large and somewhat difficult to *easily* reason about.
This commit is contained in:
Jonas Jenwald 2021-03-05 00:15:18 +01:00
parent 5b9638329c
commit a6d1cba38c
8 changed files with 525 additions and 261 deletions

View File

@ -31,8 +31,14 @@ var CMAP_URL = "../../node_modules/pdfjs-dist/cmaps/";
var CMAP_PACKED = true;
var DEFAULT_URL = "../../web/compressed.tracemonkey-pldi-09.pdf";
// To test the AcroForm and/or scripting functionality, try e.g. this file:
// var DEFAULT_URL = "../../test/pdfs/160F-2019.pdf";
var SEARCH_FOR = ""; // try 'Mozilla';
// For scripting support, note also `enableScripting` below.
var SANDBOX_BUNDLE_SRC = "../../node_modules/pdfjs-dist/build/pdf.sandbox.js";
var container = document.getElementById("viewerContainer");
var eventBus = new pdfjsViewer.EventBus();
@ -48,13 +54,22 @@ var pdfFindController = new pdfjsViewer.PDFFindController({
linkService: pdfLinkService,
});
// (Optionally) enable scripting support.
var pdfScriptingManager = new pdfjsViewer.PDFScriptingManager({
eventBus,
sandboxBundleSrc: SANDBOX_BUNDLE_SRC,
});
var pdfViewer = new pdfjsViewer.PDFViewer({
container,
eventBus,
linkService: pdfLinkService,
findController: pdfFindController,
scriptingManager: pdfScriptingManager,
enableScripting: true,
});
pdfLinkService.setViewer(pdfViewer);
pdfScriptingManager.setViewer(pdfViewer);
eventBus.on("pagesinit", function () {
// We can use pdfViewer now, e.g. let's change default scale.

View File

@ -31,8 +31,14 @@ var CMAP_URL = "../../node_modules/pdfjs-dist/cmaps/";
var CMAP_PACKED = true;
var DEFAULT_URL = "../../web/compressed.tracemonkey-pldi-09.pdf";
// To test the AcroForm and/or scripting functionality, try e.g. this file:
// var DEFAULT_URL = "../../test/pdfs/160F-2019.pdf";
var SEARCH_FOR = ""; // try 'Mozilla';
// For scripting support, note also `enableScripting` below.
var SANDBOX_BUNDLE_SRC = "../../node_modules/pdfjs-dist/build/pdf.sandbox.js";
var container = document.getElementById("viewerContainer");
var eventBus = new pdfjsViewer.EventBus();
@ -48,13 +54,22 @@ var pdfFindController = new pdfjsViewer.PDFFindController({
linkService: pdfLinkService,
});
// (Optionally) enable scripting support.
var pdfScriptingManager = new pdfjsViewer.PDFScriptingManager({
eventBus,
sandboxBundleSrc: SANDBOX_BUNDLE_SRC,
});
var pdfSinglePageViewer = new pdfjsViewer.PDFSinglePageViewer({
container,
eventBus,
linkService: pdfLinkService,
findController: pdfFindController,
scriptingManager: pdfScriptingManager,
enableScripting: true,
});
pdfLinkService.setViewer(pdfSinglePageViewer);
pdfScriptingManager.setViewer(pdfSinglePageViewer);
eventBus.on("pagesinit", function () {
// We can use pdfSinglePageViewer now, e.g. let's change default scale.

View File

@ -71,6 +71,7 @@ import { PDFLayerViewer } from "./pdf_layer_viewer.js";
import { PDFLinkService } from "./pdf_link_service.js";
import { PDFOutlineViewer } from "./pdf_outline_viewer.js";
import { PDFPresentationMode } from "./pdf_presentation_mode.js";
import { PDFScriptingManager } from "./pdf_scripting_manager.js";
import { PDFSidebar } from "./pdf_sidebar.js";
import { PDFSidebarResizer } from "./pdf_sidebar_resizer.js";
import { PDFThumbnailViewer } from "./pdf_thumbnail_viewer.js";
@ -228,6 +229,8 @@ const PDFViewerApplication = {
pdfLayerViewer: null,
/** @type {PDFCursorTools} */
pdfCursorTools: null,
/** @type {PDFScriptingManager} */
pdfScriptingManager: null,
/** @type {ViewHistory} */
store: null,
/** @type {DownloadManager} */
@ -259,8 +262,6 @@ const PDFViewerApplication = {
_saveInProgress: false,
_wheelUnusedTicks: 0,
_idleCallbacks: new Set(),
_scriptingInstance: null,
_mouseState: Object.create(null),
// Called once when the document is loaded.
async initialize(appConfig) {
@ -484,6 +485,18 @@ const PDFViewerApplication = {
});
this.findController = findController;
const pdfScriptingManager = new PDFScriptingManager({
eventBus,
sandboxBundleSrc:
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC || CHROME")
? AppOptions.get("sandboxBundleSrc")
: null,
scriptingFactory: this.externalServices,
docPropertiesLookup: this._scriptingDocProperties.bind(this),
});
this.pdfScriptingManager = pdfScriptingManager;
const container = appConfig.mainContainer;
const viewer = appConfig.viewerContainer;
this.pdfViewer = new PDFViewer({
@ -494,6 +507,7 @@ const PDFViewerApplication = {
linkService: pdfLinkService,
downloadManager,
findController,
scriptingManager: pdfScriptingManager,
renderer: AppOptions.get("renderer"),
enableWebGL: AppOptions.get("enableWebGL"),
l10n: this.l10n,
@ -504,10 +518,10 @@ const PDFViewerApplication = {
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
enableScripting: AppOptions.get("enableScripting"),
mouseState: this._mouseState,
});
pdfRenderingQueue.setViewer(this.pdfViewer);
pdfLinkService.setViewer(this.pdfViewer);
pdfScriptingManager.setViewer(this.pdfViewer);
this.pdfThumbnailViewer = new PDFThumbnailViewer({
container: appConfig.sidebar.thumbnailView,
@ -774,32 +788,6 @@ const PDFViewerApplication = {
this._idleCallbacks.clear();
},
/**
* @private
*/
async _destroyScriptingInstance() {
if (!this._scriptingInstance) {
return;
}
const { scripting, internalEvents, domEvents } = this._scriptingInstance;
try {
await scripting.destroySandbox();
} catch (ex) {}
for (const [name, listener] of internalEvents) {
this.eventBus._off(name, listener);
}
internalEvents.clear();
for (const [name, listener] of domEvents) {
window.removeEventListener(name, listener);
}
domEvents.clear();
delete this._mouseState.isDown;
this._scriptingInstance = null;
},
/**
* Closes opened PDF document.
* @returns {Promise} - Returns the promise, which is resolved when all
@ -843,7 +831,7 @@ const PDFViewerApplication = {
this._saveInProgress = false;
this._cancelIdleCallbacks();
promises.push(this._destroyScriptingInstance());
promises.push(this.pdfScriptingManager.destroyPromise);
this.pdfSidebar.reset();
this.pdfOutlineViewer.reset();
@ -1002,11 +990,7 @@ const PDFViewerApplication = {
return;
}
this._saveInProgress = true;
await this._scriptingInstance?.scripting.dispatchEventInSandbox({
id: "doc",
name: "WillSave",
});
await this.pdfScriptingManager.dispatchWillSave();
this.pdfDocument
.saveDocument(this.pdfDocument.annotationStorage)
@ -1018,11 +1002,7 @@ const PDFViewerApplication = {
this.download({ sourceEventType });
})
.finally(async () => {
await this._scriptingInstance?.scripting.dispatchEventInSandbox({
id: "doc",
name: "DidSave",
});
await this.pdfScriptingManager.dispatchDidSave();
this._saveInProgress = false;
});
},
@ -1421,7 +1401,6 @@ const PDFViewerApplication = {
);
this._idleCallbacks.add(callback);
}
this._initializeJavaScript(pdfDocument);
});
this._initializePageLabels(pdfDocument);
@ -1431,40 +1410,7 @@ const PDFViewerApplication = {
/**
* @private
*/
async _initializeJavaScript(pdfDocument) {
if (!AppOptions.get("enableScripting")) {
return;
}
const [objects, calculationOrder, docActions] = await Promise.all([
pdfDocument.getFieldObjects(),
pdfDocument.getCalculationOrderIds(),
pdfDocument.getJSActions(),
]);
if (!objects && !docActions) {
// No FieldObjects or JavaScript actions were found in the document.
return;
}
if (pdfDocument !== this.pdfDocument) {
return; // The document was closed while the data resolved.
}
const scripting = this.externalServices.createScripting(
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC || CHROME")
? { sandboxBundleSrc: AppOptions.get("sandboxBundleSrc") }
: null
);
// Store a reference to the current scripting-instance, to allow destruction
// of the sandbox and removal of the event listeners at document closing.
const internalEvents = new Map(),
domEvents = new Map();
this._scriptingInstance = {
scripting,
ready: false,
internalEvents,
domEvents,
};
async _scriptingDocProperties(pdfDocument) {
if (!this.documentInfo) {
// It should be *extremely* rare for metadata to not have been resolved
// when this code runs, but ensure that we handle that case here.
@ -1472,7 +1418,7 @@ const PDFViewerApplication = {
this.eventBus._on("metadataloaded", resolve, { once: true });
});
if (pdfDocument !== this.pdfDocument) {
return; // The document was closed while the metadata resolved.
return null; // The document was closed while the metadata resolved.
}
}
if (!this._contentLength) {
@ -1485,170 +1431,20 @@ const PDFViewerApplication = {
this.eventBus._on("documentloaded", resolve, { once: true });
});
if (pdfDocument !== this.pdfDocument) {
return; // The document was closed while the downloadInfo resolved.
return null; // The document was closed while the downloadInfo resolved.
}
}
const updateFromSandbox = ({ detail }) => {
const { id, command, value } = detail;
if (!id) {
switch (command) {
case "clear":
console.clear();
break;
case "error":
console.error(value);
break;
case "layout":
this.pdfViewer.spreadMode = apiPageLayoutToSpreadMode(value);
break;
case "page-num":
this.pdfViewer.currentPageNumber = value + 1;
break;
case "print":
this.pdfViewer.pagesPromise.then(() => {
this.triggerPrinting();
});
break;
case "println":
console.log(value);
break;
case "zoom":
this.pdfViewer.currentScaleValue = value;
break;
}
return;
}
const element = document.getElementById(id);
if (element) {
element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail }));
} else {
if (value !== undefined && value !== null) {
// The element hasn't been rendered yet, use the AnnotationStorage.
pdfDocument.annotationStorage.setValue(id, value);
}
}
return {
...this.documentInfo,
baseURL: this.baseUrl,
filesize: this._contentLength,
filename: this._docFilename,
metadata: this.metadata?.getRaw(),
authors: this.metadata?.get("dc:creator"),
numPages: this.pagesCount,
URL: this.url,
};
internalEvents.set("updatefromsandbox", updateFromSandbox);
const visitedPages = new Map();
const pageOpen = ({ pageNumber, actionsPromise }) => {
visitedPages.set(
pageNumber,
(async () => {
// Avoid sending, and thus serializing, the `actions` data
// when the same page is opened several times.
let actions = null;
if (!visitedPages.has(pageNumber)) {
actions = await actionsPromise;
if (pdfDocument !== this.pdfDocument) {
return; // The document was closed while the actions resolved.
}
}
await this._scriptingInstance?.scripting.dispatchEventInSandbox({
id: "page",
name: "PageOpen",
pageNumber,
actions,
});
})()
);
};
const pageClose = async ({ pageNumber }) => {
const actionsPromise = visitedPages.get(pageNumber);
if (!actionsPromise) {
// Ensure that the "pageclose" event was preceded by a "pageopen" event.
return;
}
visitedPages.set(pageNumber, null);
// Ensure that the "pageopen" event is handled first.
await actionsPromise;
if (pdfDocument !== this.pdfDocument) {
return; // The document was closed while the actions resolved.
}
await this._scriptingInstance?.scripting.dispatchEventInSandbox({
id: "page",
name: "PageClose",
pageNumber,
});
};
internalEvents.set("pageopen", pageOpen);
internalEvents.set("pageclose", pageClose);
const dispatchEventInSandbox = ({ detail }) => {
scripting.dispatchEventInSandbox(detail);
};
internalEvents.set("dispatcheventinsandbox", dispatchEventInSandbox);
const mouseDown = event => {
this._mouseState.isDown = true;
};
domEvents.set("mousedown", mouseDown);
const mouseUp = event => {
this._mouseState.isDown = false;
};
domEvents.set("mouseup", mouseUp);
for (const [name, listener] of internalEvents) {
this.eventBus._on(name, listener);
}
for (const [name, listener] of domEvents) {
window.addEventListener(name, listener);
}
try {
await scripting.createSandbox({
objects,
calculationOrder,
appInfo: {
platform: navigator.platform,
language: navigator.language,
},
docInfo: {
...this.documentInfo,
baseURL: this.baseUrl,
filesize: this._contentLength,
filename: this._docFilename,
metadata: this.metadata?.getRaw(),
authors: this.metadata?.get("dc:creator"),
numPages: pdfDocument.numPages,
URL: this.url,
actions: docActions,
},
});
if (this.externalServices.isInAutomation) {
this.eventBus.dispatch("sandboxcreated", { source: this });
}
} catch (error) {
console.error(`_initializeJavaScript: "${error?.message}".`);
this._destroyScriptingInstance();
return;
}
await scripting.dispatchEventInSandbox({
id: "doc",
name: "Open",
});
await this.pdfViewer.initializeScriptingEvents();
// Used together with the integration-tests, see the `scriptingReady`
// getter, to enable awaiting full initialization of the scripting/sandbox.
// (Defer this slightly, to make absolutely sure that everything is done.)
Promise.resolve().then(() => {
if (this._scriptingInstance) {
this._scriptingInstance.ready = true;
}
});
},
/**
@ -1674,7 +1470,7 @@ const PDFViewerApplication = {
async _initializeAutoPrint(pdfDocument, openActionPromise) {
const [openAction, javaScript] = await Promise.all([
openActionPromise,
!AppOptions.get("enableScripting") ? pdfDocument.getJavaScript() : null,
!this.pdfViewer.enableScripting ? pdfDocument.getJavaScript() : null,
]);
if (pdfDocument !== this.pdfDocument) {
@ -1988,10 +1784,7 @@ const PDFViewerApplication = {
beforePrint() {
// Given that the "beforeprint" browser event is synchronous, we
// unfortunately cannot await the scripting event dispatching here.
this._scriptingInstance?.scripting.dispatchEventInSandbox({
id: "doc",
name: "WillPrint",
});
this.pdfScriptingManager.dispatchWillPrint();
if (this.printService) {
// There is no way to suppress beforePrint/afterPrint events,
@ -2044,10 +1837,7 @@ const PDFViewerApplication = {
afterPrint() {
// Given that the "afterprint" browser event is synchronous, we
// unfortunately cannot await the scripting event dispatching here.
this._scriptingInstance?.scripting.dispatchEventInSandbox({
id: "doc",
name: "DidPrint",
});
this.pdfScriptingManager.dispatchDidPrint();
if (this.printService) {
this.printService.destroy();
@ -2302,7 +2092,7 @@ const PDFViewerApplication = {
* initialization of the scripting/sandbox.
*/
get scriptingReady() {
return this._scriptingInstance?.ready || false;
return this.pdfScriptingManager.ready;
},
};

View File

@ -55,6 +55,8 @@ const DEFAULT_CACHE_SIZE = 10;
* component.
* @property {PDFFindController} [findController] - The find controller
* component.
* @property {PDFScriptingManager} [scriptingManager] - The scripting manager
* component.
* @property {PDFRenderingQueue} [renderingQueue] - The rendering queue object.
* @property {boolean} [removePageBorders] - Removes the border shadow around
* the pages. The default value is `false`.
@ -77,10 +79,8 @@ const DEFAULT_CACHE_SIZE = 10;
* total pixels, i.e. width * height. Use -1 for no limit. The default value
* is 4096 * 4096 (16 mega-pixels).
* @property {IL10n} l10n - Localization service.
* @property {boolean} [enableScripting] - Enable embedded script execution.
* The default value is `false`.
* @property {Object} [mouseState] - The mouse button state. The default value
* is `null`.
* @property {boolean} [enableScripting] - Enable embedded script execution
* (also requires {scriptingManager} being set). The default value is `false`.
*/
function PDFPageViewBuffer(size) {
@ -183,6 +183,7 @@ class BaseViewer {
this.linkService = options.linkService || new SimpleLinkService();
this.downloadManager = options.downloadManager || null;
this.findController = options.findController || null;
this._scriptingManager = options.scriptingManager || null;
this.removePageBorders = options.removePageBorders || false;
this.textLayerMode = Number.isInteger(options.textLayerMode)
? options.textLayerMode
@ -195,8 +196,8 @@ class BaseViewer {
this.useOnlyCssZoom = options.useOnlyCssZoom || false;
this.maxCanvasPixels = options.maxCanvasPixels;
this.l10n = options.l10n || NullL10n;
this.enableScripting = options.enableScripting || false;
this._mouseState = options.mouseState || null;
this.enableScripting =
options.enableScripting === true && !!this._scriptingManager;
this.defaultRenderingQueue = !options.renderingQueue;
if (this.defaultRenderingQueue) {
@ -468,6 +469,12 @@ class BaseViewer {
if (this.findController) {
this.findController.setDocument(null);
}
if (this._scriptingManager) {
// Defer this slightly, to allow the "pageclose" event to be handled.
Promise.resolve().then(() => {
this._scriptingManager.setDocument(null);
});
}
}
this.pdfDocument = pdfDocument;
@ -562,6 +569,9 @@ class BaseViewer {
if (this.findController) {
this.findController.setDocument(pdfDocument); // Enable searching.
}
if (this.enableScripting) {
this._scriptingManager.setDocument(pdfDocument);
}
// In addition to 'disableAutoFetch' being set, also attempt to reduce
// resource usage when loading *very* long/large documents.
@ -1299,7 +1309,7 @@ class BaseViewer {
enableScripting,
hasJSActionsPromise:
hasJSActionsPromise || this.pdfDocument?.hasJSActions(),
mouseState: mouseState || this._mouseState,
mouseState: mouseState || this._scriptingManager?.mouseState,
});
}
@ -1645,7 +1655,7 @@ class BaseViewer {
}
const eventBus = this.eventBus,
pageOpenPendingSet = (this._pageOpenPendingSet = new Set()),
scriptingEvents = (this._scriptingEvents ||= Object.create(null));
scriptingEvents = (this._scriptingEvents = Object.create(null));
const dispatchPageClose = pageNumber => {
if (pageOpenPendingSet.has(pageNumber)) {
@ -1709,15 +1719,11 @@ class BaseViewer {
// Remove the event listeners.
eventBus._off("pagechanging", scriptingEvents.onPageChanging);
scriptingEvents.onPageChanging = null;
eventBus._off("pagerendered", scriptingEvents.onPageRendered);
scriptingEvents.onPageRendered = null;
eventBus._off("pagesdestroy", scriptingEvents.onPagesDestroy);
scriptingEvents.onPagesDestroy = null;
this._pageOpenPendingSet = null;
this._scriptingEvents = null;
}
}

View File

@ -13,8 +13,38 @@
* limitations under the License.
*/
import { getPDFFileNameFromURL } from "./ui_utils.js";
import { loadScript } from "pdfjs-lib";
async function docPropertiesLookup(pdfDocument) {
const url = "",
baseUrl = url.split("#")[0];
/* eslint-disable prefer-const */
let {
info,
metadata,
contentDispositionFilename,
contentLength,
} = await pdfDocument.getMetadata();
/* eslint-enable prefer-const */
if (!contentLength) {
const { length } = await pdfDocument.getDownloadInfo();
contentLength = length;
}
return {
...info,
baseURL: baseUrl,
filesize: contentLength,
filename: contentDispositionFilename || getPDFFileNameFromURL(url),
metadata: metadata?.getRaw(),
authors: metadata?.get("dc:creator"),
numPages: pdfDocument.numPages,
URL: url,
};
}
class GenericScripting {
constructor(sandboxBundleSrc) {
this._ready = loadScript(
@ -41,4 +71,4 @@ class GenericScripting {
}
}
export { GenericScripting };
export { docPropertiesLookup, GenericScripting };

View File

@ -105,7 +105,7 @@ class PDFPageView {
this.renderer = options.renderer || RendererType.CANVAS;
this.enableWebGL = options.enableWebGL || false;
this.l10n = options.l10n || NullL10n;
this.enableScripting = options.enableScripting || false;
this.enableScripting = options.enableScripting === true;
this.paintTask = null;
this.paintedViewportMap = new WeakMap();

View File

@ -0,0 +1,406 @@
/* Copyright 2021 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 { createPromiseCapability, shadow } from "pdfjs-lib";
import { apiPageLayoutToSpreadMode } from "./ui_utils.js";
/**
* @typedef {Object} PDFScriptingManagerOptions
* @property {EventBus} eventBus - The application event bus.
* @property {string} sandboxBundleSrc - The path and filename of the scripting
* bundle.
* @property {Object} [scriptingFactory] - The factory that is used when
* initializing scripting; must contain a `createScripting` method.
* PLEASE NOTE: Primarily intended for the default viewer use-case.
* @property {function} [docPropertiesLookup] - The function that is used to
* lookup the necessary document properties.
*/
class PDFScriptingManager {
/**
* @param {PDFScriptingManager} options
*/
constructor({
eventBus,
sandboxBundleSrc = null,
scriptingFactory = null,
docPropertiesLookup = null,
}) {
this._pdfDocument = null;
this._pdfViewer = null;
this._destroyCapability = null;
this._scripting = null;
this._mouseState = Object.create(null);
this._ready = false;
this._eventBus = eventBus;
this._sandboxBundleSrc = sandboxBundleSrc;
this._scriptingFactory = scriptingFactory;
this._docPropertiesLookup = docPropertiesLookup;
// The default viewer already handles adding/removing of DOM events,
// hence limit this to only the viewer components.
if (
typeof PDFJSDev !== "undefined" &&
PDFJSDev.test("COMPONENTS") &&
!this._scriptingFactory
) {
window.addEventListener("updatefromsandbox", event => {
this._eventBus.dispatch("updatefromsandbox", {
source: window,
detail: event.detail,
});
});
}
}
setViewer(pdfViewer) {
this._pdfViewer = pdfViewer;
}
async setDocument(pdfDocument) {
if (this._pdfDocument) {
await this._destroyScripting();
}
this._pdfDocument = pdfDocument;
if (!pdfDocument) {
return;
}
const [objects, calculationOrder, docActions] = await Promise.all([
pdfDocument.getFieldObjects(),
pdfDocument.getCalculationOrderIds(),
pdfDocument.getJSActions(),
]);
if (!objects && !docActions) {
// No FieldObjects or JavaScript actions were found in the document.
return;
}
if (pdfDocument !== this._pdfDocument) {
return; // The document was closed while the data resolved.
}
this._scripting = this._createScripting();
this._internalEvents.set("updatefromsandbox", event => {
this._updateFromSandbox(event.detail);
});
this._internalEvents.set("dispatcheventinsandbox", event => {
this._scripting?.dispatchEventInSandbox(event.detail);
});
this._internalEvents.set("pageopen", event => {
this._pageOpen(event.pageNumber, event.actionsPromise);
});
this._internalEvents.set("pageclose", event => {
this._pageClose(event.pageNumber);
});
this._domEvents.set("mousedown", event => {
this._mouseState.isDown = true;
});
this._domEvents.set("mouseup", event => {
this._mouseState.isDown = false;
});
for (const [name, listener] of this._internalEvents) {
this._eventBus._on(name, listener);
}
for (const [name, listener] of this._domEvents) {
window.addEventListener(name, listener);
}
try {
const docProperties = await this._getDocProperties();
if (pdfDocument !== this._pdfDocument) {
return; // The document was closed while the properties resolved.
}
await this._scripting.createSandbox({
objects,
calculationOrder,
appInfo: {
platform: navigator.platform,
language: navigator.language,
},
docInfo: {
...docProperties,
actions: docActions,
},
});
this._eventBus.dispatch("sandboxcreated", { source: this });
} catch (error) {
console.error(`PDFScriptingManager.setDocument: "${error?.message}".`);
await this._destroyScripting();
return;
}
await this._scripting?.dispatchEventInSandbox({
id: "doc",
name: "Open",
});
await this._pdfViewer.initializeScriptingEvents();
// Defer this slightly, to ensure that scripting is *fully* initialized.
Promise.resolve().then(() => {
if (pdfDocument === this._pdfDocument) {
this._ready = true;
}
});
}
async dispatchWillSave(detail) {
return this._scripting?.dispatchEventInSandbox({
id: "doc",
name: "WillSave",
});
}
async dispatchDidSave(detail) {
return this._scripting?.dispatchEventInSandbox({
id: "doc",
name: "DidSave",
});
}
async dispatchWillPrint(detail) {
return this._scripting?.dispatchEventInSandbox({
id: "doc",
name: "WillPrint",
});
}
async dispatchDidPrint(detail) {
return this._scripting?.dispatchEventInSandbox({
id: "doc",
name: "DidPrint",
});
}
get mouseState() {
return this._mouseState;
}
get destroyPromise() {
return this._destroyCapability?.promise || null;
}
get ready() {
return this._ready;
}
/**
* @private
*/
get _internalEvents() {
return shadow(this, "_internalEvents", new Map());
}
/**
* @private
*/
get _domEvents() {
return shadow(this, "_domEvents", new Map());
}
/**
* @private
*/
get _visitedPages() {
return shadow(this, "_visitedPages", new Map());
}
/**
* @private
*/
_updateFromSandbox(detail) {
const { id, command, value } = detail;
if (!id) {
switch (command) {
case "clear":
console.clear();
break;
case "error":
console.error(value);
break;
case "layout":
this._pdfViewer.spreadMode = apiPageLayoutToSpreadMode(value);
break;
case "page-num":
this._pdfViewer.currentPageNumber = value + 1;
break;
case "print":
this._pdfViewer.pagesPromise.then(() => {
this._eventBus.dispatch("print", { source: this });
});
break;
case "println":
console.log(value);
break;
case "zoom":
this._pdfViewer.currentScaleValue = value;
break;
}
return;
}
const element = document.getElementById(id);
if (element) {
element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail }));
} else {
if (value !== undefined && value !== null) {
// The element hasn't been rendered yet, use the AnnotationStorage.
this._pdfDocument?.annotationStorage.setValue(id, value);
}
}
}
/**
* @private
*/
async _pageOpen(pageNumber, actionsPromise) {
const pdfDocument = this._pdfDocument,
visitedPages = this._visitedPages;
visitedPages.set(
pageNumber,
(async () => {
// Avoid sending, and thus serializing, the `actions` data when the
// *same* page is opened several times.
let actions = null;
if (!visitedPages.has(pageNumber)) {
actions = await actionsPromise;
if (pdfDocument !== this._pdfDocument) {
return; // The document was closed while the actions resolved.
}
}
await this._scripting?.dispatchEventInSandbox({
id: "page",
name: "PageOpen",
pageNumber,
actions,
});
})()
);
}
/**
* @private
*/
async _pageClose(pageNumber) {
const pdfDocument = this._pdfDocument,
visitedPages = this._visitedPages;
const actionsPromise = visitedPages.get(pageNumber);
if (!actionsPromise) {
// Ensure that the "pageclose" event was preceded by a "pageopen" event.
return;
}
visitedPages.set(pageNumber, null);
// Ensure that the "pageopen" event is handled first.
await actionsPromise;
if (pdfDocument !== this._pdfDocument) {
return; // The document was closed while the actions resolved.
}
await this._scripting?.dispatchEventInSandbox({
id: "page",
name: "PageClose",
pageNumber,
});
}
/**
* @returns {Promise<Object>} A promise that is resolved with an {Object}
* containing the necessary document properties; please find the expected
* format in `PDFViewerApplication._scriptingDocProperties`.
* @private
*/
async _getDocProperties() {
// The default viewer use-case.
if (this._docPropertiesLookup) {
return this._docPropertiesLookup(this._pdfDocument);
}
// Fallback, to support the viewer components use-case.
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) {
const { docPropertiesLookup } = require("./generic_scripting.js");
return docPropertiesLookup(this._pdfDocument);
}
throw new Error("_getDocProperties: Unable to lookup properties.");
}
/**
* @private
*/
_createScripting() {
this._destroyCapability = createPromiseCapability();
if (this._scripting) {
throw new Error("_createScripting: Scripting already exists.");
}
if (this._scriptingFactory) {
return this._scriptingFactory.createScripting({
sandboxBundleSrc: this._sandboxBundleSrc,
});
}
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) {
const { GenericScripting } = require("./generic_scripting.js");
return new GenericScripting(this._sandboxBundleSrc);
}
throw new Error("_createScripting: Cannot create scripting.");
}
/**
* @private
*/
async _destroyScripting() {
this._pdfDocument = null; // Ensure that it's *always* reset synchronously.
if (!this._scripting) {
this._destroyCapability?.resolve();
return;
}
try {
await this._scripting.destroySandbox();
} catch (ex) {}
for (const [name, listener] of this._internalEvents) {
this._eventBus._off(name, listener);
}
this._internalEvents.clear();
for (const [name, listener] of this._domEvents) {
window.removeEventListener(name, listener);
}
this._domEvents.clear();
this._visitedPages.clear();
this._scripting = null;
delete this._mouseState.isDown;
this._ready = false;
this._destroyCapability?.resolve();
}
}
export { PDFScriptingManager };

View File

@ -29,6 +29,7 @@ import { NullL10n } from "./l10n_utils.js";
import { PDFFindController } from "./pdf_find_controller.js";
import { PDFHistory } from "./pdf_history.js";
import { PDFPageView } from "./pdf_page_view.js";
import { PDFScriptingManager } from "./pdf_scripting_manager.js";
import { PDFSinglePageViewer } from "./pdf_single_page_viewer.js";
import { PDFViewer } from "./pdf_viewer.js";
@ -49,6 +50,7 @@ export {
PDFHistory,
PDFLinkService,
PDFPageView,
PDFScriptingManager,
PDFSinglePageViewer,
PDFViewer,
ProgressBar,