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

Merge pull request #19425 from calixteman/signature_save

[Editor] Add the possibility to compress/decompress the signature data in  order to store them in the logins storage in Firefox (bug 1946171)
This commit is contained in:
calixteman 2025-02-10 19:20:32 +01:00 committed by GitHub
commit e3cca6d513
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 459 additions and 17 deletions

View file

@ -461,13 +461,15 @@ const PDFViewerApplication = {
this.editorUndoBar = new EditorUndoBar(appConfig.editorUndoBar, eventBus);
}
const signatureManager = appConfig.addSignatureDialog
? new SignatureManager(
appConfig.addSignatureDialog,
this.overlayManager,
this.l10n
)
: null;
const signatureManager =
AppOptions.get("enableSignatureEditor") && appConfig.addSignatureDialog
? new SignatureManager(
appConfig.addSignatureDialog,
this.overlayManager,
this.l10n,
externalServices.createSignatureStorage()
)
: null;
const enableHWA = AppOptions.get("enableHWA");
const pdfViewer = new PDFViewer({

View file

@ -19,8 +19,10 @@ import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js";
import { GenericL10n } from "./genericl10n.js";
import { GenericScripting } from "./generic_scripting.js";
import { SignatureStorage } from "./generic_signature_storage.js";
// These strings are from chrome/app/resources/generated_resources_*.xtb.
// eslint-disable-next-line sort-imports
import i18nFileAccessLabels from "./chrome-i18n-allow-access-to-file-urls.json" with { type: "json" };
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME")) {
@ -419,6 +421,10 @@ class ExternalServices extends BaseExternalServices {
createScripting() {
return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
}
createSignatureStorage() {
return new SignatureStorage();
}
}
class MLManager {

View file

@ -44,6 +44,10 @@ class BaseExternalServices {
throw new Error("Not implemented: createScripting");
}
createSignatureStorage() {
throw new Error("Not implemented: createSignatureStorage");
}
updateEditorStates(data) {
throw new Error("Not implemented: updateEditorStates");
}

View file

@ -495,6 +495,72 @@ class MLManager {
}
}
class SignatureStorage {
#signatures = null;
#handleSignature(data) {
return FirefoxCom.requestAsync("handleSignature", data);
}
async getAll() {
if (!this.#signatures) {
this.#signatures = Object.create(null);
const data = await this.#handleSignature({ action: "get" });
if (data) {
for (const { uuid, description, signatureData } of data) {
this.#signatures[uuid] = { description, signatureData };
}
}
}
return this.#signatures;
}
async isFull() {
// We want to store at most 5 signatures.
return Object.keys(await this.getAll()).length === 5;
}
async create(data) {
if (await this.isFull()) {
return null;
}
const uuid = await this.#handleSignature({
action: "create",
...data,
});
if (!uuid) {
return null;
}
this.#signatures[uuid] = data;
return uuid;
}
async delete(uuid) {
const signatures = await this.getAll();
if (!signatures[uuid]) {
return false;
}
if (await this.#handleSignature({ action: "delete", uuid })) {
delete signatures[uuid];
return true;
}
return false;
}
async update(uuid, data) {
const signatures = await this.getAll();
const oldData = signatures[uuid];
if (!oldData) {
return false;
}
if (await this.#handleSignature({ action: "update", uuid, ...data })) {
Object.assign(oldData, data);
return true;
}
return false;
}
}
class ExternalServices extends BaseExternalServices {
updateFindControlState(data) {
FirefoxCom.request("updateFindControlState", data);
@ -581,6 +647,10 @@ class ExternalServices extends BaseExternalServices {
return FirefoxScripting;
}
createSignatureStorage() {
return new SignatureStorage();
}
dispatchGlobalEvent(event) {
FirefoxCom.request("dispatchGlobalEvent", event);
}

View file

@ -0,0 +1,76 @@
/* 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 { getUuid } from "pdfjs-lib";
class SignatureStorage {
// TODO: Encrypt the data in using a password and add a UI for entering it.
// We could use the Web Crypto API for this (see https://bradyjoslin.com/blog/encryption-webcrypto/
// for an example).
#signatures = null;
#save() {
localStorage.setItem("pdfjs.signature", JSON.stringify(this.#signatures));
}
async getAll() {
if (!this.#signatures) {
const data = localStorage.getItem("pdfjs.signature");
this.#signatures = data ? JSON.parse(data) : Object.create(null);
}
return this.#signatures;
}
async isFull() {
return Object.keys(await this.getAll()).length === 5;
}
async create(data) {
if (await this.isFull()) {
return null;
}
const uuid = getUuid();
this.#signatures[uuid] = data;
this.#save();
return uuid;
}
async delete(uuid) {
const signatures = await this.getAll();
if (!signatures[uuid]) {
return false;
}
delete signatures[uuid];
this.#save();
return true;
}
async update(uuid, data) {
const signatures = await this.getAll();
const oldData = signatures[uuid];
if (!oldData) {
return false;
}
Object.assign(oldData, data);
this.#save();
return true;
}
}
export { SignatureStorage };

View file

@ -18,6 +18,7 @@ import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js";
import { GenericL10n } from "./genericl10n.js";
import { GenericScripting } from "./generic_scripting.js";
import { SignatureStorage } from "./generic_signature_storage.js";
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) {
throw new Error(
@ -45,6 +46,10 @@ class ExternalServices extends BaseExternalServices {
createScripting() {
return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
}
createSignatureStorage() {
return new SignatureStorage();
}
}
class MLManager {

View file

@ -32,6 +32,7 @@ const {
getDocument,
getFilenameFromUrl,
getPdfFilenameFromUrl,
getUuid,
getXfaPageViewport,
GlobalWorkerOptions,
ImageKind,
@ -52,6 +53,7 @@ const {
ResponseException,
setLayerDimensions,
shadow,
SignatureExtractor,
stopEvent,
SupportedImageMimeTypes,
TextLayer,
@ -81,6 +83,7 @@ export {
getDocument,
getFilenameFromUrl,
getPdfFilenameFromUrl,
getUuid,
getXfaPageViewport,
GlobalWorkerOptions,
ImageKind,
@ -101,6 +104,7 @@ export {
ResponseException,
setLayerDimensions,
shadow,
SignatureExtractor,
stopEvent,
SupportedImageMimeTypes,
TextLayer,

View file

@ -571,14 +571,23 @@
user-select: none;
}
#addSignatureSaveWarning {
&:not(.fullStorage) #addSignatureSaveWarning {
display: none;
}
&.fullStorage #addSignatureSaveWarning {
display: block;
opacity: 1;
color: var(--save-warning-color);
font-size: 11px;
}
&[disabled] {
&:is([disabled], .fullStorage) {
pointer-events: none;
opacity: 0.4;
> :not(#addSignatureSaveWarning) {
opacity: 0.4;
}
}
}
}

View file

@ -16,6 +16,7 @@
import {
DOMSVGFactory,
noContextMenu,
SignatureExtractor,
stopEvent,
SupportedImageMimeTypes,
} from "pdfjs-lib";
@ -81,6 +82,8 @@ class SignatureManager {
#overlayManager;
#signatureStorage;
#uiManager = null;
static #l10nDescription = null;
@ -111,7 +114,8 @@ class SignatureManager {
saveContainer,
},
overlayManager,
l10n
l10n,
signatureStorage
) {
this.#addButton = addButton;
this.#clearButton = clearButton;
@ -131,6 +135,7 @@ class SignatureManager {
this.#saveContainer = saveContainer;
this.#typeInput = typeInput;
this.#l10n = l10n;
this.#signatureStorage = signatureStorage;
SignatureManager.#l10nDescription ||= Object.freeze({
signature: "pdfjs-editor-add-signature-description-default-when-drawing",
@ -564,7 +569,7 @@ class SignatureManager {
return;
}
const outline = (this.#extractedSignatureData =
const { outline } = (this.#extractedSignatureData =
this.#currentEditor.getFromImage(data.bitmap));
if (!outline) {
@ -617,6 +622,10 @@ class SignatureManager {
this.#currentEditor = editor;
this.#uiManager.removeEditListeners();
const isStorageFull = await this.#signatureStorage.isFull();
this.#saveContainer.classList.toggle("fullStorage", isStorageFull);
this.#saveCheckbox.checked = !isStorageFull;
await this.#overlayManager.open(this.#dialog);
const tabType = this.#tabButtons.get("type");
@ -653,7 +662,7 @@ class SignatureManager {
this.#tabsToAltText = null;
}
#add() {
async #add() {
let data;
switch (this.#currentTab) {
case "type":
@ -667,12 +676,28 @@ class SignatureManager {
break;
}
this.#currentEditor.addSignature(
data,
data.outline,
/* heightInPage */ 40,
this.#description.value
);
if (this.#saveCheckbox.checked) {
// TODO
const description = this.#description.value;
const { newCurves, areContours, thickness, width, height } = data;
const signatureData = await SignatureExtractor.compressSignature({
outlines: newCurves,
areContours,
thickness,
width,
height,
});
const uuid = (this.#currentEditor._signatureUUID =
await this.#signatureStorage.create({
description,
signatureData,
}));
if (!uuid) {
console.warn("SignatureManager.add: cannot save the signature.");
}
}
this.#finish();
}

View file

@ -742,7 +742,7 @@ See https://github.com/adobe-type-tools/cmap-resources
<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>
<span id="addSignatureSaveWarning" 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">