[JS] Fix several issues found in pdf in #13269

- app.alert and few other function can use an object as parameter ({cMsg: ...});
  - support app.alert with a question and a yes/no answer;
  - update field siblings when one is changed in an action;
  - stop calculation if calculate is set to false in the middle of calculations;
  - get a boolean for checkboxes when they've been set through annotationStorage instead of a string.
This commit is contained in:
Calixte Denizet 2021-04-20 19:21:52 +02:00
parent 3f187c2c6d
commit 3f29892d63
12 changed files with 224 additions and 50 deletions

View File

@ -922,12 +922,17 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
const storage = this.annotationStorage; const storage = this.annotationStorage;
const data = this.data; const data = this.data;
const id = data.id; const id = data.id;
const value = storage.getValue(id, { let value = storage.getValue(id, {
value: value:
data.fieldValue && data.fieldValue &&
((data.exportValue && data.exportValue === data.fieldValue) || ((data.exportValue && data.exportValue === data.fieldValue) ||
(!data.exportValue && data.fieldValue !== "Off")), (!data.exportValue && data.fieldValue !== "Off")),
}).value; }).value;
if (typeof value === "string") {
// The value has been changed through js and set in annotationStorage.
value = value !== "Off";
storage.setValue(id, { value });
}
this.container.className = "buttonWidgetAnnotation checkBox"; this.container.className = "buttonWidgetAnnotation checkBox";
@ -1012,9 +1017,14 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
const storage = this.annotationStorage; const storage = this.annotationStorage;
const data = this.data; const data = this.data;
const id = data.id; const id = data.id;
const value = storage.getValue(id, { let value = storage.getValue(id, {
value: data.fieldValue === data.buttonValue, value: data.fieldValue === data.buttonValue,
}).value; }).value;
if (typeof value === "string") {
// The value has been changed through js and set in annotationStorage.
value = value !== data.buttonValue;
storage.setValue(id, { value });
}
const element = document.createElement("input"); const element = document.createElement("input");
element.disabled = data.readOnly; element.disabled = data.readOnly;

View File

@ -117,6 +117,12 @@ class SandboxSupportBase {
} }
this.win.alert(cMsg); this.win.alert(cMsg);
}, },
confirm: cMsg => {
if (typeof cMsg !== "string") {
return false;
}
return this.win.confirm(cMsg);
},
prompt: (cQuestion, cDefault) => { prompt: (cQuestion, cDefault) => {
if (typeof cQuestion !== "string" || typeof cDefault !== "string") { if (typeof cQuestion !== "string" || typeof cDefault !== "string") {
return null; return null;

View File

@ -28,8 +28,6 @@ class App extends PDFObject {
constructor(data) { constructor(data) {
super(data); super(data);
this.calculate = true;
this._constants = null; this._constants = null;
this._focusRect = true; this._focusRect = true;
this._fs = null; this._fs = null;
@ -68,6 +66,7 @@ class App extends PDFObject {
this._timeoutCallbackId = 0; this._timeoutCallbackId = 0;
this._globalEval = data.globalEval; this._globalEval = data.globalEval;
this._externalCall = data.externalCall; this._externalCall = data.externalCall;
this._document = data._document;
} }
// This function is called thanks to the proxy // This function is called thanks to the proxy
@ -191,6 +190,14 @@ class App extends PDFObject {
throw new Error("app.activeDocs is read-only"); throw new Error("app.activeDocs is read-only");
} }
get calculate() {
return this._document.obj.calculate;
}
set calculate(calculate) {
this._document.obj.calculate = calculate;
}
get constants() { get constants() {
if (!this._constants) { if (!this._constants) {
this._constants = Object.freeze({ this._constants = Object.freeze({
@ -427,7 +434,21 @@ class App extends PDFObject {
oDoc = null, oDoc = null,
oCheckbox = null oCheckbox = null
) { ) {
if (typeof cMsg === "object") {
nType = cMsg.nType;
cMsg = cMsg.cMsg;
}
cMsg = (cMsg || "").toString();
nType =
typeof nType !== "number" || isNaN(nType) || nType < 0 || nType > 3
? 0
: nType;
if (nType >= 2) {
return this._externalCall("confirm", [cMsg]) ? 4 : 3;
}
this._externalCall("alert", [cMsg]); this._externalCall("alert", [cMsg]);
return 1;
} }
beep() { beep() {
@ -543,10 +564,21 @@ class App extends PDFObject {
} }
response(cQuestion, cTitle = "", cDefault = "", bPassword = "", cLabel = "") { response(cQuestion, cTitle = "", cDefault = "", bPassword = "", cLabel = "") {
if (typeof cQuestion === "object") {
cDefault = cQuestion.cDefault;
cQuestion = cQuestion.cQuestion;
}
cQuestion = (cQuestion || "").toString();
cDefault = (cDefault || "").toString();
return this._externalCall("prompt", [cQuestion, cDefault || ""]); return this._externalCall("prompt", [cQuestion, cDefault || ""]);
} }
setInterval(cExpr, nMilliseconds) { setInterval(cExpr, nMilliseconds = 0) {
if (typeof cExpr === "object") {
nMilliseconds = cExpr.nMilliseconds || 0;
cExpr = cExpr.cExpr;
}
if (typeof cExpr !== "string") { if (typeof cExpr !== "string") {
throw new TypeError("First argument of app.setInterval must be a string"); throw new TypeError("First argument of app.setInterval must be a string");
} }
@ -560,7 +592,12 @@ class App extends PDFObject {
return this._registerTimeout(callbackId, true); return this._registerTimeout(callbackId, true);
} }
setTimeOut(cExpr, nMilliseconds) { setTimeOut(cExpr, nMilliseconds = 0) {
if (typeof cExpr === "object") {
nMilliseconds = cExpr.nMilliseconds || 0;
cExpr = cExpr.cExpr;
}
if (typeof cExpr !== "string") { if (typeof cExpr !== "string") {
throw new TypeError("First argument of app.setTimeOut must be a string"); throw new TypeError("First argument of app.setTimeOut must be a string");
} }

View File

@ -820,6 +820,9 @@ class Doc extends PDFObject {
} }
getField(cName) { getField(cName) {
if (typeof cName === "object") {
cName = cName.cName;
}
if (typeof cName !== "string") { if (typeof cName !== "string") {
throw new TypeError("Invalid field name: must be a string"); throw new TypeError("Invalid field name: must be a string");
} }
@ -852,7 +855,7 @@ class Doc extends PDFObject {
} }
} }
return undefined; return null;
} }
_getChildren(fieldName) { _getChildren(fieldName) {
@ -885,6 +888,9 @@ class Doc extends PDFObject {
} }
getNthFieldName(nIndex) { getNthFieldName(nIndex) {
if (typeof nIndex === "object") {
nIndex = nIndex.nIndex;
}
if (typeof nIndex !== "number") { if (typeof nIndex !== "number") {
throw new TypeError("Invalid field index: must be a number"); throw new TypeError("Invalid field index: must be a number");
} }
@ -1020,6 +1026,18 @@ class Doc extends PDFObject {
bAnnotations = true, bAnnotations = true,
printParams = null printParams = null
) { ) {
if (typeof bUI === "object") {
nStart = bUI.nStart;
nEnd = bUI.nEnd;
bSilent = bUI.bSilent;
bShrinkToFit = bUI.bShrinkToFit;
bPrintAsImage = bUI.bPrintAsImage;
bReverse = bUI.bReverse;
bAnnotations = bUI.bAnnotations;
printParams = bUI.printParams;
bUI = bUI.bUI;
}
// TODO: for now just use nStart and nEnd // TODO: for now just use nStart and nEnd
// so need to see how to deal with the other params // so need to see how to deal with the other params
// (if possible) // (if possible)
@ -1084,16 +1102,23 @@ class Doc extends PDFObject {
} }
resetForm(aFields = null) { resetForm(aFields = null) {
if (aFields && !Array.isArray(aFields) && typeof aFields === "object") {
aFields = aFields.aFields;
}
let mustCalculate = false; let mustCalculate = false;
if (aFields) { if (aFields) {
for (const fieldName of aFields) { for (const fieldName of aFields) {
if (!fieldName) {
continue;
}
const field = this.getField(fieldName); const field = this.getField(fieldName);
if (field) { if (!field) {
continue;
}
field.value = field.defaultValue; field.value = field.defaultValue;
field.valueAsString = field.value; field.valueAsString = field.value;
mustCalculate = true; mustCalculate = true;
} }
}
} else { } else {
mustCalculate = this._fields.size !== 0; mustCalculate = this._fields.size !== 0;
for (const field of this._fields.values()) { for (const field of this._fields.values()) {

View File

@ -96,23 +96,33 @@ class EventDispatcher {
} }
} }
if (name === "Keystroke") { switch (name) {
case "Keystroke":
savedChange = { savedChange = {
value: event.value, value: event.value,
change: event.change, change: event.change,
selStart: event.selStart, selStart: event.selStart,
selEnd: event.selEnd, selEnd: event.selEnd,
}; };
} else if (name === "Blur" || name === "Focus") { break;
case "Blur":
case "Focus":
Object.defineProperty(event, "value", { Object.defineProperty(event, "value", {
configurable: false, configurable: false,
writable: false, writable: false,
enumerable: true, enumerable: true,
value: event.value, value: event.value,
}); });
} else if (name === "Validate") { break;
case "Validate":
this.runValidation(source, event); this.runValidation(source, event);
return; return;
case "Action":
this.runActions(source, source, event, name);
if (this._document.obj.calculate) {
this.runCalculate(source, event);
}
return;
} }
this.runActions(source, source, event, name); this.runActions(source, source, event, name);
@ -143,8 +153,10 @@ class EventDispatcher {
if (event.rc) { if (event.rc) {
if (hasRan) { if (hasRan) {
source.wrapped.value = event.value; source.wrapped.value = event.value;
source.wrapped.valueAsString = event.value;
} else { } else {
source.obj.value = event.value; source.obj.value = event.value;
source.obj.valueAsString = event.value;
} }
if (this._document.obj.calculate) { if (this._document.obj.calculate) {
@ -187,6 +199,11 @@ class EventDispatcher {
continue; continue;
} }
if (!this._document.obj.calculate) {
// An action may have changed calculate value.
continue;
}
event.value = null; event.value = null;
const target = this._objects[targetId]; const target = this._objects[targetId];
this.runActions(source, target, event, "Calculate"); this.runActions(source, target, event, "Calculate");

View File

@ -81,12 +81,14 @@ class Field extends PDFObject {
this._strokeColor = data.strokeColor || ["G", 0]; this._strokeColor = data.strokeColor || ["G", 0];
this._textColor = data.textColor || ["G", 0]; this._textColor = data.textColor || ["G", 0];
this._value = data.value || ""; this._value = data.value || "";
this._valueAsString = data.valueAsString;
this._kidIds = data.kidIds || null; this._kidIds = data.kidIds || null;
this._fieldType = getFieldType(this._actions); this._fieldType = getFieldType(this._actions);
this._siblings = data.siblings || null;
this._globalEval = data.globalEval; this._globalEval = data.globalEval;
this._appObjects = data.appObjects; this._appObjects = data.appObjects;
this.valueAsString = data.valueAsString || this._value;
} }
get currentValueIndices() { get currentValueIndices() {
@ -246,6 +248,9 @@ class Field extends PDFObject {
} }
get valueAsString() { get valueAsString() {
if (this._valueAsString === undefined) {
this._valueAsString = this._value ? this._value.toString() : "";
}
return this._valueAsString; return this._valueAsString;
} }
@ -286,6 +291,9 @@ class Field extends PDFObject {
} }
this._buttonCaption[nFace] = cCaption; this._buttonCaption[nFace] = cCaption;
// TODO: send to the annotation layer // TODO: send to the annotation layer
// Right now the button is drawn on the canvas using its appearance so
// update the caption means redraw...
// We should probably have an html button for this annotation.
} }
buttonSetIcon(oIcon, nFace = 0) { buttonSetIcon(oIcon, nFace = 0) {
@ -512,7 +520,7 @@ class RadioButtonField extends Field {
} }
set value(value) { set value(value) {
if (value === null) { if (value === null || value === undefined) {
this._value = ""; this._value = "";
} }
const i = this.exportValues.indexOf(value); const i = this.exportValues.indexOf(value);
@ -574,7 +582,7 @@ class CheckboxField extends RadioButtonField {
} }
set value(value) { set value(value) {
if (value === "Off") { if (!value || value === "Off") {
this._value = "Off"; this._value = "Off";
} else { } else {
super.value = value; super.value = value;

View File

@ -94,14 +94,28 @@ function initSandbox(params) {
obj.doc = _document; obj.doc = _document;
obj.fieldPath = name; obj.fieldPath = name;
obj.appObjects = appObjects; obj.appObjects = appObjects;
let field; let field;
if (obj.type === "radiobutton") { switch (obj.type) {
case "radiobutton": {
const otherButtons = annotations.slice(1); const otherButtons = annotations.slice(1);
field = new RadioButtonField(otherButtons, obj); field = new RadioButtonField(otherButtons, obj);
} else if (obj.type === "checkbox") { break;
}
case "checkbox": {
const otherButtons = annotations.slice(1); const otherButtons = annotations.slice(1);
field = new CheckboxField(otherButtons, obj); field = new CheckboxField(otherButtons, obj);
} else { break;
}
case "text":
if (annotations.length <= 1) {
field = new Field(obj);
break;
}
obj.siblings = annotations.map(x => x.id).slice(1);
field = new Field(obj);
break;
default:
field = new Field(obj); field = new Field(obj);
} }

View File

@ -38,6 +38,14 @@ class ProxyHandler {
} }
set(obj, prop, value) { set(obj, prop, value) {
if (obj._kidIds) {
// If the field is a container for other fields then
// dispatch the kids.
obj._kidIds.forEach(id => {
obj._appObjects[id].wrapped[prop] = value;
});
}
if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) { if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) {
const old = obj[prop]; const old = obj[prop];
obj[prop] = value; obj[prop] = value;
@ -46,7 +54,12 @@ class ProxyHandler {
data[prop] = obj[prop]; data[prop] = obj[prop];
// send the updated value to the other side // send the updated value to the other side
if (!obj._siblings) {
obj._send(data); obj._send(data);
} else {
data.siblings = obj._siblings;
obj._send(data);
}
} }
} else { } else {
obj._expandos[prop] = value; obj._expandos[prop] = value;

View File

@ -802,4 +802,43 @@ describe("Interaction", () => {
); );
}); });
}); });
describe("in issue13269.pdf", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("issue13269.pdf", "#\\32 7R");
});
afterAll(async () => {
await closePages(pages);
});
it("must update fields with the same name from JS", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForFunction(
"window.PDFViewerApplication.scriptingReady === true"
);
await page.type("#\\32 7R", "hello");
await page.keyboard.press("Enter");
await Promise.all(
[4, 5, 6].map(async n =>
page.waitForFunction(
`document.querySelector("#\\\\32 ${n}R").value !== ""`
)
)
);
const expected = "hello world";
for (const n of [4, 5, 6]) {
const text = await page.$eval(`#\\32 ${n}R`, el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual(expected);
}
})
);
});
});
}); });

View File

@ -8,6 +8,7 @@
!franz_2.pdf !franz_2.pdf
!fraction-highlight.pdf !fraction-highlight.pdf
!german-umlaut-r.pdf !german-umlaut-r.pdf
!issue13269.pdf
!xref_command_missing.pdf !xref_command_missing.pdf
!issue1155r.pdf !issue1155r.pdf
!issue2017r.pdf !issue2017r.pdf

BIN
test/pdfs/issue13269.pdf Normal file

Binary file not shown.

View File

@ -271,7 +271,7 @@ class PDFScriptingManager {
this._pdfViewer.isInPresentationMode || this._pdfViewer.isInPresentationMode ||
this._pdfViewer.isChangingPresentationMode; this._pdfViewer.isChangingPresentationMode;
const { id, command, value } = detail; const { id, siblings, command, value } = detail;
if (!id) { if (!id) {
switch (command) { switch (command) {
case "clear": case "clear":
@ -309,13 +309,17 @@ class PDFScriptingManager {
} }
} }
const element = document.getElementById(id); delete detail.id;
const ids = siblings ? [id, ...siblings] : [id];
for (const elementId of ids) {
const element = document.getElementById(elementId);
if (element) { if (element) {
element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail })); element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail }));
} else { } else {
delete detail.id;
// The element hasn't been rendered yet, use the AnnotationStorage. // The element hasn't been rendered yet, use the AnnotationStorage.
this._pdfDocument?.annotationStorage.setValue(id, detail); this._pdfDocument?.annotationStorage.setValue(elementId, detail);
}
} }
} }