Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix AcroForm printing/saving edge cases #12263

Merged
merged 5 commits into from
Aug 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 42 additions & 19 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -803,11 +803,14 @@ class WidgetAnnotation extends Annotation {

data.annotationType = AnnotationType.WIDGET;
data.fieldName = this._constructFieldName(dict);
data.fieldValue = getInheritableProperty({

const fieldValue = getInheritableProperty({
dict,
key: "V",
getArray: true,
});
data.fieldValue = this._decodeFormValue(fieldValue);

data.alternativeText = stringToPDFString(dict.get("TU") || "");
data.defaultAppearance =
getInheritableProperty({ dict, key: "DA" }) ||
Expand Down Expand Up @@ -882,6 +885,28 @@ class WidgetAnnotation extends Annotation {
return fieldName.join(".");
}

/**
* Decode the given form value.
*
* @private
* @memberof WidgetAnnotation
* @param {Array<string>|Name|string} formValue - The (possibly encoded)
* form value.
* @returns {Array<string>|string|null}
*/
_decodeFormValue(formValue) {
if (Array.isArray(formValue)) {
return formValue
.filter(item => isString(item))
.map(item => stringToPDFString(item));
} else if (isName(formValue)) {
return stringToPDFString(formValue.name);
} else if (isString(formValue)) {
return stringToPDFString(formValue);
}
return null;
}

/**
* Check if a provided field flag is set.
*
Expand Down Expand Up @@ -1194,7 +1219,9 @@ class TextWidgetAnnotation extends WidgetAnnotation {
const dict = params.dict;

// The field value is always a string.
this.data.fieldValue = stringToPDFString(this.data.fieldValue || "");
if (!isString(this.data.fieldValue)) {
this.data.fieldValue = "";
}

// Determine the alignment of text in the field.
let alignment = getInheritableProperty({ dict, key: "Q" });
Expand Down Expand Up @@ -1499,34 +1526,28 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
}

_processCheckBox(params) {
if (isName(this.data.fieldValue)) {
this.data.fieldValue = this.data.fieldValue.name;
}

const customAppearance = params.dict.get("AP");
if (!isDict(customAppearance)) {
return;
}

const exportValueOptionsDict = customAppearance.get("D");
if (!isDict(exportValueOptionsDict)) {
const normalAppearance = customAppearance.get("N");
if (!isDict(normalAppearance)) {
return;
}

const exportValues = exportValueOptionsDict.getKeys();
const hasCorrectOptionCount = exportValues.length === 2;
if (!hasCorrectOptionCount) {
const exportValues = normalAppearance.getKeys();
if (!exportValues.includes("Off")) {
// The /Off appearance is optional.
exportValues.push("Off");
}
if (exportValues.length !== 2) {
return;
}

this.data.exportValue =
exportValues[0] === "Off" ? exportValues[1] : exportValues[0];

const normalAppearance = customAppearance.get("N");
if (!isDict(normalAppearance)) {
return;
}

this.checkedAppearance = normalAppearance.get(this.data.exportValue);
this.uncheckedAppearance = normalAppearance.get("Off") || null;
}
Expand All @@ -1541,7 +1562,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
const fieldParentValue = fieldParent.get("V");
if (isName(fieldParentValue)) {
this.parent = params.dict.getRaw("Parent");
this.data.fieldValue = fieldParentValue.name;
this.data.fieldValue = this._decodeFormValue(fieldParentValue);
}
}

Expand Down Expand Up @@ -1602,8 +1623,10 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
const isOptionArray = Array.isArray(option);

this.data.options[i] = {
exportValue: isOptionArray ? xref.fetchIfRef(option[0]) : option,
displayValue: stringToPDFString(
exportValue: this._decodeFormValue(
isOptionArray ? xref.fetchIfRef(option[0]) : option
),
displayValue: this._decodeFormValue(
isOptionArray ? xref.fetchIfRef(option[1]) : option
),
};
Expand Down
17 changes: 14 additions & 3 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,18 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
const storage = this.annotationStorage;
const id = this.data.id;

// For printing/saving we currently only support choice widgets with one
// option selection. Therefore, listboxes (#12189) and comboboxes (#12224)
// are not properly printed/saved yet, so we only store the first item in
// the field value array instead of the entire array. Once support for those
// two field types is implemented, we should use the same pattern as the
// other interactive widgets where the return value of `getOrCreateValue` is
// used and the full array of field values is stored.
storage.getOrCreateValue(
id,
this.data.fieldValue.length > 0 ? this.data.fieldValue[0] : null
);

const selectElement = document.createElement("select");
selectElement.disabled = this.data.readOnly;
selectElement.name = this.data.fieldName;
Expand All @@ -682,16 +694,15 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
const optionElement = document.createElement("option");
optionElement.textContent = option.displayValue;
optionElement.value = option.exportValue;
if (this.data.fieldValue.includes(option.displayValue)) {
if (this.data.fieldValue.includes(option.exportValue)) {
optionElement.setAttribute("selected", true);
storage.setValue(id, option.displayValue);
}
selectElement.appendChild(optionElement);
}

selectElement.addEventListener("input", function (event) {
const options = event.target.options;
const value = options[options.selectedIndex].text;
const value = options[options.selectedIndex].value;
storage.setValue(id, value);
});

Expand Down
1 change: 1 addition & 0 deletions test/pdfs/issue12233.pdf.link
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/5112498/OoPdfFormExample.pdf
19 changes: 19 additions & 0 deletions test/test_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4492,6 +4492,25 @@
"63R": ""
}
},
{ "id": "issue12233-forms",
"file": "pdfs/issue12233.pdf",
"md5": "6099fc695fe018ce444752929d86f9c8",
"link": true,
"rounds": 1,
"type": "eq",
"forms": true
},
{ "id": "issue12233-print",
"file": "pdfs/issue12233.pdf",
"md5": "6099fc695fe018ce444752929d86f9c8",
"link": true,
"rounds": 1,
"type": "eq",
"print": true,
"annotationStorage": {
"20R": true
}
},
{ "id": "issue11931",
"file": "pdfs/issue11931.pdf",
"md5": "9ea233037992e1f10280420a49e72845",
Expand Down
79 changes: 45 additions & 34 deletions test/unit/annotation_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1882,11 +1882,11 @@ describe("annotation", function () {
buttonWidgetDict.set("V", Name.get("1"));

const appearanceStatesDict = new Dict();
const exportValueOptionsDict = new Dict();
const normalAppearanceDict = new Dict();

exportValueOptionsDict.set("Off", 0);
exportValueOptionsDict.set("Checked", 1);
appearanceStatesDict.set("D", exportValueOptionsDict);
normalAppearanceDict.set("Off", 0);
normalAppearanceDict.set("Checked", 1);
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("AP", appearanceStatesDict);

const buttonWidgetRef = Ref.get(124, 0);
Expand Down Expand Up @@ -1931,9 +1931,38 @@ describe("annotation", function () {
}, done.fail);
});

it("should handle checkboxes without /Off appearance", function (done) {
buttonWidgetDict.set("V", Name.get("1"));

const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();

normalAppearanceDict.set("Checked", 1);
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("AP", appearanceStatesDict);

const buttonWidgetRef = Ref.get(124, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);

AnnotationFactory.create(
xref,
buttonWidgetRef,
pdfManagerMock,
idFactoryMock
).then(({ data }) => {
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.checkBox).toEqual(true);
expect(data.fieldValue).toEqual("1");
expect(data.radioButton).toEqual(false);
expect(data.exportValue).toEqual("Checked");
done();
}, done.fail);
});

it("should render checkboxes for printing", function (done) {
const appearanceStatesDict = new Dict();
const exportValueOptionsDict = new Dict();
const normalAppearanceDict = new Dict();
const checkedAppearanceDict = new Dict();
const uncheckedAppearanceDict = new Dict();
Expand All @@ -1949,9 +1978,6 @@ describe("annotation", function () {
checkedAppearanceDict.set("Matrix", [1, 0, 0, 1, 0, 0]);
normalAppearanceDict.set("Checked", checkedStream);
normalAppearanceDict.set("Off", uncheckedStream);
exportValueOptionsDict.set("Off", 0);
exportValueOptionsDict.set("Checked", 1);
appearanceStatesDict.set("D", exportValueOptionsDict);
appearanceStatesDict.set("N", normalAppearanceDict);

buttonWidgetDict.set("AP", appearanceStatesDict);
Expand Down Expand Up @@ -2019,14 +2045,10 @@ describe("annotation", function () {

it("should save checkboxes", function (done) {
const appearanceStatesDict = new Dict();
const exportValueOptionsDict = new Dict();
const normalAppearanceDict = new Dict();

normalAppearanceDict.set("Checked", Ref.get(314, 0));
normalAppearanceDict.set("Off", Ref.get(271, 0));
exportValueOptionsDict.set("Off", 0);
exportValueOptionsDict.set("Checked", 1);
appearanceStatesDict.set("D", exportValueOptionsDict);
appearanceStatesDict.set("N", normalAppearanceDict);

buttonWidgetDict.set("AP", appearanceStatesDict);
Expand Down Expand Up @@ -2059,8 +2081,7 @@ describe("annotation", function () {
expect(oldData.data).toEqual(
"123 0 obj\n" +
"<< /Type /Annot /Subtype /Widget /FT /Btn " +
"/AP << /D << /Off 0 /Checked 1>> " +
"/N << /Checked 314 0 R /Off 271 0 R>>>> " +
"/AP << /N << /Checked 314 0 R /Off 271 0 R>>>> " +
"/V /Checked /AS /Checked /M (date)>>\nendobj\n"
);
return annotation;
Expand Down Expand Up @@ -2142,7 +2163,6 @@ describe("annotation", function () {

it("should render radio buttons for printing", function (done) {
const appearanceStatesDict = new Dict();
const exportValueOptionsDict = new Dict();
const normalAppearanceDict = new Dict();
const checkedAppearanceDict = new Dict();
const uncheckedAppearanceDict = new Dict();
Expand All @@ -2158,9 +2178,6 @@ describe("annotation", function () {
checkedAppearanceDict.set("Matrix", [1, 0, 0, 1, 0, 0]);
normalAppearanceDict.set("Checked", checkedStream);
normalAppearanceDict.set("Off", uncheckedStream);
exportValueOptionsDict.set("Off", 0);
exportValueOptionsDict.set("Checked", 1);
appearanceStatesDict.set("D", exportValueOptionsDict);
appearanceStatesDict.set("N", normalAppearanceDict);

buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
Expand Down Expand Up @@ -2229,14 +2246,10 @@ describe("annotation", function () {

it("should save radio buttons", function (done) {
const appearanceStatesDict = new Dict();
const exportValueOptionsDict = new Dict();
const normalAppearanceDict = new Dict();

normalAppearanceDict.set("Checked", Ref.get(314, 0));
normalAppearanceDict.set("Off", Ref.get(271, 0));
exportValueOptionsDict.set("Off", 0);
exportValueOptionsDict.set("Checked", 1);
appearanceStatesDict.set("D", exportValueOptionsDict);
appearanceStatesDict.set("N", normalAppearanceDict);

buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
Expand Down Expand Up @@ -2282,8 +2295,7 @@ describe("annotation", function () {
expect(radioData.data).toEqual(
"123 0 obj\n" +
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 32768 " +
"/AP << /D << /Off 0 /Checked 1>> " +
"/N << /Checked 314 0 R /Off 271 0 R>>>> " +
"/AP << /N << /Checked 314 0 R /Off 271 0 R>>>> " +
"/Parent 456 0 R /AS /Checked /M (date)>>\nendobj\n"
);
expect(parentData.ref).toEqual(Ref.get(456, 0));
Expand Down Expand Up @@ -2450,16 +2462,12 @@ describe("annotation", function () {
}, done.fail);
});

it("should sanitize display values in option arrays (issue 8947)", function (done) {
// The option value is a UTF-16BE string. The display value should be
// sanitized, but the export value should remain the same since that
// may be used as a unique identifier when exporting form values.
const options = ["\xFE\xFF\x00F\x00o\x00o"];
const expected = [
{ exportValue: "\xFE\xFF\x00F\x00o\x00o", displayValue: "Foo" },
];
it("should decode form values", function (done) {
const encodedString = "\xFE\xFF\x00F\x00o\x00o";
const decodedString = "Foo";

choiceWidgetDict.set("Opt", options);
choiceWidgetDict.set("Opt", [encodedString]);
choiceWidgetDict.set("V", encodedString);

const choiceWidgetRef = Ref.get(984, 0);
const xref = new XRefMock([
Expand All @@ -2473,7 +2481,10 @@ describe("annotation", function () {
idFactoryMock
).then(({ data }) => {
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.options).toEqual(expected);
expect(data.fieldValue).toEqual([decodedString]);
expect(data.options).toEqual([
{ exportValue: decodedString, displayValue: decodedString },
]);
done();
}, done.fail);
});
Expand Down