Compare commits

..

No commits in common. "5873e1cbc082dfe710b7b5d31c8ffad0337d125e" and "4781194b3795b1bf54864af840fa268adbe08d9d" have entirely different histories.

19 changed files with 201 additions and 415 deletions

View File

@ -785,14 +785,6 @@ class Annotation {
this._needAppearances = false; this._needAppearances = false;
} }
_getOperatorListNoAppearance() {
return {
opList: new OperatorList(),
separateForm: false,
separateCanvas: false,
};
}
/** /**
* @private * @private
*/ */
@ -1221,32 +1213,28 @@ class Annotation {
return resources; 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) { async getOperatorList(evaluator, task, intent, annotationStorage) {
const { hasOwnCanvas, id, rect } = this.data; const { hasOwnCanvas, id, rect } = this.data;
let appearance = this.appearance; let appearance = this.appearance;
const isUsingOwnCanvas = !!( const isUsingOwnCanvas = !!(
hasOwnCanvas && hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY
intent & RenderingIntentFlag.DISPLAY &&
(!this._ownCanvasRequiresForms ||
intent & RenderingIntentFlag.ANNOTATIONS_FORMS)
); );
if (isUsingOwnCanvas && (this.width === 0 || this.height === 0)) { if (isUsingOwnCanvas && (this.width === 0 || this.height === 0)) {
// Empty annotation, don't draw anything. // Empty annotation, don't draw anything.
this.data.hasOwnCanvas = false; this.data.hasOwnCanvas = false;
return this._getOperatorListNoAppearance(); return {
opList: new OperatorList(),
separateForm: false,
separateCanvas: false,
};
} }
if (!appearance) { if (!appearance) {
if (!isUsingOwnCanvas) { if (!isUsingOwnCanvas) {
return this._getOperatorListNoAppearance(); return {
opList: new OperatorList(),
separateForm: false,
separateCanvas: false,
};
} }
appearance = new StringStream("", new Dict()); appearance = new StringStream("", new Dict());
} }
@ -1256,12 +1244,7 @@ class Annotation {
RESOURCES_KEYS_OPERATOR_LIST, RESOURCES_KEYS_OPERATOR_LIST,
appearance appearance
); );
const bbox = lookupRect(appearanceDict.getArray("BBox"), [ const bbox = lookupRect(appearanceDict.getArray("BBox"), [0, 0, 1, 1]);
0,
0,
this.width,
this.height,
]);
const matrix = lookupMatrix( const matrix = lookupMatrix(
appearanceDict.getArray("Matrix"), appearanceDict.getArray("Matrix"),
IDENTITY_MATRIX IDENTITY_MATRIX
@ -2109,9 +2092,11 @@ class WidgetAnnotation extends Annotation {
!this.data.noHTML && !this.data.noHTML &&
!this.data.hasOwnCanvas !this.data.hasOwnCanvas
) { ) {
const list = this._getOperatorListNoAppearance(); return {
list.separateForm = true; opList: new OperatorList(),
return list; separateForm: true,
separateCanvas: false,
};
} }
if (!this._hasText) { if (!this._hasText) {
@ -3163,58 +3148,20 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
this.data.radioButton = isRadio && !isPushButton; this.data.radioButton = isRadio && !isPushButton;
this.data.pushButton = isPushButton; this.data.pushButton = isPushButton;
this.data.isTooltipOnly = false; this.data.isTooltipOnly = false;
this.data.hasOwnCanvas = true;
this.data.noHTML = false;
if (this.data.checkBox) { if (this.data.checkBox) {
this._processCheckBox(params); this._processCheckBox(params);
} else if (this.data.radioButton) { } else if (this.data.radioButton) {
this._processRadioButton(params); this._processRadioButton(params);
} else if (this.data.pushButton) { } else if (this.data.pushButton) {
this.data.hasOwnCanvas = true;
this.data.noHTML = false;
this._processPushButton(params); this._processPushButton(params);
} else { } else {
warn("Invalid field flags for button widget annotation"); 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) { async getOperatorList(evaluator, task, intent, annotationStorage) {
if (this.data.pushButton) { if (this.data.pushButton) {
return super.getOperatorList( return super.getOperatorList(
@ -3226,45 +3173,6 @@ 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 value = null;
let rotation = null; let rotation = null;
if (annotationStorage) { if (annotationStorage) {
@ -3287,14 +3195,41 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
: this.data.fieldValue === this.data.buttonValue; : this.data.fieldValue === this.data.buttonValue;
} }
return this.#getOperatorListForAppearance( const appearance = value
evaluator, ? this.checkedAppearance
task, : this.uncheckedAppearance;
intent, if (appearance) {
annotationStorage, const savedAppearance = this.appearance;
rotation, const savedMatrix = lookupMatrix(
value ? this.checkedAppearance : this.uncheckedAppearance 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,
};
} }
async save(evaluator, task, annotationStorage, changes) { async save(evaluator, task, annotationStorage, changes) {
@ -3487,11 +3422,13 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
_processCheckBox(params) { _processCheckBox(params) {
const customAppearance = params.dict.get("AP"); const customAppearance = params.dict.get("AP");
let normalAppearance = if (!(customAppearance instanceof Dict)) {
customAppearance instanceof Dict ? customAppearance.get("N") : null; return;
}
const normalAppearance = customAppearance.get("N");
if (!(normalAppearance instanceof Dict)) { if (!(normalAppearance instanceof Dict)) {
// Synthesize a default appearance below when the field defines none. return;
normalAppearance = null;
} }
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1722036. // See https://bugzilla.mozilla.org/show_bug.cgi?id=1722036.
@ -3507,9 +3444,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
: "Yes"; : "Yes";
// Don't decode the keys which are names. // Don't decode the keys which are names.
const exportValues = normalAppearance const exportValues = [...normalAppearance.getKeys()];
? [...normalAppearance.getKeys()]
: [];
if (exportValues.length === 0) { if (exportValues.length === 0) {
exportValues.push("Off", yes); exportValues.push("Off", yes);
} else if (exportValues.length === 1) { } else if (exportValues.length === 1) {
@ -3535,10 +3470,10 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
this.data.exportValue = exportValues[1]; this.data.exportValue = exportValues[1];
const checkedAppearance = normalAppearance?.get(this.data.exportValue); const checkedAppearance = normalAppearance.get(this.data.exportValue);
this.checkedAppearance = this.checkedAppearance =
checkedAppearance instanceof BaseStream ? checkedAppearance : null; checkedAppearance instanceof BaseStream ? checkedAppearance : null;
const uncheckedAppearance = normalAppearance?.get("Off"); const uncheckedAppearance = normalAppearance.get("Off");
this.uncheckedAppearance = this.uncheckedAppearance =
uncheckedAppearance instanceof BaseStream ? uncheckedAppearance : null; uncheckedAppearance instanceof BaseStream ? uncheckedAppearance : null;

View File

@ -27,15 +27,7 @@ import {
getNewAnnotationsMap, getNewAnnotationsMap,
numberToString, numberToString,
} from "../core_utils.js"; } from "../core_utils.js";
import { import { Dict, isName, Name, Ref, RefSet, RefSetCache } from "../primitives.js";
Dict,
isDict,
isName,
Name,
Ref,
RefSet,
RefSetCache,
} from "../primitives.js";
import { incrementalUpdate, writeValue } from "../writer.js"; import { incrementalUpdate, writeValue } from "../writer.js";
import { isArrayEqual, stringToBytes } from "../../shared/util.js"; import { isArrayEqual, stringToBytes } from "../../shared/util.js";
import { NameTree, NumberTree } from "../name_number_tree.js"; import { NameTree, NumberTree } from "../name_number_tree.js";
@ -288,7 +280,11 @@ class PDFEditor {
oldRefMapping.put(oldRef, newRef); oldRefMapping.put(oldRef, newRef);
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
if (isDict(obj, "Page") && !this.currentDocument.pagesMap.has(oldRef)) { if (
obj instanceof Dict &&
isName(obj.get("Type"), "Page") &&
!this.currentDocument.pagesMap.has(oldRef)
) {
throw new Error( throw new Error(
"Add a deleted page to the document is not supported." "Add a deleted page to the document is not supported."
); );

View File

@ -19,7 +19,7 @@ import {
stringToUTF8String, stringToUTF8String,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { Dict, isDict, isName, Name, Ref, RefSetCache } from "./primitives.js"; import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js";
import { stringToAsciiOrUTF16BE, stringToPDFString } from "./string_utils.js"; import { stringToAsciiOrUTF16BE, stringToPDFString } from "./string_utils.js";
import { BaseStream } from "./base_stream.js"; import { BaseStream } from "./base_stream.js";
import { lookupNormalRect } from "./core_utils.js"; import { lookupNormalRect } from "./core_utils.js";
@ -594,7 +594,10 @@ class StructElementNode {
} }
for (let af of AFs) { for (let af of AFs) {
af = this.xref.fetchIfRef(af); af = this.xref.fetchIfRef(af);
if (!isDict(af, "Filespec")) { if (!(af instanceof Dict)) {
continue;
}
if (!isName(af.get("Type"), "Filespec")) {
continue; continue;
} }
if (!isName(af.get("AFRelationship"), "Supplement")) { if (!isName(af.get("AFRelationship"), "Supplement")) {

View File

@ -439,6 +439,9 @@ class AnnotationElement {
if (horizontalRadius > 0 || verticalRadius > 0) { if (horizontalRadius > 0 || verticalRadius > 0) {
const radius = `calc(${horizontalRadius}px * var(--total-scale-factor)) / calc(${verticalRadius}px * var(--total-scale-factor))`; const radius = `calc(${horizontalRadius}px * var(--total-scale-factor)) / calc(${verticalRadius}px * var(--total-scale-factor))`;
style.borderRadius = radius; 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) { switch (data.borderStyle.style) {
@ -2006,6 +2009,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
); );
} }
this._setBackgroundColor(element);
this._setDefaultPropertiesFromJS(element); this._setDefaultPropertiesFromJS(element);
this.container.append(element); this.container.append(element);
@ -2024,9 +2028,7 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
const data = this.data; const data = this.data;
const id = data.id; const id = data.id;
let value = storage.getValue(id, { let value = storage.getValue(id, {
// A radio without an on-state (`buttonValue === null`, e.g. no /AP) must value: data.fieldValue === data.buttonValue,
// not be checked by default, otherwise `null === null` would select it.
value: data.buttonValue !== null && data.fieldValue === data.buttonValue,
}).value; }).value;
if (typeof value === "string") { if (typeof value === "string") {
// The value has been changed through js and set in annotationStorage. // The value has been changed through js and set in annotationStorage.
@ -2113,6 +2115,7 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
); );
} }
this._setBackgroundColor(element);
this._setDefaultPropertiesFromJS(element); this._setDefaultPropertiesFromJS(element);
this.container.append(element); this.container.append(element);
@ -4136,44 +4139,18 @@ class AnnotationLayer {
if (!element) { if (!element) {
continue; continue;
} }
if (Array.isArray(canvas)) {
for (const cvs of canvas) { canvas.className = "annotationContent";
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; const { firstChild } = element;
if (!firstChild) { if (!firstChild) {
element.append(firstCanvas); element.append(canvas);
} else if (firstChild.nodeName === "CANVAS") {
firstChild.replaceWith(canvas);
} else if (!firstChild.classList.contains("annotationContent")) { } else if (!firstChild.classList.contains("annotationContent")) {
firstChild.before(firstCanvas); firstChild.before(canvas);
} else { } else {
firstChild.after(firstCanvas); firstChild.after(canvas);
} }
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); const editableAnnotation = this.#editableAnnotations.get(id);
if (!editableAnnotation) { if (!editableAnnotation) {
@ -4191,12 +4168,7 @@ class AnnotationLayer {
editableAnnotation.canvas = canvas; 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() { getEditableAnnotations() {

View File

@ -3630,15 +3630,7 @@ class CanvasGraphics {
} }
} }
beginAnnotation( beginAnnotation(opIdx, id, rect, transform, matrix, hasOwnCanvas) {
opIdx,
id,
rect,
transform,
matrix,
hasOwnCanvas,
canvasName
) {
// The annotations are drawn just after the page content. // The annotations are drawn just after the page content.
// The page content drawing can potentially have set a transform, // The page content drawing can potentially have set a transform,
// a clipping path, whatever... // a clipping path, whatever...
@ -3681,26 +3673,7 @@ class CanvasGraphics {
canvasHeight canvasHeight
); );
const { canvas, context } = this.annotationCanvas; const { canvas, context } = this.annotationCanvas;
if (canvasName) { this.annotationCanvasMap.set(id, canvas);
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.annotationCanvas.savedCtx = this.ctx;
this.ctx = context; this.ctx = context;
this.ctx.save(); this.ctx.save();

View File

@ -190,6 +190,7 @@ class PageViewport {
* @returns {Array} Array containing `x`- and `y`-coordinates of the * @returns {Array} Array containing `x`- and `y`-coordinates of the
* point in the viewport coordinate space. * point in the viewport coordinate space.
* @see {@link convertToPdfPoint} * @see {@link convertToPdfPoint}
* @see {@link convertToViewportRectangle}
*/ */
convertToViewportPoint(x, y) { convertToViewportPoint(x, y) {
const p = [x, y]; const p = [x, y];
@ -197,6 +198,21 @@ class PageViewport {
return p; return p;
} }
/**
* Converts PDF rectangle to the viewport coordinates.
* @param {Array} rect - The xMin, yMin, xMax and yMax coordinates.
* @returns {Array} Array containing corresponding coordinates of the
* rectangle in the viewport coordinate space.
* @see {@link convertToViewportPoint}
*/
convertToViewportRectangle(rect) {
const topLeft = [rect[0], rect[1]];
Util.applyTransform(topLeft, this.transform);
const bottomRight = [rect[2], rect[3]];
Util.applyTransform(bottomRight, this.transform);
return [topLeft[0], topLeft[1], bottomRight[0], bottomRight[1]];
}
/** /**
* Converts viewport coordinates to the PDF location. For examples, useful * Converts viewport coordinates to the PDF location. For examples, useful
* for converting canvas pixel location into PDF one. * for converting canvas pixel location into PDF one.

View File

@ -68,26 +68,4 @@
color: red; color: red;
font-size: 10px; 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,7 +94,6 @@ async function writeSVG(svgElement, ctx) {
setTimeout(resolve, 10); setTimeout(resolve, 10);
}); });
} }
return loadImage(svg_xml, ctx); return loadImage(svg_xml, ctx);
} }
@ -151,40 +150,21 @@ async function inlineImages(node, silentErrors = false) {
async function convertCanvasesToImages(annotationCanvasMap, outputScale) { async function convertCanvasesToImages(annotationCanvasMap, outputScale) {
const results = new Map(); const results = new Map();
const promises = []; 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) { for (const [key, canvas] of annotationCanvasMap) {
if (Array.isArray(canvas)) { promises.push(
for (const canvasItem of canvas) { new Promise(resolve => {
canvasToImage(canvasItem, key); canvas.toBlob(blob => {
} const image = document.createElement("img");
} else { image.classList.add("wasCanvas");
canvasToImage(canvas, key); image.onload = function () {
} image.style.width = Math.floor(image.width / outputScale) + "px";
resolve();
};
results.set(key, image);
image.src = URL.createObjectURL(blob);
});
})
);
} }
await Promise.all(promises); await Promise.all(promises);
return results; return results;

View File

@ -122,12 +122,12 @@ describe("Checkbox annotation", () => {
for (const selector of selectors) { for (const selector of selectors) {
await page.click(selector); await page.click(selector);
await page.waitForFunction( await page.waitForFunction(
`document.querySelector('${selector} input').checked` `document.querySelector('${selector} > :first-child').checked`
); );
for (const otherSelector of selectors) { for (const otherSelector of selectors) {
const checked = await page.$eval( const checked = await page.$eval(
`${otherSelector} input`, `${otherSelector} > :first-child`,
el => el.checked el => el.checked
); );
expect(checked) expect(checked)
@ -157,7 +157,7 @@ describe("Checkbox annotation", () => {
const selector = getAnnotationSelector("7R"); const selector = getAnnotationSelector("7R");
await page.click(selector); await page.click(selector);
await page.waitForFunction( await page.waitForFunction(
`document.querySelector('${selector} input').checked` `document.querySelector('${selector} > :first-child').checked`
); );
expect(true).withContext(`In ${browserName}`).toEqual(true); expect(true).withContext(`In ${browserName}`).toEqual(true);
}) })
@ -185,7 +185,7 @@ describe("Checkbox annotation", () => {
for (const selector of selectors) { for (const selector of selectors) {
await page.click(selector); await page.click(selector);
await page.waitForFunction( await page.waitForFunction(
`document.querySelector('${selector} input').checked` `document.querySelector('${selector} > :first-child').checked`
); );
} }
}) })

View File

@ -2413,6 +2413,49 @@ 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", () => { describe("Date creation must be timezone consistent", () => {
let pages; let pages;

View File

@ -933,5 +933,3 @@
!cidfont_cmap_overflow.pdf !cidfont_cmap_overflow.pdf
!jbig2_file_header.pdf !jbig2_file_header.pdf
!text_field_own_canvas_calc.pdf !text_field_own_canvas_calc.pdf
!bug1802506.pdf
!checkbox_no_appearance.pdf

Binary file not shown.

View File

@ -1,30 +0,0 @@
%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,23 +14387,5 @@
"rounds": 1, "rounds": 1,
"link": true, "link": true,
"type": "eq" "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([ expect(opList.argsArray[0]).toEqual([
"271R", "271R",
[0, 0, 32, 10], [0, 0, 32, 10],
[1, 0, 0, 1, 0, 0], [32, 0, 0, 10, 0, 0],
[1, 0, 0, 1, 0, 0], [1, 0, 0, 1, 0, 0],
false, false,
]); ]);
@ -2607,44 +2607,6 @@ describe("annotation", function () {
expect(opList.argsArray[3][0][0].unicode).toEqual("4"); 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 () { it("should render checkboxes for printing", async function () {
const appearanceStatesDict = new Dict(); const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict(); const normalAppearanceDict = new Dict();
@ -2723,7 +2685,7 @@ describe("annotation", function () {
expect(opList2.argsArray[0]).toEqual([ expect(opList2.argsArray[0]).toEqual([
"124R", "124R",
[0, 0, 0, 0], [0, 0, 0, 0],
[1, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0], [1, 0, 0, 1, 0, 0],
false, false,
]); ]);
@ -3122,7 +3084,7 @@ describe("annotation", function () {
expect(opList2.argsArray[0]).toEqual([ expect(opList2.argsArray[0]).toEqual([
"124R", "124R",
[0, 0, 0, 0], [0, 0, 0, 0],
[1, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0], [1, 0, 0, 1, 0, 0],
false, false,
]); ]);
@ -3185,7 +3147,7 @@ describe("annotation", function () {
expect(opList.argsArray[0]).toEqual([ expect(opList.argsArray[0]).toEqual([
"124R", "124R",
[0, 0, 0, 0], [0, 0, 0, 0],
[1, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0], [1, 0, 0, 1, 0, 0],
false, false,
]); ]);

View File

@ -17,7 +17,6 @@
color-scheme: only light; 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-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-border-color: Highlight;
--input-focus-outline: 1px solid Canvas; --input-focus-outline: 1px solid Canvas;
--input-unfocused-border-color: transparent; --input-unfocused-border-color: transparent;
@ -209,6 +208,10 @@
padding: 0; padding: 0;
} }
.buttonWidgetAnnotation.radioButton input {
border-radius: 50%;
}
.textWidgetAnnotation textarea { .textWidgetAnnotation textarea {
resize: none; resize: none;
} }
@ -256,34 +259,36 @@
outline: var(--input-focus-outline); outline: var(--input-focus-outline);
} }
.buttonWidgetAnnotation:is(.checkBox, .radioButton) { .buttonWidgetAnnotation.checkBox input:checked::before,
[data-canvas-name] { .buttonWidgetAnnotation.checkBox input:checked::after,
filter: var(--annotation-unfocused-field-filter); .buttonWidgetAnnotation.radioButton input:checked::before {
} background-color: CanvasText;
content: "";
display: block;
position: absolute;
}
&:focus-within [data-canvas-name] { .buttonWidgetAnnotation.checkBox input:checked::before,
filter: none; .buttonWidgetAnnotation.checkBox input:checked::after {
} height: 80%;
left: 45%;
width: 1px;
}
[data-canvas-name="checked"] { .buttonWidgetAnnotation.checkBox input:checked::before {
&:has(~ input:checked) { transform: rotate(45deg);
display: block; }
}
&:has(~ input:not(:checked)) { .buttonWidgetAnnotation.checkBox input:checked::after {
display: none; transform: rotate(-45deg);
} }
}
[data-canvas-name="unchecked"] { .buttonWidgetAnnotation.radioButton input:checked::before {
&:has(~ input:checked) { border-radius: 50%;
display: none; height: 50%;
} left: 25%;
top: 25%;
&:has(~ input:not(:checked)) { width: 50%;
display: block;
}
}
} }
.textWidgetAnnotation input.comb { .textWidgetAnnotation input.comb {
@ -312,17 +317,6 @@
appearance: none; 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 { .fileAttachmentAnnotation .popupTriggerArea {
height: 100%; height: 100%;
width: 100%; width: 100%;

View File

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

View File

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

View File

@ -484,17 +484,6 @@ 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() { async #renderAnnotationEditorLayer() {
let error = null; let error = null;
try { try {