mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-22 16:05:56 +02:00
Merge pull request #21428 from calixteman/checkubtton_opt
Resolve checkbox/radio export values from the 'Opt' entry
This commit is contained in:
commit
67e6ff0090
@ -3346,7 +3346,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
value: value ? this.data.exportValue : "",
|
||||
};
|
||||
|
||||
const name = Name.get(value ? this.data.exportValue : "Off");
|
||||
const name = Name.get(value ? this._onStateName : "Off");
|
||||
this.setValue(dict, name, evaluator.xref, changes);
|
||||
|
||||
dict.set("AS", name);
|
||||
@ -3406,7 +3406,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
value: value ? this.data.buttonValue : "",
|
||||
};
|
||||
|
||||
const name = Name.get(value ? this.data.buttonValue : "Off");
|
||||
const name = Name.get(value ? this._onStateName : "Off");
|
||||
if (value) {
|
||||
this.setValue(dict, name, evaluator.xref, changes);
|
||||
}
|
||||
@ -3485,6 +3485,107 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
this._streams.push(this.checkedAppearance);
|
||||
}
|
||||
|
||||
_getOnStateName(dict) {
|
||||
const appearanceStates = dict.get("AP");
|
||||
if (!(appearanceStates instanceof Dict)) {
|
||||
return null;
|
||||
}
|
||||
const normalAppearance = appearanceStates.get("N");
|
||||
if (!(normalAppearance instanceof Dict)) {
|
||||
return null;
|
||||
}
|
||||
for (const key of normalAppearance.getKeys()) {
|
||||
if (key !== "Off") {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_getExportValueForOptIndex(index, opt, xref) {
|
||||
if (Number.isInteger(index) && index >= 0 && index < opt.length) {
|
||||
const value = this._decodeFormValue(xref.fetchIfRef(opt[index]));
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_getOptInfo(dict, onState, opt, xref) {
|
||||
if (!Array.isArray(opt)) {
|
||||
return null;
|
||||
}
|
||||
const stateToIndex = new Map();
|
||||
let currentIndex = null;
|
||||
|
||||
const fieldParent = dict.get("Parent");
|
||||
const kids = fieldParent instanceof Dict ? fieldParent.get("Kids") : null;
|
||||
if (Array.isArray(kids)) {
|
||||
for (let i = 0, ii = Math.min(kids.length, opt.length); i < ii; i++) {
|
||||
const kid = kids[i];
|
||||
if (kid instanceof Ref && isRefsEqual(kid, this.ref)) {
|
||||
currentIndex = i;
|
||||
}
|
||||
|
||||
const kidDict = xref.fetchIfRef(kid);
|
||||
if (!(kidDict instanceof Dict)) {
|
||||
continue;
|
||||
}
|
||||
if (kidDict === dict) {
|
||||
currentIndex = i;
|
||||
}
|
||||
|
||||
const kidOnState = this._getOnStateName(kidDict);
|
||||
if (typeof kidOnState === "string" && !stateToIndex.has(kidOnState)) {
|
||||
stateToIndex.set(kidOnState, i);
|
||||
}
|
||||
}
|
||||
} else if (opt.length === 1 && typeof onState === "string") {
|
||||
// A single widget is sometimes used as its own field dictionary.
|
||||
currentIndex = 0;
|
||||
stateToIndex.set(onState, 0);
|
||||
}
|
||||
|
||||
return { currentIndex, opt, stateToIndex };
|
||||
}
|
||||
|
||||
// The appearance state is a Name; its real export value can be overridden by
|
||||
// the "Opt" array, whose entries are ordered like the field's "Kids".
|
||||
_getExportValue(state, optInfo, xref) {
|
||||
if (!optInfo || typeof state !== "string" || state === "Off") {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (state === this._onStateName) {
|
||||
const exportValue = this._getExportValueForOptIndex(
|
||||
optInfo.currentIndex,
|
||||
optInfo.opt,
|
||||
xref
|
||||
);
|
||||
if (exportValue !== null) {
|
||||
return exportValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (optInfo.stateToIndex.has(state)) {
|
||||
const exportValue = this._getExportValueForOptIndex(
|
||||
optInfo.stateToIndex.get(state),
|
||||
optInfo.opt,
|
||||
xref
|
||||
);
|
||||
if (exportValue !== null) {
|
||||
return exportValue;
|
||||
}
|
||||
}
|
||||
|
||||
const index = parseInt(state, 10);
|
||||
if (Number.isInteger(index) && String(index) === state) {
|
||||
return this._getExportValueForOptIndex(index, optInfo.opt, xref) || state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
_processCheckBox(params) {
|
||||
const customAppearance = params.dict.get("AP");
|
||||
let normalAppearance =
|
||||
@ -3527,15 +3628,33 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
exportValues.push("Off", otherYes);
|
||||
}
|
||||
|
||||
const onState = exportValues[1];
|
||||
this._onStateName = onState;
|
||||
|
||||
const opt = getInheritableProperty({ dict: params.dict, key: "Opt" });
|
||||
const optInfo = this._getOptInfo(params.dict, onState, opt, params.xref);
|
||||
this.data.exportValue = this._getExportValue(onState, optInfo, params.xref);
|
||||
|
||||
// Don't use a "V" entry pointing to a non-existent appearance state,
|
||||
// see e.g. bug1720411.pdf where it's an *empty* Name-instance.
|
||||
if (!exportValues.includes(this.data.fieldValue)) {
|
||||
if (
|
||||
!exportValues.includes(this.data.fieldValue) &&
|
||||
this.data.fieldValue !== this.data.exportValue
|
||||
) {
|
||||
this.data.fieldValue = "Off";
|
||||
}
|
||||
this.data.fieldValue = this._getExportValue(
|
||||
this.data.fieldValue,
|
||||
optInfo,
|
||||
params.xref
|
||||
);
|
||||
this.data.defaultFieldValue = this._getExportValue(
|
||||
this.data.defaultFieldValue,
|
||||
optInfo,
|
||||
params.xref
|
||||
);
|
||||
|
||||
this.data.exportValue = exportValues[1];
|
||||
|
||||
const checkedAppearance = normalAppearance?.get(this.data.exportValue);
|
||||
const checkedAppearance = normalAppearance?.get(onState);
|
||||
this.checkedAppearance =
|
||||
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
|
||||
const uncheckedAppearance = normalAppearance?.get("Off");
|
||||
@ -3579,14 +3698,30 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
if (!(normalAppearance instanceof Dict)) {
|
||||
return;
|
||||
}
|
||||
let onState = null;
|
||||
for (const key of normalAppearance.getKeys()) {
|
||||
if (key !== "Off") {
|
||||
this.data.buttonValue = key;
|
||||
onState = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._onStateName = onState;
|
||||
|
||||
const checkedAppearance = normalAppearance.get(this.data.buttonValue);
|
||||
const opt = getInheritableProperty({ dict: params.dict, key: "Opt" });
|
||||
const optInfo = this._getOptInfo(params.dict, onState, opt, params.xref);
|
||||
this.data.buttonValue = this._getExportValue(onState, optInfo, params.xref);
|
||||
this.data.fieldValue = this._getExportValue(
|
||||
this.data.fieldValue,
|
||||
optInfo,
|
||||
params.xref
|
||||
);
|
||||
this.data.defaultFieldValue = this._getExportValue(
|
||||
this.data.defaultFieldValue,
|
||||
optInfo,
|
||||
params.xref
|
||||
);
|
||||
|
||||
const checkedAppearance = normalAppearance.get(onState);
|
||||
this.checkedAppearance =
|
||||
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
|
||||
const uncheckedAppearance = normalAppearance.get("Off");
|
||||
|
||||
@ -2745,4 +2745,60 @@ describe("Interaction", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("in opt_demo.pdf", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait("opt_demo.pdf", getSelector("19R"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must expose the Opt export value of checkboxes and radio buttons", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForScripting(page);
|
||||
|
||||
// Selecting a button runs a script that writes the field's value into
|
||||
// the read-only "result" field (19R). The appearance states are
|
||||
// indices, so without the "Opt" mapping we'd see the index here.
|
||||
const cases = [
|
||||
["8R", "fruit = [Cherry]"],
|
||||
["6R", "fruit = [りんご]"],
|
||||
["10R", "shared = [same]"],
|
||||
["12R", "agree = [I Agree to terms]"],
|
||||
];
|
||||
for (const [id, expected] of cases) {
|
||||
await page.click(getSelector(id));
|
||||
await page.waitForFunction(
|
||||
`${getQuerySelector("19R")}.value === ${JSON.stringify(expected)}`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("must expose the Opt export value when the parent has no Kids", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForScripting(page);
|
||||
|
||||
// The "veg" parent carries "Opt" but no "Kids", so its export value
|
||||
// is resolved from the numeric appearance-state name.
|
||||
await page.click(getSelector("22R"));
|
||||
await page.waitForFunction(
|
||||
`${getQuerySelector("19R")}.value === "veg = [Carrot]"`
|
||||
);
|
||||
|
||||
await page.click(getSelector("23R"));
|
||||
await page.waitForFunction(
|
||||
`${getQuerySelector("19R")}.value === "veg = [Potato]"`
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -935,3 +935,4 @@
|
||||
!text_field_own_canvas_calc.pdf
|
||||
!bug1802506.pdf
|
||||
!checkbox_no_appearance.pdf
|
||||
!opt_demo.pdf
|
||||
|
||||
133
test/pdfs/opt_demo.pdf
Normal file
133
test/pdfs/opt_demo.pdf
Normal file
@ -0,0 +1,133 @@
|
||||
%PDF-1.7
|
||||
%âãÏÓ
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Names << /JavaScript << /Names [(doc) 20 0 R] >> >> >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 540 360] /Contents 17 0 R /Resources << /Font << /Helv 18 0 R >> >> /Annots [6 0 R 7 0 R 8 0 R 10 0 R 11 0 R 12 0 R 19 0 R 22 0 R 23 0 R] >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Fields [5 0 R 9 0 R 12 0 R 19 0 R 22 0 R 23 0 R] /DR << /Font << /Helv 18 0 R >> >> /DA (/Helv 0 Tf 0 g) /NeedAppearances false >>
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /FT /Btn /Ff 49152 /T (fruit) /V /1 /DV /1 /Kids [6 0 R 7 0 R 8 0 R] /Opt [<FEFF308A30933054> (Banane) (Cherry)] >>
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [50 300 70 320] /AP << /N << /0 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "fruit = [" + this.getField("fruit").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [50 270 70 290] /AP << /N << /1 13 0 R /Off 14 0 R >> >> /AS /1 /A << /S /JavaScript /JS (this.getField("result").value = "fruit = [" + this.getField("fruit").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
|
||||
endobj
|
||||
8 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [50 240 70 260] /AP << /N << /2 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "fruit = [" + this.getField("fruit").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
|
||||
endobj
|
||||
9 0 obj
|
||||
<< /FT /Btn /Ff 49152 /T (shared) /V /Off /Kids [10 0 R 11 0 R] /Opt [(same) (same)] >>
|
||||
endobj
|
||||
10 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /Parent 9 0 R /Rect [50 178 70 198] /AP << /N << /0 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "shared = [" + this.getField("shared").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
|
||||
endobj
|
||||
11 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /Parent 9 0 R /Rect [50 148 70 168] /AP << /N << /1 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "shared = [" + this.getField("shared").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
|
||||
endobj
|
||||
12 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /Rect [50 86 70 106] /AP << /N << /0 15 0 R /Off 16 0 R >> >> /AS /Off /Opt [(I Agree to terms)] /A << /S /JavaScript /JS (this.getField("result").value = "agree = [" + this.getField("agree").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
|
||||
endobj
|
||||
13 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 20 20] /Resources << >> /Length 112 >>
|
||||
stream
|
||||
0 0 0 rg 16 10 m 16 13.31 13.31 16 10 16 c 6.69 16 4 13.31 4 10 c 4 6.69 6.69 4 10 4 c 13.31 4 16 6.69 16 10 c f
|
||||
endstream
|
||||
endobj
|
||||
14 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 20 20] /Resources << >> /Length 0 >>
|
||||
stream
|
||||
|
||||
endstream
|
||||
endobj
|
||||
15 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 20 20] /Resources << >> /Length 35 >>
|
||||
stream
|
||||
0 0 0 RG 2 w 4 10 m 9 5 l 16 16 l S
|
||||
endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 20 20] /Resources << >> /Length 0 >>
|
||||
stream
|
||||
|
||||
endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<< /Length 929 >>
|
||||
stream
|
||||
BT
|
||||
/Helv 9 Tf 0 g
|
||||
1 0 0 1 50 332 Tm (Radio "fruit": AP states = kid indices; Opt = real export values) Tj
|
||||
1 0 0 1 75 306 Tm (state 0 -> Opt[0] = JP ringo [U+308A U+3093 U+3054]) Tj
|
||||
1 0 0 1 75 276 Tm (state 1 -> Opt[1] = Banane [selected]) Tj
|
||||
1 0 0 1 75 246 Tm (state 2 -> Opt[2] = Cherry) Tj
|
||||
1 0 0 1 50 210 Tm (Radio "shared": both buttons share Opt = "same") Tj
|
||||
1 0 0 1 75 184 Tm (state 0 -> "same") Tj
|
||||
1 0 0 1 75 154 Tm (state 1 -> "same") Tj
|
||||
1 0 0 1 50 118 Tm (Checkbox "agree": AP state "0", Opt = "I Agree to terms") Tj
|
||||
1 0 0 1 75 92 Tm (agree) Tj
|
||||
1 0 0 1 330 250 Tm (Last changed field -> value:) Tj
|
||||
1 0 0 1 330 182 Tm (Radio "veg" \(parent has Opt but no /Kids\)) Tj
|
||||
1 0 0 1 355 154 Tm (state 0 -> Opt[0] = Carrot) Tj
|
||||
1 0 0 1 355 124 Tm (state 1 -> Opt[1] = Potato [selected]) Tj
|
||||
1 0 0 1 50 26 Tm (Change any option; the box shows that field's value. pdf.js shows the index, Acrobat the Opt value.) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
|
||||
endobj
|
||||
19 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /FT /Tx /Ff 1 /T (result) /Rect [330 218 530 243] /DA (/Helv 11 Tf 0 g) /V () /MK << /BC [0] /BG [0.95 0.95 0.95] >> /F 4 >>
|
||||
endobj
|
||||
20 0 obj
|
||||
<< /S /JavaScript /JS (0;) >>
|
||||
endobj
|
||||
21 0 obj
|
||||
<< /FT /Btn /Ff 49152 /T (veg) /V /1 /DV /1 /Opt [(Carrot) (Potato)] >>
|
||||
endobj
|
||||
22 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /Parent 21 0 R /Rect [330 148 350 168] /AP << /N << /0 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "veg = [" + this.getField("veg").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
|
||||
endobj
|
||||
23 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /Parent 21 0 R /Rect [330 118 350 138] /AP << /N << /1 13 0 R /Off 14 0 R >> >> /AS /1 /A << /S /JavaScript /JS (this.getField("result").value = "veg = [" + this.getField("veg").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
|
||||
endobj
|
||||
xref
|
||||
0 24
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000133 00000 n
|
||||
0000000190 00000 n
|
||||
0000000390 00000 n
|
||||
0000000540 00000 n
|
||||
0000000674 00000 n
|
||||
0000000954 00000 n
|
||||
0000001232 00000 n
|
||||
0000001512 00000 n
|
||||
0000001615 00000 n
|
||||
0000001898 00000 n
|
||||
0000002181 00000 n
|
||||
0000002493 00000 n
|
||||
0000002734 00000 n
|
||||
0000002861 00000 n
|
||||
0000003024 00000 n
|
||||
0000003151 00000 n
|
||||
0000004132 00000 n
|
||||
0000004230 00000 n
|
||||
0000004404 00000 n
|
||||
0000004450 00000 n
|
||||
0000004538 00000 n
|
||||
0000004818 00000 n
|
||||
trailer
|
||||
<< /Size 24 /Root 1 0 R >>
|
||||
startxref
|
||||
5096
|
||||
%%EOF
|
||||
@ -2494,6 +2494,80 @@ describe("annotation", function () {
|
||||
expect(data.exportValue).toEqual("Checked");
|
||||
});
|
||||
|
||||
it("should handle checkboxes with an Opt export value", async function () {
|
||||
// The appearance state is the index "0"; the real export value lives in
|
||||
// the "Opt" array (e.g. for values that aren't valid Name objects).
|
||||
buttonWidgetDict.set("V", Name.get("0"));
|
||||
buttonWidgetDict.set("DV", Name.get("0"));
|
||||
buttonWidgetDict.set("Opt", ["I Agree to terms"]);
|
||||
|
||||
const appearanceStatesDict = new Dict();
|
||||
const normalAppearanceDict = new Dict();
|
||||
|
||||
normalAppearanceDict.set("Off", 0);
|
||||
normalAppearanceDict.set("0", 1);
|
||||
appearanceStatesDict.set("N", normalAppearanceDict);
|
||||
buttonWidgetDict.set("AP", appearanceStatesDict);
|
||||
|
||||
const buttonWidgetRef = Ref.get(124, 0);
|
||||
const xref = new XRefMock([
|
||||
{ ref: buttonWidgetRef, data: buttonWidgetDict },
|
||||
]);
|
||||
|
||||
const { data } = await AnnotationFactory.create(
|
||||
xref,
|
||||
buttonWidgetRef,
|
||||
annotationGlobalsMock,
|
||||
idFactoryMock
|
||||
);
|
||||
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
|
||||
expect(data.checkBox).toEqual(true);
|
||||
expect(data.radioButton).toEqual(false);
|
||||
expect(data.exportValue).toEqual("I Agree to terms");
|
||||
expect(data.fieldValue).toEqual("I Agree to terms");
|
||||
expect(data.defaultFieldValue).toEqual("I Agree to terms");
|
||||
});
|
||||
|
||||
it("should handle checkboxes with an Opt export value in the parent", async function () {
|
||||
const buttonWidgetRef = Ref.get(124, 0);
|
||||
const parentRef = Ref.get(125, 0);
|
||||
|
||||
const parentDict = new Dict();
|
||||
parentDict.set("V", Name.get("CheckedState"));
|
||||
parentDict.set("DV", Name.get("CheckedState"));
|
||||
parentDict.set("Kids", [buttonWidgetRef]);
|
||||
parentDict.set("T", "CheckboxGroup");
|
||||
parentDict.set("Opt", ["I Agree to terms"]);
|
||||
|
||||
const appearanceStatesDict = new Dict();
|
||||
const normalAppearanceDict = new Dict();
|
||||
|
||||
normalAppearanceDict.set("Off", 0);
|
||||
normalAppearanceDict.set("CheckedState", 1);
|
||||
appearanceStatesDict.set("N", normalAppearanceDict);
|
||||
buttonWidgetDict.set("AP", appearanceStatesDict);
|
||||
buttonWidgetDict.set("Parent", parentRef);
|
||||
|
||||
const xref = new XRefMock([
|
||||
{ ref: buttonWidgetRef, data: buttonWidgetDict },
|
||||
{ ref: parentRef, data: parentDict },
|
||||
]);
|
||||
buttonWidgetDict.xref = parentDict.xref = xref;
|
||||
|
||||
const { data } = await AnnotationFactory.create(
|
||||
xref,
|
||||
buttonWidgetRef,
|
||||
annotationGlobalsMock,
|
||||
idFactoryMock
|
||||
);
|
||||
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
|
||||
expect(data.checkBox).toEqual(true);
|
||||
expect(data.radioButton).toEqual(false);
|
||||
expect(data.exportValue).toEqual("I Agree to terms");
|
||||
expect(data.fieldValue).toEqual("I Agree to terms");
|
||||
expect(data.defaultFieldValue).toEqual("I Agree to terms");
|
||||
});
|
||||
|
||||
it("should handle checkboxes without export value", async function () {
|
||||
buttonWidgetDict.set("V", Name.get("Checked"));
|
||||
buttonWidgetDict.set("DV", Name.get("Off"));
|
||||
@ -2982,6 +3056,126 @@ describe("annotation", function () {
|
||||
expect(data.buttonValue).toEqual("2");
|
||||
});
|
||||
|
||||
it("should handle radio buttons with an Opt export value", async function () {
|
||||
const parentDict = new Dict();
|
||||
parentDict.set("V", Name.get("1"));
|
||||
parentDict.set("Opt", ["Apple", "Banane", "Cherry"]);
|
||||
|
||||
const normalAppearanceStateDict = new Dict();
|
||||
normalAppearanceStateDict.set("2", null);
|
||||
|
||||
const appearanceStatesDict = new Dict();
|
||||
appearanceStatesDict.set("N", normalAppearanceStateDict);
|
||||
|
||||
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
|
||||
buttonWidgetDict.set("Parent", parentDict);
|
||||
buttonWidgetDict.set("AP", appearanceStatesDict);
|
||||
|
||||
const buttonWidgetRef = Ref.get(124, 0);
|
||||
const xref = new XRefMock([
|
||||
{ ref: buttonWidgetRef, data: buttonWidgetDict },
|
||||
]);
|
||||
|
||||
const { data } = await AnnotationFactory.create(
|
||||
xref,
|
||||
buttonWidgetRef,
|
||||
annotationGlobalsMock,
|
||||
idFactoryMock
|
||||
);
|
||||
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
|
||||
expect(data.radioButton).toEqual(true);
|
||||
// The field value (parent "V" = "1") and this widget's own on-state ("2")
|
||||
// are both mapped through "Opt" to their real export values.
|
||||
expect(data.fieldValue).toEqual("Banane");
|
||||
expect(data.buttonValue).toEqual("Cherry");
|
||||
});
|
||||
|
||||
it("should not map non-canonical numeric radio states through Opt", async function () {
|
||||
const parentDict = new Dict();
|
||||
parentDict.set("V", Name.get("02"));
|
||||
parentDict.set("Opt", ["Apple", "Banane", "Cherry"]);
|
||||
|
||||
const normalAppearanceStateDict = new Dict();
|
||||
normalAppearanceStateDict.set("02", null);
|
||||
|
||||
const appearanceStatesDict = new Dict();
|
||||
appearanceStatesDict.set("N", normalAppearanceStateDict);
|
||||
|
||||
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
|
||||
buttonWidgetDict.set("Parent", parentDict);
|
||||
buttonWidgetDict.set("AP", appearanceStatesDict);
|
||||
|
||||
const buttonWidgetRef = Ref.get(124, 0);
|
||||
const xref = new XRefMock([
|
||||
{ ref: buttonWidgetRef, data: buttonWidgetDict },
|
||||
]);
|
||||
|
||||
const { data } = await AnnotationFactory.create(
|
||||
xref,
|
||||
buttonWidgetRef,
|
||||
annotationGlobalsMock,
|
||||
idFactoryMock
|
||||
);
|
||||
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
|
||||
expect(data.radioButton).toEqual(true);
|
||||
expect(data.fieldValue).toEqual("02");
|
||||
expect(data.buttonValue).toEqual("02");
|
||||
});
|
||||
|
||||
it("should handle radio buttons with non-numeric Opt export values", async function () {
|
||||
const appleRef = Ref.get(122, 0);
|
||||
const bananaRef = Ref.get(123, 0);
|
||||
const cherryRef = Ref.get(124, 0);
|
||||
const parentRef = Ref.get(125, 0);
|
||||
|
||||
const parentDict = new Dict();
|
||||
parentDict.set("V", Name.get("BananaState"));
|
||||
parentDict.set("Kids", [appleRef, bananaRef, cherryRef]);
|
||||
parentDict.set("T", "Fruits");
|
||||
parentDict.set("Opt", ["Apple", "Banane", "Cherry"]);
|
||||
|
||||
const createRadioDict = state => {
|
||||
const radioDict = buttonWidgetDict.clone();
|
||||
const normalAppearanceStateDict = new Dict();
|
||||
normalAppearanceStateDict.set(state, null);
|
||||
|
||||
const appearanceStatesDict = new Dict();
|
||||
appearanceStatesDict.set("N", normalAppearanceStateDict);
|
||||
|
||||
radioDict.set("Ff", AnnotationFieldFlag.RADIO);
|
||||
radioDict.set("Parent", parentRef);
|
||||
radioDict.set("AP", appearanceStatesDict);
|
||||
return radioDict;
|
||||
};
|
||||
|
||||
const appleDict = createRadioDict("AppleState");
|
||||
const bananaDict = createRadioDict("BananaState");
|
||||
const cherryDict = createRadioDict("CherryState");
|
||||
|
||||
const xref = new XRefMock([
|
||||
{ ref: appleRef, data: appleDict },
|
||||
{ ref: bananaRef, data: bananaDict },
|
||||
{ ref: cherryRef, data: cherryDict },
|
||||
{ ref: parentRef, data: parentDict },
|
||||
]);
|
||||
appleDict.xref =
|
||||
bananaDict.xref =
|
||||
cherryDict.xref =
|
||||
parentDict.xref =
|
||||
xref;
|
||||
|
||||
const { data } = await AnnotationFactory.create(
|
||||
xref,
|
||||
cherryRef,
|
||||
annotationGlobalsMock,
|
||||
idFactoryMock
|
||||
);
|
||||
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
|
||||
expect(data.radioButton).toEqual(true);
|
||||
expect(data.fieldValue).toEqual("Banane");
|
||||
expect(data.buttonValue).toEqual("Cherry");
|
||||
});
|
||||
|
||||
it("should handle radio buttons with a field value that's not an ASCII string", async function () {
|
||||
const parentDict = new Dict();
|
||||
const name = "\x91I=\x91\xf0\x93\xe0\x97e3";
|
||||
@ -3256,6 +3450,113 @@ describe("annotation", function () {
|
||||
expect(changes.size).toEqual(0);
|
||||
});
|
||||
|
||||
it("should save radio buttons with Opt export values", async function () {
|
||||
const appearanceStatesDict = new Dict();
|
||||
const normalAppearanceDict = new Dict();
|
||||
|
||||
normalAppearanceDict.set("CheckedState", Ref.get(314, 0));
|
||||
normalAppearanceDict.set("Off", Ref.get(271, 0));
|
||||
appearanceStatesDict.set("N", normalAppearanceDict);
|
||||
|
||||
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
|
||||
buttonWidgetDict.set("AP", appearanceStatesDict);
|
||||
|
||||
const buttonWidgetRef = Ref.get(123, 0);
|
||||
const parentRef = Ref.get(456, 0);
|
||||
|
||||
const parentDict = new Dict();
|
||||
parentDict.set("V", Name.get("Off"));
|
||||
parentDict.set("Kids", [buttonWidgetRef]);
|
||||
parentDict.set("T", "RadioGroup");
|
||||
parentDict.set("Opt", ["I Agree to terms"]);
|
||||
buttonWidgetDict.set("Parent", parentRef);
|
||||
|
||||
const xref = new XRefMock([
|
||||
{ ref: buttonWidgetRef, data: buttonWidgetDict },
|
||||
{ ref: parentRef, data: parentDict },
|
||||
]);
|
||||
|
||||
parentDict.xref = xref;
|
||||
buttonWidgetDict.xref = xref;
|
||||
partialEvaluator.xref = xref;
|
||||
const task = new WorkerTask("test save");
|
||||
|
||||
const annotation = await AnnotationFactory.create(
|
||||
xref,
|
||||
buttonWidgetRef,
|
||||
annotationGlobalsMock,
|
||||
idFactoryMock
|
||||
);
|
||||
expect(annotation.data.buttonValue).toEqual("I Agree to terms");
|
||||
|
||||
const annotationStorage = new Map();
|
||||
annotationStorage.set(annotation.data.id, { value: true });
|
||||
const changes = new RefSetCache();
|
||||
|
||||
await annotation.save(partialEvaluator, task, annotationStorage, changes);
|
||||
const data = await writeChanges(changes, xref);
|
||||
expect(data.length).toEqual(2);
|
||||
const [radioData, parentData] = data;
|
||||
radioData.data = radioData.data.replace(/\(D:\d+\)/, "(date)");
|
||||
expect(radioData.ref).toEqual(Ref.get(123, 0));
|
||||
expect(radioData.data).toEqual(
|
||||
"123 0 obj\n" +
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 32768 " +
|
||||
"/AP << /N << /CheckedState 314 0 R /Off 271 0 R>>>> " +
|
||||
"/Parent 456 0 R /AS /CheckedState /M (date)>>\nendobj\n"
|
||||
);
|
||||
expect(parentData.ref).toEqual(Ref.get(456, 0));
|
||||
expect(parentData.data).toEqual(
|
||||
"456 0 obj\n<< /V /CheckedState /Kids [123 0 R] /T (RadioGroup) " +
|
||||
"/Opt [(I Agree to terms)]>>\nendobj\n"
|
||||
);
|
||||
});
|
||||
|
||||
it("should save checkboxes with Opt export values", async function () {
|
||||
const appearanceStatesDict = new Dict();
|
||||
const normalAppearanceDict = new Dict();
|
||||
|
||||
normalAppearanceDict.set("CheckedState", Ref.get(314, 0));
|
||||
normalAppearanceDict.set("Off", Ref.get(271, 0));
|
||||
appearanceStatesDict.set("N", normalAppearanceDict);
|
||||
|
||||
buttonWidgetDict.set("AP", appearanceStatesDict);
|
||||
buttonWidgetDict.set("V", Name.get("Off"));
|
||||
buttonWidgetDict.set("Opt", ["I Agree to terms"]);
|
||||
|
||||
const buttonWidgetRef = Ref.get(123, 0);
|
||||
const xref = new XRefMock([
|
||||
{ ref: buttonWidgetRef, data: buttonWidgetDict },
|
||||
]);
|
||||
buttonWidgetDict.xref = xref;
|
||||
partialEvaluator.xref = xref;
|
||||
const task = new WorkerTask("test save");
|
||||
|
||||
const annotation = await AnnotationFactory.create(
|
||||
xref,
|
||||
buttonWidgetRef,
|
||||
annotationGlobalsMock,
|
||||
idFactoryMock
|
||||
);
|
||||
expect(annotation.data.exportValue).toEqual("I Agree to terms");
|
||||
|
||||
const annotationStorage = new Map();
|
||||
annotationStorage.set(annotation.data.id, { value: true });
|
||||
const changes = new RefSetCache();
|
||||
|
||||
await annotation.save(partialEvaluator, task, annotationStorage, changes);
|
||||
const [data] = await writeChanges(changes, xref);
|
||||
data.data = data.data.replace(/\(D:\d+\)/, "(date)");
|
||||
expect(data.ref).toEqual(Ref.get(123, 0));
|
||||
expect(data.data).toEqual(
|
||||
"123 0 obj\n" +
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn " +
|
||||
"/AP << /N << /CheckedState 314 0 R /Off 271 0 R>>>> " +
|
||||
"/V /CheckedState /Opt [(I Agree to terms)] " +
|
||||
"/AS /CheckedState /M (date)>>\nendobj\n"
|
||||
);
|
||||
});
|
||||
|
||||
it("should save radio buttons without a field value", async function () {
|
||||
const appearanceStatesDict = new Dict();
|
||||
const normalAppearanceDict = new Dict();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user