Skip to content

Commit

Permalink
Add the possibility to compress/decompress the signature data in orde…
Browse files Browse the repository at this point in the history
…r to store them in the logins storage in Firefox (bug 1946171)
  • Loading branch information
calixteman committed Feb 7, 2025
1 parent 651d712 commit e05c1a3
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 11 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
5 changes: 5 additions & 0 deletions src/display/editor/signature.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class SignatureEditor extends DrawingEditor {
constructor(params) {
super({ ...params, mustBeCommitted: true, name: "signatureEditor" });
this._willKeepAspectRatio = true;
this._signatureUUID = null;
}

/** @inheritdoc */
Expand Down Expand Up @@ -210,6 +211,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);
});
});
15 changes: 13 additions & 2 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ import {
} from "pdfjs-lib";
import { AppOptions, OptionKind } from "./app_options.js";
import { EventBus, FirefoxEventBus } from "./event_utils.js";
import { ExternalServices, initCom, MLManager } from "web-external_services";
import {
ExternalServices,
initCom,
MLManager,
SignatureStorage,
} from "web-external_services";
import {
ImageAltTextSettings,
NewAltTextManager,
Expand Down Expand Up @@ -164,6 +169,7 @@ const PDFViewerApplication = {
url: "",
baseUrl: "",
mlManager: null,
signatureStorage: null,
_downloadUrl: "",
_eventBusAbortController: null,
_windowAbortController: null,
Expand Down Expand Up @@ -237,6 +243,10 @@ const PDFViewerApplication = {
});
}

if (AppOptions.get("enableSignatureEditor")) {
this.signatureStorage = new SignatureStorage();
}

// Ensure that the `L10n`-instance has been initialized before creating
// e.g. the various viewer components.
this.l10n = await this.externalServices.createL10n();
Expand Down Expand Up @@ -464,7 +474,8 @@ const PDFViewerApplication = {
? new SignatureManager(
appConfig.addSignatureDialog,
this.overlayManager,
this.l10n
this.l10n,
this.signatureStorage
)
: null;

Expand Down
42 changes: 41 additions & 1 deletion web/chromecom.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,44 @@ class MLManager {
}
}

export { ExternalServices, initCom, MLManager, Preferences };
class SignatureStorage {
#signatures = null;

async getAll() {
return (this.#signatures ||= new Map());
}

async isFull() {
return (await this.getAll()).size === 4;
}

async create(data) {
if (await this.isFull()) {
return null;
}
const uuid = crypto.randomUUID();
this.#signatures.set(uuid, data);
return uuid;
}

async delete(uuid) {
const signatures = await this.getAll();
if (!signatures.has(uuid)) {
return false;
}
signatures.delete(uuid);
return true;
}

async update(uuid, data) {
const signatures = await this.getAll();
const oldData = signatures.get(uuid);
if (!oldData) {
return false;
}
Object.assign(oldData, data);
return true;
}
}

export { ExternalServices, initCom, MLManager, Preferences, SignatureStorage };
Loading

0 comments on commit e05c1a3

Please sign in to comment.