1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 06:38:07 +02:00

Merge pull request #19414 from calixteman/signature_dialog2

[Editor] Add a new dialog for the signature editor (bug 1945574)
This commit is contained in:
calixteman 2025-02-06 16:20:01 +01:00 committed by GitHub
commit 08663f715b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1596 additions and 66 deletions

View file

@ -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",
};

View file

@ -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 = Youve reached the limit of 5 saved signatures. Remove one to save more.
pdfjs-editor-add-signature-image-upload-error-title = Couldnt 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

View file

@ -318,6 +318,10 @@ class InkDrawOutline extends Outline {
this.#computeBbox();
}
get thickness() {
return this.#thickness;
}
setLastElement(element) {
this.#lines.push(element);
return {

View file

@ -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 };

View file

@ -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,69 +125,90 @@ 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();
addSignature(outline, heightInPage) {
const { x: savedX, y: savedY } = this;
this.#isExtracted = outline instanceof ContourDrawOutline;
let drawingOptions;
if (this.#isExtracted) {
drawingOptions = SignatureEditor.getDefaultDrawingOptions();
} else {
drawingOptions = SignatureEditor._defaultDrawnSignatureOptions.clone();
drawingOptions.updateProperties({ "stroke-width": outline.thickness });
}
this._addOutlines({
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;
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();
this.width *= newHeight / this.height;
this.height = newHeight;
this.setDims(parentWidth * this.width, parentHeight * this.height);
this.x = savedX;
this.y = savedY;
this.center();
const bitmap = await promise;
this._onResized();
this.onScaleChanging();
this.rotate();
this._uiManager.addToAnnotationStorage(this);
this.div.hidden = false;
}
getFromImage(bitmap) {
const {
rawDims: { pageWidth, pageHeight },
rotation,
} = this.parent.viewport;
let drawOutlines;
if (bitmap?.bitmap) {
drawOutlines = SignatureExtractor.process(
bitmap.bitmap,
pageWidth,
pageHeight,
rotation,
SignatureEditor._INNER_MARGIN
);
} else {
drawOutlines = SignatureExtractor.extractContoursFromText(
"Hello PDF.js' World !!",
{ fontStyle: "italic", fontWeight: "400", fontFamily: "cursive" },
pageWidth,
pageHeight,
rotation,
SignatureEditor._INNER_MARGIN
);
}
this._addOutlines({
drawOutlines,
drawingOptions: SignatureEditor.getDefaultDrawingOptions(),
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,
});
this.onScaleChanging();
this.rotate();
this.div.hidden = false;
}
}

View file

@ -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, {

View file

@ -15,6 +15,7 @@
@import url(draw_layer_builder.css);
@import url(toggle_button.css);
@import url(signature_manager.css);
:root {
--outline-width: 2px;

View file

@ -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:

View file

@ -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 {

View file

@ -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,

586
web/signature_manager.css Normal file
View file

@ -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;
}
}
}
}
}
}

682
web/signature_manager.js Normal file
View file

@ -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 };

View file

@ -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,
};

View file

@ -85,6 +85,7 @@ See https://github.com/adobe-type-tools/cmap-resources
"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"
}
}

View file

@ -88,6 +88,7 @@ See https://github.com/adobe-type-tools/cmap-resources
"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 @@ See https://github.com/adobe-type-tools/cmap-resources
</div>
</div>
</dialog>
<dialog class="dialog" id="addSignatureDialog" aria-labelledby="addSignatureDialogLabel">
<span id="addSignatureDialogLabel" data-l10n-id="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.
</span>
<div id="addSignatureContainer" class="mainContainer">
<div class="title">
<span id="addSignatureTitle" role="sectionhead" data-l10n-id="pdfjs-editor-add-signature-dialog-title" tabindex="0" class="title">Add a signature</span>
</div>
<div role="tablist" id="addSignatureOptions">
<button id="addSignatureTypeButton" type="button" role="tab" aria-selected="true" aria-controls="addSignatureTypeContainer" data-l10n-id="pdfjs-editor-add-signature-type-button" tabindex="0">
Type
</button>
<button id="addSignatureDrawButton" type="button" role="tab" aria-selected="false" aria-controls="addSignatureDrawContainer" data-l10n-id="pdfjs-editor-add-signature-draw-button" tabindex="0">
Draw
</button>
<button id="addSignatureImageButton" type="button" role="tab" aria-selected="false" aria-controls="addSignatureImageContainer" data-l10n-id="pdfjs-editor-add-signature-image-button" tabindex="-1">
Image
</button>
</div>
<div id="addSignatureActionContainer" data-selected="type">
<div id="addSignatureTypeContainer" role="tabpanel" aria-labelledby="addSignatureTypeContainer">
<input id="addSignatureTypeInput" type="text" data-l10n-id="pdfjs-editor-add-signature-type-input" aria-label="Type your signature" placeholder="Type your signature" tabindex="0"></input>
</div>
<div id="addSignatureDrawContainer" role="tabpanel" aria-labelledby="addSignatureDrawButton" tabindex="-1">
<svg id="addSignatureDraw" xmlns="http://www.w3.org/2000/svg" aria-labelledby="addSignatureDrawPlaceholder"></svg>
<span id="addSignatureDrawPlaceholder" data-l10n-id="pdfjs-editor-add-signature-draw-placeholder">Draw your signature</span>
<div id="thickness">
<div>
<label for="addSignatureDrawThickness" data-l10n-id="pdfjs-editor-add-signature-draw-thickness-range-label">Thickness</label>
<input type="range" id="addSignatureDrawThickness" min="1" max="5" step="1" value="1" data-l10n-id="pdfjs-editor-add-signature-draw-thickness-range" data-l10n-args='{ "thickness": 1 }' tabindex="0">
</div>
</div>
</div>
<div id="addSignatureImageContainer" role="tabpanel" aria-labelledby="addSignatureImageButton" tabindex="-1">
<svg id="addSignatureImage" xmlns="http://www.w3.org/2000/svg" aria-labelledby="addSignatureImagePlaceholder"></svg>
<div id="addSignatureImagePlaceholder">
<span data-l10n-id="pdfjs-editor-add-signature-image-placeholder">Drag a file here to upload</span>
<label id="addSignatureImageBrowse" for="addSignatureFilePicker" tabindex="0">
<a data-l10n-id="pdfjs-editor-add-signature-image-browse-link">Or browse image files</a>
</label>
<input id="addSignatureFilePicker" type="file"></input>
</div>
</div>
<div id="addSignatureControls" disabled>
<div id="horizontalContainer">
<div id="addSignatureDescriptionContainer">
<label for="addSignatureDescription" data-l10n-id="pdfjs-editor-add-signature-description-label">Description (alt text)</span></label>
<span id="inputWithClearButton">
<input type="text" id="addSignatureDescription" data-l10n-id="pdfjs-editor-add-signature-description-input" data-l10n-args='{ "default": "", "useDefault": true }' tabindex="0"></input>
<button id="addSignatureDescriptionClearButton" type="button" tabindex="0" aria-hidden="true"></button>
</span>
</div>
<button id="clearSignatureButton" type="button" data-l10n-id="pdfjs-editor-add-signature-clear-button" tabindex="0"><span data-l10n-id="pdfjs-editor-add-signature-clear-button-label">Clear signature</span></button>
</div>
<div id="addSignatureSaveContainer">
<input type="checkbox" id="addSignatureSaveCheckbox" checked="true" tabindex="0"></input>
<label for="addSignatureSaveCheckbox" data-l10n-id="pdfjs-editor-add-signature-save-checkbox">Save signature</label>
<span></span>
<span id="addSignatureSaveWarning" hidden="true" data-l10n-id="pdfjs-editor-add-signature-save-warning-message">Youve reached the limit of 5 saved signatures. Remove one to save more.</span>
</div>
</div>
<div id="addSignatureError" hidden="true" class="messageBar">
<div>
<div>
<span class="title" data-l10n-id="pdfjs-editor-add-signature-image-upload-error-title">Couldnt upload image</span>
<span class="description" data-l10n-id="pdfjs-editor-add-signature-image-upload-error-description">Check your network connection or try another image.</span>
</div>
<button id="addSignatureErrorCloseButton" class="closeButton" type="button" tabindex="0" title="Close"><span data-l10n-id="pdfjs-editor-add-signature-error-close-button">Close</span></button>
</div>
</div>
<div id="buttons" class="dialogButtonsGroup">
<button id="addSignatureCancelButton" type="button" class="secondaryButton" tabindex="0"><span data-l10n-id="pdfjs-editor-add-signature-cancel-button">Cancel</span></button>
<button id="addSignatureAddButton" type="button" class="primaryButton" disabled tabindex="0"><span data-l10n-id="pdfjs-editor-add-signature-add-button">Add</span></button>
</div>
</div>
</div>
</dialog>
<!--#if !MOZCENTRAL-->
<dialog id="printServiceDialog" style="min-width: 200px;">
<div class="row">

View file

@ -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"),