mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-30 02:47:22 +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;
|
this._needAppearances = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getOperatorListNoAppearance() {
|
||||||
|
return {
|
||||||
|
opList: new OperatorList(),
|
||||||
|
separateForm: false,
|
||||||
|
separateCanvas: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
@ -1213,28 +1221,32 @@ 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 && intent & RenderingIntentFlag.DISPLAY
|
hasOwnCanvas &&
|
||||||
|
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 {
|
return this._getOperatorListNoAppearance();
|
||||||
opList: new OperatorList(),
|
|
||||||
separateForm: false,
|
|
||||||
separateCanvas: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (!appearance) {
|
if (!appearance) {
|
||||||
if (!isUsingOwnCanvas) {
|
if (!isUsingOwnCanvas) {
|
||||||
return {
|
return this._getOperatorListNoAppearance();
|
||||||
opList: new OperatorList(),
|
|
||||||
separateForm: false,
|
|
||||||
separateCanvas: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
appearance = new StringStream("", new Dict());
|
appearance = new StringStream("", new Dict());
|
||||||
}
|
}
|
||||||
@ -1244,7 +1256,12 @@ class Annotation {
|
|||||||
RESOURCES_KEYS_OPERATOR_LIST,
|
RESOURCES_KEYS_OPERATOR_LIST,
|
||||||
appearance
|
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(
|
const matrix = lookupMatrix(
|
||||||
appearanceDict.getArray("Matrix"),
|
appearanceDict.getArray("Matrix"),
|
||||||
IDENTITY_MATRIX
|
IDENTITY_MATRIX
|
||||||
@ -2092,11 +2109,9 @@ class WidgetAnnotation extends Annotation {
|
|||||||
!this.data.noHTML &&
|
!this.data.noHTML &&
|
||||||
!this.data.hasOwnCanvas
|
!this.data.hasOwnCanvas
|
||||||
) {
|
) {
|
||||||
return {
|
const list = this._getOperatorListNoAppearance();
|
||||||
opList: new OperatorList(),
|
list.separateForm = true;
|
||||||
separateForm: true,
|
return list;
|
||||||
separateCanvas: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._hasText) {
|
if (!this._hasText) {
|
||||||
@ -3148,20 +3163,58 @@ 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(
|
||||||
@ -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 value = null;
|
||||||
let rotation = null;
|
let rotation = null;
|
||||||
if (annotationStorage) {
|
if (annotationStorage) {
|
||||||
@ -3195,41 +3287,14 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||||||
: this.data.fieldValue === this.data.buttonValue;
|
: this.data.fieldValue === this.data.buttonValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appearance = value
|
return this.#getOperatorListForAppearance(
|
||||||
? this.checkedAppearance
|
evaluator,
|
||||||
: this.uncheckedAppearance;
|
task,
|
||||||
if (appearance) {
|
intent,
|
||||||
const savedAppearance = this.appearance;
|
annotationStorage,
|
||||||
const savedMatrix = lookupMatrix(
|
rotation,
|
||||||
appearance.dict.getArray("Matrix"),
|
value ? this.checkedAppearance : this.uncheckedAppearance
|
||||||
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) {
|
||||||
@ -3422,13 +3487,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||||||
|
|
||||||
_processCheckBox(params) {
|
_processCheckBox(params) {
|
||||||
const customAppearance = params.dict.get("AP");
|
const customAppearance = params.dict.get("AP");
|
||||||
if (!(customAppearance instanceof Dict)) {
|
let normalAppearance =
|
||||||
return;
|
customAppearance instanceof Dict ? customAppearance.get("N") : null;
|
||||||
}
|
|
||||||
|
|
||||||
const normalAppearance = customAppearance.get("N");
|
|
||||||
if (!(normalAppearance instanceof Dict)) {
|
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.
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1722036.
|
||||||
@ -3444,7 +3507,9 @@ 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.getKeys()];
|
const exportValues = normalAppearance
|
||||||
|
? [...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) {
|
||||||
@ -3470,10 +3535,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;
|
||||||
|
|
||||||
|
|||||||
@ -439,9 +439,6 @@ 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) {
|
||||||
@ -2009,7 +2006,6 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setBackgroundColor(element);
|
|
||||||
this._setDefaultPropertiesFromJS(element);
|
this._setDefaultPropertiesFromJS(element);
|
||||||
|
|
||||||
this.container.append(element);
|
this.container.append(element);
|
||||||
@ -2028,7 +2024,9 @@ 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, {
|
||||||
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;
|
}).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.
|
||||||
@ -2115,7 +2113,6 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setBackgroundColor(element);
|
|
||||||
this._setDefaultPropertiesFromJS(element);
|
this._setDefaultPropertiesFromJS(element);
|
||||||
|
|
||||||
this.container.append(element);
|
this.container.append(element);
|
||||||
@ -4139,18 +4136,44 @@ class AnnotationLayer {
|
|||||||
if (!element) {
|
if (!element) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(canvas)) {
|
||||||
canvas.className = "annotationContent";
|
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;
|
const { firstChild } = element;
|
||||||
if (!firstChild) {
|
if (!firstChild) {
|
||||||
element.append(canvas);
|
element.append(firstCanvas);
|
||||||
} else if (firstChild.nodeName === "CANVAS") {
|
|
||||||
firstChild.replaceWith(canvas);
|
|
||||||
} else if (!firstChild.classList.contains("annotationContent")) {
|
} else if (!firstChild.classList.contains("annotationContent")) {
|
||||||
firstChild.before(canvas);
|
firstChild.before(firstCanvas);
|
||||||
} else {
|
} 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);
|
const editableAnnotation = this.#editableAnnotations.get(id);
|
||||||
if (!editableAnnotation) {
|
if (!editableAnnotation) {
|
||||||
@ -4168,7 +4191,12 @@ 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() {
|
||||||
|
|||||||
@ -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 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...
|
||||||
@ -3673,7 +3681,26 @@ class CanvasGraphics {
|
|||||||
canvasHeight
|
canvasHeight
|
||||||
);
|
);
|
||||||
const { canvas, context } = this.annotationCanvas;
|
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.annotationCanvas.savedCtx = this.ctx;
|
||||||
this.ctx = context;
|
this.ctx = context;
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
|
|||||||
@ -68,4 +68,26 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,6 +94,7 @@ async function writeSVG(svgElement, ctx) {
|
|||||||
setTimeout(resolve, 10);
|
setTimeout(resolve, 10);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return loadImage(svg_xml, ctx);
|
return loadImage(svg_xml, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,21 +151,40 @@ 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) {
|
||||||
promises.push(
|
if (Array.isArray(canvas)) {
|
||||||
new Promise(resolve => {
|
for (const canvasItem of canvas) {
|
||||||
canvas.toBlob(blob => {
|
canvasToImage(canvasItem, key);
|
||||||
const image = document.createElement("img");
|
}
|
||||||
image.classList.add("wasCanvas");
|
} else {
|
||||||
image.onload = function () {
|
canvasToImage(canvas, key);
|
||||||
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;
|
||||||
|
|||||||
@ -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} > :first-child').checked`
|
`document.querySelector('${selector} input').checked`
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const otherSelector of selectors) {
|
for (const otherSelector of selectors) {
|
||||||
const checked = await page.$eval(
|
const checked = await page.$eval(
|
||||||
`${otherSelector} > :first-child`,
|
`${otherSelector} input`,
|
||||||
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} > :first-child').checked`
|
`document.querySelector('${selector} input').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} > :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", () => {
|
describe("Date creation must be timezone consistent", () => {
|
||||||
let pages;
|
let pages;
|
||||||
|
|
||||||
|
|||||||
2
test/pdfs/.gitignore
vendored
2
test/pdfs/.gitignore
vendored
@ -933,3 +933,5 @@
|
|||||||
!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
|
||||||
|
|||||||
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,
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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],
|
||||||
[32, 0, 0, 10, 0, 0],
|
[1, 0, 0, 1, 0, 0],
|
||||||
[1, 0, 0, 1, 0, 0],
|
[1, 0, 0, 1, 0, 0],
|
||||||
false,
|
false,
|
||||||
]);
|
]);
|
||||||
@ -2607,6 +2607,44 @@ 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();
|
||||||
@ -2685,7 +2723,7 @@ describe("annotation", function () {
|
|||||||
expect(opList2.argsArray[0]).toEqual([
|
expect(opList2.argsArray[0]).toEqual([
|
||||||
"124R",
|
"124R",
|
||||||
[0, 0, 0, 0],
|
[0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0],
|
[1, 0, 0, 1, 0, 0],
|
||||||
[1, 0, 0, 1, 0, 0],
|
[1, 0, 0, 1, 0, 0],
|
||||||
false,
|
false,
|
||||||
]);
|
]);
|
||||||
@ -3084,7 +3122,7 @@ describe("annotation", function () {
|
|||||||
expect(opList2.argsArray[0]).toEqual([
|
expect(opList2.argsArray[0]).toEqual([
|
||||||
"124R",
|
"124R",
|
||||||
[0, 0, 0, 0],
|
[0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0],
|
[1, 0, 0, 1, 0, 0],
|
||||||
[1, 0, 0, 1, 0, 0],
|
[1, 0, 0, 1, 0, 0],
|
||||||
false,
|
false,
|
||||||
]);
|
]);
|
||||||
@ -3147,7 +3185,7 @@ describe("annotation", function () {
|
|||||||
expect(opList.argsArray[0]).toEqual([
|
expect(opList.argsArray[0]).toEqual([
|
||||||
"124R",
|
"124R",
|
||||||
[0, 0, 0, 0],
|
[0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0],
|
[1, 0, 0, 1, 0, 0],
|
||||||
[1, 0, 0, 1, 0, 0],
|
[1, 0, 0, 1, 0, 0],
|
||||||
false,
|
false,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
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;
|
||||||
@ -208,10 +209,6 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonWidgetAnnotation.radioButton input {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textWidgetAnnotation textarea {
|
.textWidgetAnnotation textarea {
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
@ -259,36 +256,34 @@
|
|||||||
outline: var(--input-focus-outline);
|
outline: var(--input-focus-outline);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonWidgetAnnotation.checkBox input:checked::before,
|
.buttonWidgetAnnotation:is(.checkBox, .radioButton) {
|
||||||
.buttonWidgetAnnotation.checkBox input:checked::after,
|
[data-canvas-name] {
|
||||||
.buttonWidgetAnnotation.radioButton input:checked::before {
|
filter: var(--annotation-unfocused-field-filter);
|
||||||
background-color: CanvasText;
|
}
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonWidgetAnnotation.checkBox input:checked::before,
|
&:focus-within [data-canvas-name] {
|
||||||
.buttonWidgetAnnotation.checkBox input:checked::after {
|
filter: none;
|
||||||
height: 80%;
|
}
|
||||||
left: 45%;
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonWidgetAnnotation.checkBox input:checked::before {
|
[data-canvas-name="checked"] {
|
||||||
transform: rotate(45deg);
|
&:has(~ input:checked) {
|
||||||
}
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.buttonWidgetAnnotation.checkBox input:checked::after {
|
&:has(~ input:not(:checked)) {
|
||||||
transform: rotate(-45deg);
|
display: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.buttonWidgetAnnotation.radioButton input:checked::before {
|
[data-canvas-name="unchecked"] {
|
||||||
border-radius: 50%;
|
&:has(~ input:checked) {
|
||||||
height: 50%;
|
display: none;
|
||||||
left: 25%;
|
}
|
||||||
top: 25%;
|
|
||||||
width: 50%;
|
&:has(~ input:not(:checked)) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.textWidgetAnnotation input.comb {
|
.textWidgetAnnotation input.comb {
|
||||||
@ -317,6 +312,17 @@
|
|||||||
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%;
|
||||||
|
|||||||
@ -224,6 +224,10 @@ class AnnotationLayerBuilder {
|
|||||||
this.#eventAC = null;
|
this.#eventAC = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshCanvases() {
|
||||||
|
this.annotationLayer?.refreshCanvases();
|
||||||
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
if (!this.div) {
|
if (!this.div) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -297,6 +297,7 @@ 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
|
||||||
|
|||||||
@ -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() {
|
async #renderAnnotationEditorLayer() {
|
||||||
let error = null;
|
let error = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user