mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-19 06:38:07 +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
|
@ -71,6 +71,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
|
"enableSignatureEditor": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"enableUpdatedAddImage": {
|
"enableUpdatedAddImage": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { FreeTextEditor } from "./freetext.js";
|
||||||
import { HighlightEditor } from "./highlight.js";
|
import { HighlightEditor } from "./highlight.js";
|
||||||
import { InkEditor } from "./ink.js";
|
import { InkEditor } from "./ink.js";
|
||||||
import { setLayerDimensions } from "../display_utils.js";
|
import { setLayerDimensions } from "../display_utils.js";
|
||||||
|
import { SignatureEditor } from "./signature.js";
|
||||||
import { StampEditor } from "./stamp.js";
|
import { StampEditor } from "./stamp.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,10 +90,13 @@ class AnnotationEditorLayer {
|
||||||
static _initialized = false;
|
static _initialized = false;
|
||||||
|
|
||||||
static #editorTypes = new Map(
|
static #editorTypes = new Map(
|
||||||
[FreeTextEditor, InkEditor, StampEditor, HighlightEditor].map(type => [
|
[
|
||||||
type._editorType,
|
FreeTextEditor,
|
||||||
type,
|
InkEditor,
|
||||||
])
|
StampEditor,
|
||||||
|
HighlightEditor,
|
||||||
|
SignatureEditor,
|
||||||
|
].map(type => [type._editorType, type])
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -758,7 +762,11 @@ class AnnotationEditorLayer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#uiManager.getMode() === AnnotationEditorType.STAMP) {
|
const currentMode = this.#uiManager.getMode();
|
||||||
|
if (
|
||||||
|
currentMode === AnnotationEditorType.STAMP ||
|
||||||
|
currentMode === AnnotationEditorType.SIGNATURE
|
||||||
|
) {
|
||||||
this.#uiManager.unselectAll();
|
this.#uiManager.unselectAll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,6 +91,10 @@ class DrawingEditor extends AnnotationEditor {
|
||||||
super(params);
|
super(params);
|
||||||
this.#mustBeCommitted = params.mustBeCommitted || false;
|
this.#mustBeCommitted = params.mustBeCommitted || false;
|
||||||
|
|
||||||
|
this._addOutlines(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
_addOutlines(params) {
|
||||||
if (params.drawOutlines) {
|
if (params.drawOutlines) {
|
||||||
this.#createDrawOutlines(params);
|
this.#createDrawOutlines(params);
|
||||||
this.#addToDrawLayer();
|
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",
|
highlight: "pdfjs-editor-remove-highlight-button",
|
||||||
ink: "pdfjs-editor-remove-ink-button",
|
ink: "pdfjs-editor-remove-ink-button",
|
||||||
stamp: "pdfjs-editor-remove-stamp-button",
|
stamp: "pdfjs-editor-remove-stamp-button",
|
||||||
|
signature: "pdfjs-editor-remove-signature-button",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,7 @@ const AnnotationEditorType = {
|
||||||
HIGHLIGHT: 9,
|
HIGHLIGHT: 9,
|
||||||
STAMP: 13,
|
STAMP: 13,
|
||||||
INK: 15,
|
INK: 15,
|
||||||
|
SIGNATURE: 101,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnnotationEditorParamsType = {
|
const AnnotationEditorParamsType = {
|
||||||
|
|
|
@ -162,7 +162,8 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) {
|
.annotationEditorLayer
|
||||||
|
:is(.freeTextEditor, .inkEditor, .stampEditor, .signatureEditor) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
@ -220,7 +221,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotationEditorLayer
|
.annotationEditorLayer
|
||||||
:is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),
|
:is(
|
||||||
|
.freeTextEditor,
|
||||||
|
.inkEditor,
|
||||||
|
.stampEditor,
|
||||||
|
.highlightEditor,
|
||||||
|
.signatureEditor
|
||||||
|
),
|
||||||
.textLayer {
|
.textLayer {
|
||||||
.editToolbar {
|
.editToolbar {
|
||||||
--editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg);
|
--editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg);
|
||||||
|
@ -622,7 +629,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotationEditorLayer {
|
.annotationEditorLayer {
|
||||||
:is(.freeTextEditor, .inkEditor, .stampEditor) {
|
:is(.freeTextEditor, .inkEditor, .stampEditor, .signatureEditor) {
|
||||||
& > .resizers {
|
& > .resizers {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { AnnotationEditorParamsType } from "pdfjs-lib";
|
||||||
* @property {HTMLButtonElement} editorStampAddImage
|
* @property {HTMLButtonElement} editorStampAddImage
|
||||||
* @property {HTMLInputElement} editorFreeHighlightThickness
|
* @property {HTMLInputElement} editorFreeHighlightThickness
|
||||||
* @property {HTMLButtonElement} editorHighlightShowAll
|
* @property {HTMLButtonElement} editorHighlightShowAll
|
||||||
|
* @property {HTMLButtonElement} editorSignatureAddSignature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class AnnotationEditorParams {
|
class AnnotationEditorParams {
|
||||||
|
@ -51,6 +52,7 @@ class AnnotationEditorParams {
|
||||||
editorStampAddImage,
|
editorStampAddImage,
|
||||||
editorFreeHighlightThickness,
|
editorFreeHighlightThickness,
|
||||||
editorHighlightShowAll,
|
editorHighlightShowAll,
|
||||||
|
editorSignatureAddSignature,
|
||||||
}) {
|
}) {
|
||||||
const { eventBus } = this;
|
const { eventBus } = this;
|
||||||
|
|
||||||
|
@ -94,6 +96,9 @@ class AnnotationEditorParams {
|
||||||
this.setAttribute("aria-pressed", !checked);
|
this.setAttribute("aria-pressed", !checked);
|
||||||
dispatchEvent("HIGHLIGHT_SHOW_ALL", !checked);
|
dispatchEvent("HIGHLIGHT_SHOW_ALL", !checked);
|
||||||
});
|
});
|
||||||
|
editorSignatureAddSignature.addEventListener("click", () => {
|
||||||
|
dispatchEvent("CREATE");
|
||||||
|
});
|
||||||
|
|
||||||
eventBus._on("annotationeditorparamschanged", evt => {
|
eventBus._on("annotationeditorparamschanged", evt => {
|
||||||
for (const [type, value] of evt.details) {
|
for (const [type, value] of evt.details) {
|
||||||
|
|
|
@ -536,6 +536,10 @@ const PDFViewerApplication = {
|
||||||
typeof AbortSignal.any === "function") &&
|
typeof AbortSignal.any === "function") &&
|
||||||
annotationEditorMode !== AnnotationEditorType.DISABLE
|
annotationEditorMode !== AnnotationEditorType.DISABLE
|
||||||
) {
|
) {
|
||||||
|
const editorSignatureButton = appConfig.toolbar?.editorSignatureButton;
|
||||||
|
if (editorSignatureButton && AppOptions.get("enableSignatureEditor")) {
|
||||||
|
editorSignatureButton.parentElement.hidden = false;
|
||||||
|
}
|
||||||
this.annotationEditorParams = new AnnotationEditorParams(
|
this.annotationEditorParams = new AnnotationEditorParams(
|
||||||
appConfig.annotationEditorParams,
|
appConfig.annotationEditorParams,
|
||||||
eventBus
|
eventBus
|
||||||
|
|
|
@ -228,6 +228,11 @@ const defaultOptions = {
|
||||||
value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME"),
|
value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME"),
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
},
|
},
|
||||||
|
enableSignatureEditor: {
|
||||||
|
/** @type {boolean} */
|
||||||
|
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
||||||
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
|
},
|
||||||
enableUpdatedAddImage: {
|
enableUpdatedAddImage: {
|
||||||
// We'll probably want to make some experiments before enabling this
|
// We'll probably want to make some experiments before enabling this
|
||||||
// in Firefox release, but it has to be temporary.
|
// in Firefox release, but it has to be temporary.
|
||||||
|
|
|
@ -39,6 +39,7 @@ class EditorUndoBar {
|
||||||
freetext: "pdfjs-editor-undo-bar-message-freetext",
|
freetext: "pdfjs-editor-undo-bar-message-freetext",
|
||||||
stamp: "pdfjs-editor-undo-bar-message-stamp",
|
stamp: "pdfjs-editor-undo-bar-message-stamp",
|
||||||
ink: "pdfjs-editor-undo-bar-message-ink",
|
ink: "pdfjs-editor-undo-bar-message-ink",
|
||||||
|
signature: "pdfjs-editor-undo-bar-message-signature",
|
||||||
_multiple: "pdfjs-editor-undo-bar-message-multiple",
|
_multiple: "pdfjs-editor-undo-bar-message-multiple",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
6
web/images/toolbarButton-editorSignature.svg
Normal file
6
web/images/toolbarButton-editorSignature.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 7.5" width="12" height="12">
|
||||||
|
<path d="M1.5,4.5c.75,0,1.31.75,1.68,1.49A4.47,4.47,0,0,1,7.5,2.25C10.5,2.25,12,5,12,7.5h-.75V9.38a.37.37,0,0,1-.37.38H9.38A.38.38,0,0,1,9,9.38V8.25H5.25V9.38a.37.37,0,0,1-.37.38H3.38A.37.37,0,0,1,3,9.38V7.5H1.5a1.5,1.5,0,0,1,0-3Z" transform="translate(0 -2.25)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 571 B |
|
@ -119,6 +119,18 @@ class Toolbar {
|
||||||
data: { action: "pdfjs.image.icon_click" },
|
data: { action: "pdfjs.image.icon_click" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
element: options.editorSignatureButton,
|
||||||
|
eventName: "switchannotationeditormode",
|
||||||
|
eventDetails: {
|
||||||
|
get mode() {
|
||||||
|
const { classList } = options.editorSignatureButton;
|
||||||
|
return classList.contains("toggled")
|
||||||
|
? AnnotationEditorType.NONE
|
||||||
|
: AnnotationEditorType.SIGNATURE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Bind the event listeners for click and various other actions.
|
// Bind the event listeners for click and various other actions.
|
||||||
|
@ -274,6 +286,8 @@ class Toolbar {
|
||||||
editorInkParamsToolbar,
|
editorInkParamsToolbar,
|
||||||
editorStampButton,
|
editorStampButton,
|
||||||
editorStampParamsToolbar,
|
editorStampParamsToolbar,
|
||||||
|
editorSignatureButton,
|
||||||
|
editorSignatureParamsToolbar,
|
||||||
} = this.#opts;
|
} = this.#opts;
|
||||||
|
|
||||||
toggleExpandedBtn(
|
toggleExpandedBtn(
|
||||||
|
@ -296,12 +310,18 @@ class Toolbar {
|
||||||
mode === AnnotationEditorType.STAMP,
|
mode === AnnotationEditorType.STAMP,
|
||||||
editorStampParamsToolbar
|
editorStampParamsToolbar
|
||||||
);
|
);
|
||||||
|
toggleExpandedBtn(
|
||||||
|
editorSignatureButton,
|
||||||
|
mode === AnnotationEditorType.SIGNATURE,
|
||||||
|
editorSignatureParamsToolbar
|
||||||
|
);
|
||||||
|
|
||||||
const isDisable = mode === AnnotationEditorType.DISABLE;
|
const isDisable = mode === AnnotationEditorType.DISABLE;
|
||||||
editorFreeTextButton.disabled = isDisable;
|
editorFreeTextButton.disabled = isDisable;
|
||||||
editorHighlightButton.disabled = isDisable;
|
editorHighlightButton.disabled = isDisable;
|
||||||
editorInkButton.disabled = isDisable;
|
editorInkButton.disabled = isDisable;
|
||||||
editorStampButton.disabled = isDisable;
|
editorStampButton.disabled = isDisable;
|
||||||
|
editorSignatureButton.disabled = isDisable;
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateUIState(resetNumPages = false) {
|
#updateUIState(resetNumPages = false) {
|
||||||
|
|
|
@ -89,6 +89,7 @@
|
||||||
--toolbarButton-editorHighlight-icon: url(images/toolbarButton-editorHighlight.svg);
|
--toolbarButton-editorHighlight-icon: url(images/toolbarButton-editorHighlight.svg);
|
||||||
--toolbarButton-editorInk-icon: url(images/toolbarButton-editorInk.svg);
|
--toolbarButton-editorInk-icon: url(images/toolbarButton-editorInk.svg);
|
||||||
--toolbarButton-editorStamp-icon: url(images/toolbarButton-editorStamp.svg);
|
--toolbarButton-editorStamp-icon: url(images/toolbarButton-editorStamp.svg);
|
||||||
|
--toolbarButton-editorSignature-icon: url(images/toolbarButton-editorSignature.svg);
|
||||||
--toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
|
--toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
|
||||||
--toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg);
|
--toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg);
|
||||||
--toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg);
|
--toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg);
|
||||||
|
@ -572,6 +573,10 @@ body {
|
||||||
mask-image: var(--toolbarButton-editorStamp-icon);
|
mask-image: var(--toolbarButton-editorStamp-icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#editorSignatureButton::before {
|
||||||
|
mask-image: var(--toolbarButton-editorSignature-icon);
|
||||||
|
}
|
||||||
|
|
||||||
#printButton::before {
|
#printButton::before {
|
||||||
mask-image: var(--toolbarButton-print-icon);
|
mask-image: var(--toolbarButton-print-icon);
|
||||||
}
|
}
|
||||||
|
@ -1119,7 +1124,7 @@ dialog :link {
|
||||||
z-index: 30000;
|
z-index: 30000;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
#editorStampAddImage::before {
|
:is(#editorStampAddImage, #editorSignatureAddSignature)::before {
|
||||||
mask-image: var(--editorParams-stampAddImage-icon);
|
mask-image: var(--editorParams-stampAddImage-icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -318,6 +318,18 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="editorSignature" class="toolbarButtonWithContainer" hidden="true">
|
||||||
|
<button id="editorSignatureButton" class="toolbarButton" type="button" disabled="disabled" title="Add or edit signatures" role="radio" aria-expanded="false" aria-haspopup="true" aria-controls="editorSignatureParamsToolbar" tabindex="0">
|
||||||
|
<span>Add or edit signatures</span>
|
||||||
|
</button>
|
||||||
|
<div class="editorParamsToolbar hidden doorHangerRight menu" id="editorSignatureParamsToolbar">
|
||||||
|
<div class="menuContainer">
|
||||||
|
<button id="editorSignatureAddSignature" class="toolbarButton labeled" type="button" title="Add signature" tabindex="0">
|
||||||
|
<span class="editorParamsLabel">Add signature</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editorModeSeparator" class="verticalToolbarSeparator"></div>
|
<div id="editorModeSeparator" class="verticalToolbarSeparator"></div>
|
||||||
|
|
|
@ -68,6 +68,10 @@ function getViewerConfiguration() {
|
||||||
editorStampParamsToolbar: document.getElementById(
|
editorStampParamsToolbar: document.getElementById(
|
||||||
"editorStampParamsToolbar"
|
"editorStampParamsToolbar"
|
||||||
),
|
),
|
||||||
|
editorSignatureButton: document.getElementById("editorSignatureButton"),
|
||||||
|
editorSignatureParamsToolbar: document.getElementById(
|
||||||
|
"editorSignatureParamsToolbar"
|
||||||
|
),
|
||||||
download: document.getElementById("downloadButton"),
|
download: document.getElementById("downloadButton"),
|
||||||
},
|
},
|
||||||
secondaryToolbar: {
|
secondaryToolbar: {
|
||||||
|
@ -217,6 +221,9 @@ function getViewerConfiguration() {
|
||||||
editorInkThickness: document.getElementById("editorInkThickness"),
|
editorInkThickness: document.getElementById("editorInkThickness"),
|
||||||
editorInkOpacity: document.getElementById("editorInkOpacity"),
|
editorInkOpacity: document.getElementById("editorInkOpacity"),
|
||||||
editorStampAddImage: document.getElementById("editorStampAddImage"),
|
editorStampAddImage: document.getElementById("editorStampAddImage"),
|
||||||
|
editorSignatureAddSignature: document.getElementById(
|
||||||
|
"editorSignatureAddSignature"
|
||||||
|
),
|
||||||
editorFreeHighlightThickness: document.getElementById(
|
editorFreeHighlightThickness: document.getElementById(
|
||||||
"editorFreeHighlightThickness"
|
"editorFreeHighlightThickness"
|
||||||
),
|
),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue