mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-19 14:48:08 +02:00
Merge pull request #18834 from calixteman/edit_stamp1
[Editor] Make stamp annotations editable (bug 1921291)
This commit is contained in:
commit
e9c81a6e54
11 changed files with 281 additions and 18 deletions
|
@ -4859,14 +4859,34 @@ class StrikeOutAnnotation extends MarkupAnnotation {
|
|||
}
|
||||
|
||||
class StampAnnotation extends MarkupAnnotation {
|
||||
#savedHasOwnCanvas;
|
||||
|
||||
constructor(params) {
|
||||
super(params);
|
||||
|
||||
this.data.annotationType = AnnotationType.STAMP;
|
||||
this.data.hasOwnCanvas = this.data.noRotate;
|
||||
this.#savedHasOwnCanvas = this.data.hasOwnCanvas = this.data.noRotate;
|
||||
this.data.isEditable = !this.data.noHTML;
|
||||
// We want to be able to add mouse listeners to the annotation.
|
||||
this.data.noHTML = false;
|
||||
}
|
||||
|
||||
mustBeViewedWhenEditing(isEditing, modifiedIds = null) {
|
||||
if (isEditing) {
|
||||
if (!this.data.isEditable) {
|
||||
return false;
|
||||
}
|
||||
// When we're editing, we want to ensure that the stamp annotation is
|
||||
// drawn on a canvas in order to use it in the annotation editor layer.
|
||||
this.#savedHasOwnCanvas = this.data.hasOwnCanvas;
|
||||
this.data.hasOwnCanvas = true;
|
||||
return true;
|
||||
}
|
||||
this.data.hasOwnCanvas = this.#savedHasOwnCanvas;
|
||||
|
||||
return !modifiedIds?.has(this.data.id);
|
||||
}
|
||||
|
||||
static async createImage(bitmap, xref) {
|
||||
// TODO: when printing, we could have a specific internal colorspace
|
||||
// (e.g. something like DeviceRGBA) in order avoid any conversion (i.e. no
|
||||
|
|
|
@ -2863,10 +2863,8 @@ class InkAnnotationElement extends AnnotationElement {
|
|||
}
|
||||
|
||||
this.container.append(svg);
|
||||
this._editOnDoubleClick();
|
||||
|
||||
if (this._isEditable) {
|
||||
this._editOnDoubleClick();
|
||||
}
|
||||
return this.container;
|
||||
}
|
||||
|
||||
|
@ -2961,6 +2959,7 @@ class StrikeOutAnnotationElement extends AnnotationElement {
|
|||
class StampAnnotationElement extends AnnotationElement {
|
||||
constructor(parameters) {
|
||||
super(parameters, { isRenderable: true, ignoreBorder: true });
|
||||
this.annotationEditorType = AnnotationEditorType.STAMP;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -2970,6 +2969,8 @@ class StampAnnotationElement extends AnnotationElement {
|
|||
if (!this.data.popupRef && this.hasPopupData) {
|
||||
this._createPopup();
|
||||
}
|
||||
this._editOnDoubleClick();
|
||||
|
||||
return this.container;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../annotation_layer.js").AnnotationLayer} AnnotationLayer */
|
||||
/** @typedef {import("../draw_layer.js").DrawLayer} DrawLayer */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */
|
||||
|
||||
import { AnnotationEditorType, FeatureTest } from "../../shared/util.js";
|
||||
import { AnnotationEditor } from "./editor.js";
|
||||
|
@ -35,6 +37,7 @@ import { StampEditor } from "./stamp.js";
|
|||
* @typedef {Object} AnnotationEditorLayerOptions
|
||||
* @property {Object} mode
|
||||
* @property {HTMLDivElement} div
|
||||
* @property {StructTreeLayerBuilder} structTreeLayer
|
||||
* @property {AnnotationEditorUIManager} uiManager
|
||||
* @property {boolean} enabled
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
|
@ -95,6 +98,7 @@ class AnnotationEditorLayer {
|
|||
uiManager,
|
||||
pageIndex,
|
||||
div,
|
||||
structTreeLayer,
|
||||
accessibilityManager,
|
||||
annotationLayer,
|
||||
drawLayer,
|
||||
|
@ -119,6 +123,7 @@ class AnnotationEditorLayer {
|
|||
this.viewport = viewport;
|
||||
this.#textLayer = textLayer;
|
||||
this.drawLayer = drawLayer;
|
||||
this._structTree = structTreeLayer;
|
||||
|
||||
this.#uiManager.addLayer(this);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AnnotationEditorType, shadow } from "../../shared/util.js";
|
||||
import {
|
||||
AnnotationEditorType,
|
||||
AnnotationPrefix,
|
||||
shadow,
|
||||
} from "../../shared/util.js";
|
||||
import { OutputScale, PixelsPerInch } from "../display_utils.js";
|
||||
import { AnnotationEditor } from "./editor.js";
|
||||
import { StampAnnotationElement } from "../annotation_layer.js";
|
||||
|
@ -383,7 +387,7 @@ class StampEditor extends AnnotationEditor {
|
|||
this.#getBitmap();
|
||||
}
|
||||
|
||||
if (this.width) {
|
||||
if (this.width && !this.annotationElementId) {
|
||||
// This editor was created in using copy (ctrl+c).
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
this.setAt(
|
||||
|
@ -431,7 +435,8 @@ class StampEditor extends AnnotationEditor {
|
|||
|
||||
if (
|
||||
!this._uiManager.useNewAltTextWhenAddingImage ||
|
||||
!this._uiManager.useNewAltTextFlow
|
||||
!this._uiManager.useNewAltTextFlow ||
|
||||
this.annotationElementId
|
||||
) {
|
||||
div.hidden = false;
|
||||
}
|
||||
|
@ -769,13 +774,55 @@ class StampEditor extends AnnotationEditor {
|
|||
|
||||
/** @inheritdoc */
|
||||
static async deserialize(data, parent, uiManager) {
|
||||
let initialData = null;
|
||||
if (data instanceof StampAnnotationElement) {
|
||||
return null;
|
||||
const {
|
||||
data: { rect, rotation, id, structParent, popupRef },
|
||||
container,
|
||||
parent: {
|
||||
page: { pageNumber },
|
||||
},
|
||||
} = data;
|
||||
const canvas = container.querySelector("canvas");
|
||||
const imageData = uiManager.imageManager.getFromCanvas(
|
||||
container.id,
|
||||
canvas
|
||||
);
|
||||
canvas.remove();
|
||||
|
||||
// When switching to edit mode, we wait for the structure tree to be
|
||||
// ready (see pdf_viewer.js), so it's fine to use getAriaAttributesSync.
|
||||
const altText =
|
||||
(
|
||||
await parent._structTree.getAriaAttributes(`${AnnotationPrefix}${id}`)
|
||||
)?.get("aria-label") || "";
|
||||
|
||||
initialData = data = {
|
||||
annotationType: AnnotationEditorType.STAMP,
|
||||
bitmapId: imageData.id,
|
||||
bitmap: imageData.bitmap,
|
||||
pageIndex: pageNumber - 1,
|
||||
rect: rect.slice(0),
|
||||
rotation,
|
||||
id,
|
||||
deleted: false,
|
||||
accessibilityData: {
|
||||
decorative: false,
|
||||
altText,
|
||||
},
|
||||
isSvg: false,
|
||||
structParent,
|
||||
popupRef,
|
||||
};
|
||||
}
|
||||
const editor = await super.deserialize(data, parent, uiManager);
|
||||
const { rect, bitmapUrl, bitmapId, isSvg, accessibilityData } = data;
|
||||
const { rect, bitmap, bitmapUrl, bitmapId, isSvg, accessibilityData } =
|
||||
data;
|
||||
if (bitmapId && uiManager.imageManager.isValidId(bitmapId)) {
|
||||
editor.#bitmapId = bitmapId;
|
||||
if (bitmap) {
|
||||
editor.#bitmap = bitmap;
|
||||
}
|
||||
} else {
|
||||
editor.#bitmapUrl = bitmapUrl;
|
||||
}
|
||||
|
@ -785,9 +832,11 @@ class StampEditor extends AnnotationEditor {
|
|||
editor.width = (rect[2] - rect[0]) / parentWidth;
|
||||
editor.height = (rect[3] - rect[1]) / parentHeight;
|
||||
|
||||
editor.annotationElementId = data.id || null;
|
||||
if (accessibilityData) {
|
||||
editor.altTextData = accessibilityData;
|
||||
}
|
||||
editor._initialData = initialData;
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
@ -798,6 +847,10 @@ class StampEditor extends AnnotationEditor {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (this.deleted) {
|
||||
return this.serializeDeleted();
|
||||
}
|
||||
|
||||
const serialized = {
|
||||
annotationType: AnnotationEditorType.STAMP,
|
||||
bitmapId: this.#bitmapId,
|
||||
|
@ -821,6 +874,20 @@ class StampEditor extends AnnotationEditor {
|
|||
if (!decorative && altText) {
|
||||
serialized.accessibilityData = { type: "Figure", alt: altText };
|
||||
}
|
||||
if (this.annotationElementId) {
|
||||
const changes = this.#hasElementChanged(serialized);
|
||||
if (changes.isSame) {
|
||||
// Nothing has been changed.
|
||||
return null;
|
||||
}
|
||||
if (changes.isSameAltText) {
|
||||
delete serialized.accessibilityData;
|
||||
} else {
|
||||
serialized.accessibilityData.structParent =
|
||||
this._initialData.structParent ?? -1;
|
||||
}
|
||||
}
|
||||
serialized.id = this.annotationElementId;
|
||||
|
||||
if (context === null) {
|
||||
return serialized;
|
||||
|
@ -848,6 +915,34 @@ class StampEditor extends AnnotationEditor {
|
|||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
#hasElementChanged(serialized) {
|
||||
const {
|
||||
rect,
|
||||
pageIndex,
|
||||
accessibilityData: { altText },
|
||||
} = this._initialData;
|
||||
|
||||
const isSameRect = serialized.rect.every(
|
||||
(x, i) => Math.abs(x - rect[i]) < 1
|
||||
);
|
||||
const isSamePageIndex = serialized.pageIndex === pageIndex;
|
||||
const isSameAltText = (serialized.accessibilityData?.alt || "") === altText;
|
||||
|
||||
return {
|
||||
isSame: isSameRect && isSamePageIndex && isSameAltText,
|
||||
isSameAltText,
|
||||
};
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
renderAnnotationElement(annotation) {
|
||||
annotation.updateEdited({
|
||||
rect: this.getRect(0, 0),
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { StampEditor };
|
||||
|
|
|
@ -41,7 +41,7 @@ class EditorToolbar {
|
|||
|
||||
render() {
|
||||
const editToolbar = (this.#toolbar = document.createElement("div"));
|
||||
editToolbar.className = "editToolbar";
|
||||
editToolbar.classList.add("editToolbar", "hidden");
|
||||
editToolbar.setAttribute("role", "toolbar");
|
||||
const signal = this.#editor._uiManager._signal;
|
||||
editToolbar.addEventListener("contextmenu", noContextMenu, { signal });
|
||||
|
|
|
@ -200,6 +200,27 @@ class ImageManager {
|
|||
return this.getFromUrl(data.url);
|
||||
}
|
||||
|
||||
getFromCanvas(id, canvas) {
|
||||
this.#cache ||= new Map();
|
||||
let data = this.#cache.get(id);
|
||||
if (data?.bitmap) {
|
||||
data.refCounter += 1;
|
||||
return data;
|
||||
}
|
||||
const offscreen = new OffscreenCanvas(canvas.width, canvas.height);
|
||||
const ctx = offscreen.getContext("2d");
|
||||
ctx.drawImage(canvas, 0, 0);
|
||||
data = {
|
||||
bitmap: offscreen.transferToImageBitmap(),
|
||||
id: `image_${this.#baseId}_${this.#id++}`,
|
||||
refCounter: 1,
|
||||
isSvg: false,
|
||||
};
|
||||
this.#cache.set(id, data);
|
||||
this.#cache.set(data.id, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
getSvgUrl(id) {
|
||||
const data = this.#cache.get(id);
|
||||
if (!data?.isSvg) {
|
||||
|
@ -218,6 +239,7 @@ class ImageManager {
|
|||
if (data.refCounter !== 0) {
|
||||
return;
|
||||
}
|
||||
data.bitmap.close?.();
|
||||
data.bitmap = null;
|
||||
}
|
||||
|
||||
|
@ -1619,7 +1641,8 @@ class AnnotationEditorUIManager {
|
|||
if (editor.annotationElementId === editId) {
|
||||
this.setSelected(editor);
|
||||
editor.enterInEditMode();
|
||||
break;
|
||||
} else {
|
||||
editor.unselect();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,10 @@ import {
|
|||
closePages,
|
||||
copy,
|
||||
copyToClipboard,
|
||||
dragAndDropAnnotation,
|
||||
getAnnotationSelector,
|
||||
getEditorDimensions,
|
||||
getEditors,
|
||||
getEditorSelector,
|
||||
getFirstSerialized,
|
||||
getRect,
|
||||
|
@ -1281,4 +1284,108 @@ describe("Stamp Editor", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stamp (move existing)", () => {
|
||||
let pages;
|
||||
|
||||
beforeAll(async () => {
|
||||
pages = await loadAndWait("stamps.pdf", getAnnotationSelector("25R"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must move an annotation", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await page.click(getAnnotationSelector("25R"), { count: 2 });
|
||||
await waitForSelectedEditor(page, getEditorSelector(0));
|
||||
|
||||
const editorIds = await getEditors(page, "stamp");
|
||||
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5);
|
||||
|
||||
// All the current annotations should be serialized as null objects
|
||||
// because they haven't been edited yet.
|
||||
const serialized = await getSerialized(page);
|
||||
expect(serialized).withContext(`In ${browserName}`).toEqual([]);
|
||||
|
||||
const editorRect = await page.$eval(getEditorSelector(0), el => {
|
||||
const { x, y, width, height } = el.getBoundingClientRect();
|
||||
return { x, y, width, height };
|
||||
});
|
||||
|
||||
// Select the annotation we want to move.
|
||||
await page.mouse.click(editorRect.x + 2, editorRect.y + 2);
|
||||
await waitForSelectedEditor(page, getEditorSelector(0));
|
||||
|
||||
await dragAndDropAnnotation(
|
||||
page,
|
||||
editorRect.x + editorRect.width / 2,
|
||||
editorRect.y + editorRect.height / 2,
|
||||
100,
|
||||
100
|
||||
);
|
||||
await waitForSerialized(page, 1);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stamp (change alt-text)", () => {
|
||||
let pages;
|
||||
|
||||
beforeAll(async () => {
|
||||
pages = await loadAndWait("stamps.pdf", getAnnotationSelector("58R"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must update an existing alt-text", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await page.click(getAnnotationSelector("58R"), { count: 2 });
|
||||
await waitForSelectedEditor(page, getEditorSelector(4));
|
||||
|
||||
const editorIds = await getEditors(page, "stamp");
|
||||
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5);
|
||||
|
||||
await page.click(`${getEditorSelector(4)} button.altText`);
|
||||
await page.waitForSelector("#altTextDialog", { visible: true });
|
||||
|
||||
const textareaSelector = "#altTextDialog textarea";
|
||||
await page.waitForFunction(
|
||||
sel => document.querySelector(sel).value !== "",
|
||||
{},
|
||||
textareaSelector
|
||||
);
|
||||
|
||||
const altText = await page.evaluate(
|
||||
sel => document.querySelector(sel).value,
|
||||
textareaSelector
|
||||
);
|
||||
expect(altText).toEqual("An elephant");
|
||||
|
||||
await page.evaluate(sel => {
|
||||
document.querySelector(sel).value = "";
|
||||
}, textareaSelector);
|
||||
|
||||
await page.click(textareaSelector);
|
||||
await page.type(textareaSelector, "Hello World");
|
||||
|
||||
// All the current annotations should be serialized as null objects
|
||||
// because they haven't been edited yet.
|
||||
const serialized = await getSerialized(page);
|
||||
expect(serialized).withContext(`In ${browserName}`).toEqual([]);
|
||||
|
||||
const saveButtonSelector = "#altTextDialog #altTextSave";
|
||||
await page.click(saveButtonSelector);
|
||||
|
||||
await waitForSerialized(page, 1);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -225,6 +225,10 @@ function getEditorSelector(n) {
|
|||
return `#pdfjs_internal_editor_${n}`;
|
||||
}
|
||||
|
||||
function getAnnotationSelector(id) {
|
||||
return `[data-annotation-id="${id}"]`;
|
||||
}
|
||||
|
||||
function getSelectedEditors(page) {
|
||||
return page.evaluate(() => {
|
||||
const elements = document.querySelectorAll(".selectedEditor");
|
||||
|
@ -769,6 +773,7 @@ export {
|
|||
createPromise,
|
||||
dragAndDropAnnotation,
|
||||
firstPageOnTop,
|
||||
getAnnotationSelector,
|
||||
getAnnotationStorage,
|
||||
getComputedStyleSelector,
|
||||
getEditorDimensions,
|
||||
|
|
|
@ -82,7 +82,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
#viewerContainer.pdfPresentationMode:fullscreen {
|
||||
#viewerContainer.pdfPresentationMode:fullscreen,
|
||||
.annotationEditorLayer.disabled {
|
||||
.noAltTextBadge {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/annotation_layer.js").AnnotationLayer} AnnotationLayer */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */
|
||||
|
||||
import { AnnotationEditorLayer } from "pdfjs-lib";
|
||||
import { GenericL10n } from "web-null_l10n";
|
||||
|
@ -32,6 +34,7 @@ import { GenericL10n } from "web-null_l10n";
|
|||
* @property {AnnotationEditorUIManager} [uiManager]
|
||||
* @property {PDFPageProxy} pdfPage
|
||||
* @property {IL10n} [l10n]
|
||||
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {AnnotationLayer} [annotationLayer]
|
||||
* @property {TextLayer} [textLayer]
|
||||
|
@ -46,6 +49,8 @@ class AnnotationEditorLayerBuilder {
|
|||
|
||||
#onAppend = null;
|
||||
|
||||
#structTreeLayer = null;
|
||||
|
||||
#textLayer = null;
|
||||
|
||||
#uiManager;
|
||||
|
@ -68,6 +73,7 @@ class AnnotationEditorLayerBuilder {
|
|||
this.#textLayer = options.textLayer || null;
|
||||
this.#drawLayer = options.drawLayer || null;
|
||||
this.#onAppend = options.onAppend || null;
|
||||
this.#structTreeLayer = options.structTreeLayer || null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,6 +106,7 @@ class AnnotationEditorLayerBuilder {
|
|||
this.annotationEditorLayer = new AnnotationEditorLayer({
|
||||
uiManager: this.#uiManager,
|
||||
div,
|
||||
structTreeLayer: this.#structTreeLayer,
|
||||
accessibilityManager: this.accessibilityManager,
|
||||
pageIndex: this.pdfPage.pageNumber - 1,
|
||||
l10n: this.l10n,
|
||||
|
|
|
@ -1098,12 +1098,10 @@ class PDFPageView {
|
|||
showCanvas?.(true);
|
||||
await this.#finishRenderTask(renderTask);
|
||||
|
||||
if (this.textLayer || this.annotationLayer) {
|
||||
this.structTreeLayer ||= new StructTreeLayerBuilder(
|
||||
pdfPage,
|
||||
viewport.rawDims
|
||||
);
|
||||
}
|
||||
this.structTreeLayer ||= new StructTreeLayerBuilder(
|
||||
pdfPage,
|
||||
viewport.rawDims
|
||||
);
|
||||
|
||||
this.#renderTextLayer();
|
||||
|
||||
|
@ -1126,6 +1124,7 @@ class PDFPageView {
|
|||
uiManager: annotationEditorUIManager,
|
||||
pdfPage,
|
||||
l10n,
|
||||
structTreeLayer: this.structTreeLayer,
|
||||
accessibilityManager: this._accessibilityManager,
|
||||
annotationLayer: this.annotationLayer?.annotationLayer,
|
||||
textLayer: this.textLayer,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue