mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-23 00:15:51 +02:00
Merge pull request #18907 from calixteman/bug1802506
Use the checkboxes and radio button appearances as defined in the pdf to render them in the annotation layer (bug 1802506)
This commit is contained in:
commit
35d275d3b1
@ -785,6 +785,14 @@ class Annotation {
|
||||
this._needAppearances = false;
|
||||
}
|
||||
|
||||
_getOperatorListNoAppearance() {
|
||||
return {
|
||||
opList: new OperatorList(),
|
||||
separateForm: false,
|
||||
separateCanvas: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@ -1213,28 +1221,32 @@ class Annotation {
|
||||
return resources;
|
||||
}
|
||||
|
||||
// Whether the annotation should only be rendered on its own canvas when
|
||||
// interactive forms are enabled. This is the case for checkbox/radio button
|
||||
// widgets, whose checked/unchecked appearances are toggled in forms mode;
|
||||
// other annotations (e.g. push buttons) keep their own canvas in any display
|
||||
// mode.
|
||||
get _ownCanvasRequiresForms() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async getOperatorList(evaluator, task, intent, annotationStorage) {
|
||||
const { hasOwnCanvas, id, rect } = this.data;
|
||||
let appearance = this.appearance;
|
||||
const isUsingOwnCanvas = !!(
|
||||
hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY
|
||||
hasOwnCanvas &&
|
||||
intent & RenderingIntentFlag.DISPLAY &&
|
||||
(!this._ownCanvasRequiresForms ||
|
||||
intent & RenderingIntentFlag.ANNOTATIONS_FORMS)
|
||||
);
|
||||
if (isUsingOwnCanvas && (this.width === 0 || this.height === 0)) {
|
||||
// Empty annotation, don't draw anything.
|
||||
this.data.hasOwnCanvas = false;
|
||||
return {
|
||||
opList: new OperatorList(),
|
||||
separateForm: false,
|
||||
separateCanvas: false,
|
||||
};
|
||||
return this._getOperatorListNoAppearance();
|
||||
}
|
||||
if (!appearance) {
|
||||
if (!isUsingOwnCanvas) {
|
||||
return {
|
||||
opList: new OperatorList(),
|
||||
separateForm: false,
|
||||
separateCanvas: false,
|
||||
};
|
||||
return this._getOperatorListNoAppearance();
|
||||
}
|
||||
appearance = new StringStream("", new Dict());
|
||||
}
|
||||
@ -1244,7 +1256,12 @@ class Annotation {
|
||||
RESOURCES_KEYS_OPERATOR_LIST,
|
||||
appearance
|
||||
);
|
||||
const bbox = lookupRect(appearanceDict.getArray("BBox"), [0, 0, 1, 1]);
|
||||
const bbox = lookupRect(appearanceDict.getArray("BBox"), [
|
||||
0,
|
||||
0,
|
||||
this.width,
|
||||
this.height,
|
||||
]);
|
||||
const matrix = lookupMatrix(
|
||||
appearanceDict.getArray("Matrix"),
|
||||
IDENTITY_MATRIX
|
||||
@ -2092,11 +2109,9 @@ class WidgetAnnotation extends Annotation {
|
||||
!this.data.noHTML &&
|
||||
!this.data.hasOwnCanvas
|
||||
) {
|
||||
return {
|
||||
opList: new OperatorList(),
|
||||
separateForm: true,
|
||||
separateCanvas: false,
|
||||
};
|
||||
const list = this._getOperatorListNoAppearance();
|
||||
list.separateForm = true;
|
||||
return list;
|
||||
}
|
||||
|
||||
if (!this._hasText) {
|
||||
@ -3148,20 +3163,58 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
this.data.radioButton = isRadio && !isPushButton;
|
||||
this.data.pushButton = isPushButton;
|
||||
this.data.isTooltipOnly = false;
|
||||
this.data.hasOwnCanvas = true;
|
||||
this.data.noHTML = false;
|
||||
|
||||
if (this.data.checkBox) {
|
||||
this._processCheckBox(params);
|
||||
} else if (this.data.radioButton) {
|
||||
this._processRadioButton(params);
|
||||
} else if (this.data.pushButton) {
|
||||
this.data.hasOwnCanvas = true;
|
||||
this.data.noHTML = false;
|
||||
this._processPushButton(params);
|
||||
} else {
|
||||
warn("Invalid field flags for button widget annotation");
|
||||
}
|
||||
}
|
||||
|
||||
get _ownCanvasRequiresForms() {
|
||||
return this.data.checkBox || this.data.radioButton;
|
||||
}
|
||||
|
||||
#getOperatorListForAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
intent,
|
||||
annotationStorage,
|
||||
rotation,
|
||||
appearance
|
||||
) {
|
||||
if (!appearance) {
|
||||
return this._getOperatorListNoAppearance();
|
||||
}
|
||||
|
||||
const savedAppearance = this.appearance;
|
||||
const savedMatrix = lookupMatrix(
|
||||
appearance.dict.getArray("Matrix"),
|
||||
IDENTITY_MATRIX
|
||||
);
|
||||
|
||||
if (rotation) {
|
||||
appearance.dict.set("Matrix", this.getRotationMatrix(annotationStorage));
|
||||
}
|
||||
|
||||
this.appearance = appearance;
|
||||
const operatorList = super.getOperatorList(
|
||||
evaluator,
|
||||
task,
|
||||
intent,
|
||||
annotationStorage
|
||||
);
|
||||
this.appearance = savedAppearance;
|
||||
appearance.dict.set("Matrix", savedMatrix);
|
||||
return operatorList;
|
||||
}
|
||||
|
||||
async getOperatorList(evaluator, task, intent, annotationStorage) {
|
||||
if (this.data.pushButton) {
|
||||
return super.getOperatorList(
|
||||
@ -3173,6 +3226,45 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
intent & RenderingIntentFlag.DISPLAY &&
|
||||
intent & RenderingIntentFlag.ANNOTATIONS_FORMS &&
|
||||
(this.data.checkBox || this.data.radioButton)
|
||||
) {
|
||||
// Tag the dedicated canvas with the state it represents. The appearance
|
||||
// may start with other operators (e.g. an optional content marked-content
|
||||
// sequence), so target the `beginAnnotation` operator directly rather
|
||||
// than assuming it's the first one.
|
||||
const setCanvasName = (operatorList, name) => {
|
||||
const index = operatorList.fnArray.indexOf(OPS.beginAnnotation);
|
||||
if (index !== -1) {
|
||||
operatorList.argsArray[index].push(name);
|
||||
}
|
||||
};
|
||||
const checked = await this.#getOperatorListForAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
intent,
|
||||
annotationStorage,
|
||||
null,
|
||||
this.checkedAppearance
|
||||
);
|
||||
setCanvasName(checked.opList, "checked");
|
||||
const unchecked = await this.#getOperatorListForAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
intent,
|
||||
annotationStorage,
|
||||
null,
|
||||
this.uncheckedAppearance
|
||||
);
|
||||
setCanvasName(unchecked.opList, "unchecked");
|
||||
checked.opList.addOpList(unchecked.opList);
|
||||
checked.separateForm ||= unchecked.separateForm;
|
||||
checked.separateCanvas ||= unchecked.separateCanvas;
|
||||
return checked;
|
||||
}
|
||||
|
||||
let value = null;
|
||||
let rotation = null;
|
||||
if (annotationStorage) {
|
||||
@ -3195,41 +3287,14 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
: this.data.fieldValue === this.data.buttonValue;
|
||||
}
|
||||
|
||||
const appearance = value
|
||||
? this.checkedAppearance
|
||||
: this.uncheckedAppearance;
|
||||
if (appearance) {
|
||||
const savedAppearance = this.appearance;
|
||||
const savedMatrix = lookupMatrix(
|
||||
appearance.dict.getArray("Matrix"),
|
||||
IDENTITY_MATRIX
|
||||
);
|
||||
|
||||
if (rotation) {
|
||||
appearance.dict.set(
|
||||
"Matrix",
|
||||
this.getRotationMatrix(annotationStorage)
|
||||
);
|
||||
}
|
||||
|
||||
this.appearance = appearance;
|
||||
const operatorList = super.getOperatorList(
|
||||
evaluator,
|
||||
task,
|
||||
intent,
|
||||
annotationStorage
|
||||
);
|
||||
this.appearance = savedAppearance;
|
||||
appearance.dict.set("Matrix", savedMatrix);
|
||||
return operatorList;
|
||||
}
|
||||
|
||||
// No appearance
|
||||
return {
|
||||
opList: new OperatorList(),
|
||||
separateForm: false,
|
||||
separateCanvas: false,
|
||||
};
|
||||
return this.#getOperatorListForAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
intent,
|
||||
annotationStorage,
|
||||
rotation,
|
||||
value ? this.checkedAppearance : this.uncheckedAppearance
|
||||
);
|
||||
}
|
||||
|
||||
async save(evaluator, task, annotationStorage, changes) {
|
||||
@ -3422,13 +3487,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
|
||||
_processCheckBox(params) {
|
||||
const customAppearance = params.dict.get("AP");
|
||||
if (!(customAppearance instanceof Dict)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalAppearance = customAppearance.get("N");
|
||||
let normalAppearance =
|
||||
customAppearance instanceof Dict ? customAppearance.get("N") : null;
|
||||
if (!(normalAppearance instanceof Dict)) {
|
||||
return;
|
||||
// Synthesize a default appearance below when the field defines none.
|
||||
normalAppearance = null;
|
||||
}
|
||||
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1722036.
|
||||
@ -3444,7 +3507,9 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
: "Yes";
|
||||
|
||||
// Don't decode the keys which are names.
|
||||
const exportValues = [...normalAppearance.getKeys()];
|
||||
const exportValues = normalAppearance
|
||||
? [...normalAppearance.getKeys()]
|
||||
: [];
|
||||
if (exportValues.length === 0) {
|
||||
exportValues.push("Off", yes);
|
||||
} else if (exportValues.length === 1) {
|
||||
@ -3470,10 +3535,10 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
||||
|
||||
this.data.exportValue = exportValues[1];
|
||||
|
||||
const checkedAppearance = normalAppearance.get(this.data.exportValue);
|
||||
const checkedAppearance = normalAppearance?.get(this.data.exportValue);
|
||||
this.checkedAppearance =
|
||||
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
|
||||
const uncheckedAppearance = normalAppearance.get("Off");
|
||||
const uncheckedAppearance = normalAppearance?.get("Off");
|
||||
this.uncheckedAppearance =
|
||||
uncheckedAppearance instanceof BaseStream ? uncheckedAppearance : null;
|
||||
|
||||
|
||||
@ -439,9 +439,6 @@ class AnnotationElement {
|
||||
if (horizontalRadius > 0 || verticalRadius > 0) {
|
||||
const radius = `calc(${horizontalRadius}px * var(--total-scale-factor)) / calc(${verticalRadius}px * var(--total-scale-factor))`;
|
||||
style.borderRadius = radius;
|
||||
} else if (this instanceof RadioButtonWidgetAnnotationElement) {
|
||||
const radius = `calc(${width}px * var(--total-scale-factor)) / calc(${height}px * var(--total-scale-factor))`;
|
||||
style.borderRadius = radius;
|
||||
}
|
||||
|
||||
switch (data.borderStyle.style) {
|
||||
@ -2009,7 +2006,6 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
|
||||
);
|
||||
}
|
||||
|
||||
this._setBackgroundColor(element);
|
||||
this._setDefaultPropertiesFromJS(element);
|
||||
|
||||
this.container.append(element);
|
||||
@ -2028,7 +2024,9 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
|
||||
const data = this.data;
|
||||
const id = data.id;
|
||||
let value = storage.getValue(id, {
|
||||
value: data.fieldValue === data.buttonValue,
|
||||
// A radio without an on-state (`buttonValue === null`, e.g. no /AP) must
|
||||
// not be checked by default, otherwise `null === null` would select it.
|
||||
value: data.buttonValue !== null && data.fieldValue === data.buttonValue,
|
||||
}).value;
|
||||
if (typeof value === "string") {
|
||||
// The value has been changed through js and set in annotationStorage.
|
||||
@ -2115,7 +2113,6 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
|
||||
);
|
||||
}
|
||||
|
||||
this._setBackgroundColor(element);
|
||||
this._setDefaultPropertiesFromJS(element);
|
||||
|
||||
this.container.append(element);
|
||||
@ -4139,18 +4136,44 @@ class AnnotationLayer {
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
canvas.className = "annotationContent";
|
||||
if (Array.isArray(canvas)) {
|
||||
for (const cvs of canvas) {
|
||||
cvs.className = "annotationContent";
|
||||
cvs.ariaHidden = true;
|
||||
}
|
||||
} else {
|
||||
canvas.className = "annotationContent";
|
||||
canvas.ariaHidden = true;
|
||||
}
|
||||
const toRemove = [];
|
||||
for (const child of element.children) {
|
||||
if (child.nodeName === "CANVAS") {
|
||||
toRemove.push(child);
|
||||
}
|
||||
}
|
||||
for (const child of toRemove) {
|
||||
child.remove();
|
||||
}
|
||||
const firstCanvas = Array.isArray(canvas) ? canvas[0] : canvas;
|
||||
const { firstChild } = element;
|
||||
if (!firstChild) {
|
||||
element.append(canvas);
|
||||
} else if (firstChild.nodeName === "CANVAS") {
|
||||
firstChild.replaceWith(canvas);
|
||||
element.append(firstCanvas);
|
||||
} else if (!firstChild.classList.contains("annotationContent")) {
|
||||
firstChild.before(canvas);
|
||||
firstChild.before(firstCanvas);
|
||||
} else {
|
||||
firstChild.after(canvas);
|
||||
firstChild.after(firstCanvas);
|
||||
}
|
||||
if (Array.isArray(canvas)) {
|
||||
let lastCanvas = firstCanvas;
|
||||
for (let i = 1, ii = canvas.length; i < ii; i++) {
|
||||
lastCanvas.after(canvas[i]);
|
||||
lastCanvas = canvas[i];
|
||||
}
|
||||
}
|
||||
// Drop only the entries we inserted; keep ones whose element isn't in the
|
||||
// DOM yet so a later refresh can still pick them up instead of losing
|
||||
// them.
|
||||
this.#annotationCanvasMap.delete(id);
|
||||
|
||||
const editableAnnotation = this.#editableAnnotations.get(id);
|
||||
if (!editableAnnotation) {
|
||||
@ -4168,7 +4191,12 @@ class AnnotationLayer {
|
||||
editableAnnotation.canvas = canvas;
|
||||
}
|
||||
}
|
||||
this.#annotationCanvasMap.clear();
|
||||
}
|
||||
|
||||
// Move any pending annotation canvases (e.g. higher-resolution ones rendered
|
||||
// by the detail view) into their elements.
|
||||
refreshCanvases() {
|
||||
this.#setAnnotationCanvasMap();
|
||||
}
|
||||
|
||||
getEditableAnnotations() {
|
||||
|
||||
@ -3630,7 +3630,15 @@ class CanvasGraphics {
|
||||
}
|
||||
}
|
||||
|
||||
beginAnnotation(opIdx, id, rect, transform, matrix, hasOwnCanvas) {
|
||||
beginAnnotation(
|
||||
opIdx,
|
||||
id,
|
||||
rect,
|
||||
transform,
|
||||
matrix,
|
||||
hasOwnCanvas,
|
||||
canvasName
|
||||
) {
|
||||
// The annotations are drawn just after the page content.
|
||||
// The page content drawing can potentially have set a transform,
|
||||
// a clipping path, whatever...
|
||||
@ -3673,7 +3681,26 @@ class CanvasGraphics {
|
||||
canvasHeight
|
||||
);
|
||||
const { canvas, context } = this.annotationCanvas;
|
||||
this.annotationCanvasMap.set(id, canvas);
|
||||
if (canvasName) {
|
||||
let canvases = this.annotationCanvasMap.get(id);
|
||||
if (!canvases) {
|
||||
canvases = [];
|
||||
this.annotationCanvasMap.set(id, canvases);
|
||||
}
|
||||
canvas.setAttribute("data-canvas-name", canvasName);
|
||||
// Replace any same-named canvas from a previous render so stale
|
||||
// low-resolution canvases don't pile up across zooms.
|
||||
const index = canvases.findIndex(
|
||||
c => c.getAttribute("data-canvas-name") === canvasName
|
||||
);
|
||||
if (index === -1) {
|
||||
canvases.push(canvas);
|
||||
} else {
|
||||
canvases[index] = canvas;
|
||||
}
|
||||
} else {
|
||||
this.annotationCanvasMap.set(id, canvas);
|
||||
}
|
||||
this.annotationCanvas.savedCtx = this.ctx;
|
||||
this.ctx = context;
|
||||
this.ctx.save();
|
||||
|
||||
@ -68,4 +68,26 @@
|
||||
color: red;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton) {
|
||||
img[data-canvas-name="checked"] {
|
||||
&:has(~ input:checked) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:has(~ input:not(:checked)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
img[data-canvas-name="unchecked"] {
|
||||
&:has(~ input:checked) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:has(~ input:not(:checked)) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +94,7 @@ async function writeSVG(svgElement, ctx) {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
}
|
||||
|
||||
return loadImage(svg_xml, ctx);
|
||||
}
|
||||
|
||||
@ -150,21 +151,40 @@ async function inlineImages(node, silentErrors = false) {
|
||||
async function convertCanvasesToImages(annotationCanvasMap, outputScale) {
|
||||
const results = new Map();
|
||||
const promises = [];
|
||||
const canvasToImage = (canvas, key) => {
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
promises.push(promise);
|
||||
canvas.toBlob(blob => {
|
||||
const image = document.createElement("img");
|
||||
image.classList.add("wasCanvas");
|
||||
image.onload = function () {
|
||||
image.style.width = Math.floor(image.width / outputScale) + "px";
|
||||
resolve();
|
||||
};
|
||||
const canvasName = canvas.getAttribute("data-canvas-name");
|
||||
if (canvasName) {
|
||||
image.setAttribute("data-canvas-name", canvasName);
|
||||
let images = results.get(key);
|
||||
if (!images) {
|
||||
images = [];
|
||||
results.set(key, images);
|
||||
}
|
||||
images.push(image);
|
||||
} else {
|
||||
results.set(key, image);
|
||||
}
|
||||
image.src = URL.createObjectURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
for (const [key, canvas] of annotationCanvasMap) {
|
||||
promises.push(
|
||||
new Promise(resolve => {
|
||||
canvas.toBlob(blob => {
|
||||
const image = document.createElement("img");
|
||||
image.classList.add("wasCanvas");
|
||||
image.onload = function () {
|
||||
image.style.width = Math.floor(image.width / outputScale) + "px";
|
||||
resolve();
|
||||
};
|
||||
results.set(key, image);
|
||||
image.src = URL.createObjectURL(blob);
|
||||
});
|
||||
})
|
||||
);
|
||||
if (Array.isArray(canvas)) {
|
||||
for (const canvasItem of canvas) {
|
||||
canvasToImage(canvasItem, key);
|
||||
}
|
||||
} else {
|
||||
canvasToImage(canvas, key);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
return results;
|
||||
|
||||
@ -122,12 +122,12 @@ describe("Checkbox annotation", () => {
|
||||
for (const selector of selectors) {
|
||||
await page.click(selector);
|
||||
await page.waitForFunction(
|
||||
`document.querySelector('${selector} > :first-child').checked`
|
||||
`document.querySelector('${selector} input').checked`
|
||||
);
|
||||
|
||||
for (const otherSelector of selectors) {
|
||||
const checked = await page.$eval(
|
||||
`${otherSelector} > :first-child`,
|
||||
`${otherSelector} input`,
|
||||
el => el.checked
|
||||
);
|
||||
expect(checked)
|
||||
@ -157,7 +157,7 @@ describe("Checkbox annotation", () => {
|
||||
const selector = getAnnotationSelector("7R");
|
||||
await page.click(selector);
|
||||
await page.waitForFunction(
|
||||
`document.querySelector('${selector} > :first-child').checked`
|
||||
`document.querySelector('${selector} input').checked`
|
||||
);
|
||||
expect(true).withContext(`In ${browserName}`).toEqual(true);
|
||||
})
|
||||
@ -185,7 +185,7 @@ describe("Checkbox annotation", () => {
|
||||
for (const selector of selectors) {
|
||||
await page.click(selector);
|
||||
await page.waitForFunction(
|
||||
`document.querySelector('${selector} > :first-child').checked`
|
||||
`document.querySelector('${selector} input').checked`
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
@ -2413,49 +2413,6 @@ describe("Interaction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Change radio property", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait("bug1922766.pdf", getAnnotationSelector("44R"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must check that a change on a radio implies the change on all the radio in the group", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForScripting(page);
|
||||
|
||||
const checkColor = async color => {
|
||||
await waitForSandboxTrip(page);
|
||||
for (const i of [40, 41, 42, 43]) {
|
||||
const bgColor = await page.$eval(
|
||||
`[data-element-id='${i}R']`,
|
||||
el => getComputedStyle(el).backgroundColor
|
||||
);
|
||||
expect(bgColor)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(`rgb(${color.join(", ")})`);
|
||||
}
|
||||
};
|
||||
await checkColor([255, 0, 0]);
|
||||
await page.click(getAnnotationSelector("44R"));
|
||||
await checkColor([0, 0, 255]);
|
||||
await page.click(getAnnotationSelector("44R"));
|
||||
await checkColor([255, 0, 0]);
|
||||
|
||||
await page.click(getAnnotationSelector("43R"));
|
||||
await waitForSandboxTrip(page);
|
||||
await page.click(getAnnotationSelector("44R"));
|
||||
await checkColor([0, 0, 255]);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Date creation must be timezone consistent", () => {
|
||||
let pages;
|
||||
|
||||
|
||||
2
test/pdfs/.gitignore
vendored
2
test/pdfs/.gitignore
vendored
@ -933,3 +933,5 @@
|
||||
!cidfont_cmap_overflow.pdf
|
||||
!jbig2_file_header.pdf
|
||||
!text_field_own_canvas_calc.pdf
|
||||
!bug1802506.pdf
|
||||
!checkbox_no_appearance.pdf
|
||||
|
||||
BIN
test/pdfs/bug1802506.pdf
Normal file
BIN
test/pdfs/bug1802506.pdf
Normal file
Binary file not shown.
30
test/pdfs/checkbox_no_appearance.pdf
Normal file
30
test/pdfs/checkbox_no_appearance.pdf
Normal file
@ -0,0 +1,30 @@
|
||||
%PDF-1.7
|
||||
%ÿÿÿÿ
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R] /NeedAppearances true >> >>
|
||||
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 200 200] /Annots [4 0 R 5 0 R] /Resources << >> >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /FT /Btn /T (cbOn) /Rect [30 140 70 180] /V /Yes /AS /Yes >>
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Type /Annot /Subtype /Widget /FT /Btn /T (cbOff) /Rect [90 140 130 180] /V /Off /AS /Off >>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000124 00000 n
|
||||
0000000181 00000 n
|
||||
0000000291 00000 n
|
||||
0000000400 00000 n
|
||||
trailer
|
||||
<< /Size 6 /Root 1 0 R >>
|
||||
startxref
|
||||
511
|
||||
%%EOF
|
||||
@ -14387,5 +14387,23 @@
|
||||
"rounds": 1,
|
||||
"link": true,
|
||||
"type": "eq"
|
||||
},
|
||||
{
|
||||
"id": "bug1802506",
|
||||
"file": "pdfs/bug1802506.pdf",
|
||||
"md5": "ed56da1780b8480262c7329c4419fbb5",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"annotations": true,
|
||||
"forms": true
|
||||
},
|
||||
{
|
||||
"id": "checkbox_no_appearance",
|
||||
"file": "pdfs/checkbox_no_appearance.pdf",
|
||||
"md5": "216ff8ea24c470421ff1c528d805caea",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"annotations": true,
|
||||
"forms": true
|
||||
}
|
||||
]
|
||||
|
||||
@ -1776,7 +1776,7 @@ describe("annotation", function () {
|
||||
expect(opList.argsArray[0]).toEqual([
|
||||
"271R",
|
||||
[0, 0, 32, 10],
|
||||
[32, 0, 0, 10, 0, 0],
|
||||
[1, 0, 0, 1, 0, 0],
|
||||
[1, 0, 0, 1, 0, 0],
|
||||
false,
|
||||
]);
|
||||
@ -2607,6 +2607,44 @@ describe("annotation", function () {
|
||||
expect(opList.argsArray[3][0][0].unicode).toEqual("4");
|
||||
});
|
||||
|
||||
it("should synthesize a checked appearance for checkboxes without an /AP", async function () {
|
||||
buttonWidgetDict.set("V", Name.get("Checked"));
|
||||
buttonWidgetDict.set("Rect", [0, 0, 20, 20]);
|
||||
// Note: no /AP entry, so a default appearance must be synthesized.
|
||||
|
||||
const buttonWidgetRef = Ref.get(124, 0);
|
||||
const xref = new XRefMock([
|
||||
{ ref: buttonWidgetRef, data: buttonWidgetDict },
|
||||
]);
|
||||
const task = new WorkerTask("test checkbox without /AP");
|
||||
const checkboxEvaluator = partialEvaluator.clone({ ignoreErrors: true });
|
||||
const annotation = await AnnotationFactory.create(
|
||||
xref,
|
||||
buttonWidgetRef,
|
||||
annotationGlobalsMock,
|
||||
idFactoryMock
|
||||
);
|
||||
|
||||
expect(annotation.data.checkBox).toEqual(true);
|
||||
expect(annotation.data.exportValue).toEqual("Checked");
|
||||
|
||||
const { opList } = await annotation.getOperatorList(
|
||||
checkboxEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.DISPLAY | RenderingIntentFlag.ANNOTATIONS_FORMS,
|
||||
new Map()
|
||||
);
|
||||
|
||||
// A checkmark is drawn on its own dedicated "checked" canvas.
|
||||
expect(opList.fnArray[0]).toEqual(OPS.beginAnnotation);
|
||||
expect(opList.fnArray.at(-1)).toEqual(OPS.endAnnotation);
|
||||
expect(opList.fnArray).toContain(OPS.showText);
|
||||
const [id, , , , isUsingOwnCanvas, canvasName] = opList.argsArray[0];
|
||||
expect(id).toEqual("124R");
|
||||
expect(isUsingOwnCanvas).toEqual(true);
|
||||
expect(canvasName).toEqual("checked");
|
||||
});
|
||||
|
||||
it("should render checkboxes for printing", async function () {
|
||||
const appearanceStatesDict = new Dict();
|
||||
const normalAppearanceDict = new Dict();
|
||||
@ -2685,7 +2723,7 @@ describe("annotation", function () {
|
||||
expect(opList2.argsArray[0]).toEqual([
|
||||
"124R",
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 1, 0, 0],
|
||||
[1, 0, 0, 1, 0, 0],
|
||||
false,
|
||||
]);
|
||||
@ -3084,7 +3122,7 @@ describe("annotation", function () {
|
||||
expect(opList2.argsArray[0]).toEqual([
|
||||
"124R",
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 1, 0, 0],
|
||||
[1, 0, 0, 1, 0, 0],
|
||||
false,
|
||||
]);
|
||||
@ -3147,7 +3185,7 @@ describe("annotation", function () {
|
||||
expect(opList.argsArray[0]).toEqual([
|
||||
"124R",
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 1, 0, 0],
|
||||
[1, 0, 0, 1, 0, 0],
|
||||
false,
|
||||
]);
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
color-scheme: only light;
|
||||
|
||||
--annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
|
||||
--annotation-unfocused-field-filter: url("data:image/svg+xml;charset=UTF-8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='pdfjsFillableField' x='0%' y='0%' width='100%' height='100%' color-interpolation-filters='sRGB'><feFlood flood-color='rgb(0,54,255)' flood-opacity='0.13' result='f'/><feComposite in='f' in2='SourceGraphic' operator='over'/></filter></svg>#pdfjsFillableField");
|
||||
--input-focus-border-color: Highlight;
|
||||
--input-focus-outline: 1px solid Canvas;
|
||||
--input-unfocused-border-color: transparent;
|
||||
@ -208,10 +209,6 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.radioButton input {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.textWidgetAnnotation textarea {
|
||||
resize: none;
|
||||
}
|
||||
@ -259,36 +256,34 @@
|
||||
outline: var(--input-focus-outline);
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.checkBox input:checked::before,
|
||||
.buttonWidgetAnnotation.checkBox input:checked::after,
|
||||
.buttonWidgetAnnotation.radioButton input:checked::before {
|
||||
background-color: CanvasText;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton) {
|
||||
[data-canvas-name] {
|
||||
filter: var(--annotation-unfocused-field-filter);
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.checkBox input:checked::before,
|
||||
.buttonWidgetAnnotation.checkBox input:checked::after {
|
||||
height: 80%;
|
||||
left: 45%;
|
||||
width: 1px;
|
||||
}
|
||||
&:focus-within [data-canvas-name] {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.checkBox input:checked::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
[data-canvas-name="checked"] {
|
||||
&:has(~ input:checked) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.checkBox input:checked::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
&:has(~ input:not(:checked)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation.radioButton input:checked::before {
|
||||
border-radius: 50%;
|
||||
height: 50%;
|
||||
left: 25%;
|
||||
top: 25%;
|
||||
width: 50%;
|
||||
[data-canvas-name="unchecked"] {
|
||||
&:has(~ input:checked) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:has(~ input:not(:checked)) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textWidgetAnnotation input.comb {
|
||||
@ -317,6 +312,17 @@
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton):has(
|
||||
[data-canvas-name="checked"]
|
||||
)
|
||||
input:checked,
|
||||
.buttonWidgetAnnotation:is(.checkBox, .radioButton):has(
|
||||
[data-canvas-name="unchecked"]
|
||||
)
|
||||
input:not(:checked) {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.fileAttachmentAnnotation .popupTriggerArea {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@ -224,6 +224,10 @@ class AnnotationLayerBuilder {
|
||||
this.#eventAC = null;
|
||||
}
|
||||
|
||||
refreshCanvases() {
|
||||
this.annotationLayer?.refreshCanvases();
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.div) {
|
||||
return;
|
||||
|
||||
@ -297,6 +297,7 @@ class PDFPageDetailView extends BasePDFPageView {
|
||||
this.canvas = prevCanvas;
|
||||
},
|
||||
() => {
|
||||
this.pageView._refreshAnnotationLayer();
|
||||
this.dispatchPageRendered(
|
||||
/* cssTransform */ false,
|
||||
/* isDetailView */ true
|
||||
|
||||
@ -484,6 +484,17 @@ class PDFPageView extends BasePDFPageView {
|
||||
}
|
||||
}
|
||||
|
||||
// The detail view re-renders checkbox/radio appearances at a higher
|
||||
// resolution into the shared `annotationCanvasMap`; move them into the
|
||||
// annotation layer so they replace the lower-resolution ones. This only
|
||||
// consumes pending canvases (no re-render), and is a no-op until their
|
||||
// elements exist, so it's safe to call while the layer is still rendering.
|
||||
_refreshAnnotationLayer() {
|
||||
if (this._annotationCanvasMap?.size) {
|
||||
this.annotationLayer?.refreshCanvases();
|
||||
}
|
||||
}
|
||||
|
||||
async #renderAnnotationEditorLayer() {
|
||||
let error = null;
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user