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:
calixteman 2026-06-12 22:04:29 +02:00 committed by GitHub
commit 35d275d3b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 403 additions and 174 deletions

View File

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

View File

@ -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() {

View File

@ -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();

View File

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

View File

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

View File

@ -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`
);
}
})

View File

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

View File

@ -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

Binary file not shown.

View 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

View File

@ -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
}
]

View File

@ -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,
]);

View File

@ -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%;

View File

@ -224,6 +224,10 @@ class AnnotationLayerBuilder {
this.#eventAC = null;
}
refreshCanvases() {
this.annotationLayer?.refreshCanvases();
}
hide() {
if (!this.div) {
return;

View File

@ -297,6 +297,7 @@ class PDFPageDetailView extends BasePDFPageView {
this.canvas = prevCanvas;
},
() => {
this.pageView._refreshAnnotationLayer();
this.dispatchPageRendered(
/* cssTransform */ false,
/* isDetailView */ true

View File

@ -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 {