Skip to content

Commit

Permalink
[Editor] Add the possibility to compress/decompress the signature dat…
Browse files Browse the repository at this point in the history
…a in order to store them in the logins storage in Firefox (bug 1946171)
  • Loading branch information
calixteman committed Feb 5, 2025
1 parent 5f94674 commit b13883f
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 6 deletions.
161 changes: 160 additions & 1 deletion src/display/editor/drawers/signaturedraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* limitations under the License.
*/

import { fromBase64Util, toBase64Util, warn } from "../../../shared/util.js";
import { ContourDrawOutline } from "./contour.js";
import { InkDrawOutline } from "./inkdraw.js";
import { Outline } from "./outline.js";
Expand Down Expand Up @@ -607,12 +608,14 @@ class SignatureExtractor {
const ratio = Math.min(pageWidth / width, pageHeight / height);
const xScale = ratio / pageWidth;
const yScale = ratio / pageHeight;
const newCurves = [];

for (const { points } of curves) {
const reducedPoints = mustSmooth ? this.#douglasPeucker(points) : points;
if (!reducedPoints) {
continue;
}
newCurves.push(reducedPoints);

const len = reducedPoints.length;
const newPoints = new Float32Array(len);
Expand Down Expand Up @@ -660,7 +663,163 @@ class SignatureExtractor {
innerMargin
);

return outline;
return { outline, newCurves };
}

static async compressSignature({ outlines, areContours, thickness }) {
// We create a single array containing all the outlines.
// The format is the following:
// - 4 bytes: data length.
// - 4 bytes: version.
// - 4 bytes: 0 if it's a contour, 1 if it's an ink.
// - 4 bytes: thickness.
// - 4 bytes: number of drawings.
// - 4 bytes: size of the buffer containing the diff of the coordinates.
// - 4 bytes: number of points in the first drawing.
// - 4 bytes: x coordinate of the first point.
// - 4 bytes: y coordinate of the first point.
// - 4 bytes: number of points in the second drawing.
// - 4 bytes: x coordinate of the first point.
// - 4 bytes: y coordinate of the first point.
// - ...
// - The buffer containing the diff of the coordinates.

// The coordinates are supposed to be positive integers.

// We also compute the min and max difference between two points.
// This will help us to determine the type of the buffer (Int8, Int16 or
// Int32) in order to minimize the amount of data we have.
let minDiff = Infinity;
let maxDiff = -Infinity;
let outlinesLength = 0;
for (const points of outlines) {
outlinesLength += points.length;
for (let i = 2, ii = points.length; i < ii; i++) {
const dx = points[i] - points[i - 2];
minDiff = Math.min(minDiff, dx);
maxDiff = Math.max(maxDiff, dx);
}
}

let bufferType;
if (minDiff >= -128 && maxDiff <= 127) {
bufferType = Int8Array;
} else if (minDiff >= -32768 && maxDiff <= 32767) {
bufferType = Int16Array;
} else {
bufferType = Int32Array;
}

const len = outlines.length;
const headerLength = 6 + 3 * len;
const header = new Uint32Array(headerLength);

let offset = 0;
header[offset++] =
headerLength * Uint32Array.BYTES_PER_ELEMENT +
(outlinesLength - 2 * len) * bufferType.BYTES_PER_ELEMENT;
header[offset++] = 0; // Version.
header[offset++] = areContours ? 0 : 1;
header[offset++] = Math.max(0, Math.floor(thickness ?? 0));
header[offset++] = len;
header[offset++] = bufferType.BYTES_PER_ELEMENT;
for (const points of outlines) {
header[offset++] = points.length - 2;
header[offset++] = points[0];
header[offset++] = points[1];
}

const cs = new CompressionStream("deflate-raw");
const writer = cs.writable.getWriter();
await writer.ready;

writer.write(header);
const BufferCtor = bufferType.prototype.constructor;
for (const points of outlines) {
const diffs = new BufferCtor(points.length - 2);
for (let i = 2, ii = points.length; i < ii; i++) {
diffs[i - 2] = points[i] - points[i - 2];
}
writer.write(diffs);
}

writer.close();

const buf = await new Response(cs.readable).arrayBuffer();
const bytes = new Uint8Array(buf);

return toBase64Util(bytes);
}

static async decompressSignature(signatureData) {
try {
const bytes = fromBase64Util(signatureData);
const { readable, writable } = new DecompressionStream("deflate-raw");
const writer = writable.getWriter();
await writer.ready;

// We can't await writer.write() because it'll block until the reader
// starts which happens few lines below.
writer
.write(bytes)
.then(async () => {
await writer.ready;
await writer.close();
})
.catch(() => {});

let data = null;
let offset = 0;
for await (const chunk of readable) {
data ||= new Uint8Array(new Uint32Array(chunk.buffer)[0]);
data.set(chunk, offset);
offset += chunk.length;
}

// We take a bit too much data for the header but it's fine.
const header = new Uint32Array(data.buffer, 0, data.length >> 2);
const areContours = header[2] === 0;
const thickness = header[3];
const numberOfDrawings = header[4];
const outlines = [];
const diffsOffset =
(6 + 3 * numberOfDrawings) * Uint32Array.BYTES_PER_ELEMENT;
let diffs;

switch (header[5]) {
case Int8Array.BYTES_PER_ELEMENT:
diffs = new Int8Array(data.buffer, diffsOffset);
break;
case Int16Array.BYTES_PER_ELEMENT:
diffs = new Int16Array(data.buffer, diffsOffset);
break;
case Int32Array.BYTES_PER_ELEMENT:
diffs = new Int32Array(data.buffer, diffsOffset);
break;
}

offset = 0;
for (let i = 0; i < numberOfDrawings; i++) {
const len = header[3 * i + 6];
const points = new Float32Array(len + 2);
outlines.push(points);

points[0] = header[3 * i + 7];
points[1] = header[3 * i + 8];
for (let j = 0; j < len; j++) {
points[j + 2] = points[j] + diffs[offset++];
}
}

return {
areContours,
thickness,
outlines,
};
} catch (e) {
warn(`decompressSignature: ${e}`);
return null;
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/display/editor/signature.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ class SignatureEditor extends DrawingEditor {
areContours: false,
});
}

compress(data) {
return SignatureExtractor.compressSignature(data);
}
}

export { SignatureEditor };
46 changes: 46 additions & 0 deletions test/unit/editor_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import { CommandManager } from "../../src/display/editor/tools.js";
import { SignatureExtractor } from "../../src/display/editor/drawers/signaturedraw.js";

describe("editor", function () {
describe("Command Manager", function () {
Expand Down Expand Up @@ -90,4 +91,49 @@ describe("editor", function () {
manager.add({ ...makeDoUndo(5), mustExec: true });
expect(x).toEqual(11);
});

it("should check signature compression/decompression", async () => {
let gen = n => new Float32Array(crypto.getRandomValues(new Uint16Array(n)));
let outlines = [102, 28, 254, 4536, 10, 14532, 512].map(gen);
const signature = {
outlines,
areContours: false,
thickness: 1,
};
let compressed = await SignatureExtractor.compressSignature(signature);
let decompressed = await SignatureExtractor.decompressSignature(compressed);
expect(decompressed).toEqual(signature);

signature.thickness = 2;
compressed = await SignatureExtractor.compressSignature(signature);
decompressed = await SignatureExtractor.decompressSignature(compressed);
expect(decompressed).toEqual(signature);

signature.areContours = true;
compressed = await SignatureExtractor.compressSignature(signature);
decompressed = await SignatureExtractor.decompressSignature(compressed);
expect(decompressed).toEqual(signature);

// Numbers are small enough to be compressed with Uint8Array.
gen = n =>
new Float32Array(
crypto.getRandomValues(new Uint8Array(n)).map(x => x / 10)
);
outlines = [100, 200, 300, 10, 80].map(gen);
signature.outlines = outlines;
compressed = await SignatureExtractor.compressSignature(signature);
decompressed = await SignatureExtractor.decompressSignature(compressed);
expect(decompressed).toEqual(signature);

// Numbers are large enough to be compressed with Uint16Array.
gen = n =>
new Float32Array(
crypto.getRandomValues(new Uint16Array(n)).map(x => x / 10)
);
outlines = [100, 200, 300, 10, 80].map(gen);
signature.outlines = outlines;
compressed = await SignatureExtractor.compressSignature(signature);
decompressed = await SignatureExtractor.decompressSignature(compressed);
expect(decompressed).toEqual(signature);
});
});
3 changes: 2 additions & 1 deletion web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,8 @@ const PDFViewerApplication = {
? new SignatureManager(
appConfig.addSignatureDialog,
this.overlayManager,
this.l10n
this.l10n,
externalServices
)
: null;

Expand Down
8 changes: 8 additions & 0 deletions web/external_services.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ class BaseExternalServices {
}

dispatchGlobalEvent(_event) {}

saveSignature(data) {
throw new Error("Not implemented: saveSignature");
}

getSignatures() {
throw new Error("Not implemented: getSignatures");
}
}

export { BaseExternalServices };
8 changes: 8 additions & 0 deletions web/firefoxcom.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,14 @@ class ExternalServices extends BaseExternalServices {
dispatchGlobalEvent(event) {
FirefoxCom.request("dispatchGlobalEvent", event);
}

saveSignature(data) {
return FirefoxCom.requestAsync("saveSignature", data);
}

getSignatures() {
return FirefoxCom.requestAsync("getSignatures", null);
}
}

export { DownloadManager, ExternalServices, initCom, MLManager, Preferences };
28 changes: 24 additions & 4 deletions web/signature_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class SignatureManager {

#extractedSignatureData = null;

#externalServices;

#imagePath = null;

#imagePicker;
Expand Down Expand Up @@ -108,7 +110,8 @@ class SignatureManager {
saveCheckbox,
},
overlayManager,
l10n
l10n,
externalServices
) {
this.#addButton = addButton;
this.#clearButton = clearButton;
Expand All @@ -127,6 +130,7 @@ class SignatureManager {
this.#saveCheckbox = saveCheckbox;
this.#typeInput = typeInput;
this.#l10n = l10n;
this.#externalServices = externalServices;

SignatureManager.#l10nDescription ||= Object.freeze({
signature: "pdfjs-editor-add-signature-description-default-when-drawing",
Expand Down Expand Up @@ -556,7 +560,7 @@ class SignatureManager {
return;
}

const outline = (this.#extractedSignatureData =
const { outline } = (this.#extractedSignatureData =
this.#currentEditor.extractSignature(data.bitmap));

if (!outline) {
Expand Down Expand Up @@ -645,7 +649,7 @@ class SignatureManager {
this.#tabsToAltText = null;
}

#add() {
async #add() {
let data;
switch (this.#currentTab) {
case "type":
Expand All @@ -658,7 +662,23 @@ class SignatureManager {
data = this.#extractedSignatureData;
break;
}
this.#currentEditor.addSignature(data, /* heightInPage */ 40);
this.#currentEditor.addSignature(data.outline, /* heightInPage */ 40);
if (this.#saveCheckbox.checked) {
const description = this.#description.value;
const compressedData = await this.#currentEditor.compress({
outlines: data.newCurves,
areContours: this.#currentTab === "draw",
thickness: this.#currentTab === "draw" ? this.#drawThickness.value : 0,
});
try {
await this.#externalServices.saveSignature({
username: description,
password: compressedData,
});
} catch (e) {
console.error("SignatureManager.add.", e);
}
}
this.#finish();
}

Expand Down

0 comments on commit b13883f

Please sign in to comment.