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

Add support for checkboxes printing #12107

Merged
merged 1 commit into from
Jul 29, 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
79 changes: 67 additions & 12 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,21 @@ class AnnotationFactory {
* instance.
*/
static create(xref, ref, pdfManager, idFactory) {
return pdfManager.ensure(this, "_create", [
xref,
ref,
pdfManager,
idFactory,
]);
return pdfManager.ensureDoc("acroForm").then(acroForm => {
return pdfManager.ensure(this, "_create", [
xref,
ref,
pdfManager,
idFactory,
acroForm,
]);
});
}

/**
* @private
*/
static _create(xref, ref, pdfManager, idFactory) {
static _create(xref, ref, pdfManager, idFactory, acroForm) {
const dict = xref.fetchIfRef(ref);
if (!isDict(dict)) {
return undefined;
Expand All @@ -78,6 +81,7 @@ class AnnotationFactory {
subtype,
id,
pdfManager,
acroForm: acroForm instanceof Dict ? acroForm : Dict.empty,
};

switch (subtype) {
Expand Down Expand Up @@ -514,8 +518,9 @@ class Annotation {
return Promise.resolve(new OperatorList());
}

const appearance = this.appearance;
const data = this.data;
const appearanceDict = this.appearance.dict;
const appearanceDict = appearance.dict;
const resourcesPromise = this.loadResources([
"ExtGState",
"ColorSpace",
Expand All @@ -533,14 +538,14 @@ class Annotation {
opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]);
return evaluator
.getOperatorList({
stream: this.appearance,
stream: appearance,
task,
resources,
operatorList: opList,
})
.then(() => {
opList.addOp(OPS.endAnnotation, []);
this.appearance.reset();
appearance.reset();
return opList;
});
});
Expand Down Expand Up @@ -795,11 +800,16 @@ class WidgetAnnotation extends Annotation {
getArray: true,
});
data.alternativeText = stringToPDFString(dict.get("TU") || "");
data.defaultAppearance = getInheritableProperty({ dict, key: "DA" }) || "";
data.defaultAppearance =
getInheritableProperty({ dict, key: "DA" }) ||
params.acroForm.get("DA") ||
"";
const fieldType = getInheritableProperty({ dict, key: "FT" });
data.fieldType = isName(fieldType) ? fieldType.name : null;
this.fieldResources =
getInheritableProperty({ dict, key: "DR" }) || Dict.empty;
getInheritableProperty({ dict, key: "DR" }) ||
params.acroForm.get("DR") ||
Dict.empty;

data.fieldFlags = getInheritableProperty({ dict, key: "Ff" });
if (!Number.isInteger(data.fieldFlags) || data.fieldFlags < 0) {
Expand Down Expand Up @@ -961,6 +971,9 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
constructor(params) {
super(params);

this.checkedAppearance = null;
this.uncheckedAppearance = null;

this.data.checkBox =
!this.hasFieldFlag(AnnotationFieldFlag.RADIO) &&
!this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON);
Expand All @@ -980,6 +993,40 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
}
}

getOperatorList(evaluator, task, renderForms, annotationStorage) {
if (annotationStorage) {
const value = annotationStorage[this.data.id] || false;
let appearance;
if (value) {
appearance = this.checkedAppearance;
} else {
appearance = this.uncheckedAppearance;
}

if (appearance) {
const savedAppearance = this.appearance;
this.appearance = appearance;
const operatorList = super.getOperatorList(
evaluator,
task,
renderForms,
annotationStorage
);
this.appearance = savedAppearance;
return operatorList;
}

// No appearance
return Promise.resolve(new OperatorList());
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we treat this case just like the caser where we have no annotationStorage, i.e., let this fall through and let the parent class handle it? I would think we only want to special-case the case where we have a value and a checked appearance; any other case would need the default parent getOperatorList call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we've a checked box (in the base pdf) and we uncheck it then falling through implies that we'll print a checked box.
Anyway in reading the spec, we can have a "Off" appearance so I need to handle that case.

}
return super.getOperatorList(
evaluator,
task,
renderForms,
annotationStorage
);
}

_processCheckBox(params) {
if (isName(this.data.fieldValue)) {
this.data.fieldValue = this.data.fieldValue.name;
Expand All @@ -1003,6 +1050,14 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {

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;
}

_processRadioButton(params) {
Expand Down
16 changes: 14 additions & 2 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -542,16 +542,28 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
* @returns {HTMLSectionElement}
*/
render() {
const storage = this.annotationStorage;
const data = this.data;
const id = data.id;
const value = storage.getOrCreateValue(
id,
data.fieldValue && data.fieldValue !== "Off"
);

this.container.className = "buttonWidgetAnnotation checkBox";

const element = document.createElement("input");
element.disabled = this.data.readOnly;
element.disabled = data.readOnly;
element.type = "checkbox";
element.name = this.data.fieldName;
if (this.data.fieldValue && this.data.fieldValue !== "Off") {
if (value) {
element.setAttribute("checked", true);
}

element.addEventListener("change", function (event) {
storage.setValue(id, event.target.checked);
});

this.container.appendChild(element);
return this.container;
}
Expand Down
84 changes: 83 additions & 1 deletion test/unit/annotation_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,24 @@ import {
AnnotationFieldFlag,
AnnotationFlag,
AnnotationType,
OPS,
stringToBytes,
stringToUTF8String,
} from "../../src/shared/util.js";
import { createIdFactory, XRefMock } from "./test_utils.js";
import { Dict, Name, Ref } from "../../src/core/primitives.js";
import { Lexer, Parser } from "../../src/core/parser.js";
import { PartialEvaluator } from "../../src/core/evaluator.js";
import { StringStream } from "../../src/core/stream.js";
import { WorkerTask } from "../../src/core/worker.js";

describe("annotation", function () {
class PDFManagerMock {
constructor(params) {
this.docBaseUrl = params.docBaseUrl || null;
this.pdfDocument = {
acroForm: new Dict(),
};
}

ensure(obj, prop, args) {
Expand All @@ -49,21 +55,41 @@ describe("annotation", function () {
}
});
}

ensureDoc(prop, args) {
return this.ensure(this.pdfDocument, prop, args);
}
}

function HandlerMock() {
this.inputs = [];
}
HandlerMock.prototype = {
send(name, data) {
this.inputs.push({ name, data });
},
};

let pdfManagerMock, idFactoryMock;
let pdfManagerMock, idFactoryMock, partialEvaluator;

beforeAll(function (done) {
pdfManagerMock = new PDFManagerMock({
docBaseUrl: null,
});
idFactoryMock = createIdFactory(/* pageIndex = */ 0);
partialEvaluator = new PartialEvaluator({
xref: new XRefMock(),
handler: new HandlerMock(),
pageIndex: 0,
idFactory: createIdFactory(/* pageIndex = */ 0),
});
done();
});

afterAll(function () {
pdfManagerMock = null;
idFactoryMock = null;
partialEvaluator = null;
});

describe("AnnotationFactory", function () {
Expand Down Expand Up @@ -1630,6 +1656,62 @@ describe("annotation", function () {
}, 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 stream = new StringStream("0.1 0.2 0.3 rg");
stream.dict = checkedAppearanceDict;

checkedAppearanceDict.set("BBox", [0, 0, 8, 8]);
checkedAppearanceDict.set("FormType", 1);
checkedAppearanceDict.set("Matrix", [1, 0, 0, 1, 0, 0]);
normalAppearanceDict.set("Checked", stream);
exportValueOptionsDict.set("Off", 0);
exportValueOptionsDict.set("Checked", 1);
appearanceStatesDict.set("D", exportValueOptionsDict);
appearanceStatesDict.set("N", normalAppearanceDict);

buttonWidgetDict.set("AP", appearanceStatesDict);

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

AnnotationFactory.create(
xref,
buttonWidgetRef,
pdfManagerMock,
idFactoryMock
)
.then(annotation => {
const annotationStorage = {};
annotationStorage[annotation.data.id] = true;
return annotation.getOperatorList(
partialEvaluator,
task,
false,
annotationStorage
);
}, done.fail)
.then(opList => {
expect(opList.argsArray.length).toEqual(3);
calixteman marked this conversation as resolved.
Show resolved Hide resolved
expect(opList.fnArray).toEqual([
OPS.beginAnnotation,
OPS.setFillRGBColor,
OPS.endAnnotation,
]);
expect(opList.argsArray[1]).toEqual(
new Uint8ClampedArray([26, 51, 76])
);
done();
}, done.fail);
});

it("should handle radio buttons without a field value", function (done) {
const normalAppearanceStateDict = new Dict();
normalAppearanceStateDict.set("2", null);
Expand Down