diff --git a/pdfjs.config b/pdfjs.config index 780d4dbf3..6bb72df09 100644 --- a/pdfjs.config +++ b/pdfjs.config @@ -1,5 +1,5 @@ { "stableVersion": "6.2.108", - "baseVersion": "eddd70a2ca1054ad2e0792972c3f2774b89f0cd2", - "versionPrefix": "6.2." + "baseVersion": "ce4ff55faaa83b39b0137dc458af6eea6f96235f", + "versionPrefix": "6.3." } diff --git a/src/core/document.js b/src/core/document.js index de88df870..de8a5ffc7 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -1961,7 +1961,7 @@ class PDFDocument { const { acroForm } = annotationGlobals; const visitedRefs = new RefSet(); - const allFields = Object.create(null); + const allFields = new Map(); const fieldPromises = new Map(); const orphanFields = new RefSetCache(); for (const fieldRef of acroForm.get("Fields")) { @@ -1982,7 +1982,7 @@ class PDFDocument { Promise.all(promises).then(fields => { fields = fields.filter(field => !!field); if (fields.length > 0) { - allFields[name] = fields; + allFields.set(name, fields); } }) ); @@ -1990,7 +1990,7 @@ class PDFDocument { await Promise.all(allPromises); return { - allFields: Object.keys(allFields).length ? allFields : null, + allFields: allFields.size ? allFields : null, orphanFields, }; }); @@ -2268,9 +2268,9 @@ class PDFDocument { return true; } if (fieldObjects?.allFields) { - return Object.values(fieldObjects.allFields).some(fieldObject => - fieldObject.some(object => object.actions !== null) - ); + return fieldObjects.allFields + .values() + .some(fieldObj => fieldObj.some(obj => obj.actions !== null)); } return false; } diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 6a1661974..c265c67de 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -75,7 +75,7 @@ const TIMEZONE_OFFSET = new Date().getTimezoneOffset() * 60 * 1000; * @property {Object} svgFactory * @property {boolean} [enableScripting] * @property {boolean} [hasJSActions] - * @property {Object} [fieldObjects] + * @property {Map} [fieldObjects] */ class AnnotationElementFactory { @@ -803,7 +803,7 @@ class AnnotationElement { const fields = []; if (this._fieldObjects) { - const fieldObj = this._fieldObjects[name] || []; + const fieldObj = this._fieldObjects.get(name) || []; for (const { page, id, exportValues } of fieldObj) { if (page === -1) { @@ -1217,12 +1217,12 @@ class LinkAnnotationElement extends AnnotationElement { if (resetFormFields.length !== 0 || resetFormRefs.length !== 0) { const fieldIds = new Set(resetFormRefs); for (const fieldName of resetFormFields) { - const fields = this._fieldObjects[fieldName] || []; + const fields = this._fieldObjects.get(fieldName) || []; for (const { id } of fields) { fieldIds.add(id); } } - for (const fields of Object.values(this._fieldObjects)) { + for (const fields of this._fieldObjects.values()) { for (const field of fields) { if (fieldIds.has(field.id) === include) { allFields.push(field); @@ -1230,7 +1230,7 @@ class LinkAnnotationElement extends AnnotationElement { } } } else { - for (const fields of Object.values(this._fieldObjects)) { + for (const fields of this._fieldObjects.values()) { allFields.push(...fields); } } @@ -3953,7 +3953,7 @@ class MediaAnnotationElement extends AnnotationElement { * @property {boolean} [enableScripting] - Enable embedded script execution. * @property {boolean} [hasJSActions] - Some fields have JS actions. * The default value is `false`. - * @property {Object> | null} [fieldObjects] + * @property {Map> | null} [fieldObjects] * @property {Map} [annotationCanvasMap] * @property {TextAccessibilityManager} [accessibilityManager] * @property {AnnotationEditorUIManager} [annotationEditorUIManager] diff --git a/src/display/api.js b/src/display/api.js index bd4af17a2..908b2af6b 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1069,9 +1069,9 @@ class PDFDocumentProxy { } /** - * @returns {Promise> | null>} A promise that is - * resolved with an {Object} containing /AcroForm field data for the JS - * sandbox, or `null` when no field data is present in the PDF file. + * @returns {Promise> | null>} A promise that is + * resolved with a {Map} containing /AcroForm field data for the JS sandbox, + * or `null` when no field data is present in the PDF file. */ getFieldObjects() { return this._transport.getFieldObjects(); diff --git a/src/scripting_api/common.js b/src/scripting_api/common.js index d6f9fed0b..b0f3f92e7 100644 --- a/src/scripting_api/common.js +++ b/src/scripting_api/common.js @@ -21,10 +21,8 @@ const FieldType = { time: 4, }; -function createActionsMap(actions) { - return actions instanceof Map - ? actions - : new Map(actions ? Object.entries(actions) : null); +function createMap(val) { + return val instanceof Map ? val : new Map(val ? Object.entries(val) : null); } function getFieldType(actions) { @@ -49,4 +47,4 @@ function getFieldType(actions) { return FieldType.none; } -export { createActionsMap, FieldType, getFieldType }; +export { createMap, FieldType, getFieldType }; diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 3c10aa728..878a3fb3d 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -14,7 +14,7 @@ */ import { makeArr, makeMap, serializeError } from "./app_utils.js"; -import { createActionsMap } from "./common.js"; +import { createMap } from "./common.js"; import { PDFObject } from "./pdf_object.js"; import { PrintParams } from "./print_params.js"; import { ZoomType } from "./constants.js"; @@ -98,7 +98,7 @@ class Doc extends PDFObject { this._zoomType = ZoomType.none; this._zoom = data.zoom || 100; - this._actions = createActionsMap(data.actions); + this._actions = createMap(data.actions); this._globalEval = data.globalEval; this._userActivation = false; this._disablePrinting = false; @@ -174,7 +174,7 @@ class Doc extends PDFObject { if (name === "PageOpen") { this.#pageActions ??= new Map(); this.#pageActions.getOrInsertComputed(pageNumber, () => - createActionsMap(actions) + createMap(actions) ); this._pageNum = pageNumber - 1; } diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index 9e2808612..28edf8eab 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { createActionsMap, FieldType, getFieldType } from "./common.js"; +import { createMap, FieldType, getFieldType } from "./common.js"; import { makeArr, serializeError } from "./app_utils.js"; import { Color } from "./color.js"; import { PDFObject } from "./pdf_object.js"; @@ -65,7 +65,7 @@ class Field extends PDFObject { this.userName = data.userName; // Private - this._actions = createActionsMap(data.actions); + this._actions = createMap(data.actions); this._browseForFileToSubmit = data.browseForFileToSubmit || null; this._buttonCaption = null; this._buttonIcon = null; @@ -578,7 +578,7 @@ class RadioButtonField extends Field { for (const radioData of otherButtons) { this.exportValues.push(radioData.exportValues); this._radioIds.push(radioData.id); - this._radioActions.push(createActionsMap(radioData.actions)); + this._radioActions.push(createMap(radioData.actions)); if (this._value === radioData.exportValues) { this._id = radioData.id; } diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index 9e4727bcb..166f6953b 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -32,6 +32,7 @@ import { AForm } from "./aform.js"; import { App } from "./app.js"; import { Color } from "./color.js"; import { Console } from "./console.js"; +import { createMap } from "./common.js"; import { Doc } from "./doc.js"; import { ProxyHandler } from "./proxy.js"; import { serializeError } from "./app_utils.js"; @@ -70,61 +71,57 @@ function initSandbox(params) { const util = new Util({ externalCall }); const appObjects = app._objects; - if (data.objects) { + for (const [name, objs] of createMap(data.objects)) { const annotations = []; + let container = null; - for (const [name, objs] of Object.entries(data.objects)) { - annotations.length = 0; - let container = null; + for (const obj of objs) { + if (obj.type !== "") { + annotations.push(obj); + } else { + container = obj; + } + } - for (const obj of objs) { - if (obj.type !== "") { - annotations.push(obj); - } else { - container = obj; + let obj = container; + if (annotations.length > 0) { + obj = annotations[0]; + obj.send = send; + } + + obj.globalEval = globalEval; + obj.doc = _document; + obj.fieldPath = name; + obj.appObjects = appObjects; + obj.util = util; + + const otherFields = annotations.slice(1); + + let field; + switch (obj.type) { + case "radiobutton": { + field = new RadioButtonField(otherFields, obj); + break; + } + case "checkbox": { + field = new CheckboxField(otherFields, obj); + break; + } + default: + if (otherFields.length > 0) { + obj.siblings = otherFields.map(x => x.id); } - } + field = new Field(obj); + } - let obj = container; - if (annotations.length > 0) { - obj = annotations[0]; - obj.send = send; - } - - obj.globalEval = globalEval; - obj.doc = _document; - obj.fieldPath = name; - obj.appObjects = appObjects; - obj.util = util; - - const otherFields = annotations.slice(1); - - let field; - switch (obj.type) { - case "radiobutton": { - field = new RadioButtonField(otherFields, obj); - break; - } - case "checkbox": { - field = new CheckboxField(otherFields, obj); - break; - } - default: - if (otherFields.length > 0) { - obj.siblings = otherFields.map(x => x.id); - } - field = new Field(obj); - } - - const wrapped = new Proxy(field, proxyHandler); - const _object = { obj: field, wrapped }; - doc._addField(name, _object); - for (const object of objs) { - appObjects[object.id] = _object; - } - if (container) { - appObjects[container.id] = _object; - } + const wrapped = new Proxy(field, proxyHandler); + const _object = { obj: field, wrapped }; + doc._addField(name, _object); + for (const object of objs) { + appObjects[object.id] = _object; + } + if (container) { + appObjects[container.id] = _object; } } diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 2ae2b17d0..b9d173bfc 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -2028,56 +2028,64 @@ describe("api", function () { const pdfDoc = await loadingTask.promise; const fieldObjects = await pdfDoc.getFieldObjects(); - expect(fieldObjects).toEqual({ - Text1: [ - { - id: "25R", - value: "", - defaultValue: "", - multiline: false, - password: false, - charLimit: 0, - comb: false, - editable: true, - hidden: false, - name: "Text1", - rect: [24.1789, 719.66, 432.22, 741.66], - actions: null, - page: 0, - strokeColor: null, - fillColor: null, - rotation: 0, - datetimeFormat: undefined, - hasDatetimeHTML: false, - type: "text", - }, - ], - Button1: [ - { - id: "26R", - value: "Off", - defaultValue: null, - exportValues: undefined, - editable: true, - name: "Button1", - rect: [455.436, 719.678, 527.436, 739.678], - hidden: false, - actions: new Map([ - [ - "Action", - [ - `this.getField("Text1").value = this.info.authors.join("::");`, - ], - ], - ]), - page: 0, - strokeColor: null, - fillColor: new Uint8ClampedArray([192, 192, 192]), - rotation: 0, - type: "button", - }, - ], - }); + expect(fieldObjects).toEqual( + new Map([ + [ + "Text1", + [ + { + id: "25R", + value: "", + defaultValue: "", + multiline: false, + password: false, + charLimit: 0, + comb: false, + editable: true, + hidden: false, + name: "Text1", + rect: [24.1789, 719.66, 432.22, 741.66], + actions: null, + page: 0, + strokeColor: null, + fillColor: null, + rotation: 0, + datetimeFormat: undefined, + hasDatetimeHTML: false, + type: "text", + }, + ], + ], + [ + "Button1", + [ + { + id: "26R", + value: "Off", + defaultValue: null, + exportValues: undefined, + editable: true, + name: "Button1", + rect: [455.436, 719.678, 527.436, 739.678], + hidden: false, + actions: new Map([ + [ + "Action", + [ + `this.getField("Text1").value = this.info.authors.join("::");`, + ], + ], + ]), + page: 0, + strokeColor: null, + fillColor: new Uint8ClampedArray([192, 192, 192]), + rotation: 0, + type: "button", + }, + ], + ], + ]) + ); await loadingTask.destroy(); }); @@ -2087,8 +2095,8 @@ describe("api", function () { const pdfDoc = await loadingTask.promise; const fieldObjects = await pdfDoc.getFieldObjects(); - for (const name in fieldObjects) { - const pageIndexes = fieldObjects[name].map(o => o.page); + for (const [name, objs] of fieldObjects) { + const pageIndexes = objs.map(o => o.page); let expected; switch (name) { @@ -7758,9 +7766,12 @@ small scripts as well as for`); loadingTask = getDocument({ data: extracted }); pdfDoc = await loadingTask.promise; - expect(Object.keys(await pdfDoc.getFieldObjects())).toEqual(["group"]); + + const fieldObjects = await pdfDoc.getFieldObjects(); + expect([...fieldObjects.keys()]).toEqual(["group"]); const annotations = await (await pdfDoc.getPage(1)).getAnnotations(); expect(annotations[0].fieldName).toEqual("group"); + await loadingTask.destroy(); }); @@ -7788,7 +7799,10 @@ small scripts as well as for`); loadingTask = getDocument({ data }); pdfDoc = await loadingTask.promise; - expect(Object.keys(await pdfDoc.getFieldObjects())).toEqual(["field"]); + + const fieldObjects = await pdfDoc.getFieldObjects(); + expect([...fieldObjects.keys()]).toEqual(["field"]); + await loadingTask.destroy(); }); @@ -7846,9 +7860,10 @@ small scripts as well as for`); loadingTask = getDocument({ data }); pdfDoc = await loadingTask.promise; - expect(Object.keys(await pdfDoc.getFieldObjects())).toEqual([ - "signature", - ]); + + const fieldObjects = await pdfDoc.getFieldObjects(); + expect([...fieldObjects.keys()]).toEqual(["signature"]); + await loadingTask.destroy(); }); @@ -7894,7 +7909,7 @@ small scripts as well as for`); // reflects the T entries of the fields in the AcroForm dictionary. const fieldObjects = await pdfDoc.getFieldObjects(); expect(fieldObjects).not.toBeNull(); - expect(Object.keys(fieldObjects).sort()).toEqual(origFieldNames); + expect([...fieldObjects.keys()].sort()).toEqual(origFieldNames); await loadingTask.destroy(); }); @@ -7952,7 +7967,7 @@ small scripts as well as for`); const allOrigFieldNames = [ ...new Set([...origPage1FieldNames, ...origPage2FieldNames]), ].sort(); - expect(Object.keys(fieldObjects).sort()).toEqual(allOrigFieldNames); + expect([...fieldObjects.keys()].sort()).toEqual(allOrigFieldNames); await loadingTask.destroy(); }); @@ -7964,9 +7979,8 @@ small scripts as well as for`); let pdfDoc = await loadingTask.promise; expect(await pdfDoc.getCalculationOrderIds()).toEqual(["6R"]); - expect(Object.keys((await pdfDoc.getFieldObjects()) || {})).toEqual([ - "group", - ]); + const fieldObjects1 = await pdfDoc.getFieldObjects(); + expect([...fieldObjects1.keys()]).toEqual(["group"]); const data = await pdfDoc.extractPages([{ document: null }]); await loadingTask.destroy(); @@ -7978,9 +7992,8 @@ small scripts as well as for`); expect(Array.isArray(calculationOrder)).toBeTrue(); expect(calculationOrder.length).toEqual(1); expect(calculationOrder[0]).not.toEqual("6R"); - expect(Object.keys((await pdfDoc.getFieldObjects()) || {})).toEqual([ - "group", - ]); + const fieldObjects2 = await pdfDoc.getFieldObjects(); + expect([...fieldObjects2.keys()]).toEqual(["group"]); await loadingTask.destroy(); }); @@ -8023,9 +8036,9 @@ small scripts as well as for`); loadingTask = getDocument({ data }); pdfDoc = await loadingTask.promise; expect(pdfDoc.numPages).toEqual(2); - expect( - Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort() - ).toEqual(["first", "second"]); + const fieldObjects = await pdfDoc.getFieldObjects(); + expect([...fieldObjects.keys()].sort()).toEqual(["first", "second"]); + for (const pageNumber of [1, 2]) { const fontName = await getAppearanceFontName(pdfDoc, pageNumber); expect(fontName).not.toBeNull(); @@ -8064,9 +8077,9 @@ small scripts as well as for`); loadingTask = getDocument({ data }); pdfDoc = await loadingTask.promise; expect(pdfDoc.numPages).toEqual(2); - expect( - Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort() - ).toEqual(["broken", "main"]); + const fieldObjects = await pdfDoc.getFieldObjects(); + expect([...fieldObjects.keys()].sort()).toEqual(["broken", "main"]); + const fontName = await getAppearanceFontName(pdfDoc, 2); expect(fontName).not.toBeNull(); expect(fontName).not.toEqual("g_font_error"); @@ -8109,9 +8122,9 @@ small scripts as well as for`); loadingTask = getDocument({ data }); pdfDoc = await loadingTask.promise; expect(pdfDoc.numPages).toEqual(2); - expect( - Object.keys((await pdfDoc.getFieldObjects()) ?? {}).sort() - ).toEqual(["check", "main"]); + const fieldObjects = await pdfDoc.getFieldObjects(); + expect([...fieldObjects.keys()].sort()).toEqual(["check", "main"]); + const fontName = await getAppearanceFontName(pdfDoc, 2); expect(fontName).not.toBeNull(); expect(fontName).not.toEqual("g_font_error"); diff --git a/test/unit/document_spec.js b/test/unit/document_spec.js index ff0bb9e88..4cb032e49 100644 --- a/test/unit/document_spec.js +++ b/test/unit/document_spec.js @@ -628,39 +628,43 @@ describe("document", function () { const kid2BisRef = Ref.get(266, 0); const parentRef = Ref.get(358, 0); - const allFields = Object.create(null); + const allFieldsObj = Object.create(null); for (const name of ["parent", "kid1", "kid2", "kid11"]) { const buttonWidgetDict = new Dict(); buttonWidgetDict.set("Type", Name.get("Annot")); buttonWidgetDict.set("Subtype", Name.get("Widget")); buttonWidgetDict.set("FT", Name.get("Btn")); buttonWidgetDict.set("T", name); - allFields[name] = buttonWidgetDict; + allFieldsObj[name] = buttonWidgetDict; } - allFields.kid1.set("Kids", [kid11Ref]); - allFields.parent.set("Kids", [kid1Ref, kid2Ref, kid2BisRef]); + allFieldsObj.kid1.set("Kids", [kid11Ref]); + allFieldsObj.parent.set("Kids", [kid1Ref, kid2Ref, kid2BisRef]); const xref = new XRefMock([ - { ref: parentRef, data: allFields.parent }, - { ref: kid1Ref, data: allFields.kid1 }, - { ref: kid11Ref, data: allFields.kid11 }, - { ref: kid2Ref, data: allFields.kid2 }, - { ref: kid2BisRef, data: allFields.kid2 }, + { ref: parentRef, data: allFieldsObj.parent }, + { ref: kid1Ref, data: allFieldsObj.kid1 }, + { ref: kid11Ref, data: allFieldsObj.kid11 }, + { ref: kid2Ref, data: allFieldsObj.kid2 }, + { ref: kid2BisRef, data: allFieldsObj.kid2 }, ]); acroForm.set("Fields", [parentRef]); pdfDocument = getDocument(acroForm, xref); - fields = (await pdfDocument.fieldObjects).allFields; - for (const [name, objs] of Object.entries(fields)) { - fields[name] = objs.map(obj => obj.id); - } + const { allFields, orphanFields } = await pdfDocument.fieldObjects; - expect(fields["parent.kid1"]).toEqual(["314R"]); - expect(fields["parent.kid1.kid11"]).toEqual(["159R"]); - expect(fields["parent.kid2"]).toEqual(["265R", "266R"]); - expect(fields.parent).toEqual(["358R"]); + const objIds = Array.from(allFields.entries(), ([name, objs]) => [ + name, + objs.map(obj => obj.id), + ]); + expect(objIds).toEqual([ + ["parent", ["358R"]], + ["parent.kid1", ["314R"]], + ["parent.kid1.kid11", ["159R"]], + ["parent.kid2", ["265R", "266R"]], + ]); + expect(orphanFields.size).toEqual(3); }); it("should get field objects with a circular `Parent` chain", async function () { @@ -693,9 +697,14 @@ describe("document", function () { acroForm.set("Fields", [widgetRef]); const pdfDocument = getDocument(acroForm, xref); - const { allFields } = await pdfDocument.fieldObjects; - expect(Object.keys(allFields)).toEqual([""]); - expect(allFields[""].map(obj => obj.id)).toEqual(["1R"]); + const { allFields, orphanFields } = await pdfDocument.fieldObjects; + + const objIds = Array.from(allFields.entries(), ([name, objs]) => [ + name, + objs.map(obj => obj.id), + ]); + expect(objIds).toEqual([["", ["1R"]]]); + expect(orphanFields.size).toEqual(0); }); it("should check if fields have any actions", async function () { diff --git a/web/pdf_scripting_manager.js b/web/pdf_scripting_manager.js index b8f2ed9f9..115971564 100644 --- a/web/pdf_scripting_manager.js +++ b/web/pdf_scripting_manager.js @@ -111,7 +111,7 @@ class PDFScriptingManager { // targeting an unknown id can be ignored. if (objects) { this.#objectIds = new Set(); - for (const fields of Object.values(objects)) { + for (const fields of objects.values()) { for (const { id } of fields) { this.#objectIds.add(id); }