From fa25ab9c1379078444449820d25956c0245287fc Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 3 Feb 2025 18:30:42 +0100 Subject: [PATCH] [Editor] Add a new dialog for the signature editor (bug 1945574) --- gulpfile.mjs | 1 + l10n/en-US/viewer.ftl | 61 +++ src/display/editor/drawers/inkdraw.js | 4 + src/display/editor/ink.js | 10 +- src/display/editor/signature.js | 168 +++--- src/display/editor/tools.js | 9 + web/annotation_editor_layer_builder.css | 1 + web/app.js | 10 + web/dialog.css | 17 + web/pdf_viewer.js | 4 + web/signature_manager.css | 586 ++++++++++++++++++++ web/signature_manager.js | 682 ++++++++++++++++++++++++ web/stubs-geckoview.js | 2 + web/viewer-geckoview.html | 1 + web/viewer.html | 80 +++ web/viewer.js | 26 + 16 files changed, 1596 insertions(+), 66 deletions(-) create mode 100644 web/signature_manager.css create mode 100644 web/signature_manager.js diff --git a/gulpfile.mjs b/gulpfile.mjs index e119a570f8de7..b4f83bd84ec81 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -217,6 +217,7 @@ function createWebpackAlias(defines) { "web-preferences": "", "web-print_service": "", "web-secondary_toolbar": "web/secondary_toolbar.js", + "web-signature_manager": "web/signature_manager.js", "web-toolbar": "web/toolbar.js", }; diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index 3e4a3510cf83d..bd2c7b0a545e0 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -331,6 +331,8 @@ pdfjs-editor-remove-stamp-button = .title = Remove image pdfjs-editor-remove-highlight-button = .title = Remove highlight +pdfjs-editor-remove-signature-button = + .title = Remove signature ## @@ -510,6 +512,7 @@ pdfjs-editor-undo-bar-message-highlight = Highlight removed pdfjs-editor-undo-bar-message-freetext = Text removed pdfjs-editor-undo-bar-message-ink = Drawing removed pdfjs-editor-undo-bar-message-stamp = Image removed +pdfjs-editor-undo-bar-message-signature = Signature removed # Variables: # $count (Number) - the number of removed annotations. pdfjs-editor-undo-bar-message-multiple = @@ -524,3 +527,61 @@ pdfjs-editor-undo-bar-undo-button-label = Undo pdfjs-editor-undo-bar-close-button = .title = Close pdfjs-editor-undo-bar-close-button-label = Close + +## Add a signature dialog + +pdfjs-editor-add-signature-dialog-label = This modal allows the user to create a signature to add to a PDF document. The user can edit the name (which also serves as the alt text), and optionally save the signature for repeated use. +pdfjs-editor-add-signature-dialog-title = Add a signature + +## Tab names + +# Type is a verb (you can type your name as signature) +pdfjs-editor-add-signature-type-button = Type + .title = Type +# Draw is a verb (you can draw your signature) +pdfjs-editor-add-signature-draw-button = Draw + .title = Draw +pdfjs-editor-add-signature-image-button = Image + .title = Image + +## Tab panels + +pdfjs-editor-add-signature-type-input = + .aria-label = Type your signature + .placeholder = Type your signature +pdfjs-editor-add-signature-draw-placeholder = Draw your signature +pdfjs-editor-add-signature-draw-thickness-range-label = Thickness + +# Variables: +# $thickness (Number) - the thickness (in pixels) of the line used to draw a signature. +pdfjs-editor-add-signature-draw-thickness-range = + .title = Drawing thickness: { $thickness } + +pdfjs-editor-add-signature-image-placeholder = Drag a file here to upload +pdfjs-editor-add-signature-image-browse-link = + { PLATFORM() -> + [macos] Or choose image files + *[other] Or browse image files + } + +## Controls + +pdfjs-editor-add-signature-description-label = Description (alt text) +pdfjs-editor-add-signature-description-input = + .title = Description (alt text) +pdfjs-editor-add-signature-description-default-when-drawing = Signature + + +pdfjs-editor-add-signature-clear-button-label = Clear signature +pdfjs-editor-add-signature-clear-button = + .title = Clear signature +pdfjs-editor-add-signature-save-checkbox = Save signature +pdfjs-editor-add-signature-save-warning-message = You’ve reached the limit of 5 saved signatures. Remove one to save more. +pdfjs-editor-add-signature-image-upload-error-title = Couldn’t upload image +pdfjs-editor-add-signature-image-upload-error-description = Check your network connection or try another image. +pdfjs-editor-add-signature-error-close-button = Close + +## Dialog buttons + +pdfjs-editor-add-signature-cancel-button = Cancel +pdfjs-editor-add-signature-add-button = Add diff --git a/src/display/editor/drawers/inkdraw.js b/src/display/editor/drawers/inkdraw.js index 11447e723ba3b..f725a6924f530 100644 --- a/src/display/editor/drawers/inkdraw.js +++ b/src/display/editor/drawers/inkdraw.js @@ -318,6 +318,10 @@ class InkDrawOutline extends Outline { this.#computeBbox(); } + get thickness() { + return this.#thickness; + } + setLastElement(element) { this.#lines.push(element); return { diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 2f30dce592386..63c1e28c12be5 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -25,11 +25,9 @@ import { AnnotationEditor } from "./editor.js"; import { InkAnnotationElement } from "../annotation_layer.js"; class InkDrawingOptions extends DrawingOptions { - #viewParameters; - constructor(viewerParameters) { super(); - this.#viewParameters = viewerParameters; + this._viewParameters = viewerParameters; super.updateProperties({ fill: "none", @@ -45,13 +43,13 @@ class InkDrawingOptions extends DrawingOptions { updateSVGProperty(name, value) { if (name === "stroke-width") { value ??= this["stroke-width"]; - value *= this.#viewParameters.realScale; + value *= this._viewParameters.realScale; } super.updateSVGProperty(name, value); } clone() { - const clone = new InkDrawingOptions(this.#viewParameters); + const clone = new InkDrawingOptions(this._viewParameters); clone.updateAll(this); return clone; } @@ -284,4 +282,4 @@ class InkEditor extends DrawingEditor { } } -export { InkEditor }; +export { InkDrawingOptions, InkEditor }; diff --git a/src/display/editor/signature.js b/src/display/editor/signature.js index ec27e8b3fcece..e3f5ba6f8bc1a 100644 --- a/src/display/editor/signature.js +++ b/src/display/editor/signature.js @@ -16,15 +16,13 @@ import { AnnotationEditorType, shadow } from "../../shared/util.js"; import { DrawingEditor, DrawingOptions } from "./draw.js"; import { AnnotationEditor } from "./editor.js"; +import { ContourDrawOutline } from "./drawers/contour.js"; +import { InkDrawingOptions } from "./ink.js"; import { SignatureExtractor } from "./drawers/signaturedraw.js"; -import { SupportedImageMimeTypes } from "../display_utils.js"; class SignatureOptions extends DrawingOptions { - #viewParameters; - - constructor(viewerParameters) { + constructor() { super(); - this.#viewParameters = viewerParameters; super.updateProperties({ fill: "black", @@ -33,7 +31,24 @@ class SignatureOptions extends DrawingOptions { } clone() { - const clone = new SignatureOptions(this.#viewParameters); + const clone = new SignatureOptions(); + clone.updateAll(this); + return clone; + } +} + +class DrawnSignatureOptions extends InkDrawingOptions { + constructor(viewerParameters) { + super(viewerParameters); + + super.updateProperties({ + stroke: "black", + "stroke-width": 1, + }); + } + + clone() { + const clone = new DrawnSignatureOptions(this._viewParameters); clone.updateAll(this); return clone; } @@ -44,6 +59,8 @@ class SignatureOptions extends DrawingOptions { * a signature drawing. */ class SignatureEditor extends DrawingEditor { + #isExtracted = false; + static _type = "signature"; static _editorType = AnnotationEditorType.SIGNATURE; @@ -52,13 +69,15 @@ class SignatureEditor extends DrawingEditor { constructor(params) { super({ ...params, mustBeCommitted: true, name: "signatureEditor" }); - this._willKeepAspectRatio = false; + this._willKeepAspectRatio = true; } /** @inheritdoc */ static initialize(l10n, uiManager) { AnnotationEditor.initialize(l10n, uiManager); - this._defaultDrawingOptions = new SignatureOptions( + + this._defaultDrawingOptions = new SignatureOptions(); + this._defaultDrawnSignatureOptions = new DrawnSignatureOptions( uiManager.viewParameters ); } @@ -88,6 +107,14 @@ class SignatureEditor extends DrawingEditor { return true; } + /** @inheritdoc */ + onScaleChanging() { + if (this._drawId === null) { + return; + } + super.onScaleChanging(); + } + /** @inheritdoc */ render() { if (this.div) { @@ -98,70 +125,91 @@ class SignatureEditor extends DrawingEditor { this.div.hidden = true; this.div.setAttribute("role", "figure"); - this.#extractSignature(); + this._uiManager.getSignature(this); return this.div; } - async #extractSignature() { - const input = document.createElement("input"); - input.type = "file"; - input.accept = SupportedImageMimeTypes.join(","); - const signal = this._uiManager._signal; - const { promise, resolve } = Promise.withResolvers(); - - input.addEventListener( - "change", - async () => { - if (!input.files || input.files.length === 0) { - resolve(); - } else { - this._uiManager.enableWaiting(true); - const data = await this._uiManager.imageManager.getFromFile( - input.files[0] - ); - this._uiManager.enableWaiting(false); - resolve(data); - } - resolve(); - }, - { signal } - ); - input.addEventListener("cancel", resolve, { signal }); - input.click(); - - const bitmap = await promise; - const { - rawDims: { pageWidth, pageHeight }, - rotation, - } = this.parent.viewport; - let drawOutlines; - if (bitmap?.bitmap) { - drawOutlines = SignatureExtractor.process( - bitmap.bitmap, - pageWidth, - pageHeight, - rotation, - SignatureEditor._INNER_MARGIN - ); + addSignature(outline, heightInPage) { + const { x: savedX, y: savedY } = this; + this.#isExtracted = outline instanceof ContourDrawOutline; + let drawingOptions; + if (this.#isExtracted) { + drawingOptions = SignatureEditor.getDefaultDrawingOptions(); } else { - drawOutlines = SignatureExtractor.extractContoursFromText( - "Hello PDF.js' World !!", - { fontStyle: "italic", fontWeight: "400", fontFamily: "cursive" }, - pageWidth, - pageHeight, - rotation, - SignatureEditor._INNER_MARGIN - ); + drawingOptions = SignatureEditor._defaultDrawnSignatureOptions.clone(); + drawingOptions.updateProperties({ "stroke-width": outline.thickness }); } this._addOutlines({ - drawOutlines, - drawingOptions: SignatureEditor.getDefaultDrawingOptions(), + drawOutlines: outline, + drawingOptions, }); + const [parentWidth, parentHeight] = this.parentDimensions; + const [, pageHeight] = this.pageDimensions; + let newHeight = heightInPage / pageHeight; + // Ensure the signature doesn't exceed the page height. + // If the signature is too big, we scale it down to 50% of the page height. + newHeight = newHeight >= 1 ? 0.5 : newHeight; + + this.width *= newHeight / this.height; + this.height = newHeight; + this.setDims(parentWidth * this.width, parentHeight * this.height); + this.x = savedX; + this.y = savedY; + this.center(); + + this._onResized(); this.onScaleChanging(); this.rotate(); + this._uiManager.addToAnnotationStorage(this); + this.div.hidden = false; } + + getFromImage(bitmap) { + const { + rawDims: { pageWidth, pageHeight }, + rotation, + } = this.parent.viewport; + return SignatureExtractor.process( + bitmap, + pageWidth, + pageHeight, + rotation, + SignatureEditor._INNER_MARGIN + ); + } + + getFromText(text, fontInfo) { + const { + rawDims: { pageWidth, pageHeight }, + rotation, + } = this.parent.viewport; + return SignatureExtractor.extractContoursFromText( + text, + fontInfo, + pageWidth, + pageHeight, + rotation, + SignatureEditor._INNER_MARGIN + ); + } + + getDrawnSignature(curves) { + const { + rawDims: { pageWidth, pageHeight }, + rotation, + } = this.parent.viewport; + return SignatureExtractor.processDrawnLines({ + lines: curves, + pageWidth, + pageHeight, + rotation, + innerMargin: SignatureEditor._INNER_MARGIN, + mustSmooth: false, + areContours: false, + }); + } } export { SignatureEditor }; diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index d6bd33c11361e..389e7efce5ef6 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -664,6 +664,8 @@ class AnnotationEditorUIManager { #selectedTextNode = null; + #signatureManager = null; + #pageColors = null; #showAllStates = null; @@ -828,6 +830,7 @@ class AnnotationEditorUIManager { container, viewer, altTextManager, + signatureManager, eventBus, pdfDocument, pageColors, @@ -843,6 +846,7 @@ class AnnotationEditorUIManager { this.#container = container; this.#viewer = viewer; this.#altTextManager = altTextManager; + this.#signatureManager = signatureManager; this._eventBus = eventBus; eventBus._on("editingaction", this.onEditingAction.bind(this), { signal }); eventBus._on("pagechanging", this.onPageChanging.bind(this), { signal }); @@ -905,6 +909,7 @@ class AnnotationEditorUIManager { this.#selectedEditors.clear(); this.#commandManager.destroy(); this.#altTextManager?.destroy(); + this.#signatureManager?.destroy(); this.#highlightToolbar?.hide(); this.#highlightToolbar = null; this.#mainHighlightColorPicker?.destroy(); @@ -1003,6 +1008,10 @@ class AnnotationEditorUIManager { this.#altTextManager?.editAltText(this, editor, firstTime); } + getSignature(editor) { + this.#signatureManager?.getSignature({ uiManager: this, editor }); + } + switchToMode(mode, callback) { // Switching to a mode can be asynchronous. this._eventBus.on("annotationeditormodechanged", callback, { diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index b34bfe50bcc2a..032d44a282b86 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -15,6 +15,7 @@ @import url(draw_layer_builder.css); @import url(toggle_button.css); +@import url(signature_manager.css); :root { --outline-width: 2px; diff --git a/web/app.js b/web/app.js index 5dfdbbd0b04f7..dfea53a68910c 100644 --- a/web/app.js +++ b/web/app.js @@ -89,6 +89,7 @@ import { PDFThumbnailViewer } from "web-pdf_thumbnail_viewer"; import { PDFViewer } from "./pdf_viewer.js"; import { Preferences } from "web-preferences"; import { SecondaryToolbar } from "web-secondary_toolbar"; +import { SignatureManager } from "web-signature_manager"; import { Toolbar } from "web-toolbar"; import { ViewHistory } from "./view_history.js"; @@ -458,6 +459,14 @@ const PDFViewerApplication = { this.editorUndoBar = new EditorUndoBar(appConfig.editorUndoBar, eventBus); } + const signatureManager = appConfig.addSignatureDialog + ? new SignatureManager( + appConfig.addSignatureDialog, + this.overlayManager, + this.l10n + ) + : null; + const enableHWA = AppOptions.get("enableHWA"); const pdfViewer = new PDFViewer({ container, @@ -467,6 +476,7 @@ const PDFViewerApplication = { linkService: pdfLinkService, downloadManager, altTextManager, + signatureManager, editorUndoBar: this.editorUndoBar, findController, scriptingManager: diff --git a/web/dialog.css b/web/dialog.css index 3a5ba15d44932..c91b26966a327 100644 --- a/web/dialog.css +++ b/web/dialog.css @@ -49,6 +49,9 @@ --button-primary-hover-fg-color: var(--button-primary-fg-color); --button-primary-hover-border-color: var(--button-primary-hover-bg-color); + --button-disabled-bg-color: color-mix(in srgb, currentcolor, transparent 60%); + --button-disabled-fg-color: var(--button-disabled-bg-color); + @media (prefers-color-scheme: dark) { --dialog-bg-color: #1c1b22; --dialog-border-color: #1c1b22; @@ -103,6 +106,9 @@ --button-primary-fg-color: ButtonFace; --button-primary-hover-bg-color: AccentColor; --button-primary-hover-fg-color: AccentColorText; + + --button-disabled-bg-color: GrayText; + --button-disabled-fg-color: ButtonFace; } font: message-box; @@ -213,6 +219,10 @@ filter: var(--hover-filter); } + > span { + color: inherit; + } + &.secondaryButton { color: var(--button-secondary-fg-color); background-color: var(--button-secondary-bg-color); @@ -237,6 +247,13 @@ border-color: var(--button-primary-hover-border-color); } } + + &:disabled { + color: var(--button-disabled-fg-color) !important; + background-color: var(--button-disabled-bg-color); + border-color: var(--button-disabled-bg-color); + pointer-events: none; + } } a { diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 986cfa71236ac..eb529cf3c85b6 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -250,6 +250,8 @@ class PDFViewer { #scaleTimeoutId = null; + #signatureManager = null; + #supportsPinchToZoom = true; #textLayerMode = TextLayerMode.ENABLE; @@ -287,6 +289,7 @@ class PDFViewer { this.downloadManager = options.downloadManager || null; this.findController = options.findController || null; this.#altTextManager = options.altTextManager || null; + this.#signatureManager = options.signatureManager || null; this.#editorUndoBar = options.editorUndoBar || null; if (this.findController) { @@ -908,6 +911,7 @@ class PDFViewer { this.container, viewer, this.#altTextManager, + this.#signatureManager, eventBus, pdfDocument, pageColors, diff --git a/web/signature_manager.css b/web/signature_manager.css new file mode 100644 index 0000000000000..05bf662cf92fe --- /dev/null +++ b/web/signature_manager.css @@ -0,0 +1,586 @@ +/* Copyright 2022 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#addSignatureDialog { + --border-color: #8f8f9d; + --primary-color: var(--text-primary-color); + --secondary-color: var(--text-secondary-color); + --bg-hover: #e0e0e6; + --tab-top-line-active-color: #0060df; + --tab-top-line-active-hover-color: var(--tab-text-hover-color); + --tab-top-line-hover-color: #8f8f9d; + --tab-top-line-inactive-color: #cfcfd8; + --tab-bottom-line-active-color: var(--tab-top-line-inactive-color); + --tab-bottom-line-hover-color: var(--tab-top-line-inactive-color); + --tab-bottom-line-inactive-color: var(--tab-top-line-inactive-color); + --tab-bg: var(--dialog-bg-color); + --tab-bg-active-color: var(--tab-bg); + --tab-bg-active-hover-color: var(--bg-hover); + --tab-bg-hover: var(--bg-hover); + --tab-text-color: var(--primary-color); + --tab-text-active-color: var(--tab-top-line-active-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); + --closing-button-icon: url(images/messageBar_closingButton.svg); + --closing-button-color: var(--primary-color); + --description-input-color: var(--primary-color); + --clear-signature-button-border-width: 0; + --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); + --clear-signature-button-disabled-color: var(--clear-signature-button-color); + --clear-signature-button-focus-color: var(--clear-signature-button-color); + --clear-signature-button-bg: var(--dialog-bg-color); + --clear-signature-button-bg-hover: var(--bg-hover); + --clear-signature-button-bg-active: #cfcfd8; + --clear-signature-button-bg-focus: #f0f0f4; + --clear-signature-button-bg-disabled: color-mix( + in srgb, + #f0f0f4, + transparent 40% + ); + --save-warning-color: --var(--secondary-color); + --thickness-bg: var(--dialog-bg-color); + --thickness-label-color: var(--primary-color); + --thickness-slider-color: var(--primary-color); + --draw-cursor: url(images/cursor-editorInk.svg) 0 16, pointer; + + @media (prefers-color-scheme: dark) { + /* TODO: Update the dialog colors for dark mode but in dialog.css */ + --dialog-bg-color: #42414d; + --bg-hover: #52525e; + --primary-color: #fbfbfe; + --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( + in srgb, + #2b2a33, + transparent 40% + ); + } + + @media screen and (forced-colors: active) { + --primary-color: ButtonText; + --secondary-color: ButtonText; + --bg: HighlightText; + --bg-hover: var(--bg); + --border-color: ButtonText; + --tab-top-line-active-color: ButtonText; + --tab-top-line-active-hover-color: HighlightText; + --tab-top-line-hover-color: SelectedItem; + --tab-top-line-inactive-color: ButtonText; + --tab-bottom-line-active-color: var(--tab-top-line-active-color); + --tab-bottom-line-hover-color: var(--tab-top-line-hover-color); + --tab-bg: var(--bg); + --tab-bg-active-color: SelectedItem; + --tab-bg-active-hover-color: SelectedItem; + --tab-text-color: ButtonText; + --tab-text-active-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; + --clear-signature-button-border-color: ButtonText; + --clear-signature-button-border-disabled-color: GrayText; + --clear-signature-button-color: ButtonText; + --clear-signature-button-hover-color: HighlightText; + --clear-signature-button-active-color: SelectedItem; + --clear-signature-button-focus-color: CanvasText; + --clear-signature-button-disabled-color: GrayText; + --clear-signature-button-bg: var(--bg); + --clear-signature-button-bg-hover: SelectedItem; + --clear-signature-button-bg-active: var(--bg); + --clear-signature-button-bg-focus: var(--bg); + --clear-signature-button-bg-disabled: var(--bg); + --thickness-bg: Canvas; + --thickness-label-color: CanvasText; + --thickness-slider-color: ButtonText; + } + + width: 570px; + max-width: 100%; + min-width: 300px; + padding: 16px 0; + + #addSignatureDialogLabel { + overflow: hidden; + position: absolute; + inset: 0; + width: 0; + height: 0; + } + + &.waiting::after { + content: ""; + cursor: wait; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + } + + span { + font: menu; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + .mainContainer { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + .title { + margin-inline-start: 16px; + font-weight: 590; + } + + [role="tablist"] { + width: 100%; + display: flex; + align-items: flex-start; + gap: 0; + + > [role="tab"] { + flex: 1 0 0; + align-self: stretch; + background-color: var(--tab-bg); + padding-inline: 0; + cursor: default; + + border-inline: 0; + border-block-width: 1px; + border-block-style: solid; + border-block-start-color: var(--tab-top-line-inactive-color); + border-block-end-color: var(--tab-bottom-line-inactive-color); + border-radius: 0; + + font: menu; + font-size: 13px; + font-style: normal; + line-height: normal; + font-weight: 400; + color: var(--tab-text-color); + + &:hover { + border-block-start-width: 2px; + border-block-start-color: var(--tab-top-line-hover-color); + border-block-end-color: var(--tab-bottom-line-hover-color); + background-color: var(--tab-bg-hover); + color: var(--tab-text-hover-color); + } + + &:focus-visible { + outline: 2px solid var(--tab-top-line-active-color); + outline-offset: -2px; + } + + &[aria-selected="true"] { + border-block-start-width: 2px; + border-block-start-color: var(--tab-top-line-active-color); + border-block-end-color: var(--tab-bottom-line-active-color); + background-color: var(--tab-bg-active-color); + font-weight: 590; + color: var(--tab-text-active-color); + + &: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); + } + } + } + } + + #addSignatureActionContainer { + width: 100%; + height: auto; + display: flex; + flex-direction: column; + align-items: flex-end; + align-self: stretch; + gap: 12px; + padding-inline: 16px; + box-sizing: border-box; + + > [role="tabpanel"] { + position: relative; + width: 100%; + height: 220px; + background-color: var(--signature-bg); + border-radius: 4px; + + > svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + background-color: transparent; + } + + &#addSignatureTypeContainer { + display: none; + + #addSignatureTypeInput { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border: 0; + padding: 0; + text-align: center; + color: var(--signature-color); + background-color: transparent; + + font-family: "Brush script", "Apple Chancery", "Segoe script", + "Freestyle Script", "Palace Script MT", "Brush Script MT", TK, + cursive, serif; + font-size: 44px; + font-style: italic; + font-weight: 400; + + &::placeholder { + color: var(--signature-placeholder-color); + text-align: center; + + font: menu; + font-style: normal; + font-weight: 274; + font-size: 44px; + line-height: normal; + } + } + } + + &#addSignatureDrawContainer { + display: none; + + > span { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: grid; + align-items: center; + justify-content: center; + + background-color: transparent; + color: var(--signature-placeholder-color); + user-select: none; + } + + > svg { + stroke: var(--primary-color); + fill: none; + stroke-opacity: 1; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10; + + &:hover { + cursor: var(--draw-cursor); + } + } + + #thickness { + position: absolute; + width: 100%; + inset-block-end: 0; + display: grid; + align-items: center; + justify-content: center; + pointer-events: none; + + > span { + color: var(--signature-draw-placeholder-color); + } + + > div { + width: auto; + height: auto; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 6px 8px; + margin: 0; + background-color: var(--thickness-bg); + border-radius: 4px 4px 0 0; + pointer-events: auto; + + > label { + color: var(--thickness-label-color); + } + + > input { + width: 100px; + height: 14px; + background-color: transparent; + + /*#if !MOZCENTRAL*/ + &::-webkit-slider-runnable-track, + /*#endif*/ + &::-moz-range-track, + &::-moz-range-progress { + background-color: var(--thickness-slider-color); + } + + /*#if !MOZCENTRAL*/ + &::-webkit-slider-thumb, + /*#endif*/ + &::-moz-range-thumb { + background-color: var(--thickness-bg); + } + + border-radius: 4.5px; + border: 0; + color: var(--signature-color); + } + } + } + } + + &#addSignatureImageContainer { + display: none; + + > svg { + stroke: none; + stroke-width: 0; + fill: var(--primary-color); + fill-opacity: 1; + } + + #addSignatureImagePlaceholder { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: transparent; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + a { + text-decoration: underline; + cursor: pointer; + } + } + + #addSignatureFilePicker { + visibility: hidden; + position: relative; + width: 0; + height: 0; + } + } + } + + &[data-selected="type"] > #addSignatureTypeContainer, + &[data-selected="draw"] > #addSignatureDrawContainer, + &[data-selected="image"] > #addSignatureImageContainer { + display: block; + } + + #addSignatureControls { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 12px; + align-self: stretch; + + #horizontalContainer { + display: flex; + align-items: flex-end; + gap: 16px; + align-self: stretch; + + #addSignatureDescriptionContainer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + flex: 1 0 0; + + > #inputWithClearButton { + --button-dimension: 24px; + + width: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + > input { + width: 100%; + height: 32px; + padding: 8px 4px 8px 8px; + padding-block: 8px; + padding-inline: 8px calc(4px + var(--button-dimension)); + box-sizing: border-box; + background-color: transparent; + border-radius: 4px; + border: 1px solid var(--border-color); + color: var(--description-input-color); + } + + #addSignatureDescriptionClearButton { + position: absolute; + inset-block-start: 4px; + inset-inline-end: 4px; + display: inline-block; + width: var(--button-dimension); + height: var(--button-dimension); + background-color: var(--closing-button-color); + mask-size: cover; + mask-image: var(--closing-button-icon); + padding: 0; + border: 0; + } + } + + > label { + width: auto; + } + } + + #clearSignatureButton { + display: flex; + height: 32px; + padding: 4px 8px; + align-items: center; + background-color: var(--clear-signature-button-bg); + border-width: var(--clear-signature-button-border-width); + border-style: var(--clear-signature-button-border-style); + border-color: var(--clear-signature-button-border-color); + border-radius: 4px; + + > span { + display: flex; + height: 24px; + align-items: center; + gap: 4px; + flex-shrink: 0; + + color: var(--clear-signature-button-color); + + &::after { + content: ""; + display: inline-block; + width: 16px; + height: 16px; + mask-image: var(--clear-signature-button-icon); + mask-size: cover; + background-color: var(--clear-signature-button-color); + flex-shrink: 0; + } + } + + &:hover { + background-color: var(--clear-signature-button-bg-hover); + + > span { + color: var(--clear-signature-button-hover-color); + &::after { + background-color: var(--clear-signature-button-hover-color); + } + } + } + + &:active { + background-color: var(--clear-signature-button-bg-active); + + > span { + color: var(--clear-signature-button-active-color); + &::after { + background-color: var(--clear-signature-button-active-color); + } + } + } + + &:focus-visible { + background-color: var(--clear-signature-button-bg-focus); + + > span { + color: var(--clear-signature-button-focus-color); + &::after { + background-color: var(--clear-signature-button-focus-color); + } + } + } + + &:disabled { + background-color: var(--clear-signature-button-bg-disabled); + border-color: var(--clear-signature-button-border-disabled-color); + + > span { + color: var(--clear-signature-button-disabled-color); + &::after { + background-color: var( + --clear-signature-button-disabled-color + ); + } + } + } + } + } + + #addSignatureSaveContainer { + display: grid; + grid-template-columns: max-content max-content; + gap: 4px; + width: 100%; + + > input { + margin: 0; + } + + > label { + user-select: none; + } + + #addSignatureSaveWarning { + color: var(--save-warning-color); + font-size: 11px; + } + + &[disabled] { + pointer-events: none; + opacity: 0.4; + } + } + } + } + } +} diff --git a/web/signature_manager.js b/web/signature_manager.js new file mode 100644 index 0000000000000..0578c7eb16479 --- /dev/null +++ b/web/signature_manager.js @@ -0,0 +1,682 @@ +/* Copyright 2025 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DOMSVGFactory, + noContextMenu, + stopEvent, + SupportedImageMimeTypes, +} from "pdfjs-lib"; + +class SignatureManager { + #addButton; + + #tabsToAltText = null; + + #clearButton; + + #clearDescription; + + #currentEditor; + + #description; + + #dialog; + + #drawCurves = null; + + #drawPlaceholder; + + #drawPath = null; + + #drawPathString = ""; + + #drawPoints = null; + + #drawSVG; + + #drawThickness; + + #errorBar; + + #extractedSignatureData = null; + + #imagePath = null; + + #imagePicker; + + #imagePickerLink; + + #imagePlaceholder; + + #imageSVG; + + #saveCheckbox; + + #saveContainer; + + #tabButtons; + + #typeInput; + + #currentTab = null; + + #currentTabAC = null; + + #hasDescriptionChanged = false; + + #l10n; + + #overlayManager; + + #uiManager = null; + + static #l10nDescription = null; + + constructor( + { + dialog, + panels, + typeButton, + typeInput, + drawButton, + drawPlaceholder, + drawSVG, + drawThickness, + imageButton, + imageSVG, + imagePlaceholder, + imagePicker, + imagePickerLink, + description, + clearDescription, + clearButton, + cancelButton, + addButton, + errorCloseButton, + errorBar, + saveCheckbox, + saveContainer, + }, + overlayManager, + l10n + ) { + this.#addButton = addButton; + this.#clearButton = clearButton; + this.#clearDescription = clearDescription; + this.#description = description; + this.#dialog = dialog; + this.#drawSVG = drawSVG; + this.#drawPlaceholder = drawPlaceholder; + this.#drawThickness = drawThickness; + this.#errorBar = errorBar; + this.#imageSVG = imageSVG; + this.#imagePlaceholder = imagePlaceholder; + this.#imagePicker = imagePicker; + this.#imagePickerLink = imagePickerLink; + this.#overlayManager = overlayManager; + this.#saveCheckbox = saveCheckbox; + this.#saveContainer = saveContainer; + this.#typeInput = typeInput; + this.#l10n = l10n; + + SignatureManager.#l10nDescription ||= Object.freeze({ + signature: "pdfjs-editor-add-signature-description-default-when-drawing", + }); + + dialog.addEventListener("close", this.#close.bind(this)); + dialog.addEventListener("contextmenu", e => { + const { target } = e; + if (target !== this.#typeInput && target !== this.#description) { + e.preventDefault(); + } + }); + dialog.addEventListener("drop", e => { + stopEvent(e); + }); + cancelButton.addEventListener("click", this.#cancel.bind(this)); + addButton.addEventListener("click", this.#add.bind(this)); + clearButton.addEventListener( + "click", + () => { + this.#initTab(null); + }, + { passive: true } + ); + description.addEventListener( + "input", + () => { + clearDescription.disabled = description.value === ""; + }, + { passive: true } + ); + clearDescription.addEventListener( + "click", + () => { + this.#description.value = ""; + clearDescription.disabled = true; + }, + { passive: true } + ); + errorCloseButton.addEventListener( + "click", + () => { + errorBar.hidden = true; + }, + { passive: true } + ); + + this.#initTabButtons(typeButton, drawButton, imageButton, panels); + imagePicker.accept = SupportedImageMimeTypes.join(","); + + overlayManager.register(dialog); + } + + #initTabButtons(typeButton, drawButton, imageButton, panels) { + const buttons = (this.#tabButtons = new Map([ + ["type", typeButton], + ["draw", drawButton], + ["image", imageButton], + ])); + const tabCallback = e => { + for (const [name, button] of buttons) { + if (button === e.target) { + button.setAttribute("aria-selected", true); + button.setAttribute("tabindex", 0); + panels.setAttribute("data-selected", name); + this.#initTab(name); + } else { + button.setAttribute("aria-selected", false); + // Only the active tab is focusable: the others can be + // reached by keyboard navigation (left/right arrows). + button.setAttribute("tabindex", -1); + } + } + }; + + const buttonsArray = Array.from(buttons.values()); + for (let i = 0, ii = buttonsArray.length; i < ii; i++) { + const button = buttonsArray[i]; + button.addEventListener("click", tabCallback, { passive: true }); + button.addEventListener( + "keydown", + ({ key }) => { + if (key !== "ArrowLeft" && key !== "ArrowRight") { + return; + } + buttonsArray[i + (key === "ArrowLeft" ? -1 : 1)]?.focus(); + }, + { passive: true } + ); + } + } + + #resetCommon() { + this.#hasDescriptionChanged = false; + this.#description.value = ""; + this.#tabsToAltText.set(this.#currentTab, ""); + } + + #resetTab(name) { + switch (name) { + case "type": + this.#typeInput.value = ""; + break; + case "draw": + this.#drawCurves = null; + this.#drawPoints = null; + this.#drawPathString = ""; + this.#drawPath?.remove(); + this.#drawPath = null; + this.#drawPlaceholder.hidden = false; + this.#drawThickness.value = 1; + break; + case "image": + this.#imagePlaceholder.hidden = false; + this.#imagePath?.remove(); + this.#imagePath = null; + break; + } + } + + #initTab(name) { + if (name && this.#currentTab === name) { + return; + } + if (this.#currentTab) { + this.#tabsToAltText.set(this.#currentTab, this.#description.value); + } + if (name) { + this.#currentTab = name; + } + + const reset = !name; + if (reset) { + this.#resetCommon(); + } else { + this.#description.value = this.#tabsToAltText.get(this.#currentTab); + } + this.#clearDescription.disabled = this.#description.value === ""; + this.#currentTabAC?.abort(); + this.#currentTabAC = new AbortController(); + switch (this.#currentTab) { + case "type": + this.#initTypeTab(reset); + break; + case "draw": + this.#initDrawTab(reset); + break; + case "image": + this.#initImageTab(reset); + break; + } + } + + #disableButtons(value) { + this.#clearButton.disabled = this.#addButton.disabled = !value; + if (value) { + this.#saveContainer.removeAttribute("disabled"); + } else { + this.#saveContainer.setAttribute("disabled", true); + } + } + + #initTypeTab(reset) { + if (reset) { + this.#resetTab("type"); + } + + this.#disableButtons(this.#typeInput.value); + + const { signal } = this.#currentTabAC; + const options = { passive: true, signal }; + this.#typeInput.addEventListener( + "input", + () => { + const { value } = this.#typeInput; + if (!this.#hasDescriptionChanged) { + this.#description.value = value; + this.#clearDescription.disabled = value === ""; + } + this.#disableButtons(value); + }, + options + ); + this.#description.addEventListener( + "input", + () => { + this.#hasDescriptionChanged = + this.#typeInput.value !== this.#description.value; + }, + options + ); + } + + #initDrawTab(reset) { + if (reset) { + this.#resetTab("draw"); + } + + this.#disableButtons(this.#drawPath); + + const { signal } = this.#currentTabAC; + const options = { signal }; + let currentPointerId = NaN; + const drawCallback = e => { + const { pointerId } = e; + if (!isNaN(currentPointerId) && currentPointerId !== pointerId) { + return; + } + currentPointerId = pointerId; + e.preventDefault(); + this.#drawSVG.setPointerCapture(pointerId); + + const { width: drawWidth, height: drawHeight } = + this.#drawSVG.getBoundingClientRect(); + let { offsetX, offsetY } = e; + offsetX = Math.round(offsetX); + offsetY = Math.round(offsetY); + if (e.target === this.#drawPlaceholder) { + this.#drawPlaceholder.hidden = true; + } + if (!this.#drawCurves) { + this.#drawCurves = { + width: drawWidth, + height: drawHeight, + thickness: this.#drawThickness.value, + curves: [], + }; + this.#disableButtons(true); + + const svgFactory = new DOMSVGFactory(); + const path = (this.#drawPath = svgFactory.createElement("path")); + path.setAttribute("stroke-width", this.#drawThickness.value); + this.#drawSVG.append(path); + this.#drawSVG.addEventListener("pointerdown", drawCallback, options); + this.#drawPlaceholder.removeEventListener("pointerdown", drawCallback); + if (this.#description.value === "") { + this.#l10n + .get(SignatureManager.#l10nDescription.signature) + .then(description => { + this.#description.value ||= description; + this.#clearDescription.disabled = this.#description.value === ""; + }); + } + } + + this.#drawPoints = [offsetX, offsetY]; + this.#drawCurves.curves.push({ points: this.#drawPoints }); + this.#drawPathString += `M ${offsetX} ${offsetY}`; + this.#drawPath.setAttribute("d", this.#drawPathString); + + const finishDrawAC = new AbortController(); + const listenerDrawOptions = { + signal: AbortSignal.any([signal, finishDrawAC.signal]), + }; + this.#drawSVG.addEventListener( + "contextmenu", + noContextMenu, + listenerDrawOptions + ); + this.#drawSVG.addEventListener( + "pointermove", + evt => { + evt.preventDefault(); + let { offsetX: x, offsetY: y } = evt; + x = Math.round(x); + y = Math.round(y); + const drawPoints = this.#drawPoints; + if ( + x < 0 || + y < 0 || + x > drawWidth || + y > drawHeight || + (x === drawPoints.at(-2) && y === drawPoints.at(-1)) + ) { + return; + } + if (drawPoints.length >= 4) { + const [x1, y1, x2, y2] = drawPoints.slice(-4); + this.#drawPathString += `C${(x1 + 5 * x2) / 6} ${(y1 + 5 * y2) / 6} ${(5 * x2 + x) / 6} ${(5 * y2 + y) / 6} ${(x2 + x) / 2} ${(y2 + y) / 2}`; + } else { + this.#drawPathString += `L${x} ${y}`; + } + drawPoints.push(x, y); + this.#drawPath.setAttribute("d", this.#drawPathString); + }, + listenerDrawOptions + ); + this.#drawSVG.addEventListener( + "pointerup", + evt => { + const { pointerId: pId } = evt; + if (!isNaN(currentPointerId) && currentPointerId !== pId) { + return; + } + currentPointerId = NaN; + evt.preventDefault(); + this.#drawSVG.releasePointerCapture(pId); + finishDrawAC.abort(); + if (this.#drawPoints.length === 2) { + this.#drawPathString += `L${this.#drawPoints[0]} ${this.#drawPoints[1]}`; + this.#drawPath.setAttribute("d", this.#drawPathString); + } + }, + listenerDrawOptions + ); + }; + if (this.#drawCurves) { + this.#drawSVG.addEventListener("pointerdown", drawCallback, options); + } else { + this.#drawPlaceholder.addEventListener( + "pointerdown", + drawCallback, + options + ); + } + this.#drawThickness.addEventListener( + "input", + () => { + const { value: thickness } = this.#drawThickness; + this.#drawThickness.setAttribute( + "data-l10n-args", + JSON.stringify({ thickness }) + ); + if (!this.#drawCurves) { + return; + } + this.#drawPath.setAttribute("stroke-width", thickness); + this.#drawCurves.thickness = thickness; + }, + options + ); + } + + #initImageTab(reset) { + if (reset) { + this.#resetTab("image"); + } + + this.#disableButtons(this.#imagePath); + + const { signal } = this.#currentTabAC; + const options = { signal }; + const passiveOptions = { passive: true, signal }; + this.#imagePickerLink.addEventListener( + "keydown", + e => { + const { key } = e; + if (key === "Enter" || key === " ") { + stopEvent(e); + this.#imagePicker.click(); + } + }, + options + ); + this.#imagePicker.addEventListener( + "click", + () => { + this.#dialog.classList.toggle("waiting", true); + }, + passiveOptions + ); + this.#imagePicker.addEventListener( + "change", + async () => { + const file = this.#imagePicker.files?.[0]; + if (!file || !SupportedImageMimeTypes.includes(file.type)) { + this.#errorBar.hidden = false; + this.#dialog.classList.toggle("waiting", false); + return; + } + await this.#extractSignature(file); + }, + passiveOptions + ); + this.#imagePicker.addEventListener( + "cancel", + () => { + this.#dialog.classList.toggle("waiting", false); + }, + passiveOptions + ); + this.#imagePlaceholder.addEventListener( + "dragover", + e => { + const { dataTransfer } = e; + for (const { type } of dataTransfer.items) { + if (!SupportedImageMimeTypes.includes(type)) { + continue; + } + dataTransfer.dropEffect = + dataTransfer.effectAllowed === "copy" ? "copy" : "move"; + stopEvent(e); + return; + } + dataTransfer.dropEffect = "none"; + }, + options + ); + this.#imagePlaceholder.addEventListener( + "drop", + e => { + const { + dataTransfer: { files }, + } = e; + if (!files?.length) { + return; + } + for (const file of files) { + if (SupportedImageMimeTypes.includes(file.type)) { + this.#extractSignature(file); + break; + } + } + stopEvent(e); + this.#dialog.classList.toggle("waiting", true); + }, + options + ); + } + + async #extractSignature(file) { + let data; + try { + data = await this.#uiManager.imageManager.getFromFile(file); + } catch (e) { + console.error("SignatureManager.#extractSignature.", e); + } + if (!data) { + this.#errorBar.hidden = false; + this.#dialog.classList.toggle("waiting", false); + return; + } + + const outline = (this.#extractedSignatureData = + this.#currentEditor.getFromImage(data.bitmap)); + + if (!outline) { + this.#dialog.classList.toggle("waiting", false); + return; + } + + this.#imagePlaceholder.hidden = true; + this.#disableButtons(true); + + const svgFactory = new DOMSVGFactory(); + const path = (this.#imagePath = svgFactory.createElement("path")); + this.#imageSVG.setAttribute("viewBox", outline.viewBox); + this.#imageSVG.setAttribute("preserveAspectRatio", "xMidYMid meet"); + this.#imageSVG.append(path); + path.setAttribute("d", outline.toSVGPath()); + if (this.#description.value === "") { + this.#description.value = file.name || ""; + this.#clearDescription.disabled = this.#description.value === ""; + } + + this.#dialog.classList.toggle("waiting", false); + } + + #getOutlineForType() { + return this.#currentEditor.getFromText( + this.#typeInput.value, + window.getComputedStyle(this.#typeInput) + ); + } + + #getOutlineForDraw() { + const { width, height } = this.#drawSVG.getBoundingClientRect(); + return this.#currentEditor.getDrawnSignature( + this.#drawCurves, + width, + height + ); + } + + getSignature(params) { + return this.open(params); + } + + async open({ uiManager, editor }) { + this.#tabsToAltText ||= new Map( + this.#tabButtons.keys().map(name => [name, ""]) + ); + this.#uiManager = uiManager; + this.#currentEditor = editor; + this.#uiManager.removeEditListeners(); + + await this.#overlayManager.open(this.#dialog); + + const tabType = this.#tabButtons.get("type"); + tabType.focus(); + tabType.click(); + } + + #cancel() { + this.#finish(); + } + + #finish() { + if (this.#overlayManager.active === this.#dialog) { + this.#overlayManager.close(this.#dialog); + } + } + + #close() { + if (this.#currentEditor._drawId === null) { + this.#currentEditor.remove(); + } + this.#uiManager?.addEditListeners(); + this.#currentTabAC?.abort(); + this.#currentTabAC = null; + this.#uiManager = null; + this.#currentEditor = null; + + this.#resetCommon(); + for (const [name] of this.#tabButtons) { + this.#resetTab(name); + } + this.#disableButtons(false); + this.#currentTab = null; + this.#tabsToAltText = null; + } + + #add() { + let data; + switch (this.#currentTab) { + case "type": + data = this.#getOutlineForType(); + break; + case "draw": + data = this.#getOutlineForDraw(); + break; + case "image": + data = this.#extractedSignatureData; + break; + } + this.#currentEditor.addSignature(data, /* heightInPage */ 40); + if (this.#saveCheckbox.checked) { + // TODO + } + this.#finish(); + } + + destroy() { + this.#uiManager = null; + this.#finish(); + } +} + +export { SignatureManager }; diff --git a/web/stubs-geckoview.js b/web/stubs-geckoview.js index 23b4ebb6af285..eabba37d208cd 100644 --- a/web/stubs-geckoview.js +++ b/web/stubs-geckoview.js @@ -27,6 +27,7 @@ const PDFPresentationMode = null; const PDFSidebar = null; const PDFThumbnailViewer = null; const SecondaryToolbar = null; +const SignatureManager = null; export { AltTextManager, @@ -43,4 +44,5 @@ export { PDFSidebar, PDFThumbnailViewer, SecondaryToolbar, + SignatureManager, }; diff --git a/web/viewer-geckoview.html b/web/viewer-geckoview.html index 846d704d905c7..907e2c3946f14 100644 --- a/web/viewer-geckoview.html +++ b/web/viewer-geckoview.html @@ -85,6 +85,7 @@ "web-preferences": "./genericcom.js", "web-print_service": "./pdf_print_service.js", "web-secondary_toolbar": "./stubs-geckoview.js", + "web-signature_manager": "./stubs-geckoview.js", "web-toolbar": "./toolbar-geckoview.js" } } diff --git a/web/viewer.html b/web/viewer.html index 4e12d41260529..9f71b65e0e415 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -88,6 +88,7 @@ "web-preferences": "./genericcom.js", "web-print_service": "./pdf_print_service.js", "web-secondary_toolbar": "./secondary_toolbar.js", + "web-signature_manager": "./signature_manager.js", "web-toolbar": "./toolbar.js" } } @@ -682,6 +683,85 @@ + + + + This modal allows the user to create a signature to add to a PDF document. The user can edit the name (which also serves as the alt text), and optionally save the signature for repeated use. + +
+
+ Add a signature +
+
+ + + +
+
+
+ +
+
+ + Draw your signature +
+
+ + +
+
+
+
+ +
+ Drag a file here to upload + + +
+
+
+
+
+ + + + + +
+ +
+
+ + + + +
+
+ +
+ + +
+
+
+
+
diff --git a/web/viewer.js b/web/viewer.js index 2af123a536674..007edc8c1a5e9 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -214,6 +214,32 @@ function getViewerConfiguration() { ), closeButton: document.getElementById("altTextSettingsCloseButton"), }, + addSignatureDialog: { + dialog: document.getElementById("addSignatureDialog"), + panels: document.getElementById("addSignatureActionContainer"), + typeButton: document.getElementById("addSignatureTypeButton"), + typeInput: document.getElementById("addSignatureTypeInput"), + drawButton: document.getElementById("addSignatureDrawButton"), + drawSVG: document.getElementById("addSignatureDraw"), + drawPlaceholder: document.getElementById("addSignatureDrawPlaceholder"), + drawThickness: document.getElementById("addSignatureDrawThickness"), + imageButton: document.getElementById("addSignatureImageButton"), + imageSVG: document.getElementById("addSignatureImage"), + imagePlaceholder: document.getElementById("addSignatureImagePlaceholder"), + imagePicker: document.getElementById("addSignatureFilePicker"), + imagePickerLink: document.getElementById("addSignatureImageBrowse"), + description: document.getElementById("addSignatureDescription"), + clearDescription: document.getElementById( + "addSignatureDescriptionClearButton" + ), + clearButton: document.getElementById("clearSignatureButton"), + saveContainer: document.getElementById("addSignatureSaveContainer"), + saveCheckbox: document.getElementById("addSignatureSaveCheckbox"), + errorBar: document.getElementById("addSignatureError"), + errorCloseButton: document.getElementById("addSignatureErrorCloseButton"), + cancelButton: document.getElementById("addSignatureCancelButton"), + addButton: document.getElementById("addSignatureAddButton"), + }, annotationEditorParams: { editorFreeTextFontSize: document.getElementById("editorFreeTextFontSize"), editorFreeTextColor: document.getElementById("editorFreeTextColor"),