diff --git a/src/core/annotation.js b/src/core/annotation.js index 0ae082aea..3a811c59f 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -2887,10 +2887,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL); // Check if we have a date or time. - const { - data: { actions }, - } = this; - + const { actions } = this.data; if (!actions) { return; } @@ -2898,32 +2895,35 @@ class TextWidgetAnnotation extends WidgetAnnotation { const AFDateTime = /^AF(Date|Time)_(?:Keystroke|Format)(?:Ex)?\(['"]?([^'"]+)['"]?\);$/; let canUseHTMLDateTime = false; + + const aFormat = actions.get("Format"), + aKeystroke = actions.get("Keystroke"); if ( - (actions.Format?.length === 1 && - actions.Keystroke?.length === 1 && - AFDateTime.test(actions.Format[0]) && - AFDateTime.test(actions.Keystroke[0])) || - (actions.Format?.length === 0 && - actions.Keystroke?.length === 1 && - AFDateTime.test(actions.Keystroke[0])) || - (actions.Keystroke?.length === 0 && - actions.Format?.length === 1 && - AFDateTime.test(actions.Format[0])) + (aFormat?.length === 1 && + aKeystroke?.length === 1 && + AFDateTime.test(aFormat[0]) && + AFDateTime.test(aKeystroke[0])) || + (aFormat?.length === 0 && + aKeystroke?.length === 1 && + AFDateTime.test(aKeystroke[0])) || + (aKeystroke?.length === 0 && + aFormat?.length === 1 && + AFDateTime.test(aFormat[0])) ) { // If the Format and Keystroke actions are the same, we can just use // the Format action. canUseHTMLDateTime = true; } const actionsToVisit = []; - if (actions.Format) { - actionsToVisit.push(...actions.Format); + if (aFormat) { + actionsToVisit.push(...aFormat); } - if (actions.Keystroke) { - actionsToVisit.push(...actions.Keystroke); + if (aKeystroke) { + actionsToVisit.push(...aKeystroke); } if (canUseHTMLDateTime) { - delete actions.Keystroke; - actions.Format = actionsToVisit; + actions.delete("Keystroke"); + actions.set("Format", actionsToVisit); } for (const formatAction of actionsToVisit) { diff --git a/src/core/catalog.js b/src/core/catalog.js index d8c3552ac..984154005 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -19,6 +19,7 @@ import { DocumentActionEventType, FormatError, info, + makeArr, PermissionFlag, shadow, stringToUTF8String, @@ -1287,10 +1288,10 @@ class Catalog { ); if (javaScript) { - actions ??= Object.create(null); + actions ??= new Map(); for (const [key, val] of javaScript) { - (actions[key] ??= []).push(val); + actions.getOrInsertComputed(key, makeArr).push(val); } } return shadow(this, "jsActions", actions); diff --git a/src/core/core_utils.js b/src/core/core_utils.js index 4b5f5d905..c7d4b949d 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -449,7 +449,7 @@ function _collectJS(entry, xref, list, parents) { } function collectActions(xref, dict, eventType) { - const actions = Object.create(null); + const actions = new Map(); const additionalActionsDicts = getInheritableProperty({ dict, key: "AA", @@ -475,7 +475,7 @@ function collectActions(xref, dict, eventType) { const list = []; _collectJS(rawActionDict, xref, list, parents); if (list.length > 0) { - actions[action] = list; + actions.set(action, list); } } } @@ -487,10 +487,10 @@ function collectActions(xref, dict, eventType) { const list = []; _collectJS(actionDict, xref, list, parents); if (list.length > 0) { - actions.Action = list; + actions.set("Action", list); } } - return Object.keys(actions).length ? actions : null; + return actions.size ? actions : null; } const XMLEntities = { diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 80642ee5a..b584f6da6 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -1012,9 +1012,9 @@ class LinkAnnotationElement extends AnnotationElement { } else { if ( data.actions && - (data.actions.Action || - data.actions["Mouse Up"] || - data.actions["Mouse Down"]) && + (data.actions.has("Action") || + data.actions.has("Mouse Up") || + data.actions.has("Mouse Down")) && this.enableScripting && this.hasJSActions ) { @@ -1158,14 +1158,14 @@ class LinkAnnotationElement extends AnnotationElement { * @param {Object} data * @memberof LinkAnnotationElement */ - _bindJSAction(link, data) { + _bindJSAction(link, { actions, id, overlaidText }) { link.href = this.linkService.getAnchorUrl(""); const map = new Map([ ["Action", "onclick"], ["Mouse Up", "onmouseup"], ["Mouse Down", "onmousedown"], ]); - for (const name of Object.keys(data.actions)) { + for (const name of actions.keys()) { const jsName = map.get(name); if (!jsName) { continue; @@ -1173,16 +1173,13 @@ class LinkAnnotationElement extends AnnotationElement { link[jsName] = () => { this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, - detail: { - id: data.id, - name, - }, + detail: { id, name }, }); return false; }; } - if (data.overlaidText) { - link.title = data.overlaidText; + if (overlaidText) { + link.title = overlaidText; } link.onclick ||= () => false; @@ -1379,8 +1376,10 @@ class WidgetAnnotationElement extends AnnotationElement { } _setEventListeners(element, elementData, names, getter) { + const { actions } = this.data; + for (const [baseName, eventName] of names) { - if (eventName === "Action" || this.data.actions?.[eventName]) { + if (eventName === "Action" || actions?.has(eventName)) { if (eventName === "Focus" || eventName === "Blur") { elementData ||= { focused: false }; } @@ -1391,10 +1390,10 @@ class WidgetAnnotationElement extends AnnotationElement { eventName, getter ); - if (eventName === "Focus" && !this.data.actions?.Blur) { + if (eventName === "Focus" && !actions?.has("Blur")) { // Ensure that elementData will have the correct value. this._setEventListener(element, elementData, "blur", "Blur", null); - } else if (eventName === "Blur" && !this.data.actions?.Focus) { + } else if (eventName === "Blur" && !actions?.has("Focus")) { this._setEventListener(element, elementData, "focus", "Focus", null); } } @@ -1630,7 +1629,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { } elementData.lastCommittedValue = target.value; elementData.commitKey = 1; - if (!this.data.actions?.Focus) { + if (!this.data.actions?.has("Focus")) { elementData.focused = true; } }); @@ -1752,7 +1751,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { if (!elementData.focused || !event.relatedTarget) { return; } - if (!this.data.actions?.Blur) { + if (!this.data.actions?.has("Blur")) { elementData.focused = false; } const { target } = event; @@ -1800,7 +1799,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { _blurListener(event); }); - if (this.data.actions?.Keystroke) { + if (this.data.actions?.has("Keystroke")) { element.addEventListener("beforeinput", event => { elementData.lastCommittedValue = null; const { data, target } = event; diff --git a/src/display/api.js b/src/display/api.js index a6cfbd4f3..bd4af17a2 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -867,8 +867,8 @@ class PDFDocumentProxy { } /** - * @returns {Promise} A promise that is resolved with - * an {Object} with the JavaScript actions: + * @returns {Promise} A promise that is resolved with a {Map} with + * the JavaScript actions: * - from the name tree. * - from A or AA entries in the catalog dictionary. * , or `null` if no JavaScript exists. @@ -1429,8 +1429,8 @@ class PDFPageProxy { } /** - * @returns {Promise} A promise that is resolved with an - * {Object} with JS actions. + * @returns {Promise} A promise that is resolved with a {Map} with + * the JavaScript actions, or `null` if no JavaScript exists. */ getJSActions() { return this._transport.getPageJSActions(this._pageIndex); diff --git a/src/pdf.sandbox.js b/src/pdf.sandbox.js index 601214b29..bc3088224 100644 --- a/src/pdf.sandbox.js +++ b/src/pdf.sandbox.js @@ -19,7 +19,9 @@ class SandboxSupport extends SandboxSupportBase { exportValueToSandbox(val) { // The communication with the Quickjs sandbox is based on strings // So we use JSON.stringfy to serialize - return JSON.stringify(val); + return JSON.stringify(val, (k, v) => + v instanceof Map ? Object.fromEntries(v) : v + ); } importValueFromSandbox(val) { @@ -65,7 +67,7 @@ class Sandbox { let success = false; let buf = 0; try { - const sandboxData = JSON.stringify(data); + const sandboxData = this.support.exportValueToSandbox(data); // "pdfjsScripting.initSandbox..." MUST be the last line to be evaluated // since the returned value is used for the communication. code.push(`pdfjsScripting.initSandbox({ data: ${sandboxData} })`); diff --git a/src/scripting_api/common.js b/src/scripting_api/common.js index aa1b4c054..d6f9fed0b 100644 --- a/src/scripting_api/common.js +++ b/src/scripting_api/common.js @@ -22,7 +22,9 @@ const FieldType = { }; function createActionsMap(actions) { - return new Map(actions ? Object.entries(actions) : null); + return actions instanceof Map + ? actions + : new Map(actions ? Object.entries(actions) : null); } function getFieldType(actions) { @@ -30,10 +32,8 @@ function getFieldType(actions) { if (!format) { return FieldType.none; } + format = format[0].trim(); - format = format[0]; - - format = format.trim(); if (format.startsWith("AFNumber_")) { return FieldType.number; } diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 5b9c66d2b..3c10aa728 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -173,9 +173,9 @@ class Doc extends PDFObject { _dispatchPageEvent(name, actions, pageNumber) { if (name === "PageOpen") { this.#pageActions ??= new Map(); - if (!this.#pageActions.has(pageNumber)) { - this.#pageActions.set(pageNumber, createActionsMap(actions)); - } + this.#pageActions.getOrInsertComputed(pageNumber, () => + createActionsMap(actions) + ); this._pageNum = pageNumber - 1; } diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 6c6c8250c..a6a966161 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -2460,17 +2460,14 @@ describe("annotation", function () { annotationGlobalsMock, idFactoryMock ); - const fieldObject = await annotation.getFieldObject(); - const actions = fieldObject.actions; - expect(actions["Mouse Enter"]).toEqual(["hello()"]); - expect(actions["Mouse Exit"]).toEqual([ - "world()", - "olleh()", - "foo()", - "dlrow()", - "oof()", - ]); - expect(actions["Mouse Down"]).toEqual(["bar()"]); + const { actions } = await annotation.getFieldObject(); + expect(actions).toEqual( + new Map([ + ["Mouse Enter", ["hello()"]], + ["Mouse Exit", ["world()", "olleh()", "foo()", "dlrow()", "oof()"]], + ["Mouse Down", ["bar()"]], + ]) + ); }); it("should save Japanese text", async function () { @@ -3728,7 +3725,7 @@ describe("annotation", function () { ); expect(data.annotationType).toEqual(AnnotationType.WIDGET); expect(data.pushButton).toBeTrue(); - expect(data.actions.Action).toEqual(["do_something();"]); + expect(data.actions.get("Action")).toEqual(["do_something();"]); }); it("should handle push buttons that act as a tooltip only", async function () { diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 28551f2a2..2ae2b17d0 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -986,9 +986,9 @@ describe("api", function () { expect(pdfDocument.numPages).toEqual(1); const jsActions = await pdfDocument.getJSActions(); - expect(jsActions).toEqual({ - OpenAction: ["func=function(){app.alert(1)};func();"], - }); + expect(jsActions).toEqual( + new Map([["OpenAction", ["func=function(){app.alert(1)};func();"]]]) + ); const page = await pdfDocument.getPage(1); expect(page).toBeInstanceOf(PDFPageProxy); @@ -1943,12 +1943,17 @@ describe("api", function () { // PDF document with "JavaScript" action in the OpenAction dictionary. const loadingTask = getDocument(buildGetDocumentParams("issue6106.pdf")); const pdfDoc = await loadingTask.promise; - const { OpenAction } = await pdfDoc.getJSActions(); + const jsActions = await pdfDoc.getJSActions(); - expect(OpenAction).toEqual([ - "this.print({bUI:true,bSilent:false,bShrinkToFit:true});", - ]); - expect(OpenAction[0]).toMatch(AutoPrintRegExp); + expect(jsActions).toEqual( + new Map([ + [ + "OpenAction", + ["this.print({bUI:true,bSilent:false,bShrinkToFit:true});"], + ], + ]) + ); + expect(jsActions.get("OpenAction")[0]).toMatch(AutoPrintRegExp); await loadingTask.destroy(); }); @@ -1988,21 +1993,27 @@ describe("api", function () { const page3 = await pdfDoc.getPage(3); const page3Actions = await page3.getJSActions(); - expect(docActions).toEqual({ - DidPrint: [`this.getField("Text2").value = "DidPrint";`], - DidSave: [`this.getField("Text2").value = "DidSave";`], - WillClose: [`this.getField("Text1").value = "WillClose";`], - WillPrint: [`this.getField("Text1").value = "WillPrint";`], - WillSave: [`this.getField("Text1").value = "WillSave";`], - }); - expect(page1Actions).toEqual({ - PageOpen: [`this.getField("Text1").value = "PageOpen 1";`], - PageClose: [`this.getField("Text2").value = "PageClose 1";`], - }); - expect(page3Actions).toEqual({ - PageOpen: [`this.getField("Text5").value = "PageOpen 3";`], - PageClose: [`this.getField("Text6").value = "PageClose 3";`], - }); + expect(docActions).toEqual( + new Map([ + ["DidPrint", [`this.getField("Text2").value = "DidPrint";`]], + ["DidSave", [`this.getField("Text2").value = "DidSave";`]], + ["WillClose", [`this.getField("Text1").value = "WillClose";`]], + ["WillPrint", [`this.getField("Text1").value = "WillPrint";`]], + ["WillSave", [`this.getField("Text1").value = "WillSave";`]], + ]) + ); + expect(page1Actions).toEqual( + new Map([ + ["PageOpen", [`this.getField("Text1").value = "PageOpen 1";`]], + ["PageClose", [`this.getField("Text2").value = "PageClose 1";`]], + ]) + ); + expect(page3Actions).toEqual( + new Map([ + ["PageOpen", [`this.getField("Text5").value = "PageOpen 3";`]], + ["PageClose", [`this.getField("Text6").value = "PageClose 3";`]], + ]) + ); await loadingTask.destroy(); }); @@ -2051,11 +2062,14 @@ describe("api", function () { name: "Button1", rect: [455.436, 719.678, 527.436, 739.678], hidden: false, - actions: { - Action: [ - `this.getField("Text1").value = this.info.authors.join("::");`, + actions: new Map([ + [ + "Action", + [ + `this.getField("Text1").value = this.info.authors.join("::");`, + ], ], - }, + ]), page: 0, strokeColor: null, fillColor: new Uint8ClampedArray([192, 192, 192]),