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

[Annotations] Add support for printing/saving choice list with multiple selections #14720

Merged
merged 1 commit into from
Mar 30, 2022
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
156 changes: 148 additions & 8 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()}`);

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be

const width = this._getTextWidth(displayValue, font);

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 {
Expand Down
6 changes: 5 additions & 1 deletion src/core/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down
16 changes: 4 additions & 12 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) });
});
}

Expand Down
3 changes: 3 additions & 0 deletions src/scripting_api/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
6 changes: 5 additions & 1 deletion src/scripting_api/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 1 addition & 4 deletions test/test_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5897,10 +5897,7 @@
"value": "Dolor"
},
"62R": {
"value": "Sit"
},
"63R": {
"value": ""
"value": ["Sit", "Adipiscing"]
}
}
calixteman marked this conversation as resolved.
Show resolved Hide resolved
},
Expand Down
Loading