[Annotations] Add support for printing/saving choice list with multiple selections
- it aims to fix issue #12189.
This commit is contained in:
parent
0dd6bc9a85
commit
ad3fb71a02
@ -50,6 +50,11 @@ import { StringStream } from "./stream.js";
|
||||
import { writeDict } from "./writer.js";
|
||||
import { XFAFactory } from "./xfa/factory.js";
|
||||
|
||||
// Represent the percentage of the height of a single-line field over
|
||||
// the font size.
|
||||
// Acrobat seems to use this value.
|
||||
const LINE_FACTOR = 1.35;
|
||||
|
||||
class AnnotationFactory {
|
||||
/**
|
||||
* Create an `Annotation` object of the correct type for the given reference
|
||||
@ -1405,6 +1410,16 @@ class WidgetAnnotation extends Annotation {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Value can be an array (with choice list and multiple selections)
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
Array.isArray(this.data.fieldValue) &&
|
||||
value.length === this.data.fieldValue.length &&
|
||||
value.every((x, i) => x === this.data.fieldValue[i])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let appearance = await this._getAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
@ -1448,7 +1463,8 @@ class WidgetAnnotation extends Annotation {
|
||||
appearance = newTransform.encryptString(appearance);
|
||||
}
|
||||
|
||||
dict.set("V", isAscii(value) ? value : stringToUTF16BEString(value));
|
||||
const encoder = val => (isAscii(val) ? val : stringToUTF16BEString(val));
|
||||
dict.set("V", Array.isArray(value) ? value.map(encoder) : encoder(value));
|
||||
dict.set("AP", AP);
|
||||
dict.set("M", `D:${getModificationDate()}`);
|
||||
|
||||
@ -1629,11 +1645,6 @@ class WidgetAnnotation extends Annotation {
|
||||
|
||||
const roundWithTwoDigits = x => Math.floor(x * 100) / 100;
|
||||
|
||||
// Represent the percentage of the height of a single-line field over
|
||||
// the font size.
|
||||
// Acrobat seems to use this value.
|
||||
const LINE_FACTOR = 1.35;
|
||||
|
||||
if (lineCount === -1) {
|
||||
const textWidth = this._getTextWidth(text, font);
|
||||
fontSize = roundWithTwoDigits(
|
||||
@ -1703,14 +1714,14 @@ class WidgetAnnotation extends Annotation {
|
||||
}
|
||||
|
||||
_renderText(text, font, fontSize, totalWidth, alignment, hPadding, vPadding) {
|
||||
// We need to get the width of the text in order to align it correctly
|
||||
const width = this._getTextWidth(text, font) * fontSize;
|
||||
let shift;
|
||||
if (alignment === 1) {
|
||||
// Center
|
||||
const width = this._getTextWidth(text, font) * fontSize;
|
||||
shift = (totalWidth - width) / 2;
|
||||
} else if (alignment === 2) {
|
||||
// Right
|
||||
const width = this._getTextWidth(text, font) * fontSize;
|
||||
shift = totalWidth - width - hPadding;
|
||||
} else {
|
||||
shift = hPadding;
|
||||
@ -2483,6 +2494,135 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
async _getAppearance(evaluator, task, annotationStorage) {
|
||||
if (this.data.combo) {
|
||||
return super._getAppearance(evaluator, task, annotationStorage);
|
||||
}
|
||||
|
||||
if (!annotationStorage) {
|
||||
return null;
|
||||
}
|
||||
const storageEntry = annotationStorage.get(this.data.id);
|
||||
let exportedValue = storageEntry && storageEntry.value;
|
||||
if (exportedValue === undefined) {
|
||||
// The annotation hasn't been rendered so use the appearance
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Array.isArray(exportedValue)) {
|
||||
exportedValue = [exportedValue];
|
||||
}
|
||||
|
||||
const defaultPadding = 2;
|
||||
const hPadding = defaultPadding;
|
||||
const totalHeight = this.data.rect[3] - this.data.rect[1];
|
||||
const totalWidth = this.data.rect[2] - this.data.rect[0];
|
||||
const lineCount = this.data.options.length;
|
||||
const valueIndices = [];
|
||||
for (let i = 0; i < lineCount; i++) {
|
||||
const { exportValue } = this.data.options[i];
|
||||
if (exportedValue.includes(exportValue)) {
|
||||
valueIndices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._defaultAppearance) {
|
||||
// The DA is required and must be a string.
|
||||
// If there is no font named Helvetica in the resource dictionary,
|
||||
// the evaluator will fall back to a default font.
|
||||
// Doing so prevents exceptions and allows saving/printing
|
||||
// the file as expected.
|
||||
this.data.defaultAppearanceData = parseDefaultAppearance(
|
||||
(this._defaultAppearance = "/Helvetica 0 Tf 0 g")
|
||||
);
|
||||
}
|
||||
|
||||
const font = await this._getFontData(evaluator, task);
|
||||
|
||||
let defaultAppearance;
|
||||
let { fontSize } = this.data.defaultAppearanceData;
|
||||
if (!fontSize) {
|
||||
const lineHeight = (totalHeight - defaultPadding) / lineCount;
|
||||
let lineWidth = -1;
|
||||
let value;
|
||||
for (const { displayValue } of this.data.options) {
|
||||
const width = this._getTextWidth(displayValue);
|
||||
if (width > lineWidth) {
|
||||
lineWidth = width;
|
||||
value = displayValue;
|
||||
}
|
||||
}
|
||||
|
||||
[defaultAppearance, fontSize] = this._computeFontSize(
|
||||
lineHeight,
|
||||
totalWidth - 2 * hPadding,
|
||||
value,
|
||||
font,
|
||||
-1
|
||||
);
|
||||
} else {
|
||||
defaultAppearance = this._defaultAppearance;
|
||||
}
|
||||
|
||||
const lineHeight = fontSize * LINE_FACTOR;
|
||||
const vPadding = (lineHeight - fontSize) / 2;
|
||||
const numberOfVisibleLines = Math.floor(totalHeight / lineHeight);
|
||||
|
||||
let firstIndex;
|
||||
if (valueIndices.length === 1) {
|
||||
const valuePosition = valueIndices[0];
|
||||
const indexInPage = valuePosition % numberOfVisibleLines;
|
||||
firstIndex = valuePosition - indexInPage;
|
||||
} else {
|
||||
// If nothing is selected (valueIndice.length === 0), we render
|
||||
// from the first element.
|
||||
firstIndex = valueIndices.length ? valueIndices[0] : 0;
|
||||
}
|
||||
const end = Math.min(firstIndex + numberOfVisibleLines + 1, lineCount);
|
||||
|
||||
const buf = ["/Tx BMC q", `1 1 ${totalWidth} ${totalHeight} re W n`];
|
||||
|
||||
if (valueIndices.length) {
|
||||
// This value has been copied/pasted from annotation-choice-widget.pdf.
|
||||
// It corresponds to rgb(153, 193, 218).
|
||||
buf.push("0.600006 0.756866 0.854904 rg");
|
||||
|
||||
// Highlight the lines in filling a blue rectangle at the selected
|
||||
// positions.
|
||||
for (const index of valueIndices) {
|
||||
if (firstIndex <= index && index < end) {
|
||||
buf.push(
|
||||
`1 ${
|
||||
totalHeight - (index - firstIndex + 1) * lineHeight
|
||||
} ${totalWidth} ${lineHeight} re f`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.push("BT", defaultAppearance, `1 0 0 1 0 ${totalHeight} Tm`);
|
||||
|
||||
for (let i = firstIndex; i < end; i++) {
|
||||
const { displayValue } = this.data.options[i];
|
||||
const hpadding = i === firstIndex ? hPadding : 0;
|
||||
const vpadding = i === firstIndex ? vPadding : 0;
|
||||
buf.push(
|
||||
this._renderText(
|
||||
displayValue,
|
||||
font,
|
||||
fontSize,
|
||||
totalWidth,
|
||||
0,
|
||||
hpadding,
|
||||
-lineHeight + vpadding
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
buf.push("ET Q EMC");
|
||||
|
||||
return buf.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
class SignatureWidgetAnnotation extends WidgetAnnotation {
|
||||
|
@ -143,7 +143,11 @@ function writeXFADataForAcroform(str, newRefs) {
|
||||
}
|
||||
const node = xml.documentElement.searchNode(parseXFAPath(path), 0);
|
||||
if (node) {
|
||||
node.childNodes = [new SimpleDOMNode("#text", value)];
|
||||
if (Array.isArray(value)) {
|
||||
node.childNodes = value.map(val => new SimpleDOMNode("value", val));
|
||||
} else {
|
||||
node.childNodes = [new SimpleDOMNode("#text", value)];
|
||||
}
|
||||
} else {
|
||||
warn(`Node not found for path: ${path}`);
|
||||
}
|
||||
|
@ -1336,16 +1336,8 @@ 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 `getValue`
|
||||
// is used and the full array of field values is stored.
|
||||
storage.getValue(id, {
|
||||
value:
|
||||
this.data.fieldValue.length > 0 ? this.data.fieldValue[0] : undefined,
|
||||
const storedData = storage.getValue(id, {
|
||||
value: this.data.fieldValue,
|
||||
});
|
||||
|
||||
let { fontSize } = this.data.defaultAppearanceData;
|
||||
@ -1386,7 +1378,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
|
||||
if (this.data.combo) {
|
||||
optionElement.style.fontSize = fontSizeStyle;
|
||||
}
|
||||
if (this.data.fieldValue.includes(option.exportValue)) {
|
||||
if (storedData.value.includes(option.exportValue)) {
|
||||
optionElement.setAttribute("selected", true);
|
||||
}
|
||||
selectElement.appendChild(optionElement);
|
||||
@ -1537,7 +1529,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
|
||||
);
|
||||
} else {
|
||||
selectElement.addEventListener("input", function (event) {
|
||||
storage.setValue(id, { value: getValue(event) });
|
||||
storage.setValue(id, { value: getValue(event, /* isExport */ true) });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,9 @@ class EventDispatcher {
|
||||
|
||||
mergeChange(event) {
|
||||
let value = event.value;
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
value = value.toString();
|
||||
}
|
||||
|
@ -233,7 +233,11 @@ class Field extends PDFObject {
|
||||
if (this._isChoice) {
|
||||
if (this.multipleSelection) {
|
||||
const values = new Set(value);
|
||||
this._currentValueIndices.length = 0;
|
||||
if (Array.isArray(this._currentValueIndices)) {
|
||||
this._currentValueIndices.length = 0;
|
||||
} else {
|
||||
this._currentValueIndices = [];
|
||||
}
|
||||
this._items.forEach(({ displayValue }, i) => {
|
||||
if (values.has(displayValue)) {
|
||||
this._currentValueIndices.push(i);
|
||||
|
@ -5897,10 +5897,7 @@
|
||||
"value": "Dolor"
|
||||
},
|
||||
"62R": {
|
||||
"value": "Sit"
|
||||
},
|
||||
"63R": {
|
||||
"value": ""
|
||||
"value": ["Sit", "Adipiscing"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -3376,8 +3376,111 @@ describe("annotation", function () {
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" +
|
||||
" 2.00 3.04 Td (a value) Tj ET Q EMC"
|
||||
[
|
||||
"/Tx BMC q",
|
||||
"1 1 32 10 re W n",
|
||||
"BT",
|
||||
"/Helv 5 Tf",
|
||||
"1 0 0 1 0 10 Tm",
|
||||
"ET Q EMC",
|
||||
].join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
it("should render choice with multiple selections but one is visible for printing", async function () {
|
||||
choiceWidgetDict.set("Ff", AnnotationFieldFlag.MULTISELECT);
|
||||
choiceWidgetDict.set("Opt", [
|
||||
["A", "a"],
|
||||
["B", "b"],
|
||||
["C", "c"],
|
||||
["D", "d"],
|
||||
]);
|
||||
choiceWidgetDict.set("V", ["A"]);
|
||||
|
||||
const choiceWidgetRef = Ref.get(271, 0);
|
||||
const xref = new XRefMock([
|
||||
{ ref: choiceWidgetRef, data: choiceWidgetDict },
|
||||
fontRefObj,
|
||||
]);
|
||||
const task = new WorkerTask("test print");
|
||||
partialEvaluator.xref = xref;
|
||||
|
||||
const annotation = await AnnotationFactory.create(
|
||||
xref,
|
||||
choiceWidgetRef,
|
||||
pdfManagerMock,
|
||||
idFactoryMock
|
||||
);
|
||||
const annotationStorage = new Map();
|
||||
annotationStorage.set(annotation.data.id, { value: ["A", "C"] });
|
||||
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
[
|
||||
"/Tx BMC q",
|
||||
"1 1 32 10 re W n",
|
||||
"0.600006 0.756866 0.854904 rg",
|
||||
"1 3.25 32 6.75 re f",
|
||||
"BT",
|
||||
"/Helv 5 Tf",
|
||||
"1 0 0 1 0 10 Tm",
|
||||
"2.00 -5.88 Td (a) Tj",
|
||||
"0.00 -6.75 Td (b) Tj",
|
||||
"ET Q EMC",
|
||||
].join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
it("should render choice with multiple selections for printing", async function () {
|
||||
choiceWidgetDict.set("Ff", AnnotationFieldFlag.MULTISELECT);
|
||||
choiceWidgetDict.set("Opt", [
|
||||
["A", "a"],
|
||||
["B", "b"],
|
||||
["C", "c"],
|
||||
["D", "d"],
|
||||
]);
|
||||
choiceWidgetDict.set("V", ["A"]);
|
||||
|
||||
const choiceWidgetRef = Ref.get(271, 0);
|
||||
const xref = new XRefMock([
|
||||
{ ref: choiceWidgetRef, data: choiceWidgetDict },
|
||||
fontRefObj,
|
||||
]);
|
||||
const task = new WorkerTask("test print");
|
||||
partialEvaluator.xref = xref;
|
||||
|
||||
const annotation = await AnnotationFactory.create(
|
||||
xref,
|
||||
choiceWidgetRef,
|
||||
pdfManagerMock,
|
||||
idFactoryMock
|
||||
);
|
||||
const annotationStorage = new Map();
|
||||
annotationStorage.set(annotation.data.id, { value: ["B", "C"] });
|
||||
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
[
|
||||
"/Tx BMC q",
|
||||
"1 1 32 10 re W n",
|
||||
"0.600006 0.756866 0.854904 rg",
|
||||
"1 3.25 32 6.75 re f",
|
||||
"1 -3.5 32 6.75 re f",
|
||||
"BT",
|
||||
"/Helv 5 Tf",
|
||||
"1 0 0 1 0 10 Tm",
|
||||
"2.00 -5.88 Td (b) Tj",
|
||||
"0.00 -6.75 Td (c) Tj",
|
||||
"ET Q EMC",
|
||||
].join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
@ -3421,11 +3524,90 @@ describe("annotation", function () {
|
||||
"/AP << /N 2 0 R>> /M (date)>>\nendobj\n"
|
||||
);
|
||||
expect(newData.data).toEqual(
|
||||
"2 0 obj\n" +
|
||||
"<< /Length 67 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " +
|
||||
"/BBox [0 0 32 10]>> stream\n" +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2.00 3.04 Td (C) Tj ET Q EMC\n" +
|
||||
"endstream\nendobj\n"
|
||||
[
|
||||
"2 0 obj",
|
||||
"<< /Length 136 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " +
|
||||
"/BBox [0 0 32 10]>> stream",
|
||||
"/Tx BMC q",
|
||||
"1 1 32 10 re W n",
|
||||
"0.600006 0.756866 0.854904 rg",
|
||||
"1 3.25 32 6.75 re f",
|
||||
"BT",
|
||||
"/Helv 5 Tf",
|
||||
"1 0 0 1 0 10 Tm",
|
||||
"2.00 -5.88 Td (C) Tj",
|
||||
"ET Q EMC",
|
||||
"endstream",
|
||||
"endobj\n",
|
||||
].join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
it("should save choice with multiple selections", async function () {
|
||||
choiceWidgetDict.set("Ff", AnnotationFieldFlag.MULTISELECT);
|
||||
choiceWidgetDict.set("Opt", [
|
||||
["A", "a"],
|
||||
["B", "b"],
|
||||
["C", "c"],
|
||||
["D", "d"],
|
||||
]);
|
||||
choiceWidgetDict.set("V", ["A"]);
|
||||
|
||||
const choiceWidgetRef = Ref.get(123, 0);
|
||||
const xref = new XRefMock([
|
||||
{ ref: choiceWidgetRef, data: choiceWidgetDict },
|
||||
fontRefObj,
|
||||
]);
|
||||
const task = new WorkerTask("test save");
|
||||
partialEvaluator.xref = xref;
|
||||
|
||||
const annotation = await AnnotationFactory.create(
|
||||
xref,
|
||||
choiceWidgetRef,
|
||||
pdfManagerMock,
|
||||
idFactoryMock
|
||||
);
|
||||
const annotationStorage = new Map();
|
||||
annotationStorage.set(annotation.data.id, { value: ["B", "C"] });
|
||||
|
||||
const data = await annotation.save(
|
||||
partialEvaluator,
|
||||
task,
|
||||
annotationStorage
|
||||
);
|
||||
|
||||
expect(data.length).toEqual(2);
|
||||
const [oldData, newData] = data;
|
||||
expect(oldData.ref).toEqual(Ref.get(123, 0));
|
||||
expect(newData.ref).toEqual(Ref.get(2, 0));
|
||||
|
||||
oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
|
||||
expect(oldData.data).toEqual(
|
||||
"123 0 obj\n" +
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Ch /DA (/Helv 5 Tf) /DR " +
|
||||
"<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] /Ff 2097152 /Opt " +
|
||||
"[[(A) (a)] [(B) (b)] [(C) (c)] [(D) (d)]] /V [(B) (C)] /AP " +
|
||||
"<< /N 2 0 R>> /M (date)>>\nendobj\n"
|
||||
);
|
||||
expect(newData.data).toEqual(
|
||||
[
|
||||
"2 0 obj",
|
||||
"<< /Length 177 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " +
|
||||
"/BBox [0 0 32 10]>> stream",
|
||||
"/Tx BMC q",
|
||||
"1 1 32 10 re W n",
|
||||
"0.600006 0.756866 0.854904 rg",
|
||||
"1 3.25 32 6.75 re f",
|
||||
"1 -3.5 32 6.75 re f",
|
||||
"BT",
|
||||
"/Helv 5 Tf",
|
||||
"1 0 0 1 0 10 Tm",
|
||||
"2.00 -5.88 Td (b) Tj",
|
||||
"0.00 -6.75 Td (c) Tj",
|
||||
"ET Q EMC",
|
||||
"endstream",
|
||||
"endobj\n",
|
||||
].join("\n")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user