[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 data = this.data;
const id = data.id;
const value = storage.getValue(id, {
let value = storage.getValue(id, {
value:
data.fieldValue &&
((data.exportValue && data.exportValue === data.fieldValue) ||
(!data.exportValue && data.fieldValue !== "Off")),
}).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";
@ -1012,9 +1017,14 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
const storage = this.annotationStorage;
const data = this.data;
const id = data.id;
const value = storage.getValue(id, {
let value = storage.getValue(id, {
value: data.fieldValue === data.buttonValue,
}).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");
element.disabled = data.readOnly;

View File

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

View File

@ -28,8 +28,6 @@ class App extends PDFObject {
constructor(data) {
super(data);
this.calculate = true;
this._constants = null;
this._focusRect = true;
this._fs = null;
@ -68,6 +66,7 @@ class App extends PDFObject {
this._timeoutCallbackId = 0;
this._globalEval = data.globalEval;
this._externalCall = data.externalCall;
this._document = data._document;
}
// This function is called thanks to the proxy
@ -191,6 +190,14 @@ class App extends PDFObject {
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() {
if (!this._constants) {
this._constants = Object.freeze({
@ -427,7 +434,21 @@ class App extends PDFObject {
oDoc = 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]);
return 1;
}
beep() {
@ -543,10 +564,21 @@ class App extends PDFObject {
}
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 || ""]);
}
setInterval(cExpr, nMilliseconds) {
setInterval(cExpr, nMilliseconds = 0) {
if (typeof cExpr === "object") {
nMilliseconds = cExpr.nMilliseconds || 0;
cExpr = cExpr.cExpr;
}
if (typeof cExpr !== "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);
}
setTimeOut(cExpr, nMilliseconds) {
setTimeOut(cExpr, nMilliseconds = 0) {
if (typeof cExpr === "object") {
nMilliseconds = cExpr.nMilliseconds || 0;
cExpr = cExpr.cExpr;
}
if (typeof cExpr !== "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) {
if (typeof cName === "object") {
cName = cName.cName;
}
if (typeof cName !== "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) {
@ -885,6 +888,9 @@ class Doc extends PDFObject {
}
getNthFieldName(nIndex) {
if (typeof nIndex === "object") {
nIndex = nIndex.nIndex;
}
if (typeof nIndex !== "number") {
throw new TypeError("Invalid field index: must be a number");
}
@ -1020,6 +1026,18 @@ class Doc extends PDFObject {
bAnnotations = true,
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
// so need to see how to deal with the other params
// (if possible)
@ -1084,16 +1102,23 @@ class Doc extends PDFObject {
}
resetForm(aFields = null) {
if (aFields && !Array.isArray(aFields) && typeof aFields === "object") {
aFields = aFields.aFields;
}
let mustCalculate = false;
if (aFields) {
for (const fieldName of aFields) {
if (!fieldName) {
continue;
}
const field = this.getField(fieldName);
if (field) {
if (!field) {
continue;
}
field.value = field.defaultValue;
field.valueAsString = field.value;
mustCalculate = true;
}
}
} else {
mustCalculate = this._fields.size !== 0;
for (const field of this._fields.values()) {

View File

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

View File

@ -81,12 +81,14 @@ class Field extends PDFObject {
this._strokeColor = data.strokeColor || ["G", 0];
this._textColor = data.textColor || ["G", 0];
this._value = data.value || "";
this._valueAsString = data.valueAsString;
this._kidIds = data.kidIds || null;
this._fieldType = getFieldType(this._actions);
this._siblings = data.siblings || null;
this._globalEval = data.globalEval;
this._appObjects = data.appObjects;
this.valueAsString = data.valueAsString || this._value;
}
get currentValueIndices() {
@ -246,6 +248,9 @@ class Field extends PDFObject {
}
get valueAsString() {
if (this._valueAsString === undefined) {
this._valueAsString = this._value ? this._value.toString() : "";
}
return this._valueAsString;
}
@ -286,6 +291,9 @@ class Field extends PDFObject {
}
this._buttonCaption[nFace] = cCaption;
// 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) {
@ -512,7 +520,7 @@ class RadioButtonField extends Field {
}
set value(value) {
if (value === null) {
if (value === null || value === undefined) {
this._value = "";
}
const i = this.exportValues.indexOf(value);
@ -574,7 +582,7 @@ class CheckboxField extends RadioButtonField {
}
set value(value) {
if (value === "Off") {
if (!value || value === "Off") {
this._value = "Off";
} else {
super.value = value;

View File

@ -94,14 +94,28 @@ function initSandbox(params) {
obj.doc = _document;
obj.fieldPath = name;
obj.appObjects = appObjects;
let field;
if (obj.type === "radiobutton") {
switch (obj.type) {
case "radiobutton": {
const otherButtons = annotations.slice(1);
field = new RadioButtonField(otherButtons, obj);
} else if (obj.type === "checkbox") {
break;
}
case "checkbox": {
const otherButtons = annotations.slice(1);
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);
}

View File

@ -38,6 +38,14 @@ class ProxyHandler {
}
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) {
const old = obj[prop];
obj[prop] = value;
@ -46,7 +54,12 @@ class ProxyHandler {
data[prop] = obj[prop];
// send the updated value to the other side
if (!obj._siblings) {
obj._send(data);
} else {
data.siblings = obj._siblings;
obj._send(data);
}
}
} else {
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
!fraction-highlight.pdf
!german-umlaut-r.pdf
!issue13269.pdf
!xref_command_missing.pdf
!issue1155r.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.isChangingPresentationMode;
const { id, command, value } = detail;
const { id, siblings, command, value } = detail;
if (!id) {
switch (command) {
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) {
element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail }));
} else {
delete detail.id;
// The element hasn't been rendered yet, use the AnnotationStorage.
this._pdfDocument?.annotationStorage.setValue(id, detail);
this._pdfDocument?.annotationStorage.setValue(elementId, detail);
}
}
}