diff --git a/src/core/annotation.js b/src/core/annotation.js index cf2f6ffcb076ac..cce375aeff740f 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -15,12 +15,14 @@ /* eslint no-var: error */ import { + AnnotationActionEventType, AnnotationBorderStyleType, AnnotationFieldFlag, AnnotationFlag, AnnotationReplyType, AnnotationType, assert, + bytesToString, escapeString, getModificationDate, isString, @@ -31,7 +33,15 @@ import { warn, } from "../shared/util.js"; import { Catalog, FileSpec, ObjectLoader } from "./obj.js"; -import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js"; +import { + Dict, + isDict, + isName, + isRef, + isStream, + Name, + RefSet, +} from "./primitives.js"; import { ColorSpace } from "./colorspace.js"; import { getInheritableProperty } from "./core_utils.js"; import { OperatorList } from "./operator_list.js"; @@ -570,6 +580,10 @@ class Annotation { return null; } + getAnnotationObject() { + return null; + } + /** * Reset the annotation. * @@ -904,6 +918,7 @@ class WidgetAnnotation extends Annotation { data.annotationType = AnnotationType.WIDGET; data.fieldName = this._constructFieldName(dict); + data.actions = this._collectActions(params.xref, dict); const fieldValue = getInheritableProperty({ dict, @@ -938,6 +953,7 @@ class WidgetAnnotation extends Annotation { } data.readOnly = this.hasFieldFlag(AnnotationFieldFlag.READONLY); + data.hidden = this.hasFieldFlag(AnnotationFieldFlag.HIDDEN); // Hide signatures because we cannot validate them, and unset the fieldValue // since it's (most likely) a `Dict` which is non-serializable and will thus @@ -945,6 +961,7 @@ class WidgetAnnotation extends Annotation { if (data.fieldType === "Sig") { data.fieldValue = null; this.setFlags(AnnotationFlag.HIDDEN); + data.hidden = true; } } @@ -1367,6 +1384,76 @@ class WidgetAnnotation extends Annotation { } return localResources || Dict.empty; } + + _collectJS(entry, xref, list, parents) { + if (!entry) { + return; + } + + let parent = null; + if (isRef(entry)) { + if (parents.has(entry)) { + // If we've already found entry then we've a cycle. + return; + } + parent = entry; + parents.put(parent); + entry = xref.fetch(entry); + } + if (Array.isArray(entry)) { + for (const element of entry) { + this._collectJS(element, xref, list, parents); + } + } else if (entry instanceof Dict) { + if (isName(entry.get("S"), "JavaScript") && entry.has("JS")) { + const js = entry.get("JS"); + let code; + if (isStream(js)) { + code = bytesToString(js.getBytes()); + } else { + code = stringToPDFString(js); + } + if (code) { + list.push(code); + } + } + this._collectJS(entry.getRaw("Next"), xref, list, parents); + } + + if (parent) { + parents.remove(parent); + } + } + + _collectActions(xref, dict) { + const actions = Object.create({}); + if (dict.has("AA")) { + const additionalAction = dict.get("AA"); + for (const key of additionalAction.getKeys()) { + if (key in AnnotationActionEventType) { + const actionDict = additionalAction.getRaw(key); + const parents = new RefSet(); + const list = []; + this._collectJS(actionDict, xref, list, parents); + if (list.length > 0) { + actions[AnnotationActionEventType[key]] = list; + } + } + } + } + return actions; + } + + getAnnotationObject() { + if (this.data.fieldType === "Sig") { + return { + id: this.data.id, + value: null, + type: "signature", + }; + } + return null; + } } class TextWidgetAnnotation extends WidgetAnnotation { @@ -1517,6 +1604,23 @@ class TextWidgetAnnotation extends WidgetAnnotation { return chunks; } + + getAnnotationObject() { + return { + id: this.data.id, + value: this.data.fieldValue, + multiline: this.data.multiline, + password: this.hasFieldFlag(AnnotationFieldFlag.PASSWORD), + charLimit: this.data.maxLen, + comb: this.data.comb, + editable: !this.data.readOnly, + hidden: this.data.hidden, + name: this.data.fieldName, + rect: this.data.rect, + actions: this.data.actions, + type: "text", + }; + } } class ButtonWidgetAnnotation extends WidgetAnnotation { @@ -1791,6 +1895,28 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { docBaseUrl: params.pdfManager.docBaseUrl, }); } + + getAnnotationObject() { + let type = "button"; + let value = null; + if (this.data.checkBox) { + type = "checkbox"; + value = this.data.fieldValue && this.data.fieldValue !== "Off"; + } else if (this.data.radioButton) { + type = "radiobutton"; + value = this.data.fieldValue === this.data.buttonValue; + } + return { + id: this.data.id, + value, + editable: !this.data.readOnly, + name: this.data.fieldName, + rect: this.data.rect, + hidden: this.data.hidden, + actions: this.data.actions, + type, + }; + } } class ChoiceWidgetAnnotation extends WidgetAnnotation { @@ -1841,6 +1967,23 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation { this.data.multiSelect = this.hasFieldFlag(AnnotationFieldFlag.MULTISELECT); this._hasText = true; } + + getAnnotationObject() { + const type = this.data.combo ? "combobox" : "listbox"; + const value = + this.data.fieldValue.length > 0 ? this.data.fieldValue[0] : null; + return { + id: this.data.id, + value, + editable: !this.data.readOnly, + name: this.data.fieldName, + rect: this.data.rect, + multipleSelection: this.data.multiSelect, + hidden: this.data.hidden, + actions: this.data.actions, + type, + }; + } } class TextAnnotation extends MarkupAnnotation { diff --git a/src/core/document.js b/src/core/document.js index 82275b291d5f5e..40a2dc9d7f8aca 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -227,6 +227,25 @@ class Page { return stream; } + getAnnotationObjects() { + // Fetch the page's annotations and get annotation data + // to be used in JS sandbox. + return this._parsedAnnotations.then(function (annotations) { + const results = []; + for (const annotation of annotations) { + if (!isAnnotationRenderable(annotation, "print")) { + continue; + } + const object = annotation.getAnnotationObject(); + if (object) { + results.push(object); + } + } + + return results; + }); + } + save(handler, task, annotationStorage) { const partialEvaluator = new PartialEvaluator({ xref: this.xref, diff --git a/src/core/worker.js b/src/core/worker.js index bd6257de07a431..503b3de5cee33d 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -515,6 +515,18 @@ class WorkerMessageHandler { }); }); + handler.on("GetAnnotationObjects", function ({ numPages }) { + const promises = []; + for (let pageIndex = 0; pageIndex < numPages; pageIndex++) { + promises.push( + pdfManager.getPage(pageIndex).then(function (page) { + return page.getAnnotationObjects(); + }) + ); + } + return Promise.all(promises).then(data => data.flat()); + }); + handler.on("SaveDocument", function ({ numPages, annotationStorage, diff --git a/src/display/api.js b/src/display/api.js index bb7b184a60222c..460b97500c0de2 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -877,6 +877,14 @@ class PDFDocumentProxy { saveDocument(annotationStorage) { return this._transport.saveDocument(annotationStorage); } + + /** + * @returns {Promise>} A promise that is resolved with an + * {Array} containing annotation data. + */ + getAnnotationObjects() { + return this._transport.getAnnotationObjects(); + } } /** @@ -2550,6 +2558,12 @@ class WorkerTransport { }); } + getAnnotationObjects() { + return this.messageHandler.sendWithPromise("GetAnnotationObjects", { + numPages: this._numPages, + }); + } + getDestinations() { return this.messageHandler.sendWithPromise("GetDestinations", null); } diff --git a/src/shared/util.js b/src/shared/util.js index af3be187325ff3..2b6a0d55486538 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -310,6 +310,28 @@ const PasswordResponses = { INCORRECT_PASSWORD: 2, }; +const AnnotationActionEventType = { + E: "MouseEnter", + X: "MouseExit", + D: "MouseDown", + U: "MouseUp", + Fo: "Focus", + Bl: "Blur", + PO: "PageOpen", + PC: "PageClosed", + PV: "PageVisible", + PI: "PageInvisible", + K: "Keystroke", + F: "Format", + V: "Validate", + C: "Calculate", + WC: "WillClose", + WS: "WillSave", + DS: "DidSave", + WP: "WillPrint", + DP: "DidPrint", +}; + let verbosity = VerbosityLevel.WARNINGS; function setVerbosityLevel(level) { @@ -972,6 +994,7 @@ export { OPS, VerbosityLevel, UNSUPPORTED_FEATURES, + AnnotationActionEventType, AnnotationBorderStyleType, AnnotationFieldFlag, AnnotationFlag, diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 45fd9f93d04b76..8baac546c42aa4 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1863,6 +1863,92 @@ describe("annotation", function () { done(); }, done.fail); }); + + it("should get annotation object for use in pdf script", function (done) { + const textWidgetRef = Ref.get(123, 0); + const xDictRef = Ref.get(141, 0); + const dDictRef = Ref.get(262, 0); + const next0Ref = Ref.get(314, 0); + const next1Ref = Ref.get(271, 0); + const next2Ref = Ref.get(577, 0); + const next00Ref = Ref.get(413, 0); + const xDict = new Dict(); + const dDict = new Dict(); + const next0Dict = new Dict(); + const next1Dict = new Dict(); + const next2Dict = new Dict(); + const next00Dict = new Dict(); + + const xref = new XRefMock([ + { ref: textWidgetRef, data: textWidgetDict }, + { ref: xDictRef, data: xDict }, + { ref: dDictRef, data: dDict }, + { ref: next0Ref, data: next0Dict }, + { ref: next00Ref, data: next00Dict }, + { ref: next1Ref, data: next1Dict }, + { ref: next2Ref, data: next2Dict }, + ]); + + const JS = Name.get("JavaScript"); + const additionalActionDict = new Dict(); + const eDict = new Dict(); + eDict.set("JS", "hello()"); + eDict.set("S", JS); + additionalActionDict.set("E", eDict); + + xDict.set("JS", "world()"); + xDict.set("S", JS); + xDict.set("Next", [next0Ref, next1Ref, next2Ref, xDictRef]); + + next0Dict.set("JS", "olleh()"); + next0Dict.set("S", JS); + next0Dict.set("Next", next00Ref); + + next00Dict.set("JS", "foo()"); + next00Dict.set("S", JS); + next00Dict.set("Next", next0Ref); + + next1Dict.set("JS", "dlrow()"); + next1Dict.set("S", JS); + next1Dict.set("Next", xDictRef); + + next2Dict.set("JS", "oof()"); + next2Dict.set("S", JS); + + dDict.set("JS", "bar()"); + dDict.set("S", JS); + dDict.set("Next", dDictRef); + additionalActionDict.set("D", dDictRef); + + additionalActionDict.set("X", xDictRef); + textWidgetDict.set("AA", additionalActionDict); + + partialEvaluator.xref = xref; + + AnnotationFactory.create( + xref, + textWidgetRef, + pdfManagerMock, + idFactoryMock + ) + .then(annotation => { + return annotation.getAnnotationObject(); + }) + .then(object => { + console.log(object.actions); + const actions = object.actions; + expect(actions.MouseEnter).toEqual(["hello()"]); + expect(actions.MouseExit).toEqual([ + "world()", + "olleh()", + "foo()", + "dlrow()", + "oof()", + ]); + expect(actions.MouseDown).toEqual(["bar()"]); + done(); + }, done.fail); + }); }); describe("ButtonWidgetAnnotation", function () {