123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- /* 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.
- */
- /** @typedef {import("./event_utils").EventBus} EventBus */
- import { apiPageLayoutToViewerModes, RenderingStates } from "./ui_utils.js";
- import { createPromiseCapability, shadow } from "pdfjs-lib";
- /**
- * @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 {PDFScriptingManagerOptions} options
- */
- constructor({
- eventBus,
- sandboxBundleSrc = null,
- scriptingFactory = null,
- docPropertiesLookup = null,
- }) {
- this._pdfDocument = null;
- this._pdfViewer = null;
- this._closeCapability = null;
- this._destroyCapability = null;
- this._scripting = 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.
- await this._destroyScripting();
- return;
- }
- if (pdfDocument !== this._pdfDocument) {
- return; // The document was closed while the data resolved.
- }
- try {
- this._scripting = this._createScripting();
- } catch (error) {
- console.error(`PDFScriptingManager.setDocument: "${error?.message}".`);
- await this._destroyScripting();
- return;
- }
- this._internalEvents.set("updatefromsandbox", event => {
- if (event?.source !== window) {
- return;
- }
- this._updateFromSandbox(event.detail);
- });
- this._internalEvents.set("dispatcheventinsandbox", event => {
- this._scripting?.dispatchEventInSandbox(event.detail);
- });
- this._internalEvents.set("pagechanging", ({ pageNumber, previous }) => {
- if (pageNumber === previous) {
- return; // The current page didn't change.
- }
- this._dispatchPageClose(previous);
- this._dispatchPageOpen(pageNumber);
- });
- this._internalEvents.set("pagerendered", ({ pageNumber }) => {
- if (!this._pageOpenPending.has(pageNumber)) {
- return; // No pending "PageOpen" event for the newly rendered page.
- }
- if (pageNumber !== this._pdfViewer.currentPageNumber) {
- return; // The newly rendered page is no longer the current one.
- }
- this._dispatchPageOpen(pageNumber);
- });
- this._internalEvents.set("pagesdestroy", async event => {
- await this._dispatchPageClose(this._pdfViewer.currentPageNumber);
- await this._scripting?.dispatchEventInSandbox({
- id: "doc",
- name: "WillClose",
- });
- this._closeCapability?.resolve();
- });
- for (const [name, listener] of this._internalEvents) {
- this._eventBus._on(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._dispatchPageOpen(
- this._pdfViewer.currentPageNumber,
- /* initialize = */ true
- );
- // 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 destroyPromise() {
- return this._destroyCapability?.promise || null;
- }
- get ready() {
- return this._ready;
- }
- /**
- * @private
- */
- get _internalEvents() {
- return shadow(this, "_internalEvents", new Map());
- }
- /**
- * @private
- */
- get _pageOpenPending() {
- return shadow(this, "_pageOpenPending", new Set());
- }
- /**
- * @private
- */
- get _visitedPages() {
- return shadow(this, "_visitedPages", new Map());
- }
- /**
- * @private
- */
- async _updateFromSandbox(detail) {
- // Ignore some events, see below, that don't make sense in PresentationMode.
- const isInPresentationMode =
- this._pdfViewer.isInPresentationMode ||
- this._pdfViewer.isChangingPresentationMode;
- const { id, siblings, command, value } = detail;
- if (!id) {
- switch (command) {
- case "clear":
- console.clear();
- break;
- case "error":
- console.error(value);
- break;
- case "layout": {
- // NOTE: Always ignore the pageLayout in GeckoView since there's
- // no UI available to change Scroll/Spread modes for the user.
- if (
- (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GECKOVIEW")) ||
- isInPresentationMode
- ) {
- return;
- }
- const modes = apiPageLayoutToViewerModes(value);
- this._pdfViewer.spreadMode = modes.spreadMode;
- break;
- }
- case "page-num":
- this._pdfViewer.currentPageNumber = value + 1;
- break;
- case "print":
- await this._pdfViewer.pagesPromise;
- this._eventBus.dispatch("print", { source: this });
- break;
- case "println":
- console.log(value);
- break;
- case "zoom":
- if (isInPresentationMode) {
- return;
- }
- this._pdfViewer.currentScaleValue = value;
- break;
- case "SaveAs":
- this._eventBus.dispatch("download", { source: this });
- break;
- case "FirstPage":
- this._pdfViewer.currentPageNumber = 1;
- break;
- case "LastPage":
- this._pdfViewer.currentPageNumber = this._pdfViewer.pagesCount;
- break;
- case "NextPage":
- this._pdfViewer.nextPage();
- break;
- case "PrevPage":
- this._pdfViewer.previousPage();
- break;
- case "ZoomViewIn":
- if (isInPresentationMode) {
- return;
- }
- this._pdfViewer.increaseScale();
- break;
- case "ZoomViewOut":
- if (isInPresentationMode) {
- return;
- }
- this._pdfViewer.decreaseScale();
- break;
- }
- return;
- }
- if (isInPresentationMode) {
- if (detail.focus) {
- return;
- }
- }
- delete detail.id;
- delete detail.siblings;
- const ids = siblings ? [id, ...siblings] : [id];
- for (const elementId of ids) {
- const element = document.querySelector(
- `[data-element-id="${elementId}"]`
- );
- if (element) {
- element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail }));
- } else {
- // The element hasn't been rendered yet, use the AnnotationStorage.
- this._pdfDocument?.annotationStorage.setValue(elementId, detail);
- }
- }
- }
- /**
- * @private
- */
- async _dispatchPageOpen(pageNumber, initialize = false) {
- const pdfDocument = this._pdfDocument,
- visitedPages = this._visitedPages;
- if (initialize) {
- this._closeCapability = createPromiseCapability();
- }
- if (!this._closeCapability) {
- return; // Scripting isn't fully initialized yet.
- }
- const pageView = this._pdfViewer.getPageView(/* index = */ pageNumber - 1);
- if (pageView?.renderingState !== RenderingStates.FINISHED) {
- this._pageOpenPending.add(pageNumber);
- return; // Wait for the page to finish rendering.
- }
- this._pageOpenPending.delete(pageNumber);
- const actionsPromise = (async () => {
- // Avoid sending, and thus serializing, the `actions` data more than once.
- const actions = await (!visitedPages.has(pageNumber)
- ? pageView.pdfPage?.getJSActions()
- : null);
- if (pdfDocument !== this._pdfDocument) {
- return; // The document was closed while the actions resolved.
- }
- await this._scripting?.dispatchEventInSandbox({
- id: "page",
- name: "PageOpen",
- pageNumber,
- actions,
- });
- })();
- visitedPages.set(pageNumber, actionsPromise);
- }
- /**
- * @private
- */
- async _dispatchPageClose(pageNumber) {
- const pdfDocument = this._pdfDocument,
- visitedPages = this._visitedPages;
- if (!this._closeCapability) {
- return; // Scripting isn't fully initialized yet.
- }
- if (this._pageOpenPending.has(pageNumber)) {
- return; // The page is still rendering; no "PageOpen" event dispatched.
- }
- const actionsPromise = visitedPages.get(pageNumber);
- if (!actionsPromise) {
- return; // The "PageClose" event must be preceded by a "PageOpen" event.
- }
- visitedPages.set(pageNumber, null);
- // Ensure that the "PageOpen" event is dispatched 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() {
- if (this._docPropertiesLookup) {
- return this._docPropertiesLookup(this._pdfDocument);
- }
- 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() {
- if (!this._scripting) {
- this._pdfDocument = null;
- this._destroyCapability?.resolve();
- return;
- }
- if (this._closeCapability) {
- await Promise.race([
- this._closeCapability.promise,
- new Promise(resolve => {
- // Avoid the scripting/sandbox-destruction hanging indefinitely.
- setTimeout(resolve, 1000);
- }),
- ]).catch(reason => {
- // Ignore any errors, to ensure that the sandbox is always destroyed.
- });
- this._closeCapability = null;
- }
- this._pdfDocument = null;
- try {
- await this._scripting.destroySandbox();
- } catch (ex) {}
- for (const [name, listener] of this._internalEvents) {
- this._eventBus._off(name, listener);
- }
- this._internalEvents.clear();
- this._pageOpenPending.clear();
- this._visitedPages.clear();
- this._scripting = null;
- this._ready = false;
- this._destroyCapability?.resolve();
- }
- }
- export { PDFScriptingManager };
|