mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-22 16:18:08 +02:00
Merge pull request #19339 from calixteman/signature_tools
[Editor] (WIP) Add a new tool in order to add an handwritten signature to a pdf (bug 1942343)
This commit is contained in:
commit
42c2b7b657
18 changed files with 839 additions and 9 deletions
|
@ -31,6 +31,7 @@ import { FreeTextEditor } from "./freetext.js";
|
|||
import { HighlightEditor } from "./highlight.js";
|
||||
import { InkEditor } from "./ink.js";
|
||||
import { setLayerDimensions } from "../display_utils.js";
|
||||
import { SignatureEditor } from "./signature.js";
|
||||
import { StampEditor } from "./stamp.js";
|
||||
|
||||
/**
|
||||
|
@ -89,10 +90,13 @@ class AnnotationEditorLayer {
|
|||
static _initialized = false;
|
||||
|
||||
static #editorTypes = new Map(
|
||||
[FreeTextEditor, InkEditor, StampEditor, HighlightEditor].map(type => [
|
||||
type._editorType,
|
||||
type,
|
||||
])
|
||||
[
|
||||
FreeTextEditor,
|
||||
InkEditor,
|
||||
StampEditor,
|
||||
HighlightEditor,
|
||||
SignatureEditor,
|
||||
].map(type => [type._editorType, type])
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -758,7 +762,11 @@ class AnnotationEditorLayer {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.#uiManager.getMode() === AnnotationEditorType.STAMP) {
|
||||
const currentMode = this.#uiManager.getMode();
|
||||
if (
|
||||
currentMode === AnnotationEditorType.STAMP ||
|
||||
currentMode === AnnotationEditorType.SIGNATURE
|
||||
) {
|
||||
this.#uiManager.unselectAll();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -91,6 +91,10 @@ class DrawingEditor extends AnnotationEditor {
|
|||
super(params);
|
||||
this.#mustBeCommitted = params.mustBeCommitted || false;
|
||||
|
||||
this._addOutlines(params);
|
||||
}
|
||||
|
||||
_addOutlines(params) {
|
||||
if (params.drawOutlines) {
|
||||
this.#createDrawOutlines(params);
|
||||
this.#addToDrawLayer();
|
||||
|
|
28
src/display/editor/drawers/contour.js
Normal file
28
src/display/editor/drawers/contour.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
/* Copyright 2024 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 { InkDrawOutline } from "./inkdraw.js";
|
||||
|
||||
class ContourDrawOutline extends InkDrawOutline {
|
||||
toSVGPath() {
|
||||
let path = super.toSVGPath();
|
||||
if (!path.endsWith("Z")) {
|
||||
path += "Z";
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
export { ContourDrawOutline };
|
553
src/display/editor/drawers/signaturedraw.js
Normal file
553
src/display/editor/drawers/signaturedraw.js
Normal file
|
@ -0,0 +1,553 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
import { ContourDrawOutline } from "./contour.js";
|
||||
import { Outline } from "./outline.js";
|
||||
|
||||
/**
|
||||
* Basic text editor in order to create a Signature annotation.
|
||||
*/
|
||||
class SignatureExtractor {
|
||||
static #PARAMETERS = {
|
||||
maxDim: 512,
|
||||
sigmaSFactor: 0.02,
|
||||
sigmaR: 25,
|
||||
kernelSize: 16,
|
||||
};
|
||||
|
||||
static #neighborIndexToId(i0, j0, i, j) {
|
||||
/*
|
||||
The idea is to map the neighbors of a pixel into a unique id.
|
||||
3 2 1
|
||||
4 X 0
|
||||
5 6 7
|
||||
*/
|
||||
|
||||
i -= i0;
|
||||
j -= j0;
|
||||
|
||||
if (i === 0) {
|
||||
return j > 0 ? 0 : 4;
|
||||
}
|
||||
|
||||
if (i === 1) {
|
||||
return j + 6;
|
||||
}
|
||||
|
||||
return 2 - j;
|
||||
}
|
||||
|
||||
static #neighborIdToIndex = new Int32Array([
|
||||
0, 1, -1, 1, -1, 0, -1, -1, 0, -1, 1, -1, 1, 0, 1, 1,
|
||||
]);
|
||||
|
||||
static #clockwiseNonZero(buf, width, i0, j0, i, j, offset) {
|
||||
const id = this.#neighborIndexToId(i0, j0, i, j);
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const kk = (-k + id - offset + 16) % 8;
|
||||
const shiftI = this.#neighborIdToIndex[2 * kk];
|
||||
const shiftJ = this.#neighborIdToIndex[2 * kk + 1];
|
||||
if (buf[(i0 + shiftI) * width + (j0 + shiftJ)] !== 0) {
|
||||
return kk;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static #counterClockwiseNonZero(buf, width, i0, j0, i, j, offset) {
|
||||
const id = this.#neighborIndexToId(i0, j0, i, j);
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const kk = (k + id + offset + 16) % 8;
|
||||
const shiftI = this.#neighborIdToIndex[2 * kk];
|
||||
const shiftJ = this.#neighborIdToIndex[2 * kk + 1];
|
||||
if (buf[(i0 + shiftI) * width + (j0 + shiftJ)] !== 0) {
|
||||
return kk;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static #findContours(buf, width, height, threshold) {
|
||||
// Based on the Suzuki's algorithm:
|
||||
// https://web.archive.org/web/20231213161741/https://www.nevis.columbia.edu/~vgenty/public/suzuki_et_al.pdf
|
||||
|
||||
const N = buf.length;
|
||||
const types = new Int32Array(N);
|
||||
for (let i = 0; i < N; i++) {
|
||||
types[i] = buf[i] <= threshold ? 1 : 0;
|
||||
}
|
||||
|
||||
for (let i = 1; i < height - 1; i++) {
|
||||
types[i * width] = types[i * width + width - 1] = 0;
|
||||
}
|
||||
for (let i = 0; i < width; i++) {
|
||||
types[i] = types[width * height - 1 - i] = 0;
|
||||
}
|
||||
|
||||
let nbd = 1;
|
||||
let lnbd;
|
||||
const contours = [];
|
||||
|
||||
for (let i = 1; i < height - 1; i++) {
|
||||
lnbd = 1;
|
||||
for (let j = 1; j < width - 1; j++) {
|
||||
const ij = i * width + j;
|
||||
const pix = types[ij];
|
||||
if (pix === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let i2 = i;
|
||||
let j2 = j;
|
||||
|
||||
if (pix === 1 && types[ij - 1] === 0) {
|
||||
// Outer border.
|
||||
nbd += 1;
|
||||
j2 -= 1;
|
||||
} else if (pix >= 1 && types[ij + 1] === 0) {
|
||||
// Hole border.
|
||||
nbd += 1;
|
||||
j2 += 1;
|
||||
if (pix > 1) {
|
||||
lnbd = pix;
|
||||
}
|
||||
} else {
|
||||
if (pix !== 1) {
|
||||
lnbd = Math.abs(pix);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const points = [j, i];
|
||||
const isHole = j2 === j + 1;
|
||||
const contour = {
|
||||
isHole,
|
||||
points,
|
||||
id: nbd,
|
||||
parent: 0,
|
||||
};
|
||||
contours.push(contour);
|
||||
|
||||
let contour0;
|
||||
for (const c of contours) {
|
||||
if (c.id === lnbd) {
|
||||
contour0 = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!contour0) {
|
||||
contour.parent = isHole ? lnbd : 0;
|
||||
} else if (contour0.isHole) {
|
||||
contour.parent = isHole ? contour0.parent : lnbd;
|
||||
} else {
|
||||
contour.parent = isHole ? lnbd : contour0.parent;
|
||||
}
|
||||
|
||||
const k = this.#clockwiseNonZero(types, width, i, j, i2, j2, 0);
|
||||
if (k === -1) {
|
||||
types[ij] = -nbd;
|
||||
if (types[ij] !== 1) {
|
||||
lnbd = Math.abs(types[ij]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let shiftI = this.#neighborIdToIndex[2 * k];
|
||||
let shiftJ = this.#neighborIdToIndex[2 * k + 1];
|
||||
const i1 = i + shiftI;
|
||||
const j1 = j + shiftJ;
|
||||
i2 = i1;
|
||||
j2 = j1;
|
||||
let i3 = i;
|
||||
let j3 = j;
|
||||
|
||||
while (true) {
|
||||
const kk = this.#counterClockwiseNonZero(
|
||||
types,
|
||||
width,
|
||||
i3,
|
||||
j3,
|
||||
i2,
|
||||
j2,
|
||||
1
|
||||
);
|
||||
shiftI = this.#neighborIdToIndex[2 * kk];
|
||||
shiftJ = this.#neighborIdToIndex[2 * kk + 1];
|
||||
const i4 = i3 + shiftI;
|
||||
const j4 = j3 + shiftJ;
|
||||
points.push(j4, i4);
|
||||
const ij3 = i3 * width + j3;
|
||||
if (types[ij3 + 1] === 0) {
|
||||
types[ij3] = -nbd;
|
||||
} else if (types[ij3] === 1) {
|
||||
types[ij3] = nbd;
|
||||
}
|
||||
|
||||
if (i4 === i && j4 === j && i3 === i1 && j3 === j1) {
|
||||
if (types[ij] !== 1) {
|
||||
lnbd = Math.abs(types[ij]);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
i2 = i3;
|
||||
j2 = j3;
|
||||
i3 = i4;
|
||||
j3 = j4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return contours;
|
||||
}
|
||||
|
||||
static #douglasPeuckerHelper(points, start, end, output) {
|
||||
// Based on the Douglas-Peucker algorithm:
|
||||
// https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
|
||||
if (end - start <= 4) {
|
||||
for (let i = start; i < end - 2; i += 2) {
|
||||
output.push(points[i], points[i + 1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ax = points[start];
|
||||
const ay = points[start + 1];
|
||||
const abx = points[end - 4] - ax;
|
||||
const aby = points[end - 3] - ay;
|
||||
const dist = Math.hypot(abx, aby);
|
||||
const nabx = abx / dist;
|
||||
const naby = aby / dist;
|
||||
const aa = nabx * ay - naby * ax;
|
||||
|
||||
// Guessing the epsilon value.
|
||||
// See "A novel framework for making dominant point detection methods
|
||||
// non-parametric".
|
||||
const m = aby / abx;
|
||||
const invS = 1 / dist;
|
||||
const phi = Math.atan(m);
|
||||
const cosPhi = Math.cos(phi);
|
||||
const sinPhi = Math.sin(phi);
|
||||
const tmax = invS * (Math.abs(cosPhi) + Math.abs(sinPhi));
|
||||
const poly = invS * (1 - tmax + tmax ** 2);
|
||||
const partialPhi = Math.max(
|
||||
Math.atan(Math.abs(sinPhi + cosPhi) * poly),
|
||||
Math.atan(Math.abs(sinPhi - cosPhi) * poly)
|
||||
);
|
||||
|
||||
let dmax = 0;
|
||||
let index = start;
|
||||
for (let i = start + 2; i < end - 2; i += 2) {
|
||||
const d = Math.abs(aa - nabx * points[i + 1] + naby * points[i]);
|
||||
if (d > dmax) {
|
||||
index = i;
|
||||
dmax = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (dmax > (dist * partialPhi) ** 2) {
|
||||
this.#douglasPeuckerHelper(points, start, index + 2, output);
|
||||
this.#douglasPeuckerHelper(points, index, end, output);
|
||||
} else {
|
||||
output.push(ax, ay);
|
||||
}
|
||||
}
|
||||
|
||||
static #douglasPeucker(points) {
|
||||
const output = [];
|
||||
const len = points.length;
|
||||
this.#douglasPeuckerHelper(points, 0, len, output);
|
||||
output.push(points[len - 2], points[len - 1]);
|
||||
return output.length <= 4 ? null : output;
|
||||
}
|
||||
|
||||
static #bilateralFilter(buf, width, height, sigmaS, sigmaR, kernelSize) {
|
||||
// The bilateral filter is a nonlinear filter that does spatial averaging.
|
||||
// Its main interest is to preserve edges while removing noise.
|
||||
// See https://en.wikipedia.org/wiki/Bilateral_filter for more details.
|
||||
// sigmaS is the standard deviation of the spatial gaussian.
|
||||
// sigmaR is the standard deviation of the range (in term of pixel
|
||||
// intensity) gaussian.
|
||||
|
||||
// Create a gaussian kernel
|
||||
const kernel = new Float32Array(kernelSize ** 2);
|
||||
const sigmaS2 = -2 * sigmaS ** 2;
|
||||
const halfSize = kernelSize >> 1;
|
||||
|
||||
for (let i = 0; i < kernelSize; i++) {
|
||||
const x = (i - halfSize) ** 2;
|
||||
for (let j = 0; j < kernelSize; j++) {
|
||||
kernel[i * kernelSize + j] = Math.exp(
|
||||
(x + (j - halfSize) ** 2) / sigmaS2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the range values to be used with the distance between pixels.
|
||||
// It's a way faster with a lookup table than computing the exponential.
|
||||
const rangeValues = new Float32Array(256);
|
||||
const sigmaR2 = -2 * sigmaR ** 2;
|
||||
for (let i = 0; i < 256; i++) {
|
||||
rangeValues[i] = Math.exp(i ** 2 / sigmaR2);
|
||||
}
|
||||
|
||||
const N = buf.length;
|
||||
const out = new Uint8Array(N);
|
||||
|
||||
// We compute the histogram here instead of doing it later: it's slightly
|
||||
// faster.
|
||||
const histogram = new Uint32Array(256);
|
||||
for (let i = 0; i < height; i++) {
|
||||
for (let j = 0; j < width; j++) {
|
||||
const ij = i * width + j;
|
||||
const center = buf[ij];
|
||||
let sum = 0;
|
||||
let norm = 0;
|
||||
|
||||
for (let k = 0; k < kernelSize; k++) {
|
||||
const y = i + k - halfSize;
|
||||
if (y < 0 || y >= height) {
|
||||
continue;
|
||||
}
|
||||
for (let l = 0; l < kernelSize; l++) {
|
||||
const x = j + l - halfSize;
|
||||
if (x < 0 || x >= width) {
|
||||
continue;
|
||||
}
|
||||
const neighbour = buf[y * width + x];
|
||||
const w =
|
||||
kernel[k * kernelSize + l] *
|
||||
rangeValues[Math.abs(neighbour - center)];
|
||||
sum += neighbour * w;
|
||||
norm += w;
|
||||
}
|
||||
}
|
||||
|
||||
const pix = (out[ij] = Math.round(sum / norm));
|
||||
histogram[pix]++;
|
||||
}
|
||||
}
|
||||
|
||||
return [out, histogram];
|
||||
}
|
||||
|
||||
static #toUint8(buf) {
|
||||
// We have a RGBA buffer, containing a grayscale image.
|
||||
// We want to convert it into a basic G buffer.
|
||||
// Also, we want to normalize the values between 0 and 255 in order to
|
||||
// increase the contrast.
|
||||
const N = buf.length;
|
||||
const out = new Uint8ClampedArray(N >> 2);
|
||||
let max = -Infinity;
|
||||
let min = Infinity;
|
||||
for (let i = 0, ii = out.length; i < ii; i++) {
|
||||
const A = buf[(i << 2) + 3];
|
||||
if (A === 0) {
|
||||
max = out[i] = 0xff;
|
||||
continue;
|
||||
}
|
||||
const pix = (out[i] = buf[i << 2]);
|
||||
if (pix > max) {
|
||||
max = pix;
|
||||
}
|
||||
if (pix < min) {
|
||||
min = pix;
|
||||
}
|
||||
}
|
||||
const ratio = 255 / (max - min);
|
||||
for (let i = 0; i < N; i++) {
|
||||
out[i] = (out[i] - min) * ratio;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static #guessThreshold(histogram) {
|
||||
// We want to find the threshold that will separate the background from the
|
||||
// foreground.
|
||||
// We could have used Otsu's method, but unfortunately it doesn't work well
|
||||
// when the background has too much shade of greys.
|
||||
// So the idea is to find a maximum in the black part of the histogram and
|
||||
// figure out the value which will be the first one of the white part.
|
||||
|
||||
let i;
|
||||
let M = -Infinity;
|
||||
let L = -Infinity;
|
||||
const min = histogram.findIndex(v => v !== 0);
|
||||
let pos = min;
|
||||
let spos = min;
|
||||
for (i = min; i < 256; i++) {
|
||||
const v = histogram[i];
|
||||
if (v > M) {
|
||||
if (i - pos > L) {
|
||||
L = i - pos;
|
||||
spos = i - 1;
|
||||
}
|
||||
M = v;
|
||||
pos = i;
|
||||
}
|
||||
}
|
||||
for (i = spos - 1; i >= 0; i--) {
|
||||
if (histogram[i] > histogram[i + 1]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
static #getGrayPixels(bitmap) {
|
||||
const originalBitmap = bitmap;
|
||||
const { width, height } = bitmap;
|
||||
const { maxDim } = this.#PARAMETERS;
|
||||
let newWidth = width;
|
||||
let newHeight = height;
|
||||
|
||||
if (width > maxDim || height > maxDim) {
|
||||
let prevWidth = width;
|
||||
let prevHeight = height;
|
||||
|
||||
let steps = Math.log2(Math.max(width, height) / maxDim);
|
||||
const isteps = Math.floor(steps);
|
||||
steps = steps === isteps ? isteps - 1 : isteps;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
newWidth = prevWidth;
|
||||
newHeight = prevHeight;
|
||||
if (newWidth > maxDim) {
|
||||
newWidth = Math.ceil(newWidth / 2);
|
||||
}
|
||||
if (newHeight > maxDim) {
|
||||
newHeight = Math.ceil(newHeight / 2);
|
||||
}
|
||||
|
||||
const offscreen = new OffscreenCanvas(newWidth, newHeight);
|
||||
const ctx = offscreen.getContext("2d");
|
||||
ctx.drawImage(
|
||||
bitmap,
|
||||
0,
|
||||
0,
|
||||
prevWidth,
|
||||
prevHeight,
|
||||
0,
|
||||
0,
|
||||
newWidth,
|
||||
newHeight
|
||||
);
|
||||
prevWidth = newWidth;
|
||||
prevHeight = newHeight;
|
||||
|
||||
// Release the resources associated with the bitmap.
|
||||
if (bitmap !== originalBitmap) {
|
||||
bitmap.close();
|
||||
}
|
||||
bitmap = offscreen.transferToImageBitmap();
|
||||
}
|
||||
|
||||
const ratio = Math.min(maxDim / newWidth, maxDim / newHeight);
|
||||
newWidth = Math.round(newWidth * ratio);
|
||||
newHeight = Math.round(newHeight * ratio);
|
||||
}
|
||||
const offscreen = new OffscreenCanvas(newWidth, newHeight);
|
||||
const ctx = offscreen.getContext("2d", { willReadFrequently: true });
|
||||
ctx.filter = "grayscale(1)";
|
||||
ctx.drawImage(
|
||||
bitmap,
|
||||
0,
|
||||
0,
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
0,
|
||||
0,
|
||||
newWidth,
|
||||
newHeight
|
||||
);
|
||||
const grayImage = ctx.getImageData(0, 0, newWidth, newHeight).data;
|
||||
const uint8Buf = this.#toUint8(grayImage);
|
||||
|
||||
return [uint8Buf, newWidth, newHeight];
|
||||
}
|
||||
|
||||
static process(bitmap, pageWidth, pageHeight, rotation, innerMargin) {
|
||||
const [uint8Buf, width, height] = this.#getGrayPixels(bitmap);
|
||||
const [uint8Filtered, histogram] = this.#bilateralFilter(
|
||||
uint8Buf,
|
||||
width,
|
||||
height,
|
||||
Math.hypot(width, height) * this.#PARAMETERS.sigmaSFactor,
|
||||
this.#PARAMETERS.sigmaR,
|
||||
this.#PARAMETERS.kernelSize
|
||||
);
|
||||
|
||||
const threshold = this.#guessThreshold(histogram);
|
||||
const contourList = this.#findContours(
|
||||
uint8Filtered,
|
||||
width,
|
||||
height,
|
||||
threshold
|
||||
);
|
||||
const linesAndPoints = [];
|
||||
|
||||
if (rotation % 180 !== 0) {
|
||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||
}
|
||||
|
||||
// The points need to be converted into page coordinates.
|
||||
const ratio = 0.5 * Math.min(pageWidth / width, pageHeight / height);
|
||||
const xScale = ratio / pageWidth;
|
||||
const yScale = ratio / pageHeight;
|
||||
|
||||
for (const { points } of contourList) {
|
||||
const reducedPoints = this.#douglasPeucker(points);
|
||||
if (!reducedPoints) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const len = reducedPoints.length;
|
||||
const newPoints = new Float32Array(len);
|
||||
const line = new Float32Array(3 * (len - 2));
|
||||
|
||||
let [x1, y1, x2, y2] = reducedPoints;
|
||||
x1 *= xScale;
|
||||
y1 *= yScale;
|
||||
x2 *= xScale;
|
||||
y2 *= yScale;
|
||||
newPoints.set([x1, y1, x2, y2], 0);
|
||||
|
||||
line.set([NaN, NaN, NaN, NaN, x1, y1], 0);
|
||||
for (let i = 4; i < len; i += 2) {
|
||||
const x = (newPoints[i] = reducedPoints[i] * xScale);
|
||||
const y = (newPoints[i + 1] = reducedPoints[i + 1] * yScale);
|
||||
line.set(Outline.createBezierPoints(x1, y1, x2, y2, x, y), (i - 2) * 3);
|
||||
[x1, y1, x2, y2] = [x2, y2, x, y];
|
||||
}
|
||||
|
||||
linesAndPoints.push({ line, points: newPoints });
|
||||
}
|
||||
const outline = new ContourDrawOutline();
|
||||
outline.build(
|
||||
linesAndPoints,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
1,
|
||||
rotation,
|
||||
0,
|
||||
innerMargin
|
||||
);
|
||||
|
||||
return outline;
|
||||
}
|
||||
}
|
||||
|
||||
export { SignatureExtractor };
|
159
src/display/editor/signature.js
Normal file
159
src/display/editor/signature.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
/* 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 { AnnotationEditorType, shadow } from "../../shared/util.js";
|
||||
import { DrawingEditor, DrawingOptions } from "./draw.js";
|
||||
import { AnnotationEditor } from "./editor.js";
|
||||
import { SignatureExtractor } from "./drawers/signaturedraw.js";
|
||||
import { StampEditor } from "./stamp.js";
|
||||
|
||||
class SignatureOptions extends DrawingOptions {
|
||||
#viewParameters;
|
||||
|
||||
constructor(viewerParameters) {
|
||||
super();
|
||||
this.#viewParameters = viewerParameters;
|
||||
|
||||
super.updateProperties({
|
||||
fill: "black",
|
||||
"stroke-width": 0,
|
||||
});
|
||||
}
|
||||
|
||||
clone() {
|
||||
const clone = new SignatureOptions(this.#viewParameters);
|
||||
clone.updateAll(this);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic editor in order to generate an Stamp annotation annotation containing
|
||||
* a signature drawing.
|
||||
*/
|
||||
class SignatureEditor extends DrawingEditor {
|
||||
static _type = "signature";
|
||||
|
||||
static _editorType = AnnotationEditorType.SIGNATURE;
|
||||
|
||||
static _defaultDrawingOptions = null;
|
||||
|
||||
constructor(params) {
|
||||
super({ ...params, mustBeCommitted: true, name: "signatureEditor" });
|
||||
this._willKeepAspectRatio = false;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static initialize(l10n, uiManager) {
|
||||
AnnotationEditor.initialize(l10n, uiManager);
|
||||
this._defaultDrawingOptions = new SignatureOptions(
|
||||
uiManager.viewParameters
|
||||
);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static getDefaultDrawingOptions(options) {
|
||||
const clone = this._defaultDrawingOptions.clone();
|
||||
clone.updateProperties(options);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static get supportMultipleDrawings() {
|
||||
return false;
|
||||
}
|
||||
|
||||
static get typesMap() {
|
||||
return shadow(this, "typesMap", new Map());
|
||||
}
|
||||
|
||||
static get isDrawer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get isResizable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
render() {
|
||||
if (this.div) {
|
||||
return this.div;
|
||||
}
|
||||
|
||||
super.render();
|
||||
this.div.hidden = true;
|
||||
this.div.setAttribute("role", "figure");
|
||||
|
||||
this.#extractSignature();
|
||||
|
||||
return this.div;
|
||||
}
|
||||
|
||||
async #extractSignature() {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = StampEditor.supportedTypesStr;
|
||||
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;
|
||||
if (!bitmap?.bitmap) {
|
||||
this.remove();
|
||||
return;
|
||||
}
|
||||
const {
|
||||
rawDims: { pageWidth, pageHeight },
|
||||
rotation,
|
||||
} = this.parent.viewport;
|
||||
const drawOutlines = SignatureExtractor.process(
|
||||
bitmap.bitmap,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
rotation,
|
||||
SignatureEditor._INNER_MARGIN
|
||||
);
|
||||
this._addOutlines({
|
||||
drawOutlines,
|
||||
drawingOptions: SignatureEditor.getDefaultDrawingOptions(),
|
||||
});
|
||||
this.onScaleChanging();
|
||||
this.rotate();
|
||||
this.div.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
export { SignatureEditor };
|
|
@ -36,6 +36,7 @@ class EditorToolbar {
|
|||
highlight: "pdfjs-editor-remove-highlight-button",
|
||||
ink: "pdfjs-editor-remove-ink-button",
|
||||
stamp: "pdfjs-editor-remove-stamp-button",
|
||||
signature: "pdfjs-editor-remove-signature-button",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ const AnnotationEditorType = {
|
|||
HIGHLIGHT: 9,
|
||||
STAMP: 13,
|
||||
INK: 15,
|
||||
SIGNATURE: 101,
|
||||
};
|
||||
|
||||
const AnnotationEditorParamsType = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue