diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 91e6666a524dc..73c59a242b483 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -243,6 +243,11 @@ "type": "boolean", "default": false }, + "enableEraser": { + "description": "Enable the eraser to erase annotations like Ink.", + "type": "boolean", + "default": false + }, "enableOptimizedPartialRendering": { "description": "Enable tracking of PDF operations to optimize partial rendering.", "type": "boolean", diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 1ef6453452fef..840ea69606c05 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -31,6 +31,7 @@ import { FeatureTest, } from "../../shared/util.js"; import { AnnotationEditor } from "./editor.js"; +import { EraserEditor } from "./eraser.js"; import { FreeTextEditor } from "./freetext.js"; import { HighlightEditor } from "./highlight.js"; import { InkEditor } from "./ink.js"; @@ -99,6 +100,7 @@ class AnnotationEditorLayer { static #editorTypes = new Map( [ + EraserEditor, FreeTextEditor, InkEditor, StampEditor, @@ -168,6 +170,7 @@ class AnnotationEditorLayer { */ updateMode(mode = this.#uiManager.getMode()) { this.#cleanup(); + this.#toogleEditorPointerEvents(true); switch (mode) { case AnnotationEditorType.NONE: this.div.classList.toggle("nonEditing", true); @@ -176,6 +179,15 @@ class AnnotationEditorLayer { this.toggleAnnotationLayerPointerEvents(true); this.disableClick(); return; + case AnnotationEditorType.ERASER: + this.#toogleEditorPointerEvents(false); + this.disableTextSelection(); + this.togglePointerEvents(true); + this.enableClick(); + this.addNewEditor({ + /* eraser */ + }); + break; case AnnotationEditorType.INK: this.disableTextSelection(); this.togglePointerEvents(true); @@ -251,6 +263,20 @@ class AnnotationEditorLayer { : this.#uiManager.getEditors(this.pageIndex); } + #toogleEditorPointerEvents(enabled = false) { + const value = enabled ? "" : "none"; + for (const editor of this.#editors.values()) { + editor.div.style.pointerEvents = value; + // for highlight editors, we must also set pointer-events + // of the clipped child. + for (const child of editor.div.children) { + if (child.className === "internal") { + child.style.pointerEvents = value; + } + } + } + } + /** * Enable pointer events on the main div in order to enable * editor creation. diff --git a/src/display/editor/draw.js b/src/display/editor/draw.js index 778b1d73f16f0..6f42eeef8aa49 100644 --- a/src/display/editor/draw.js +++ b/src/display/editor/draw.js @@ -91,6 +91,10 @@ class DrawingEditor extends AnnotationEditor { this._addOutlines(params); } + get _drawOutlines() { + return this.#drawOutlines; + } + /** @inheritdoc */ onUpdatedColor() { this._colorPicker?.update(this.color); diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 043873e6de2d4..a934f4803f8dc 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -101,6 +101,8 @@ class AnnotationEditor { _editToolbar = null; + _erasable = false; + _initialOptions = Object.create(null); _initialData = null; @@ -217,6 +219,10 @@ class AnnotationEditor { return Object.getPrototypeOf(this).constructor._editorType; } + get erasable() { + return this._erasable; + } + static get isDrawer() { return false; } @@ -508,6 +514,21 @@ class AnnotationEditor { this.#translate(this.parentDimensions, x, y); } + /** + * Erase everything in a radius of (x,y) position. + * @param {number} x + * @param {number} y + * @param {number} radius + */ + erase(x, y, radius) { + unreachable("Not implemented"); + } + + /** call once the erasing operation is done */ + endErase() { + unreachable("Not implemented"); + } + /** * Translate the editor position within its page and adjust the scroll * in order to have the editor in the view. diff --git a/src/display/editor/eraser.js b/src/display/editor/eraser.js new file mode 100644 index 0000000000000..7ded3fdb1ee58 --- /dev/null +++ b/src/display/editor/eraser.js @@ -0,0 +1,430 @@ +import { + AnnotationEditorParamsType, + AnnotationEditorType, +} from "../../shared/util.js"; +import { noContextMenu, stopEvent } from "../display_utils.js"; +import { AnnotationEditor } from "./editor.js"; +import { CurrentPointers } from "./tools.js"; + +class EraserEditor extends AnnotationEditor { + static #currentCursorAC = null; + + static #currentEraserAC = null; + + #erasableEditors = []; + + static _defaultThickness = 20; + + static _thickness; + + static _type = "eraser"; + + static _editorType = AnnotationEditorType.ERASER; + + #cursor = null; + + #isErasing = false; + + constructor(params) { + super({ ...params, name: "eraserEditor" }); + this.defaultL10nId = "pdfjs-editor-eraser-editor"; + EraserEditor._thickness = + params.thickness || + EraserEditor._thickness || + EraserEditor._defaultThickness; + } + + /** @inheritdoc */ + static initialize(l10n, uiManager) { + AnnotationEditor.initialize(l10n, uiManager); + } + + /** @inheritdoc */ + static updateDefaultParams(type, value) { + switch (type) { + case AnnotationEditorParamsType.ERASER_THICKNESS: + EraserEditor._defaultThickness = value; + EraserEditor._thickness = value; + break; + } + } + + /** @inheritdoc */ + updateParams(type, value) { + switch (type) { + case AnnotationEditorParamsType.ERASER_THICKNESS: + this.updateThickness(value); + break; + } + } + + static get defaultPropertiesToUpdate() { + return [ + [ + AnnotationEditorParamsType.ERASER_THICKNESS, + EraserEditor._defaultThickness, + ], + ]; + } + + /** @inheritdoc */ + get propertiesToUpdate() { + return [ + [ + AnnotationEditorParamsType.ERASER_THICKNESS, + EraserEditor._thickness || EraserEditor._defaultThickness, + ], + ]; + } + + /** @inheritdoc */ + render() { + if (this.div) { + return this.div; + } + + const div = super.render(); + this.fixAndSetPosition(); + this.#erasableEditors = this.#getErasableEditors(); + this.enableEditing(); + return div; + } + + /** Ensures EraserEditor spans the entire AnnotationEditorLayer */ + fixAndSetPosition() { + this.x = 0; + this.y = 0; + this.width = 1; + this.height = 1; + + const [parentWidth, parentHeight] = this.parentDimensions; + this.setDims(parentWidth, parentHeight); + + return super.fixAndSetPosition(0); + } + + /** @inheritdoc */ + enableEditing() { + super.enableEditing(); + this.div?.classList.toggle("disabled", false); + + if (this.#cursor) { + this.#cursor.remove(); + this.#cursor = null; + } + + if (this.div) { + this.div.style.pointerEvents = "auto"; + this.div.style.zIndex = "1000"; + + this.#cursor = document.createElement("div"); + this.#cursor.className = "eraserCursor"; + this.#updateCursor(); + this.#cursor.style.display = "none"; + this.#cursor.style.pointerEvents = "none"; + this.div.append(this.#cursor); + + const ac = (EraserEditor.#currentCursorAC = new AbortController()); + const signal = this.parent.combinedSignal(ac); + + this.div.addEventListener("pointermove", this.#moveCursor.bind(this), { + signal, + }); + this.div.addEventListener( + "pointerenter", + this.#displayCursor.bind(this), + { signal } + ); + this.div.addEventListener("pointerleave", this.#hideCursor.bind(this), { + signal, + }); + this.div.addEventListener( + "pointerdown", + this.#startEraseSession.bind(this), + { signal } + ); + } + } + + /** @inheritdoc */ + disableEditing() { + super.disableEditing(); + this.div?.classList.toggle("disabled", true); + + this.#abortEraseSession(); + this.#abortCursor(); + } + + /** @inheritdoc */ + remove() { + super.remove(); + + this.#abortEraseSession(); + this.#abortCursor(); + } + + updateThickness(thickness) { + const setThickness = th => { + EraserEditor._thickness = th; + this.#updateCursor(); + }; + + const savedThickness = EraserEditor._thickness; + + this.addCommands({ + cmd: setThickness.bind(this, thickness), + undo: setThickness.bind(this, savedThickness), + post: this._uiManager.updateUI.bind(this._uiManager, this), + mustExec: true, + type: AnnotationEditorParamsType.ERASER_THICKNESS, + overwriteIfSameType: true, + keepUndo: true, + }); + } + + isEmpty() { + return true; + } + + #startEraseSession(event) { + if (event.button && event.button !== 0) { + return; + } + + this.#moveCursor(event); + + const { pointerId, pointerType, target } = event; + if (CurrentPointers.isInitializedAndDifferentPointerType(pointerType)) { + return; + } + CurrentPointers.setPointer(pointerType, pointerId); + + const ac = (EraserEditor.#currentEraserAC = new AbortController()); + const signal = this.parent.combinedSignal(ac); + + window.addEventListener( + "pointerup", + e => { + if (CurrentPointers.isSamePointerIdOrRemove(e.pointerId)) { + this.#endErase(e); + } + }, + { signal } + ); + + window.addEventListener( + "pointercancel", + e => { + if (CurrentPointers.isSamePointerIdOrRemove(e.pointerId)) { + this.#endErase(e); + } + }, + { signal } + ); + + window.addEventListener( + "pointerdown", + e => { + if (!CurrentPointers.isSamePointerType(pointerType)) { + return; + } + + // Multi-pointer of same type (e.g., two fingers) -> stop erasing + CurrentPointers.initializeAndAddPointerId(e.pointerId); + if (this.#isErasing) { + this.#endErase(null); + } + }, + { capture: true, passive: false, signal } + ); + + window.addEventListener("contextmenu", noContextMenu, { signal }); + + target.addEventListener("pointermove", this.#onPointerMove.bind(this), { + signal, + }); + + // Prevent touch scroll when the move is used for erasing + target.addEventListener( + "touchmove", + e => { + if (CurrentPointers.isSameTimeStamp(e.timeStamp)) { + stopEvent(e); + } + }, + { signal } + ); + + this.#isErasing = true; + this.#erase(event.clientX, event.clientY); + stopEvent(event); + } + + #onPointerMove(event) { + CurrentPointers.clearTimeStamp(); + + if (!this.#isErasing) { + return; + } + + const { pointerId } = event; + + if (!CurrentPointers.isSamePointerId(pointerId)) { + return; + } + if (CurrentPointers.isUsingMultiplePointers()) { + // The user is using multiple fingers and the first one is moving. + this.#endErase(event); + return; + } + + this.#erase(event.clientX, event.clientY); + + // We track the timestamp to know if the touchmove event is used to draw. + CurrentPointers.setTimeStamp(event.timeStamp); + + stopEvent(event); + } + + #endErase(event) { + if (event) { + this.#erase(event.clientX, event.clientY); + } + this.#commit(); + this.#abortEraseSession(); + } + + #commit() { + const cmds = [], + undos = []; + for (const editor of this.#erasableEditors) { + const { cmd, undo } = editor.endErase(); + if (cmd && undo) { + cmds.push(cmd); + undos.push(undo); + } + } + + this.parent.addCommands({ + cmd: () => cmds.forEach(f => f()), + undo: () => undos.forEach(f => f()), + mustExec: false, + type: AnnotationEditorParamsType.ERASER_STEP, + }); + } + + #abortEraseSession() { + if (EraserEditor.#currentEraserAC) { + EraserEditor.#currentEraserAC.abort(); + EraserEditor.#currentEraserAC = null; + } + CurrentPointers.clearPointerIds(); + CurrentPointers.clearTimeStamp(); + this.#isErasing = false; + } + + #abortCursor() { + if (EraserEditor.#currentCursorAC) { + EraserEditor.#currentCursorAC.abort(); + EraserEditor.#currentCursorAC = null; + } + + if (this.#cursor) { + this.#cursor.remove(); + this.#cursor = null; + } + + if (this.div) { + this.div.style.pointerEvents = ""; + this.div.style.zIndex = ""; + } + } + + #updateCursor() { + if (this.#cursor) { + this.#cursor.style.width = `${EraserEditor._thickness}px`; + this.#cursor.style.height = `${EraserEditor._thickness}px`; + } + } + + #displayCursor(event) { + this.#updateCursor(); + this.#moveCursor(event); + } + + #moveCursor(event) { + if (!this.#cursor) { + return; + } + + if ( + CurrentPointers.isInitializedAndDifferentPointerType(event.pointerType) + ) { + this.#hideCursor(); + return; + } + + const rect = this.parent.div.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + this.#cursor.style.left = `${x - EraserEditor._thickness / 2}px`; + this.#cursor.style.top = `${y - EraserEditor._thickness / 2}px`; + + this.#showCursor(); + } + + #showCursor() { + this.#cursor.style.display = "block"; + } + + #hideCursor() { + this.#cursor.style.display = "none"; + } + + #getErasableEditors() { + const editors = + Array.from(this._uiManager.getEditors(this.pageIndex)) || []; + return editors.filter(ed => ed.erasable && ed?.parent?.div && ed?.div); + } + + #erase(clientX, clientY) { + const layerRect = this.parent.div.getBoundingClientRect(); + const x = clientX - layerRect.left; + const y = clientY - layerRect.top; + const radius = EraserEditor._thickness / 2; + + for (const editor of this.#erasableEditors) { + if (!editor?.parent?.div || !editor?.div) { + continue; + } + + const pdfRect = editor.getRect(0, 0, editor.rotation); + const [, pageHeight] = editor.pageDimensions; + const [pageX, pageY] = editor.pageTranslation; + const [cx, cy, cw, ch] = editor.getRectInCurrentCoords( + pdfRect, + pageHeight + ); + const scale = editor.parentScale; + const left = (cx - pageX) * scale; + const top = (cy + pageY) * scale; + const right = left + cw * scale; + const bottom = top + ch * scale; + if (this.#hitBBox(x, y, radius, [left, top, right, bottom])) { + editor.erase(x, y, radius); + } + } + } + + #hitBBox(x, y, r, rect) { + const [left, top, right, bottom] = rect; + const cx = Math.max(left, Math.min(x, right)); + const cy = Math.max(top, Math.min(y, bottom)); + const dx = x - cx; + const dy = y - cy; + return dx * dx + dy * dy <= r * r; + } +} + +export { EraserEditor }; diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index ad741404818ba..63c6cf7688848 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -60,6 +60,10 @@ class InkDrawingOptions extends DrawingOptions { * Basic draw editor in order to generate an Ink annotation. */ class InkEditor extends DrawingEditor { + #points = null; + + #erased = false; + static _type = "ink"; static _editorType = AnnotationEditorType.INK; @@ -68,6 +72,7 @@ class InkEditor extends DrawingEditor { constructor(params) { super({ ...params, name: "inkEditor" }); + this._erasable = true; this._willKeepAspectRatio = true; this.defaultL10nId = "pdfjs-editor-ink-editor"; } @@ -314,6 +319,158 @@ class InkEditor extends DrawingEditor { return null; } + + /** + * Erase everything in a radius of (x,y) position. + * @param {number} x + * @param {number} y + * @param {number} radius + */ + erase(x, y, radius) { + this.#points ||= this.serializeDraw(false).points; + + const radius2 = radius * radius; + const newPaths = []; + let modified = false; + + for (const path of this.#points) { + if (path.length === 0) { + continue; + } + let newPath = []; + for (let i = 0; i < path.length; i += 2) { + const [lx, ly] = this.#pagePointToLayer(path[i], path[i + 1]); + const dx = lx - x; + const dy = ly - y; + const dist = dx * dx + dy * dy; + if (dist >= radius2) { + newPath.push(path[i], path[i + 1]); + } else { + modified = true; + if (newPath.length >= 4) { + newPaths.push(new Float32Array(newPath)); + } + newPath = []; + } + } + if (newPath.length >= 4) { + newPaths.push(new Float32Array(newPath)); + } + } + + if (modified) { + this.#points = newPaths; + this.#erased = true; + // remove svg path if no points are left + if (newPaths.length === 0) { + this.parent.drawLayer.updateProperties(this._drawId, { + path: { d: "" }, + }); + } else { + const tempOutline = this.#deserializePoints(); + this.parent.drawLayer.updateProperties(this._drawId, { + path: { d: tempOutline.toSVGPath() }, + }); + } + } + } + + endErase() { + // if nothing has been erased + if (!this.#erased) { + return {}; + } + + // reset erased flag + this.#erased = false; + const oldOutline = this._drawOutlines; + const drawingOptions = { ...this._drawingOptions }; + const undo = () => { + this._addOutlines({ + drawOutlines: oldOutline, + drawId: this._drawId, + drawingOptions, + }); + }; + + if (this.#points.length === 0) { + this.remove(); + return { cmd: () => this.remove(), undo }; + } + + const newOutlines = this.#deserializePoints(); + const cmd = () => + this._addOutlines({ + drawOutlines: newOutlines, + drawId: this._drawId, + drawingOptions, + }); + cmd(); + + this.#points = null; + + return { cmd, undo }; + } + + #deserializePoints() { + const { + viewport: { + rawDims: { pageWidth, pageHeight, pageX, pageY }, + }, + } = this.parent; + + const thickness = this._drawingOptions["stroke-width"]; + const rotation = this.rotation; + + const newOutline = InkEditor.deserializeDraw( + pageX, + pageY, + pageWidth, + pageHeight, + InkEditor._INNER_MARGIN, + { + paths: { points: this.#points }, + rotation, + thickness, + } + ); + + return newOutline; + } + + #pagePointToLayer(px, py) { + const [pageX, pageY] = this.pageTranslation; + const [pageW, pageH] = this.pageDimensions; + const { width: layerW, height: layerH } = + this.parent.div.getBoundingClientRect(); + + const nx = (px - pageX) / pageW; + const ny = (py - pageY) / pageH; + + let rx, ry; + switch ((this.rotation || 0) % 360) { + case 90: + rx = ny; + ry = 1 - nx; + break; + case 180: + rx = 1 - nx; + ry = 1 - ny; + break; + case 270: + rx = 1 - ny; + ry = nx; + break; + default: + rx = nx; + ry = ny; + break; + } + + const lx = rx * layerW; + const ly = (1 - ry) * layerH; + return [lx, ly]; + } } export { InkDrawingOptions, InkEditor }; diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 84d2f23c244e2..1b38171a09a56 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -1931,6 +1931,8 @@ class AnnotationEditorUIManager { * edit mode. * @param {boolean} [editComment] - true if the mode change is due to a * comment edit. + * @param {boolean} [isFromEvent] - true if the mode change is due to an event + * (e.g. toolbar button clicked). */ async updateMode( mode, @@ -1938,7 +1940,8 @@ class AnnotationEditorUIManager { isFromUser = false, isFromKeyboard = false, mustEnterInEditMode = false, - editComment = false + editComment = false, + isFromEvent = false ) { if (this.#mode === mode) { return; @@ -1982,6 +1985,10 @@ class AnnotationEditorUIManager { if (mode === AnnotationEditorType.SIGNATURE) { await this.#signatureManager?.loadSignatures(); } + if (isFromEvent) { + // reinitialize the pointer type when mode changed by an event + CurrentPointers.clearPointerType(); + } if (isFromUser) { // reinitialize the pointer type when the mode is changed by the user diff --git a/src/shared/util.js b/src/shared/util.js index b50819976f854..b12901932dcdc 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -76,6 +76,7 @@ const AnnotationEditorType = { STAMP: 13, INK: 15, POPUP: 16, + ERASER: 20, SIGNATURE: 101, COMMENT: 102, }; @@ -89,11 +90,13 @@ const AnnotationEditorParamsType = { INK_COLOR: 21, INK_THICKNESS: 22, INK_OPACITY: 23, + ERASER_THICKNESS: 25, HIGHLIGHT_COLOR: 31, HIGHLIGHT_THICKNESS: 32, HIGHLIGHT_FREE: 33, HIGHLIGHT_SHOW_ALL: 34, DRAW_STEP: 41, + ERASER_STEP: 42, }; // Permission flags from Table 22, Section 7.6.3.2 of the PDF specification. diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index e7d80a7979578..3fd7ed906fb9c 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -43,6 +43,7 @@ ); --editorFreeText-editing-cursor: text; --editorInk-editing-cursor: url(images/cursor-editorInk.svg) 0 16, pointer; + --editorEraser-thickness: 20px; --editorHighlight-editing-cursor: url(images/cursor-editorTextHighlight.svg) 24 24, text; --editorFreeHighlight-editing-cursor: @@ -172,6 +173,26 @@ box-sizing: border-box; } +.annotationEditorLayer.eraserEditing { + cursor: none; +} + +.annotationEditorLayer.eraserEditing.editToolbar { + display: none; +} + +.eraserCursor { + position: absolute; + display: none; + width: var(--editorEraser-thickness); + height: var(--editorEraser-thickness); + background-color: transparent; + border: 1px solid black; + border-radius: 50%; + pointer-events: none; + transition: transform 0.05s ease-out; +} + .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .signatureEditor) { position: absolute; diff --git a/web/annotation_editor_params.js b/web/annotation_editor_params.js index 1e4fc431db954..323a9d67c651b 100644 --- a/web/annotation_editor_params.js +++ b/web/annotation_editor_params.js @@ -24,6 +24,7 @@ import { AnnotationEditorParamsType } from "pdfjs-lib"; * @property {HTMLInputElement} editorInkColor * @property {HTMLInputElement} editorInkThickness * @property {HTMLInputElement} editorInkOpacity + * @property {HTMLInputElement} editorEraserThickness * @property {HTMLButtonElement} editorStampAddImage * @property {HTMLInputElement} editorFreeHighlightThickness * @property {HTMLButtonElement} editorHighlightShowAll @@ -49,6 +50,7 @@ class AnnotationEditorParams { editorInkColor, editorInkThickness, editorInkOpacity, + editorEraserThickness, editorStampAddImage, editorFreeHighlightThickness, editorHighlightShowAll, @@ -78,6 +80,9 @@ class AnnotationEditorParams { editorInkOpacity.addEventListener("input", function () { dispatchEvent("INK_OPACITY", this.valueAsNumber); }); + editorEraserThickness.addEventListener("input", function () { + dispatchEvent("ERASER_THICKNESS", this.valueAsNumber); + }); editorStampAddImage.addEventListener("click", () => { eventBus.dispatch("reporttelemetry", { source: this, @@ -118,6 +123,9 @@ class AnnotationEditorParams { case AnnotationEditorParamsType.INK_OPACITY: editorInkOpacity.value = value; break; + case AnnotationEditorParamsType.ERASER_THICKNESS: + editorEraserThickness.value = value; + break; case AnnotationEditorParamsType.HIGHLIGHT_COLOR: eventBus.dispatch("mainhighlightcolorpickerupdatecolor", { source: this, diff --git a/web/app.js b/web/app.js index 3d6b122f0b1ca..5e73b1ef75ac2 100644 --- a/web/app.js +++ b/web/app.js @@ -694,6 +694,10 @@ const PDFViewerApplication = { } } + if (!AppOptions.get("enableEraser")) { + appConfig.toolbar?.editorEraserButton?.parentElement.remove(); + } + if (appConfig.secondaryToolbar) { if (AppOptions.get("enableAltText")) { appConfig.secondaryToolbar.imageAltTextSettingsButton?.classList.remove( diff --git a/web/app_options.js b/web/app_options.js index 0238e78a6a27f..ce1dc36453ed0 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -236,6 +236,11 @@ const defaultOptions = { value: true, kind: OptionKind.VIEWER, }, + enableEraser: { + /** @type {boolean} */ + value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, enableGuessAltText: { /** @type {boolean} */ value: true, diff --git a/web/images/toolbarButton-editorEraser.svg b/web/images/toolbarButton-editorEraser.svg new file mode 100644 index 0000000000000..8bf602c09c479 --- /dev/null +++ b/web/images/toolbarButton-editorEraser.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 2a96a552ff496..78a55b552bf89 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -2504,7 +2504,8 @@ class PDFViewer { /* isFromUser = */ true, isFromKeyboard, mustEnterInEditMode, - editComment + editComment, + true ); if ( mode !== this.#annotationEditorMode || diff --git a/web/toolbar.js b/web/toolbar.js index 95639af9a4f23..9ceb302ad0887 100644 --- a/web/toolbar.js +++ b/web/toolbar.js @@ -115,6 +115,18 @@ class Toolbar { }, }, }, + { + element: options.editorEraserButton, + eventName: "switchannotationeditormode", + eventDetails: { + get mode() { + const { classList } = options.editorEraserButton; + return classList.contains("toggled") + ? AnnotationEditorType.NONE + : AnnotationEditorType.ERASER; + }, + }, + }, { element: options.editorStampButton, eventName: "switchannotationeditormode", @@ -290,6 +302,8 @@ class Toolbar { #editorModeChanged({ mode }) { const { + editorEraserButton, + editorEraserParamsToolbar, editorCommentButton, editorCommentParamsToolbar, editorFreeTextButton, @@ -309,6 +323,11 @@ class Toolbar { mode === AnnotationEditorType.POPUP, editorCommentParamsToolbar ); + toggleExpandedBtn( + editorEraserButton, + mode === AnnotationEditorType.ERASER, + editorEraserParamsToolbar + ); toggleExpandedBtn( editorFreeTextButton, mode === AnnotationEditorType.FREETEXT, @@ -336,6 +355,7 @@ class Toolbar { ); editorCommentButton.disabled = + editorEraserButton.disabled = editorFreeTextButton.disabled = editorHighlightButton.disabled = editorInkButton.disabled = diff --git a/web/viewer.css b/web/viewer.css index 896b91a5aaba2..74769d40e4af8 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -103,6 +103,7 @@ --toolbarButton-editorFreeText-icon: url(images/toolbarButton-editorFreeText.svg); --toolbarButton-editorHighlight-icon: url(images/toolbarButton-editorHighlight.svg); --toolbarButton-editorInk-icon: url(images/toolbarButton-editorInk.svg); + --toolbarButton-editorEraser-icon: url(images/toolbarButton-editorEraser.svg); --toolbarButton-editorStamp-icon: url(images/toolbarButton-editorStamp.svg); --toolbarButton-editorSignature-icon: url(images/toolbarButton-editorSignature.svg); --toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg); @@ -542,6 +543,10 @@ body { mask-image: var(--toolbarButton-zoomIn-icon); } +#editorEraserButton::before { + mask-image: var(--toolbarButton-editorEraser-icon); +} + #editorCommentButton::before { mask-image: var(--toolbarButton-editorComment-icon); transform: scaleX(var(--dir-factor)); diff --git a/web/viewer.html b/web/viewer.html index e373314dfcf90..faaefeea69ee7 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -493,6 +493,30 @@ +
+ + +