Skip to content

Commit

Permalink
[Editor] Add the possibility to add some handwritten signatures
Browse files Browse the repository at this point in the history
This newly added tool is very similar to the stamp editor, but the
incoming images will be analyzed in order to remove the backgroud.
For now this new feature is disabled by default because we need to
have some inputs from UI/UX to make suitable for Firefox users.
  • Loading branch information
calixteman committed Jul 15, 2023
1 parent 36fc34e commit 78d28dd
Show file tree
Hide file tree
Showing 19 changed files with 500 additions and 55 deletions.
4 changes: 4 additions & 0 deletions extensions/chromium/preferences_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"type": "boolean",
"default": true
},
"enableSignatureEditor": {
"type": "boolean",
"default": false
},
"disableRange": {
"title": "Disable range requests",
"description": "Whether to disable range requests (not recommended).",
Expand Down
103 changes: 63 additions & 40 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,15 @@ class AnnotationFactory {
return null;
}
let imagePromises;
for (const { bitmapId, bitmap } of annotations) {
for (const { bitmapId, bitmap, onlyAlpha } of annotations) {
if (!bitmap) {
continue;
}
imagePromises ||= new Map();
imagePromises.set(bitmapId, StampAnnotation.createImage(bitmap, xref));
imagePromises.set(
bitmapId,
StampAnnotation.createImage(bitmap, onlyAlpha, xref)
);
}

return imagePromises;
Expand All @@ -288,6 +291,27 @@ class AnnotationFactory {
if (annotation.deleted) {
continue;
}

let image;
if (annotation.bitmapId && isOffscreenCanvasSupported) {
image = await imagePromises.get(annotation.bitmapId);
if (image.imageStream) {
const { imageStream, smaskStream } = image;
const buffer = [];
if (smaskStream) {
const smaskRef = xref.getNewTemporaryRef();
await writeObject(smaskRef, smaskStream, buffer, null);
dependencies.push({ ref: smaskRef, data: buffer.join("") });
imageStream.dict.set("SMask", smaskRef);
buffer.length = 0;
}
const imageRef = (image.imageRef = xref.getNewTemporaryRef());
await writeObject(imageRef, imageStream, buffer, null);
dependencies.push({ ref: imageRef, data: buffer.join("") });
image.imageStream = image.smaskStream = null;
}
}

switch (annotation.annotationType) {
case AnnotationEditorType.FREETEXT:
if (!baseFontRef) {
Expand Down Expand Up @@ -316,25 +340,10 @@ class AnnotationFactory {
);
break;
case AnnotationEditorType.STAMP:
if (!isOffscreenCanvasSupported) {
case AnnotationEditorType.SIGNATURE:
if (!image) {
break;
}
const image = await imagePromises.get(annotation.bitmapId);
if (image.imageStream) {
const { imageStream, smaskStream } = image;
const buffer = [];
if (smaskStream) {
const smaskRef = xref.getNewTemporaryRef();
await writeObject(smaskRef, smaskStream, buffer, null);
dependencies.push({ ref: smaskRef, data: buffer.join("") });
imageStream.dict.set("SMask", smaskRef);
buffer.length = 0;
}
const imageRef = (image.imageRef = xref.getNewTemporaryRef());
await writeObject(imageRef, imageStream, buffer, null);
dependencies.push({ ref: imageRef, data: buffer.join("") });
image.imageStream = image.smaskStream = null;
}
promises.push(
StampAnnotation.createNewAnnotation(
xref,
Expand Down Expand Up @@ -369,6 +378,20 @@ class AnnotationFactory {
if (annotation.deleted) {
continue;
}

let image;
if (annotation.bitmapId && options.isOffscreenCanvasSupported) {
image = await imagePromises.get(annotation.bitmapId);
if (image.imageStream) {
const { imageStream, smaskStream } = image;
if (smaskStream) {
imageStream.dict.set("SMask", smaskStream);
}
image.imageRef = new JpegStream(imageStream, imageStream.length);
image.imageStream = image.smaskStream = null;
}
}

switch (annotation.annotationType) {
case AnnotationEditorType.FREETEXT:
promises.push(
Expand All @@ -387,18 +410,10 @@ class AnnotationFactory {
);
break;
case AnnotationEditorType.STAMP:
if (!options.isOffscreenCanvasSupported) {
case AnnotationEditorType.SIGNATURE:
if (!image) {
break;
}
const image = await imagePromises.get(annotation.bitmapId);
if (image.imageStream) {
const { imageStream, smaskStream } = image;
if (smaskStream) {
imageStream.dict.set("SMask", smaskStream);
}
image.imageRef = new JpegStream(imageStream, imageStream.length);
image.imageStream = image.smaskStream = null;
}
promises.push(
StampAnnotation.createNewPrintAnnotation(xref, annotation, {
image,
Expand Down Expand Up @@ -1685,7 +1700,7 @@ class WidgetAnnotation extends Annotation {
data.fieldType = fieldType instanceof Name ? fieldType.name : null;

const localResources = getInheritableProperty({ dict, key: "DR" });
const acroFormResources = params.acroForm.get("DR");
const acroFormResources = params.acroForm?.get("DR");
const appearanceResources = this.appearance?.dict.get("Resources");

this._fieldResources = {
Expand Down Expand Up @@ -4480,7 +4495,7 @@ class StampAnnotation extends MarkupAnnotation {
this.data.hasOwnCanvas = this.data.noRotate;
}

static async createImage(bitmap, xref) {
static async createImage(bitmap, onlyAlpha, xref) {
// TODO: when printing, we could have a specific internal colorspace
// (e.g. something like DeviceRGBA) in order avoid any conversion (i.e. no
// jpeg, no rgba to rgb conversion, etc...)
Expand All @@ -4493,22 +4508,30 @@ class StampAnnotation extends MarkupAnnotation {
ctx.drawImage(bitmap, 0, 0);
const data = ctx.getImageData(0, 0, width, height).data;
const buf32 = new Uint32Array(data.buffer);
const hasAlpha = buf32.some(
FeatureTest.isLittleEndian
? x => x >>> 24 !== 0xff
: x => (x & 0xff) !== 0xff
);
const hasAlpha =
onlyAlpha ||
buf32.some(
FeatureTest.isLittleEndian
? x => x >>> 24 !== 0xff
: x => (x & 0xff) !== 0xff
);

if (hasAlpha) {
// Redraw the image on a white background in order to remove the thin gray
// line which can appear when exporting to jpeg.
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
ctx.drawImage(bitmap, 0, 0);
if (!onlyAlpha) {
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
ctx.drawImage(bitmap, 0, 0);
} else {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
}
}
const quality = onlyAlpha ? 0 : 1;

const jpegBufferPromise = canvas
.convertToBlob({ type: "image/jpeg", quality: 1 })
.convertToBlob({ type: "image/jpeg", quality })
.then(blob => {
return blob.arrayBuffer();
});
Expand Down
24 changes: 24 additions & 0 deletions src/display/display_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class DOMFilterFactory extends BaseFilterFactory {

#hcmHighlightUrl;

#signatureFilter;

#signatureFilterUrl;

#id = 0;

constructor({ docId, ownerDocument = globalThis.document } = {}) {
Expand Down Expand Up @@ -310,6 +314,26 @@ class DOMFilterFactory extends BaseFilterFactory {
return this.#hcmHighlightUrl;
}

addSignatureFilter() {
if (this.#signatureFilterUrl) {
return this.#signatureFilterUrl;
}

this.#signatureFilterUrl = "none";
this.#signatureFilter?.remove();

const id = `g_${this.#docId}_signature_filter`;
const filter = (this.#signatureFilter = this.#createFilter(id));

const feMorphology = this.#document.createElementNS(SVG_NS, "feMorphology");
feMorphology.setAttribute("operator", "erode");
feMorphology.setAttribute("radius", "1");
filter.append(feMorphology);

this.#signatureFilterUrl = `url(#${id})`;
return this.#signatureFilterUrl;
}

destroy(keepHCM = false) {
if (keepHCM && (this.#hcmUrl || this.#hcmHighlightUrl)) {
return;
Expand Down
20 changes: 19 additions & 1 deletion src/display/editor/annotation_editor_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { bindEvents } from "./tools.js";
import { FreeTextEditor } from "./freetext.js";
import { InkEditor } from "./ink.js";
import { setLayerDimensions } from "../display_utils.js";
import { SignatureEditor } from "./signature.js";
import { StampEditor } from "./stamp.js";

/**
Expand Down Expand Up @@ -86,7 +87,12 @@ class AnnotationEditorLayer {
viewport,
l10n,
}) {
const editorTypes = [FreeTextEditor, InkEditor, StampEditor];
const editorTypes = [
FreeTextEditor,
InkEditor,
StampEditor,
SignatureEditor,
];
if (!AnnotationEditorLayer._initialized) {
AnnotationEditorLayer._initialized = true;
for (const editorType of editorTypes) {
Expand Down Expand Up @@ -145,6 +151,10 @@ class AnnotationEditorLayer {
"stampEditing",
mode === AnnotationEditorType.STAMP
);
this.div.classList.toggle(
"signatureEditing",
mode === AnnotationEditorType.SIGNATURE
);
this.div.hidden = false;
}
}
Expand Down Expand Up @@ -442,6 +452,8 @@ class AnnotationEditorLayer {
return new InkEditor(params);
case AnnotationEditorType.STAMP:
return new StampEditor(params);
case AnnotationEditorType.SIGNATURE:
return new SignatureEditor(params);
}
return null;
}
Expand All @@ -459,6 +471,8 @@ class AnnotationEditorLayer {
return InkEditor.deserialize(data, this, this.#uiManager);
case AnnotationEditorType.STAMP:
return StampEditor.deserialize(data, this, this.#uiManager);
case AnnotationEditorType.SIGNATURE:
return SignatureEditor.deserialize(data, this, this.#uiManager);
}
return null;
}
Expand Down Expand Up @@ -673,4 +687,8 @@ class AnnotationEditorLayer {
}
}

if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
AnnotationEditorLayer.SignatureEditor = SignatureEditor;
}

export { AnnotationEditorLayer };
Loading

0 comments on commit 78d28dd

Please sign in to comment.