[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:
parent
5b9638329c
commit
a6d1cba38c
@ -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.
|
||||
|
@ -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.
|
||||
|
282
web/app.js
282
web/app.js
@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 };
|
||||
|
@ -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();
|
||||
|
406
web/pdf_scripting_manager.js
Normal file
406
web/pdf_scripting_manager.js
Normal 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 };
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user