From 6523f8880bd91cf1f8ccbf0215dd7e1ea1590e9f Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 5 Jan 2021 15:53:30 +0100 Subject: [PATCH] JS -- Plug PageOpen and PageClose actions --- src/display/api.js | 5 +- src/scripting_api/doc.js | 23 +++++--- src/scripting_api/event.js | 5 +- test/integration/scripting_spec.js | 95 +++++++++++++++++++++++++++++- web/app.js | 60 +++++++++++++++++++ 5 files changed, 172 insertions(+), 16 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 48de0601c4f48..40870c5c2b393 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1136,9 +1136,8 @@ class PDFPageProxy { } /** - * @param {GetAnnotationsParameters} params - Annotation parameters. - * @returns {Promise>} A promise that is resolved with an - * {Array} of the annotation objects. + * @returns {Promise} A promise that is resolved with an + * {Object} with JS actions. */ getJSActions() { if (!this._jsActionsPromise) { diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index e7a10a13da3b2..063c4cf7e7816 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -91,6 +91,7 @@ class Doc extends PDFObject { this._zoom = data.zoom || 100; this._actions = createActionsMap(data.actions); this._globalEval = data.globalEval; + this._pageActions = new Map(); } _dispatchDocEvent(name) { @@ -114,22 +115,28 @@ class Doc extends PDFObject { } } - _dispatchPageEvent(name, action, pageNumber) { + _dispatchPageEvent(name, actions, pageNumber) { if (name === "PageOpen") { + if (!this._pageActions.has(pageNumber)) { + this._pageActions.set(pageNumber, createActionsMap(actions)); + } this._pageNum = pageNumber - 1; } - this._globalEval(action); + actions = this._pageActions.get(pageNumber)?.get(name); + if (actions) { + for (const action of actions) { + this._globalEval(action); + } + } } _runActions(name) { - if (!this._actions.has(name)) { - return; - } - const actions = this._actions.get(name); - for (const action of actions) { - this._globalEval(action); + if (actions) { + for (const action of actions) { + this._globalEval(action); + } } } diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index ab95eb3c86430..5e62bffa54639 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -73,11 +73,10 @@ class EventDispatcher { } if (id === "doc") { this._document.obj._dispatchDocEvent(event.name); - } - if (id === "page") { + } else if (id === "page") { this._document.obj._dispatchPageEvent( event.name, - baseEvent.action, + baseEvent.actions, baseEvent.pageNumber ); } diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index 490eedb745cd7..01f6e7ea5d47c 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -16,8 +16,10 @@ const { clearInput, closePages, loadAndWait } = require("./test_utils.js"); describe("Interaction", () => { - async function actAndWaitForInput(page, selector, action) { - await clearInput(page, selector); + async function actAndWaitForInput(page, selector, action, clear = true) { + if (clear) { + await clearInput(page, selector); + } await action(); await page.waitForFunction( `document.querySelector("${selector.replace("\\", "\\\\")}").value !== ""` @@ -307,6 +309,18 @@ describe("Interaction", () => { if (process.platform === "win32" && browserName === "firefox") { pending("Disabled in Firefox on Windows, because of bug 1662471."); } + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await clearInput(page, "#\\34 7R"); + await page.evaluate(_ => { + window.document.activeElement.blur(); + }); + await page.waitForFunction( + `document.querySelector("#\\\\34 7R").value === ""` + ); + let text = await actAndWaitForInput(page, "#\\34 7R", async () => { await page.click("#print"); }); @@ -337,6 +351,10 @@ describe("Interaction", () => { it("must execute WillSave and DidSave actions", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + try { // Disable download in chrome // (it leads to an error in firefox so the try...) @@ -345,6 +363,13 @@ describe("Interaction", () => { }); } catch (_) {} await clearInput(page, "#\\34 7R"); + await page.evaluate(_ => { + window.document.activeElement.blur(); + }); + await page.waitForFunction( + `document.querySelector("#\\\\34 7R").value === ""` + ); + let text = await actAndWaitForInput(page, "#\\34 7R", async () => { await page.click("#download"); }); @@ -360,4 +385,70 @@ describe("Interaction", () => { ); }); }); + + describe("in doc_actions.pdf for page actions", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("doc_actions.pdf", "#\\34 7R"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must execute PageOpen and PageClose actions", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + let text = await page.$eval("#\\34 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("PageOpen 1"); + + for (let run = 0; run < 5; run++) { + for (const ref of [18, 19, 20, 21, 47, 50]) { + await page.evaluate(refElem => { + const element = window.document.getElementById(`${refElem}R`); + if (element) { + element.value = ""; + } + }, ref); + } + + for (const [refOpen, refClose, pageNumOpen, pageNumClose] of [ + [18, 50, 2, 1], + [21, 19, 3, 2], + [47, 20, 1, 3], + ]) { + text = await actAndWaitForInput( + page, + `#\\3${Math.floor(refOpen / 10)} ${refOpen % 10}R`, + async () => { + await page.evaluate(refElem => { + window.document + .getElementById(`${refElem}R`) + .scrollIntoView(); + }, refOpen); + }, + false + ); + expect(text) + .withContext(`In ${browserName}`) + .toEqual(`PageOpen ${pageNumOpen}`); + + text = await page.$eval( + `#\\3${Math.floor(refClose / 10)} ${refClose % 10}R`, + el => el.value + ); + expect(text) + .withContext(`In ${browserName}`) + .toEqual(`PageClose ${pageNumClose}`); + } + } + }) + ); + }); + }); }); diff --git a/web/app.js b/web/app.js index 1c44dc54813fe..0c87dba7c3738 100644 --- a/web/app.js +++ b/web/app.js @@ -1542,6 +1542,64 @@ const PDFViewerApplication = { }; internalEvents.set("updatefromsandbox", updateFromSandbox); + const visitedPages = new Map(); + const pageOpen = ({ pageNumber }) => { + visitedPages.set( + pageNumber, + (async () => { + // Avoid sending, and thus serializing, the `actions` data + // when the same page is open several times. + let actions = null; + if (!visitedPages.has(pageNumber)) { + // visitedPages doesn't contain pageNumber: first visit. + + const pageView = this.pdfViewer.getPageView( + /* index = */ pageNumber - 1 + ); + if (pageView?.pdfPage) { + actions = await pageView.pdfPage.getJSActions(); + } else { + actions = await pdfDocument.getPage(pageNumber).getJSActions(); + } + + if (pdfDocument !== this.pdfDocument) { + return; // The document was closed while the actions resolved. + } + } + + this._scriptingInstance?.scripting.dispatchEventInSandbox({ + id: "page", + name: "PageOpen", + pageNumber, + actions, + }); + })() + ); + }; + + const pageClose = async ({ pageNumber }) => { + const promise = visitedPages.get(pageNumber); + if (!promise) { + return; + } + visitedPages.set(pageNumber, null); + + // Wait for PageOpen has been sent. + await promise; + + if (pdfDocument !== this.pdfDocument) { + return; // The document was closed while the actions resolved. + } + + this._scriptingInstance?.scripting.dispatchEventInSandbox({ + id: "page", + name: "PageClose", + pageNumber, + }); + }; + internalEvents.set("pageopen", pageOpen); + internalEvents.set("pageclose", pageClose); + const dispatchEventInSandbox = ({ detail }) => { scripting.dispatchEventInSandbox(detail); }; @@ -1613,6 +1671,8 @@ const PDFViewerApplication = { name: "Open", }); + await pageOpen({ pageNumber: this.pdfViewer.currentPageNumber }); + // 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.)