mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-22 16:18:08 +02:00
Merge pull request #12429 from calixteman/collect_js
[api-minor] Add the possibility to collect Javascript actions
This commit is contained in:
commit
a373137304
7 changed files with 383 additions and 3 deletions
|
@ -14,12 +14,14 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
AnnotationActionEventType,
|
||||
AnnotationBorderStyleType,
|
||||
AnnotationFieldFlag,
|
||||
AnnotationFlag,
|
||||
AnnotationReplyType,
|
||||
AnnotationType,
|
||||
assert,
|
||||
bytesToString,
|
||||
escapeString,
|
||||
getModificationDate,
|
||||
isString,
|
||||
|
@ -30,7 +32,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";
|
||||
|
@ -569,6 +579,20 @@ class Annotation {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field data for usage in JS sandbox.
|
||||
*
|
||||
* Field object is defined here:
|
||||
* https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/js_api_reference.pdf#page=16
|
||||
*
|
||||
* @public
|
||||
* @memberof Annotation
|
||||
* @returns {Object | null}
|
||||
*/
|
||||
getFieldObject() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the annotation.
|
||||
*
|
||||
|
@ -903,6 +927,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,
|
||||
|
@ -937,6 +962,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
|
||||
|
@ -944,6 +970,7 @@ class WidgetAnnotation extends Annotation {
|
|||
if (data.fieldType === "Sig") {
|
||||
data.fieldValue = null;
|
||||
this.setFlags(AnnotationFlag.HIDDEN);
|
||||
data.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1366,6 +1393,87 @@ 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 = js;
|
||||
}
|
||||
code = stringToPDFString(code);
|
||||
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(null);
|
||||
if (dict.has("AA")) {
|
||||
const additionalActions = dict.get("AA");
|
||||
for (const key of additionalActions.getKeys()) {
|
||||
if (key in AnnotationActionEventType) {
|
||||
const actionDict = additionalActions.getRaw(key);
|
||||
const parents = new RefSet();
|
||||
const list = [];
|
||||
this._collectJS(actionDict, xref, list, parents);
|
||||
if (list.length > 0) {
|
||||
actions[AnnotationActionEventType[key]] = list;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Collect the Action if any (we may have one on pushbutton)
|
||||
if (dict.has("A")) {
|
||||
const actionDict = dict.get("A");
|
||||
const parents = new RefSet();
|
||||
const list = [];
|
||||
this._collectJS(actionDict, xref, list, parents);
|
||||
if (list.length > 0) {
|
||||
actions.Action = list;
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
getFieldObject() {
|
||||
if (this.data.fieldType === "Sig") {
|
||||
return {
|
||||
id: this.data.id,
|
||||
value: null,
|
||||
type: "signature",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class TextWidgetAnnotation extends WidgetAnnotation {
|
||||
|
@ -1516,6 +1624,23 @@ class TextWidgetAnnotation extends WidgetAnnotation {
|
|||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
getFieldObject() {
|
||||
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 {
|
||||
|
@ -1793,6 +1918,28 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||
docBaseUrl: params.pdfManager.docBaseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
getFieldObject() {
|
||||
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 {
|
||||
|
@ -1843,6 +1990,23 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
|||
this.data.multiSelect = this.hasFieldFlag(AnnotationFieldFlag.MULTISELECT);
|
||||
this._hasText = true;
|
||||
}
|
||||
|
||||
getFieldObject() {
|
||||
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 {
|
||||
|
|
|
@ -708,7 +708,7 @@ class PDFDocument {
|
|||
}
|
||||
|
||||
get formInfo() {
|
||||
const formInfo = { hasAcroForm: false, hasXfa: false };
|
||||
const formInfo = { hasAcroForm: false, hasXfa: false, fields: null };
|
||||
const acroForm = this.catalog.acroForm;
|
||||
if (!acroForm) {
|
||||
return shadow(this, "formInfo", formInfo);
|
||||
|
@ -736,6 +736,9 @@ class PDFDocument {
|
|||
const hasOnlyDocumentSignatures =
|
||||
!!(sigFlags & 0x1) && this._hasOnlyDocumentSignatures(fields);
|
||||
formInfo.hasAcroForm = hasFields && !hasOnlyDocumentSignatures;
|
||||
if (hasFields) {
|
||||
formInfo.fields = fields;
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex instanceof MissingDataException) {
|
||||
throw ex;
|
||||
|
@ -935,6 +938,73 @@ class PDFDocument {
|
|||
? this.catalog.cleanup(manuallyTriggered)
|
||||
: clearPrimitiveCaches();
|
||||
}
|
||||
|
||||
_collectFieldObjects(name, fieldRef, promises) {
|
||||
const field = this.xref.fetchIfRef(fieldRef);
|
||||
if (field.has("T")) {
|
||||
const partName = stringToPDFString(field.get("T"));
|
||||
if (name === "") {
|
||||
name = partName;
|
||||
} else {
|
||||
name = `${name}.${partName}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(name in promises)) {
|
||||
promises.set(name, []);
|
||||
}
|
||||
promises.get(name).push(
|
||||
AnnotationFactory.create(
|
||||
this.xref,
|
||||
fieldRef,
|
||||
this.pdfManager,
|
||||
this._localIdFactory
|
||||
)
|
||||
.then(annotation => annotation.getFieldObject())
|
||||
.catch(function (reason) {
|
||||
warn(`_collectFieldObjects: "${reason}".`);
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
if (field.has("Kids")) {
|
||||
const kids = field.get("Kids");
|
||||
for (const kid of kids) {
|
||||
this._collectFieldObjects(name, kid, promises);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get fieldObjects() {
|
||||
const formInfo = this.formInfo;
|
||||
if (!formInfo.fields) {
|
||||
return shadow(this, "fieldObjects", Promise.resolve(null));
|
||||
}
|
||||
|
||||
const allFields = Object.create(null);
|
||||
const fieldPromises = new Map();
|
||||
for (const fieldRef of formInfo.fields) {
|
||||
this._collectFieldObjects("", fieldRef, fieldPromises);
|
||||
}
|
||||
|
||||
const allPromises = [];
|
||||
for (const [name, promises] of fieldPromises.entries()) {
|
||||
allPromises.push(
|
||||
Promise.all(promises).then(fields => {
|
||||
fields = fields.filter(field => field !== null);
|
||||
if (fields.length > 0) {
|
||||
allFields[name] = fields;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return shadow(
|
||||
this,
|
||||
"fieldObjects",
|
||||
Promise.all(allPromises).then(() => allFields)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Page, PDFDocument };
|
||||
|
|
|
@ -516,6 +516,10 @@ class WorkerMessageHandler {
|
|||
});
|
||||
});
|
||||
|
||||
handler.on("GetFieldObjects", function (data) {
|
||||
return pdfManager.ensureDoc("fieldObjects");
|
||||
});
|
||||
|
||||
handler.on("SaveDocument", function ({
|
||||
numPages,
|
||||
annotationStorage,
|
||||
|
|
|
@ -876,6 +876,14 @@ class PDFDocumentProxy {
|
|||
saveDocument(annotationStorage) {
|
||||
return this._transport.saveDocument(annotationStorage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Array<Object>>} A promise that is resolved with an
|
||||
* {Array<Object>} containing field data for the JS sandbox.
|
||||
*/
|
||||
getFieldObjects() {
|
||||
return this._transport.getFieldObjects();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2549,6 +2557,10 @@ class WorkerTransport {
|
|||
});
|
||||
}
|
||||
|
||||
getFieldObjects() {
|
||||
return this.messageHandler.sendWithPromise("GetFieldObjects", null);
|
||||
}
|
||||
|
||||
getDestinations() {
|
||||
return this.messageHandler.sendWithPromise("GetDestinations", null);
|
||||
}
|
||||
|
|
|
@ -144,6 +144,28 @@ const AnnotationBorderStyleType = {
|
|||
UNDERLINE: 5,
|
||||
};
|
||||
|
||||
const AnnotationActionEventType = {
|
||||
E: "MouseEnter",
|
||||
X: "MouseExit",
|
||||
D: "MouseDown",
|
||||
U: "MouseUp",
|
||||
Fo: "Focus",
|
||||
Bl: "Blur",
|
||||
PO: "PageOpen",
|
||||
PC: "PageClose",
|
||||
PV: "PageVisible",
|
||||
PI: "PageInvisible",
|
||||
K: "Keystroke",
|
||||
F: "Format",
|
||||
V: "Validate",
|
||||
C: "Calculate",
|
||||
WC: "WillClose",
|
||||
WS: "WillSave",
|
||||
DS: "DidSave",
|
||||
WP: "WillPrint",
|
||||
DP: "DidPrint",
|
||||
};
|
||||
|
||||
const StreamType = {
|
||||
UNKNOWN: "UNKNOWN",
|
||||
FLATE: "FLATE",
|
||||
|
@ -971,6 +993,7 @@ export {
|
|||
OPS,
|
||||
VerbosityLevel,
|
||||
UNSUPPORTED_FEATURES,
|
||||
AnnotationActionEventType,
|
||||
AnnotationBorderStyleType,
|
||||
AnnotationFieldFlag,
|
||||
AnnotationFlag,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue