1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

JS - Collect and execute actions at doc and pages level

* the goal is to execute actions like Open or OpenAction
 * can be tested with issue6106.pdf (auto-print)
 * once #12701 is merged, we can add page actions
This commit is contained in:
Calixte Denizet 2020-12-07 19:22:14 +01:00
parent 142f131ee1
commit 1e2173f038
18 changed files with 829 additions and 125 deletions

View file

@ -21,11 +21,9 @@ import {
AnnotationReplyType,
AnnotationType,
assert,
bytesToString,
escapeString,
getModificationDate,
isString,
objectSize,
OPS,
shadow,
stringToPDFString,
@ -34,17 +32,9 @@ import {
warn,
} from "../shared/util.js";
import { Catalog, FileSpec, ObjectLoader } from "./obj.js";
import {
Dict,
isDict,
isName,
isRef,
isStream,
Name,
RefSet,
} from "./primitives.js";
import { collectActions, getInheritableProperty } from "./core_utils.js";
import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js";
import { ColorSpace } from "./colorspace.js";
import { getInheritableProperty } from "./core_utils.js";
import { OperatorList } from "./operator_list.js";
import { StringStream } from "./stream.js";
import { writeDict } from "./writer.js";
@ -977,7 +967,7 @@ class WidgetAnnotation extends Annotation {
data.annotationType = AnnotationType.WIDGET;
data.fieldName = this._constructFieldName(dict);
data.actions = this._collectActions(params.xref, dict);
data.actions = collectActions(params.xref, dict, AnnotationActionEventType);
const fieldValue = getInheritableProperty({
dict,
@ -1459,78 +1449,6 @@ 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()) {
const action = AnnotationActionEventType[key];
if (!action) {
continue;
}
const actionDict = additionalActions.getRaw(key);
const parents = new RefSet();
const list = [];
this._collectJS(actionDict, xref, list, parents);
if (list.length > 0) {
actions[action] = 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 objectSize(actions) > 0 ? actions : null;
}
getFieldObject() {
if (this.data.fieldType === "Sig") {
return {

View file

@ -13,7 +13,15 @@
* limitations under the License.
*/
import { assert, BaseException, warn } from "../shared/util.js";
import {
assert,
BaseException,
bytesToString,
objectSize,
stringToPDFString,
warn,
} from "../shared/util.js";
import { Dict, isName, isRef, isStream, RefSet } from "./primitives.js";
function getLookupTableFactory(initializer) {
let lookup;
@ -240,7 +248,80 @@ function escapePDFName(str) {
return buffer.join("");
}
function _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) {
_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);
}
}
_collectJS(entry.getRaw("Next"), xref, list, parents);
}
if (parent) {
parents.remove(parent);
}
}
function collectActions(xref, dict, eventType) {
const actions = Object.create(null);
if (dict.has("AA")) {
const additionalActions = dict.get("AA");
for (const key of additionalActions.getKeys()) {
const action = eventType[key];
if (!action) {
continue;
}
const actionDict = additionalActions.getRaw(key);
const parents = new RefSet();
const list = [];
_collectJS(actionDict, xref, list, parents);
if (list.length > 0) {
actions[action] = 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 = [];
_collectJS(actionDict, xref, list, parents);
if (list.length > 0) {
actions.Action = list;
}
}
return objectSize(actions) > 0 ? actions : null;
}
export {
collectActions,
escapePDFName,
getLookupTableFactory,
getArrayLookupTableFactory,

View file

@ -24,6 +24,7 @@ import {
isNum,
isString,
OPS,
PageActionEventType,
shadow,
stringToBytes,
stringToPDFString,
@ -42,6 +43,7 @@ import {
Ref,
} from "./primitives.js";
import {
collectActions,
getInheritableProperty,
isWhiteSpace,
MissingDataException,
@ -467,6 +469,16 @@ class Page {
return shadow(this, "_parsedAnnotations", parsedAnnotations);
}
get jsActions() {
const actions = collectActions(
this.xref,
this.pageDict,
PageActionEventType
);
return shadow(this, "jsActions", actions);
}
}
const PDF_HEADER_SIGNATURE = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d]);

View file

@ -19,6 +19,7 @@ import {
bytesToString,
createPromiseCapability,
createValidAbsoluteUrl,
DocumentActionEventType,
FormatError,
info,
InvalidPDFException,
@ -47,13 +48,14 @@ import {
RefSet,
RefSetCache,
} from "./primitives.js";
import { Lexer, Parser } from "./parser.js";
import {
collectActions,
MissingDataException,
toRomanNumerals,
XRefEntryException,
XRefParseException,
} from "./core_utils.js";
import { Lexer, Parser } from "./parser.js";
import { CipherTransformFactory } from "./crypto.js";
import { ColorSpace } from "./colorspace.js";
import { GlobalImageCache } from "./image_utils.js";
@ -873,11 +875,11 @@ class Catalog {
return shadow(this, "attachments", attachments);
}
get javaScript() {
_collectJavaScript() {
const obj = this._catDict.get("Names");
let javaScript = null;
function appendIfJavaScriptDict(jsDict) {
function appendIfJavaScriptDict(name, jsDict) {
const type = jsDict.get("S");
if (!isName(type, "JavaScript")) {
return;
@ -890,10 +892,10 @@ class Catalog {
return;
}
if (!javaScript) {
javaScript = [];
if (javaScript === null) {
javaScript = Object.create(null);
}
javaScript.push(stringToPDFString(js));
javaScript[name] = stringToPDFString(js);
}
if (obj && obj.has("JavaScript")) {
@ -904,7 +906,7 @@ class Catalog {
// defensive so we don't cause errors on document load.
const jsDict = names[name];
if (isDict(jsDict)) {
appendIfJavaScriptDict(jsDict);
appendIfJavaScriptDict(name, jsDict);
}
}
}
@ -912,10 +914,43 @@ class Catalog {
// Append OpenAction "JavaScript" actions to the JavaScript array.
const openAction = this._catDict.get("OpenAction");
if (isDict(openAction) && isName(openAction.get("S"), "JavaScript")) {
appendIfJavaScriptDict(openAction);
appendIfJavaScriptDict("OpenAction", openAction);
}
return shadow(this, "javaScript", javaScript);
return javaScript;
}
get javaScript() {
const javaScript = this._collectJavaScript();
return shadow(
this,
"javaScript",
javaScript ? Object.values(javaScript) : null
);
}
get jsActions() {
const js = this._collectJavaScript();
let actions = collectActions(
this.xref,
this._catDict,
DocumentActionEventType
);
if (!actions && js) {
actions = Object.create(null);
}
if (actions && js) {
for (const [key, val] of Object.entries(js)) {
if (key in actions) {
actions[key].push(val);
} else {
actions[key] = [val];
}
}
}
return shadow(this, "jsActions", actions);
}
fontFallback(id, handler) {

View file

@ -481,6 +481,16 @@ class WorkerMessageHandler {
return pdfManager.ensureCatalog("javaScript");
});
handler.on("GetDocJSActions", function wphSetupGetDocJSActions(data) {
return pdfManager.ensureCatalog("jsActions");
});
handler.on("GetPageJSActions", function ({ pageIndex }) {
return pdfManager.getPage(pageIndex).then(function (page) {
return page.jsActions;
});
});
handler.on("GetOutline", function wphSetupGetOutline(data) {
return pdfManager.ensureCatalog("documentOutline");
});

View file

@ -753,6 +753,17 @@ class PDFDocumentProxy {
return this._transport.getJavaScript();
}
/**
* @returns {Promise<Object | null>} A promise that is resolved with
* an {Object} with the JavaScript actions:
* - from the name tree (like getJavaScript);
* - from A or AA entries in the catalog dictionary.
* , or `null` if no JavaScript exists.
*/
getJSActions() {
return this._transport.getDocJSActions();
}
/**
* @typedef {Object} OutlineNode
* @property {string} title
@ -1124,6 +1135,20 @@ class PDFPageProxy {
return this.annotationsPromise;
}
/**
* @param {GetAnnotationsParameters} params - Annotation parameters.
* @returns {Promise<Array<any>>} A promise that is resolved with an
* {Array} of the annotation objects.
*/
getJSActions() {
if (!this._jsActionsPromise) {
this._jsActionsPromise = this._transport.getPageJSActions(
this._pageIndex
);
}
return this._jsActionsPromise;
}
/**
* Begins the process of rendering a page to the desired context.
*
@ -1405,6 +1430,7 @@ class PDFPageProxy {
}
this.objs.clear();
this.annotationsPromise = null;
this._jsActionsPromise = null;
this.pendingCleanup = false;
return Promise.all(waitOn);
}
@ -1438,6 +1464,7 @@ class PDFPageProxy {
this._intentStates.clear();
this.objs.clear();
this.annotationsPromise = null;
this._jsActionsPromise = null;
if (resetStats && this._stats) {
this._stats = new StatTimer();
}
@ -2631,6 +2658,16 @@ class WorkerTransport {
return this.messageHandler.sendWithPromise("GetJavaScript", null);
}
getDocJSActions() {
return this.messageHandler.sendWithPromise("GetDocJSActions", null);
}
getPageJSActions(pageIndex) {
return this.messageHandler.sendWithPromise("GetPageJSActions", {
pageIndex,
});
}
getOutline() {
return this.messageHandler.sendWithPromise("GetOutline", null);
}

View file

@ -0,0 +1,26 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function createActionsMap(actions) {
const actionsMap = new Map();
if (actions) {
for (const [eventType, actionsForEvent] of Object.entries(actions)) {
actionsMap.set(eventType, actionsForEvent);
}
}
return actionsMap;
}
export { createActionsMap };

View file

@ -13,6 +13,7 @@
* limitations under the License.
*/
import { createActionsMap } from "./common.js";
import { PDFObject } from "./pdf_object.js";
import { PrintParams } from "./print_params.js";
import { ZoomType } from "./constants.js";
@ -88,6 +89,48 @@ class Doc extends PDFObject {
this._zoomType = ZoomType.none;
this._zoom = data.zoom || 100;
this._actions = createActionsMap(data.actions);
this._globalEval = data.globalEval;
}
_dispatchDocEvent(name) {
if (name === "Open") {
const dontRun = new Set([
"WillClose",
"WillSave",
"DidSave",
"WillPrint",
"DidPrint",
"OpenAction",
]);
for (const actionName of this._actions.keys()) {
if (!dontRun.has(actionName)) {
this._runActions(actionName);
}
}
this._runActions("OpenAction");
} else {
this._runActions(name);
}
}
_dispatchPageEvent(name, action, pageNumber) {
if (name === "PageOpen") {
this._pageNum = pageNumber - 1;
}
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);
}
}
_addField(name, field) {
@ -954,7 +997,7 @@ class Doc extends PDFObject {
nEnd = -1;
}
this._send({ id: "print", start: nStart, end: nEnd });
this._send({ command: "print", start: nStart, end: nEnd });
}
removeDataObject() {

View file

@ -65,12 +65,28 @@ class EventDispatcher {
dispatch(baseEvent) {
const id = baseEvent.id;
if (!(id in this._objects)) {
let event;
if (id === "doc" || id === "page") {
event = globalThis.event = new Event(baseEvent);
event.source = event.target = this._document.wrapped;
event.name = baseEvent.name;
}
if (id === "doc") {
this._document.obj._dispatchDocEvent(event.name);
}
if (id === "page") {
this._document.obj._dispatchPageEvent(
event.name,
baseEvent.action,
baseEvent.pageNumber
);
}
return;
}
const name = baseEvent.name.replace(" ", "");
const source = this._objects[id];
globalThis.event = new Event(baseEvent);
const event = (globalThis.event = new Event(baseEvent));
let savedChange;
if (source.obj._isButton()) {
@ -156,7 +172,7 @@ class EventDispatcher {
const first = this._calculationOrder[0];
const source = this._objects[first];
globalThis.event = new Event({});
this.runCalculate(source, event);
this.runCalculate(source, globalThis.event);
}
runCalculate(source, event) {

View file

@ -14,6 +14,7 @@
*/
import { Color } from "./color.js";
import { createActionsMap } from "./common.js";
import { PDFObject } from "./pdf_object.js";
class Field extends PDFObject {
@ -72,7 +73,7 @@ class Field extends PDFObject {
// Private
this._document = data.doc;
this._actions = this._createActionsMap(data.actions);
this._actions = createActionsMap(data.actions);
this._fillColor = data.fillColor || ["T"];
this._strokeColor = data.strokeColor || ["G", 0];
@ -133,16 +134,6 @@ class Field extends PDFObject {
this._send({ id: this._id, focus: true });
}
_createActionsMap(actions) {
const actionsMap = new Map();
if (actions) {
for (const [eventType, actionsForEvent] of Object.entries(actions)) {
actionsMap.set(eventType, actionsForEvent);
}
}
return actionsMap;
}
_isButton() {
return false;
}

View file

@ -51,6 +51,7 @@ function initSandbox(params) {
const { data } = params;
const doc = new Doc({
send,
globalEval,
...data.docInfo,
});
const _document = { obj: doc, wrapped: new Proxy(doc, proxyHandler) };
@ -67,16 +68,17 @@ function initSandbox(params) {
const util = new Util({ externalCall });
const aform = new AForm(doc, app, util);
for (const [name, objs] of Object.entries(data.objects)) {
const obj = objs[0];
obj.send = send;
obj.globalEval = globalEval;
obj.doc = _document.wrapped;
obj.globalEval = globalEval;
const field = new Field(obj);
const wrapped = new Proxy(field, proxyHandler);
doc._addField(name, wrapped);
app._objects[obj.id] = { obj: field, wrapped };
if (data.objects) {
for (const [name, objs] of Object.entries(data.objects)) {
const obj = objs[0];
obj.send = send;
obj.globalEval = globalEval;
obj.doc = _document.wrapped;
const field = new Field(obj);
const wrapped = new Proxy(field, proxyHandler);
doc._addField(name, wrapped);
app._objects[obj.id] = { obj: field, wrapped };
}
}
globalThis.event = null;

View file

@ -159,6 +159,9 @@ const AnnotationActionEventType = {
F: "Format",
V: "Validate",
C: "Calculate",
};
const DocumentActionEventType = {
WC: "WillClose",
WS: "WillSave",
DS: "DidSave",
@ -166,6 +169,11 @@ const AnnotationActionEventType = {
DP: "DidPrint",
};
const PageActionEventType = {
O: "PageOpen",
C: "PageClose",
};
const StreamType = {
UNKNOWN: "UNKNOWN",
FLATE: "FLATE",
@ -1011,9 +1019,11 @@ export {
FontType,
ImageKind,
CMapCompressionType,
DocumentActionEventType,
AbortException,
InvalidPDFException,
MissingPDFException,
PageActionEventType,
PasswordException,
PasswordResponses,
PermissionFlag,