From 68451fe17e4db5bec6ae44ad4cd3bb6a04eef4c1 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 12 Feb 2025 19:29:16 +0100 Subject: [PATCH] [Editor] Populate the 'Add signature' menu with the saved signatures (bug 1947828) --- l10n/en-US/viewer.ftl | 12 ++ src/display/editor/annotation_editor_layer.js | 8 +- src/display/editor/drawers/signaturedraw.js | 2 +- src/display/editor/signature.js | 62 +++++- src/display/editor/tools.js | 5 +- web/app.js | 7 +- web/dialog.css | 4 - web/firefoxcom.js | 14 +- web/generic_signature_storage.js | 23 ++- web/message_bar.css | 5 - web/pdf_viewer.css | 11 +- web/pdf_viewer.js | 20 +- web/signature_manager.css | 176 ++++++++++++++++- web/signature_manager.js | 182 +++++++++++++++++- web/viewer.html | 10 +- 15 files changed, 474 insertions(+), 67 deletions(-) diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index bd2c7b0a5..c448db888 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -320,6 +320,9 @@ pdfjs-highlight-floating-button1 = .title = Highlight .aria-label = Highlight pdfjs-highlight-floating-button-label = Highlight +pdfjs-editor-signature-button = + .title = Add signature +pdfjs-editor-signature-button-label = Add signature ## Remove button for the various kind of editor. @@ -349,6 +352,9 @@ pdfjs-editor-stamp-add-image-button-label = Add image pdfjs-editor-free-highlight-thickness-input = Thickness pdfjs-editor-free-highlight-thickness-title = .title = Change thickness when highlighting items other than text +pdfjs-editor-signature-add-signature-button = + .title = Add new signature +pdfjs-editor-signature-add-signature-button-label = Add new signature # .default-content is used as a placeholder in an empty text editor. pdfjs-free-text2 = @@ -585,3 +591,9 @@ pdfjs-editor-add-signature-error-close-button = Close pdfjs-editor-add-signature-cancel-button = Cancel pdfjs-editor-add-signature-add-button = Add + +## Main menu for adding/removing signatures + +pdfjs-editor-delete-signature-button = + .title = Remove signature +pdfjs-editor-delete-signature-button-label = Remove signature diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index ee0377935..2dbcfa13b 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -698,8 +698,12 @@ class AnnotationEditorLayer { /** * Create and add a new editor. */ - addNewEditor() { - this.createAndAddNewEditor(this.#getCenterPoint(), /* isCentered = */ true); + addNewEditor(data = {}) { + this.createAndAddNewEditor( + this.#getCenterPoint(), + /* isCentered = */ true, + data + ); } /** diff --git a/src/display/editor/drawers/signaturedraw.js b/src/display/editor/drawers/signaturedraw.js index d148ea524..5fa0e864b 100644 --- a/src/display/editor/drawers/signaturedraw.js +++ b/src/display/editor/drawers/signaturedraw.js @@ -784,7 +784,7 @@ class SignatureExtractor { let data = null; let offset = 0; for await (const chunk of readable) { - data ||= new Uint8Array(new Uint32Array(chunk.buffer)[0]); + data ||= new Uint8Array(new Uint32Array(chunk.buffer, 0, 4)[0]); data.set(chunk, offset); offset += chunk.length; } diff --git a/src/display/editor/signature.js b/src/display/editor/signature.js index 512a22b3f..fdf894489 100644 --- a/src/display/editor/signature.js +++ b/src/display/editor/signature.js @@ -26,7 +26,7 @@ class SignatureOptions extends DrawingOptions { super(); super.updateProperties({ - fill: "black", + fill: "CanvasText", "stroke-width": 0, }); } @@ -43,7 +43,7 @@ class DrawnSignatureOptions extends InkDrawingOptions { super(viewerParameters); super.updateProperties({ - stroke: "black", + stroke: "CanvasText", "stroke-width": 1, }); } @@ -62,6 +62,12 @@ class DrawnSignatureOptions extends InkDrawingOptions { class SignatureEditor extends DrawingEditor { #isExtracted = false; + #signatureData = null; + + #description = null; + + #signatureUUID = null; + static _type = "signature"; static _editorType = AnnotationEditorType.SIGNATURE; @@ -71,8 +77,7 @@ class SignatureEditor extends DrawingEditor { constructor(params) { super({ ...params, mustBeCommitted: true, name: "signatureEditor" }); this._willKeepAspectRatio = true; - this._description = ""; - this._signatureUUID = null; + this.#signatureData = params.signatureData || null; } /** @inheritdoc */ @@ -128,17 +133,52 @@ class SignatureEditor extends DrawingEditor { this.div.setAttribute("role", "figure"); if (this._drawId === null) { - this.div.hidden = true; - this._uiManager.getSignature(this); + if (this.#signatureData) { + const { + lines, + mustSmooth, + areContours, + description, + uuid, + heightInPage, + } = this.#signatureData; + const { + rawDims: { pageWidth, pageHeight }, + rotation, + } = this.parent.viewport; + const outline = SignatureExtractor.processDrawnLines({ + lines, + pageWidth, + pageHeight, + rotation, + innerMargin: SignatureEditor._INNER_MARGIN, + mustSmooth, + areContours, + }); + this.#signatureData = null; + this.#signatureUUID = uuid; + this.addSignature(outline.outline, heightInPage, description); + } else { + this.div.hidden = true; + this._uiManager.getSignature(this); + } } return this.div; } + setUuid(uuid) { + this.#signatureUUID = uuid; + } + + setDescription(description) { + this.#description = description; + } + addSignature(outline, heightInPage, description) { const { x: savedX, y: savedY } = this; this.#isExtracted = outline instanceof ContourDrawOutline; - this._description = description; + this.#description = description; let drawingOptions; if (this.#isExtracted) { drawingOptions = SignatureEditor.getDefaultDrawingOptions(); @@ -251,11 +291,12 @@ class SignatureEditor extends DrawingEditor { }; if (isForCopying) { serialized.paths = { lines, points }; + serialized.uuid = this.#signatureUUID; } else { serialized.lines = lines; } - if (this._description) { - serialized.accessibilityData = { type: "Figure", alt: this._description }; + if (this.#description) { + serialized.accessibilityData = { type: "Figure", alt: this.#description }; } return serialized; } @@ -294,7 +335,8 @@ class SignatureEditor extends DrawingEditor { static async deserialize(data, parent, uiManager) { const editor = await super.deserialize(data, parent, uiManager); editor.#isExtracted = data.areContours; - editor._description = data.accessibilityData?.alt || ""; + editor.#description = data.accessibilityData?.alt || ""; + editor.#signatureUUID = data.uuid; return editor; } } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 389e7efce..cb9310e60 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -1698,6 +1698,9 @@ class AnnotationEditorUIManager { this.#updateModeCapability.resolve(); return; } + if (mode === AnnotationEditorType.SIGNATURE) { + await this.#signatureManager?.loadSignatures(); + } this.setEditingState(true); await this.#enableAll(); this.unselectAll(); @@ -1758,7 +1761,7 @@ class AnnotationEditorUIManager { switch (type) { case AnnotationEditorParamsType.CREATE: - this.currentLayer.addNewEditor(); + this.currentLayer.addNewEditor(value); return; case AnnotationEditorParamsType.HIGHLIGHT_DEFAULT_COLOR: this.#mainHighlightColorPicker?.updateColor(value); diff --git a/web/app.js b/web/app.js index 4d105c04f..394425a4a 100644 --- a/web/app.js +++ b/web/app.js @@ -465,9 +465,12 @@ const PDFViewerApplication = { AppOptions.get("enableSignatureEditor") && appConfig.addSignatureDialog ? new SignatureManager( appConfig.addSignatureDialog, + appConfig.annotationEditorParams?.editorSignatureAddSignature || + null, this.overlayManager, - this.l10n, - externalServices.createSignatureStorage() + l10n, + externalServices.createSignatureStorage(), + eventBus ) : null; diff --git a/web/dialog.css b/web/dialog.css index c91b26966..0170f9cb2 100644 --- a/web/dialog.css +++ b/web/dialog.css @@ -20,8 +20,6 @@ --text-primary-color: #15141a; --text-secondary-color: #5b5b66; --hover-filter: brightness(0.9); - --focus-ring-color: #0060df; - --focus-ring-outline: 2px solid var(--focus-ring-color); --link-fg-color: #0060df; --link-hover-fg-color: #0250bb; --separator-color: #f0f0f4; @@ -58,7 +56,6 @@ --dialog-shadow: 0 2px 14px 0 #15141a; --text-primary-color: #fbfbfe; --text-secondary-color: #cfcfd8; - --focus-ring-color: #0df; --hover-filter: brightness(1.4); --link-fg-color: #0df; --link-hover-fg-color: #80ebff; @@ -82,7 +79,6 @@ --text-primary-color: CanvasText; --text-secondary-color: CanvasText; --hover-filter: none; - --focus-ring-color: ButtonBorder; --link-fg-color: LinkText; --link-hover-fg-color: LinkText; --separator-color: CanvasText; diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 983fcceed..536172a5c 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -504,11 +504,11 @@ class SignatureStorage { async getAll() { if (!this.#signatures) { - this.#signatures = Object.create(null); + this.#signatures = new Map(); const data = await this.#handleSignature({ action: "get" }); if (data) { for (const { uuid, description, signatureData } of data) { - this.#signatures[uuid] = { description, signatureData }; + this.#signatures.set(uuid, { description, signatureData }); } } } @@ -517,7 +517,7 @@ class SignatureStorage { async isFull() { // We want to store at most 5 signatures. - return Object.keys(await this.getAll()).length === 5; + return (await this.getAll()).size === 5; } async create(data) { @@ -531,17 +531,17 @@ class SignatureStorage { if (!uuid) { return null; } - this.#signatures[uuid] = data; + this.#signatures.set(uuid, data); return uuid; } async delete(uuid) { const signatures = await this.getAll(); - if (!signatures[uuid]) { + if (!signatures.has(uuid)) { return false; } if (await this.#handleSignature({ action: "delete", uuid })) { - delete signatures[uuid]; + signatures.delete(uuid); return true; } return false; @@ -549,7 +549,7 @@ class SignatureStorage { async update(uuid, data) { const signatures = await this.getAll(); - const oldData = signatures[uuid]; + const oldData = signatures.get(uuid); if (!oldData) { return false; } diff --git a/web/generic_signature_storage.js b/web/generic_signature_storage.js index 443231162..bfd11ac0e 100644 --- a/web/generic_signature_storage.js +++ b/web/generic_signature_storage.js @@ -23,19 +23,28 @@ class SignatureStorage { #signatures = null; #save() { - localStorage.setItem("pdfjs.signature", JSON.stringify(this.#signatures)); + localStorage.setItem( + "pdfjs.signature", + JSON.stringify(Object.fromEntries(this.#signatures.entries())) + ); } async getAll() { if (!this.#signatures) { + this.#signatures = new Map(); const data = localStorage.getItem("pdfjs.signature"); - this.#signatures = data ? JSON.parse(data) : Object.create(null); + if (data) { + for (const [key, value] of Object.entries(JSON.parse(data))) { + this.#signatures.set(key, value); + } + } } return this.#signatures; } async isFull() { - return Object.keys(await this.getAll()).length === 5; + // Only allow 5 signatures to be saved. + return (await this.getAll()).size === 5; } async create(data) { @@ -43,7 +52,7 @@ class SignatureStorage { return null; } const uuid = getUuid(); - this.#signatures[uuid] = data; + this.#signatures.set(uuid, data); this.#save(); return uuid; @@ -51,10 +60,10 @@ class SignatureStorage { async delete(uuid) { const signatures = await this.getAll(); - if (!signatures[uuid]) { + if (!signatures.has(uuid)) { return false; } - delete signatures[uuid]; + signatures.delete(uuid); this.#save(); return true; @@ -62,7 +71,7 @@ class SignatureStorage { async update(uuid, data) { const signatures = await this.getAll(); - const oldData = signatures[uuid]; + const oldData = signatures.get(uuid); if (!oldData) { return false; } diff --git a/web/message_bar.css b/web/message_bar.css index 695168c77..9cc647b3f 100644 --- a/web/message_bar.css +++ b/web/message_bar.css @@ -143,9 +143,6 @@ --undo-button-fg-color-hover: var(--undo-button-fg-color); --undo-button-fg-color-active: var(--undo-button-fg-color); - --focus-ring-color: #0060df; - --focus-ring-outline: 2px solid var(--focus-ring-color); - @media (prefers-color-scheme: dark) { --text-primary-color: #fbfbfe; @@ -172,8 +169,6 @@ --undo-button-fg-color: ButtonFace; --undo-button-fg-color-hover: SelectedItemText; --undo-button-fg-color-active: SelectedItemText; - - --focus-ring-color: CanvasText; } position: fixed; diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index 4599f0795..d12767910 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -27,14 +27,19 @@ --page-border: 9px solid transparent; --spreadHorizontalWrapped-margin-LR: -3.5px; --loading-icon-delay: 400ms; -} + --focus-ring-color: #0060df; + --focus-ring-outline: 2px solid var(--focus-ring-color); -@media screen and (forced-colors: active) { - :root { + @media (prefers-color-scheme: dark) { + --focus-ring-color: #0df; + } + + @media screen and (forced-colors: active) { --pdfViewer-padding-bottom: 9px; --page-margin: 8px auto -1px; --page-border: 1px solid CanvasText; --spreadHorizontalWrapped-margin-LR: 3.5px; + --focus-ring-color: CanvasText; } } diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index c0cdd8752..a52e81fda 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -933,9 +933,7 @@ class PDFViewer { uiManager: this.#annotationEditorUIManager, }); if (mode !== AnnotationEditorType.NONE) { - if (mode === AnnotationEditorType.STAMP) { - this.#mlManager?.loadModel("altText"); - } + this.#preloadEditingData(mode); this.#annotationEditorUIManager.updateMode(mode); } } else { @@ -2315,6 +2313,18 @@ class PDFViewer { } } + #preloadEditingData(mode) { + switch (mode) { + case AnnotationEditorType.STAMP: + this.#mlManager?.loadModel("altText"); + break; + case AnnotationEditorType.SIGNATURE: + // Start to load the signature data. + this.#signatureManager?.loadSignatures(); + break; + } + } + get annotationEditorMode() { return this.#annotationEditorUIManager ? this.#annotationEditorMode @@ -2345,9 +2355,7 @@ class PDFViewer { if (!this.pdfDocument) { return; } - if (mode === AnnotationEditorType.STAMP) { - this.#mlManager?.loadModel("altText"); - } + this.#preloadEditingData(mode); const { eventBus, pdfDocument } = this; const updater = async () => { diff --git a/web/signature_manager.css b/web/signature_manager.css index f1c90b76d..810e6bc19 100644 --- a/web/signature_manager.css +++ b/web/signature_manager.css @@ -13,6 +13,39 @@ * limitations under the License. */ +:root { + --clear-signature-button-icon: url(images/editor-toolbar-delete.svg); + --signature-bg: #f9f9fb; + --signature-hover-bg: #f0f0f4; + --button-signature-bg: transparent; + --button-signature-color: var(--main-color); + --button-signature-active-bg: #cfcfd8; + --button-signature-active-border: none; + --button-signature-active-color: var(--button-signature-color); + --button-signature-border: none; + --button-signature-hover-bg: #e0e0e6; + --button-signature-hover-color: var(--button-signature-color); + + @media (prefers-color-scheme: dark) { + --signature-bg: #2b2a33; + --signature-hover-bg: var(--signature-bg); + --button-signature-active-bg: #5b5b66; + --button-signature-hover-bg: #52525e; + } + + @media screen and (forced-colors: active) { + --signature-bg: HighlightText; + --signature-hover-bg: var(--signature-bg); + --button-signature-bg: HighlightText; + --button-signature-color: ButtonText; + --button-signature-active-bg: ButtonText; + --button-signature-active-color: HighlightText; + --button-signature-border: 1px solid ButtonText; + --button-signature-hover-bg: Highlight; + --button-signature-hover-color: HighlightText; + } +} + #addSignatureDialog { --border-color: #8f8f9d; --primary-color: var(--text-primary-color); @@ -31,8 +64,8 @@ --tab-bg-hover: var(--bg-hover); --tab-text-color: var(--primary-color); --tab-text-active-color: var(--tab-top-line-active-color); + --tab-text-active-hover-color: var(--tab-text-hover-color); --tab-text-hover-color: var(--tab-text-color); - --signature-bg: #f9f9fb; --signature-placeholder-color: var(--secondary-color); --signature-draw-placeholder-color: var(--primary-color); --signature-color: var(--primary-color); @@ -43,7 +76,6 @@ --clear-signature-button-border-style: solid; --clear-signature-button-border-color: transparent; --clear-signature-button-border-disabled-color: transparent; - --clear-signature-button-icon: url(images/editor-toolbar-delete.svg); --clear-signature-button-color: var(--primary-color); --clear-signature-button-hover-color: var(--clear-signature-button-color); --clear-signature-button-active-color: var(--clear-signature-button-color); @@ -72,7 +104,6 @@ --secondary-color: #cfcfd8; --tab-top-line-active-color: #0df; --tab-top-line-inactive-color: #8f8f9d; - --signature-bg: #2b2a33; --clear-signature-button-bg-active: #5b5b66; --clear-signature-button-bg-focus: #2b2a33; --clear-signature-button-bg-disabled: color-mix( @@ -99,8 +130,8 @@ --tab-bg-active-hover-color: SelectedItem; --tab-text-color: ButtonText; --tab-text-active-color: HighlightText; + --tab-text-active-hover-color: HighlightText; --tab-text-hover-color: SelectedItem; - --signature-bg: var(--bg); --signature-color: ButtonText; --clear-signature-button-border-width: 1px; --clear-signature-button-border-style: solid; @@ -214,7 +245,7 @@ &:hover { border-block-start-color: var(--tab-top-line-active-hover-color); background-color: var(--tab-bg-active-hover-color); - color: var(--tab-text-hover-color); + color: var(--tab-text-active-hover-color); } } } @@ -594,3 +625,138 @@ } } } + +#editorSignatureParamsToolbar { + padding: 10px; + + #addSignatureDoorHanger { + gap: 8px; + overflow-y: unset; + + .toolbarAddSignatureButtonContainer { + height: 32px; + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; + gap: 8px; + + button { + border: var(--button-signature-border); + border-radius: 4px; + background-color: var(--button-signature-bg); + color: var(--button-signature-color); + + &:hover { + background-color: var(--button-signature-hover-bg); + } + + &:active { + border: var(--button-signature-active-border); + background-color: var(--button-signature-active-bg); + color: var(--button-signature-active-color); + + &::before { + background-color: var(--button-signature-active-color); + } + } + + &:focus-visible { + outline: var(--focus-ring-outline); + + &::before { + background-color: var(--button-signature-color); + } + } + } + + .deleteButton { + &::before { + mask-image: var(--clear-signature-button-icon); + } + } + + .toolbarAddSignatureButton { + width: auto; + height: 100%; + min-height: var(--menuitem-height); + aspect-ratio: unset; + display: flex; + align-items: center; + justify-content: flex-start; + outline: none; + border-radius: 4px; + box-sizing: border-box; + font: message-box; + position: relative; + flex: 1 1 auto; + padding: 0; + gap: 8px; + text-align: start; + white-space: normal; + cursor: default; + overflow: hidden; + + > svg { + display: inline-block; + height: 100%; + aspect-ratio: 1; + background-color: var(--signature-bg); + flex: none; + padding: 4px; + box-sizing: border-box; + border: none; + border-radius: 4px; + + > path { + stroke: var(--button-signature-color); + stroke-width: 1px; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10; + vector-effect: non-scaling-stroke; + fill: none; + } + + &.contours > path { + fill: var(--button-signature-color); + stroke-width: 0.5px; + } + } + + &:is(:hover, :active) > svg { + border-radius: 4px 0 0 4px; + background-color: var(--signature-hover-bg); + } + + &:hover { + > span { + color: var(--button-signature-hover-color); + } + } + + &:active { + background-color: var(--button-signature-active-bg); + } + + &:is([disabled="disabled"], [disabled]) { + opacity: 0.5; + pointer-events: none; + } + + > span { + height: auto; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1 1 auto; + font: menu; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: normal; + overflow: hidden; + } + } + } + } +} diff --git a/web/signature_manager.js b/web/signature_manager.js index d25dda7a2..996a8685b 100644 --- a/web/signature_manager.js +++ b/web/signature_manager.js @@ -14,6 +14,7 @@ */ import { + AnnotationEditorParamsType, DOMSVGFactory, noContextMenu, SignatureExtractor, @@ -21,6 +22,9 @@ import { SupportedImageMimeTypes, } from "pdfjs-lib"; +// Default height of the added signature in page coordinates. +const DEFAULT_HEIGHT_IN_PAGE = 40; + class SignatureManager { #addButton; @@ -70,6 +74,10 @@ class SignatureManager { #tabButtons; + #addSignatureToolbarButton; + + #loadSignaturesPromise = null; + #typeInput; #currentTab = null; @@ -78,6 +86,8 @@ class SignatureManager { #hasDescriptionChanged = false; + #eventBus; + #l10n; #overlayManager; @@ -113,9 +123,11 @@ class SignatureManager { saveCheckbox, saveContainer, }, + addSignatureToolbarButton, overlayManager, l10n, - signatureStorage + signatureStorage, + eventBus ) { this.#addButton = addButton; this.#clearButton = clearButton; @@ -133,9 +145,11 @@ class SignatureManager { this.#overlayManager = overlayManager; this.#saveCheckbox = saveCheckbox; this.#saveContainer = saveContainer; + this.#addSignatureToolbarButton = addSignatureToolbarButton; this.#typeInput = typeInput; this.#l10n = l10n; this.#signatureStorage = signatureStorage; + this.#eventBus = eventBus; SignatureManager.#l10nDescription ||= Object.freeze({ signature: "pdfjs-editor-add-signature-description-default-when-drawing", @@ -360,7 +374,7 @@ class SignatureManager { this.#drawCurves = { width: drawWidth, height: drawHeight, - thickness: this.#drawThickness.value, + thickness: parseInt(this.#drawThickness.value), curves: [], }; this.#disableButtons(true); @@ -610,10 +624,147 @@ class SignatureManager { ); } + #addToolbarButton(signatureData, uuid, description) { + const { curves, areContours, thickness, width, height } = signatureData; + const maxDim = Math.max(width, height); + const outlineData = SignatureExtractor.processDrawnLines({ + lines: { + curves, + thickness, + width, + height, + }, + pageWidth: maxDim, + pageHeight: maxDim, + rotation: 0, + innerMargin: 0, + mustSmooth: false, + areContours, + }); + if (!outlineData) { + return; + } + + const { outline } = outlineData; + const svgFactory = new DOMSVGFactory(); + + const div = document.createElement("div"); + const button = document.createElement("button"); + button.addEventListener("click", () => { + this.#eventBus.dispatch("switchannotationeditorparams", { + source: this, + type: AnnotationEditorParamsType.CREATE, + value: { + signatureData: { + lines: { + curves, + thickness, + width, + height, + }, + mustSmooth: false, + areContours, + description, + uuid, + heightInPage: DEFAULT_HEIGHT_IN_PAGE, + }, + }, + }); + }); + div.append(button); + div.classList.add("toolbarAddSignatureButtonContainer"); + + const svg = svgFactory.create(1, 1, true); + button.append(svg); + + const span = document.createElement("span"); + button.append(span); + + button.classList.add("toolbarAddSignatureButton"); + button.type = "button"; + button.title = span.textContent = description; + button.tabIndex = 0; + + const path = svgFactory.createElement("path"); + svg.append(path); + svg.setAttribute("viewBox", outline.viewBox); + svg.setAttribute("preserveAspectRatio", "xMidYMid meet"); + if (areContours) { + svg.classList.add("contours"); + } + path.setAttribute("d", outline.toSVGPath()); + + const deleteButton = document.createElement("button"); + div.append(deleteButton); + deleteButton.classList.add("toolbarButton", "deleteButton"); + deleteButton.setAttribute( + "data-l10n-id", + "pdfjs-editor-delete-signature-button" + ); + deleteButton.type = "button"; + deleteButton.tabIndex = 0; + deleteButton.addEventListener("click", async () => { + if (await this.#signatureStorage.delete(uuid)) { + div.remove(); + } + }); + const deleteSpan = document.createElement("span"); + deleteButton.append(deleteSpan); + deleteSpan.setAttribute( + "data-l10n-id", + "pdfjs-editor-delete-signature-button-label" + ); + + this.#addSignatureToolbarButton.before(div); + } + getSignature(params) { return this.open(params); } + async loadSignatures() { + if ( + !this.#addSignatureToolbarButton || + this.#addSignatureToolbarButton.previousElementSibling || + !this.#signatureStorage + ) { + return; + } + + if (!this.#loadSignaturesPromise) { + // The first call of loadSignatures() starts loading the signatures. + // The second one will wait until the signatures are loaded in the DOM. + this.#loadSignaturesPromise = this.#signatureStorage + .getAll() + .then(async signatures => [ + signatures, + await Promise.all( + Array.from( + signatures + .values() + .map(({ signatureData }) => + SignatureExtractor.decompressSignature(signatureData) + ) + ) + ), + ]); + return; + } + const [signatures, signaturesData] = await this.#loadSignaturesPromise; + this.#loadSignaturesPromise = null; + + let i = 0; + for (const [uuid, { description }] of signatures) { + const data = signaturesData[i++]; + if (!data) { + continue; + } + data.curves = data.outlines.map(points => ({ points })); + delete data.outlines; + this.#addToolbarButton(data, uuid, description); + } + } + async open({ uiManager, editor }) { this.#tabsToAltText ||= new Map( this.#tabButtons.keys().map(name => [name, ""]) @@ -677,9 +828,10 @@ class SignatureManager { } this.#currentEditor.addSignature( data.outline, - /* heightInPage */ 40, + DEFAULT_HEIGHT_IN_PAGE, this.#description.value ); + let uuid = null; if (this.#saveCheckbox.checked) { const description = this.#description.value; const { newCurves, areContours, thickness, width, height } = data; @@ -690,15 +842,27 @@ class SignatureManager { width, height, }); - const uuid = (this.#currentEditor._signatureUUID = - await this.#signatureStorage.create({ - description, - signatureData, - })); - if (!uuid) { + uuid = await this.#signatureStorage.create({ + description, + signatureData, + }); + if (uuid) { + this.#addToolbarButton( + { + curves: newCurves.map(points => ({ points })), + areContours, + thickness, + width, + height, + }, + uuid, + description + ); + } else { console.warn("SignatureManager.add: cannot save the signature."); } } + this.#currentEditor.setUuid(uuid); this.#finish(); } diff --git a/web/viewer.html b/web/viewer.html index 420a26f7a..c75766df0 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -245,13 +245,13 @@ See https://github.com/adobe-type-tools/cmap-resources