jscanify 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "jscanify",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Open-source Javascript mobile document scanner.",
5
- "main": "src/jscanify.js",
5
+ "main": "src/jscanify-node.js",
6
6
  "directories": {
7
7
  "doc": "docs"
8
8
  },
9
9
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
10
+ "test": "mocha"
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",
@@ -23,5 +23,10 @@
23
23
  "bugs": {
24
24
  "url": "https://github.com/ColonelParrot/jscanify/issues"
25
25
  },
26
- "homepage": "https://colonelparrot.github.io/jscanify/"
26
+ "homepage": "https://colonelparrot.github.io/jscanify/",
27
+ "dependencies": {
28
+ "canvas": "^2.11.2",
29
+ "jsdom": "^22.0.0",
30
+ "mocha": "^10.2.0"
31
+ }
27
32
  }
@@ -0,0 +1,261 @@
1
+ /*! jscanify v1.1.0 | (c) ColonelParrot and other contributors | MIT License */
2
+
3
+ const { Canvas, createCanvas, Image, ImageData } = require("canvas");
4
+ const { JSDOM } = require("jsdom");
5
+
6
+ function installDOM() {
7
+ const dom = new JSDOM();
8
+
9
+ global.document = dom.window.document;
10
+ global.Image = Image;
11
+ global.HTMLCanvasElement = Canvas;
12
+ global.ImageData = ImageData;
13
+ global.HTMLImageElement = Image;
14
+ }
15
+
16
+ let cv;
17
+
18
+ /**
19
+ * Calculates distance between two points. Each point must have `x` and `y` property
20
+ * @param {*} p1 point 1
21
+ * @param {*} p2 point 2
22
+ * @returns distance between two points
23
+ */
24
+ function distance(p1, p2) {
25
+ return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
26
+ }
27
+
28
+ class jscanify {
29
+ constructor() {
30
+ installDOM();
31
+ }
32
+
33
+ loadOpenCV(callback) {
34
+ cv = require("./opencv");
35
+ cv["onRuntimeInitialized"] = () => {
36
+ callback(cv);
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Finds the contour of the paper within the image
42
+ * @param {*} img image to process (cv.Mat)
43
+ * @returns the biggest contour inside the image
44
+ */
45
+ findPaperContour(img) {
46
+ const imgGray = new cv.Mat();
47
+ cv.cvtColor(img, imgGray, cv.COLOR_RGBA2GRAY);
48
+
49
+ const imgBlur = new cv.Mat();
50
+ cv.GaussianBlur(
51
+ imgGray,
52
+ imgBlur,
53
+ new cv.Size(5, 5),
54
+ 0,
55
+ 0,
56
+ cv.BORDER_DEFAULT
57
+ );
58
+
59
+ const imgThresh = new cv.Mat();
60
+ cv.threshold(imgBlur, imgThresh, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU);
61
+
62
+ let contours = new cv.MatVector();
63
+ let hierarchy = new cv.Mat();
64
+
65
+ cv.findContours(
66
+ imgThresh,
67
+ contours,
68
+ hierarchy,
69
+ cv.RETR_CCOMP,
70
+ cv.CHAIN_APPROX_SIMPLE
71
+ );
72
+ let maxArea = 0;
73
+ let maxContourIndex = -1;
74
+ for (let i = 0; i < contours.size(); ++i) {
75
+ let contourArea = cv.contourArea(contours.get(i));
76
+ if (contourArea > maxArea) {
77
+ maxArea = contourArea;
78
+ maxContourIndex = i;
79
+ }
80
+ }
81
+
82
+ const maxContour = contours.get(maxContourIndex);
83
+
84
+ imgGray.delete();
85
+ imgBlur.delete();
86
+ imgThresh.delete();
87
+ contours.delete();
88
+ hierarchy.delete();
89
+ return maxContour;
90
+ }
91
+
92
+ /**
93
+ * Highlights the paper detected inside the image.
94
+ * @param {*} image image to process
95
+ * @param {*} options options for highlighting. Accepts `color` and `thickness` parameter
96
+ * @returns `HTMLCanvasElement` with original image and paper highlighted
97
+ */
98
+ highlightPaper(image, options) {
99
+ options = options || {};
100
+ options.color = options.color || "orange";
101
+ options.thickness = options.thickness || 10;
102
+ const canvas = createCanvas();
103
+ const ctx = canvas.getContext("2d");
104
+ const img = cv.imread(image);
105
+
106
+ const maxContour = this.findPaperContour(img);
107
+ cv.imshow(canvas, img);
108
+ if (maxContour) {
109
+ const {
110
+ topLeftCorner,
111
+ topRightCorner,
112
+ bottomLeftCorner,
113
+ bottomRightCorner,
114
+ } = this.getCornerPoints(maxContour, img);
115
+
116
+ if (
117
+ topLeftCorner &&
118
+ topRightCorner &&
119
+ bottomLeftCorner &&
120
+ bottomRightCorner
121
+ ) {
122
+ ctx.strokeStyle = options.color;
123
+ ctx.lineWidth = options.thickness;
124
+ ctx.beginPath();
125
+ ctx.moveTo(...Object.values(topLeftCorner));
126
+ ctx.lineTo(...Object.values(topRightCorner));
127
+ ctx.lineTo(...Object.values(bottomRightCorner));
128
+ ctx.lineTo(...Object.values(bottomLeftCorner));
129
+ ctx.lineTo(...Object.values(topLeftCorner));
130
+ ctx.stroke();
131
+ }
132
+ }
133
+
134
+ img.delete();
135
+ return canvas;
136
+ }
137
+
138
+ /**
139
+ * Extracts and undistorts the image detected within the frame.
140
+ * @param {*} image image to process
141
+ * @param {*} resultWidth desired result paper width
142
+ * @param {*} resultHeight desired result paper height
143
+ * @param {*} onComplete callback with `HTMLCanvasElement` passed - the unwarped paper
144
+ * @param {*} cornerPoints optional custom corner points, in case automatic corner points are incorrect
145
+ */
146
+ extractPaper(image, resultWidth, resultHeight, onComplete, cornerPoints) {
147
+ const canvas = createCanvas();
148
+ const img = cv.imread(image);
149
+ const maxContour = this.findPaperContour(img);
150
+ const {
151
+ topLeftCorner,
152
+ topRightCorner,
153
+ bottomLeftCorner,
154
+ bottomRightCorner,
155
+ } = cornerPoints || this.getCornerPoints(maxContour, img);
156
+ let warpedDst = new cv.Mat();
157
+ let dsize = new cv.Size(resultWidth, resultHeight);
158
+ let srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
159
+ topLeftCorner.x,
160
+ topLeftCorner.y,
161
+ topRightCorner.x,
162
+ topRightCorner.y,
163
+ bottomLeftCorner.x,
164
+ bottomLeftCorner.y,
165
+ bottomRightCorner.x,
166
+ bottomRightCorner.y,
167
+ ]);
168
+ let dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
169
+ 0,
170
+ 0,
171
+ resultWidth,
172
+ 0,
173
+ 0,
174
+ resultHeight,
175
+ resultWidth,
176
+ resultHeight,
177
+ ]);
178
+ let M = cv.getPerspectiveTransform(srcTri, dstTri);
179
+ cv.warpPerspective(
180
+ img,
181
+ warpedDst,
182
+ M,
183
+ dsize,
184
+ cv.INTER_LINEAR,
185
+ cv.BORDER_CONSTANT,
186
+ new cv.Scalar()
187
+ );
188
+
189
+ cv.imshow(canvas, warpedDst);
190
+ const canvasContext = canvas.getContext("2d");
191
+ canvasContext.save();
192
+ canvasContext.scale(1, -1);
193
+ canvasContext.drawImage(canvas, 0, -resultHeight);
194
+ canvasContext.restore();
195
+
196
+ img.delete();
197
+ warpedDst.delete();
198
+ onComplete(canvas);
199
+ }
200
+
201
+ /**
202
+ * Calculates the corner points of a contour.
203
+ * @param {*} contour contour from {@link findPaperContour}
204
+ * @returns object with properties `topLeftCorner`, `topRightCorner`, `bottomLeftCorner`, `bottomRightCorner`, each with `x` and `y` property
205
+ */
206
+ getCornerPoints(contour) {
207
+ let rect = cv.minAreaRect(contour);
208
+ const center = rect.center;
209
+
210
+ let topLeftCorner;
211
+ let topLeftCornerDist = 0;
212
+
213
+ let topRightCorner;
214
+ let topRightCornerDist = 0;
215
+
216
+ let bottomLeftCorner;
217
+ let bottomLeftCornerDist = 0;
218
+
219
+ let bottomRightCorner;
220
+ let bottomRightCornerDist = 0;
221
+
222
+ for (let i = 0; i < contour.data32S.length; i += 2) {
223
+ const point = { x: contour.data32S[i], y: contour.data32S[i + 1] };
224
+ const dist = distance(point, center);
225
+ if (point.x < center.x && point.y > center.y) {
226
+ // top left
227
+ if (dist > topLeftCornerDist) {
228
+ topLeftCorner = point;
229
+ topLeftCornerDist = dist;
230
+ }
231
+ } else if (point.x > center.x && point.y > center.y) {
232
+ // top right
233
+ if (dist > topRightCornerDist) {
234
+ topRightCorner = point;
235
+ topRightCornerDist = dist;
236
+ }
237
+ } else if (point.x < center.x && point.y < center.y) {
238
+ // bottom left
239
+ if (dist > bottomLeftCornerDist) {
240
+ bottomLeftCorner = point;
241
+ bottomLeftCornerDist = dist;
242
+ }
243
+ } else if (point.x > center.x && point.y < center.y) {
244
+ // bottom right
245
+ if (dist > bottomRightCornerDist) {
246
+ bottomRightCorner = point;
247
+ bottomRightCornerDist = dist;
248
+ }
249
+ }
250
+ }
251
+
252
+ return {
253
+ topLeftCorner,
254
+ topRightCorner,
255
+ bottomLeftCorner,
256
+ bottomRightCorner,
257
+ };
258
+ }
259
+ }
260
+
261
+ module.exports = jscanify;