Skip to content

Commit

Permalink
[Editor] Add some functions in order to extract contours from text an…
Browse files Browse the repository at this point in the history
…d to generate a drawing from a drawn signature
  • Loading branch information
calixteman committed Jan 31, 2025
1 parent 58c8f06 commit bdb34d2
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/display/editor/drawers/inkdraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ class InkDrawOutline extends Outline {
buffer.push("Z");
continue;
}
if (line.length === 12) {
if (line.length === 12 && isNaN(line[6])) {
buffer.push(
`L${Outline.svgRound(line[10])} ${Outline.svgRound(line[11])}`
);
Expand Down
144 changes: 129 additions & 15 deletions src/display/editor/drawers/signaturedraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import { ContourDrawOutline } from "./contour.js";
import { InkDrawOutline } from "./inkdraw.js";
import { Outline } from "./outline.js";

/**
Expand Down Expand Up @@ -343,6 +344,14 @@ class SignatureExtractor {
return [out, histogram];
}

static #getHistogram(buf) {
const histogram = new Uint32Array(256);
for (const g of buf) {
histogram[g]++;
}
return histogram;
}

static #toUint8(buf) {
// We have a RGBA buffer, containing a grayscale image.
// We want to convert it into a basic G buffer.
Expand Down Expand Up @@ -479,9 +488,85 @@ class SignatureExtractor {
return [uint8Buf, newWidth, newHeight];
}

static extractContoursFromText(
text,
{ fontFamily, fontStyle, fontWeight },
pageWidth,
pageHeight,
rotation,
innerMargin
) {
let canvas = new OffscreenCanvas(1, 1);
let ctx = canvas.getContext("2d", { alpha: false });
const fontSize = 200;
const font =
(ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`);
const {
actualBoundingBoxLeft,
actualBoundingBoxRight,
actualBoundingBoxAscent,
actualBoundingBoxDescent,
fontBoundingBoxAscent,
fontBoundingBoxDescent,
width,
} = ctx.measureText(text);

// We rescale the canvas to make "sure" the text fits.
const SCALE = 1.5;
const canvasWidth = Math.ceil(
Math.max(
Math.abs(actualBoundingBoxLeft) + Math.abs(actualBoundingBoxRight) || 0,
width
) * SCALE
);
const canvasHeight = Math.ceil(
Math.max(
Math.abs(actualBoundingBoxAscent) +
Math.abs(actualBoundingBoxDescent) || fontSize,
Math.abs(fontBoundingBoxAscent) + Math.abs(fontBoundingBoxDescent) ||
fontSize
) * SCALE
);
canvas = new OffscreenCanvas(canvasWidth, canvasHeight);
ctx = canvas.getContext("2d", { alpha: true, willReadFrequently: true });
ctx.font = font;
ctx.filter = "grayscale(1)";
ctx.fillStyle = "white";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = "black";
ctx.fillText(
text,
(canvasWidth * (SCALE - 1)) / 2,
(canvasHeight * (3 - SCALE)) / 2
);

const uint8Buf = this.#toUint8(
ctx.getImageData(0, 0, canvasWidth, canvasHeight).data
);
const histogram = this.#getHistogram(uint8Buf);
const threshold = this.#guessThreshold(histogram);

const contourList = this.#findContours(
uint8Buf,
canvasWidth,
canvasHeight,
threshold
);

return this.processDrawnLines(
{ curves: contourList, width: canvasWidth, height: canvasHeight },
pageWidth,
pageHeight,
rotation,
innerMargin,
true,
true
);
}

static process(bitmap, pageWidth, pageHeight, rotation, innerMargin) {
const [uint8Buf, width, height] = this.#getGrayPixels(bitmap);
const [uint8Filtered, histogram] = this.#bilateralFilter(
const [buffer, histogram] = this.#bilateralFilter(
uint8Buf,
width,
height,
Expand All @@ -491,32 +576,55 @@ class SignatureExtractor {
);

const threshold = this.#guessThreshold(histogram);
const contourList = this.#findContours(
uint8Filtered,
width,
height,
threshold
const contourList = this.#findContours(buffer, width, height, threshold);

return this.processDrawnLines(
{ curves: contourList, width, height },
pageWidth,
pageHeight,
rotation,
innerMargin,
true,
true
);
const linesAndPoints = [];
}

static processDrawnLines(
lines,
pageWidth,
pageHeight,
rotation,
innerMargin,
mustSmooth,
areContours
) {
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 { curves, thickness, width, height } = lines;
const linesAndPoints = [];
const ratio = Math.min(pageWidth / width, pageHeight / height);
const xScale = ratio / pageWidth;
const yScale = ratio / pageHeight;

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

const len = reducedPoints.length;
const newPoints = new Float32Array(len);
const line = new Float32Array(3 * (len - 2));
const line = new Float32Array(3 * (len === 2 ? 2 : len - 2));
linesAndPoints.push({ line, points: newPoints });

if (len === 2) {
newPoints[0] = reducedPoints[0] * xScale;
newPoints[1] = reducedPoints[1] * yScale;
line.set([NaN, NaN, NaN, NaN, newPoints[0], newPoints[1]], 0);
continue;
}

let [x1, y1, x2, y2] = reducedPoints;
x1 *= xScale;
Expand All @@ -532,17 +640,23 @@ class SignatureExtractor {
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 });
if (linesAndPoints.length === 0) {
return null;
}
const outline = new ContourDrawOutline();

const outline = areContours
? new ContourDrawOutline()
: new InkDrawOutline();

outline.build(
linesAndPoints,
pageWidth,
pageHeight,
1,
rotation,
0,
areContours ? 0 : thickness,
innerMargin
);

Expand Down
30 changes: 19 additions & 11 deletions src/display/editor/signature.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,21 +131,29 @@ class SignatureEditor extends DrawingEditor {
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
);
let drawOutlines;
if (bitmap?.bitmap) {
drawOutlines = SignatureExtractor.process(
bitmap.bitmap,
pageWidth,
pageHeight,
rotation,
SignatureEditor._INNER_MARGIN
);
} else {
drawOutlines = SignatureExtractor.extractContoursFromText(
"Hello PDF.js' World !!",
{ fontStyle: "italic", fontWeight: "400", fontFamily: "cursive" },
pageWidth,
pageHeight,
rotation,
SignatureEditor._INNER_MARGIN
);
}
this._addOutlines({
drawOutlines,
drawingOptions: SignatureEditor.getDefaultDrawingOptions(),
Expand Down

0 comments on commit bdb34d2

Please sign in to comment.