jscanify 1.0.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/LICENSE +21 -0
- package/README.md +97 -0
- package/docs/favicon.ico +0 -0
- package/docs/images/galaxy.png +0 -0
- package/docs/images/highlight-paper1.png +0 -0
- package/docs/images/highlight-paper2.png +0 -0
- package/docs/images/logo-full-small.png +0 -0
- package/docs/images/logo-full.png +0 -0
- package/docs/images/logo.png +0 -0
- package/docs/images/scanned-paper1.png +0 -0
- package/docs/images/scanned-paper2.png +0 -0
- package/docs/images/test/test.png +0 -0
- package/docs/images/test/test2.avif +0 -0
- package/docs/index.css +142 -0
- package/docs/index.html +106 -0
- package/docs/script.js +42 -0
- package/package.json +27 -0
- package/src/jscanify.js +275 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 ColonelParrot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/images/logo-full.png" height="100">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
Open-source pure Javascript implemented mobile document scanner. Powered with <a href="https://docs.opencv.org/3.4/d5/d10/tutorial_js_root.html">opencv.js</a><br/><br/>
|
|
7
|
+
Available on <a href="https://www.npmjs.com/package/jscanify">npm</a> or via cdn
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
**Features**:
|
|
11
|
+
|
|
12
|
+
- paper detection & highlighting
|
|
13
|
+
- paper scanning with distortion correction
|
|
14
|
+
|
|
15
|
+
| Image Highlighting | Scanned Result |
|
|
16
|
+
| ------------------------------------------------- | ----------------------------------------------- |
|
|
17
|
+
| <img src="docs/images/highlight-paper1.png"> | <img src="docs/images/scanned-paper1.png"> |
|
|
18
|
+
| <img src="docs/images/highlight-paper2.png"> | <img src="docs/images/scanned-paper2.png"> |
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
### Import
|
|
23
|
+
|
|
24
|
+
npm:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
$ npm i jscanify
|
|
28
|
+
import jscanify from 'jscanify'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
cdn:
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<script src="https://docs.opencv.org/4.7.0/opencv.js" async></script>
|
|
35
|
+
<!-- warning: loading OpenCV can take some time. Load asynchronously -->
|
|
36
|
+
<script src="https://cdn.jsdelivr.net/gh/ColonelParrot/jscanify@master/src/jscanify.min.js"></script>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Highlight Paper in Image
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<img src="/path/to/your/image.png" id="image" />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const scanner = new jscanify();
|
|
47
|
+
image.onload = function () {
|
|
48
|
+
const highlightedCanvas = scanner.highlightPaper(image);
|
|
49
|
+
document.body.appendChild(highlightedCanvas);
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Extract Paper
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const scanner = new jscanify();
|
|
57
|
+
const paperWidth = 500;
|
|
58
|
+
const paperHeight = 1000;
|
|
59
|
+
image.onload = function () {
|
|
60
|
+
scanner.extractPaper(image, paperWidth, paperHeight, (resultCanvas) => {
|
|
61
|
+
document.body.appendChild(resultCanvas);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Highlighting Paper in User Camera
|
|
67
|
+
|
|
68
|
+
The following code continuously reads from the user's camera and highlights the paper:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<video id="video"></video>
|
|
72
|
+
<canvas id="canvas"></canvas> <!-- original video -->
|
|
73
|
+
<canvas id="result"></canvas> <!-- highlighted video -->
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const scanner = new jscanify();
|
|
78
|
+
const canvasCtx = canvas.getContext("2d");
|
|
79
|
+
const resultCtx = result.getContext("2d");
|
|
80
|
+
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
|
|
81
|
+
video.srcObject = stream;
|
|
82
|
+
video.onloadedmetadata = () => {
|
|
83
|
+
video.play();
|
|
84
|
+
|
|
85
|
+
setInterval(() => {
|
|
86
|
+
canvasCtx.drawImage(video, 0, 0);
|
|
87
|
+
const resultCanvas = scanner.highlightPaper(canvas);
|
|
88
|
+
resultCtx.drawImage(resultCanvas, 0, 0);
|
|
89
|
+
}, 10);
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
To export the paper to a PDF, see [here](https://stackoverflow.com/questions/23681325/convert-canvas-to-pdf)
|
|
95
|
+
### Notes
|
|
96
|
+
- for optimal paper detection, the paper should be placed on a flat surface with a solid background color
|
|
97
|
+
- we recommend wrapping your code using `jscanify` in a window `load` event listener to ensure OpenCV is loaded
|
package/docs/favicon.ico
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/index.css
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
@import url("https://fonts.googleapis.com/css?family=Lato:100,200,300,400,500,600,700,800,900");
|
|
2
|
+
|
|
3
|
+
html,
|
|
4
|
+
body {
|
|
5
|
+
margin: 0;
|
|
6
|
+
font-family: "Lato";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#hero {
|
|
10
|
+
width: 100%;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
background-image: url("images/galaxy.png");
|
|
13
|
+
background-size: cover;
|
|
14
|
+
background-position: 0 -170px;
|
|
15
|
+
text-align: center;
|
|
16
|
+
color: white;
|
|
17
|
+
padding: 50px 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#hero h2 {
|
|
21
|
+
font-weight: 300;
|
|
22
|
+
margin: 0 20px;
|
|
23
|
+
margin-top: 15px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#hero img {
|
|
27
|
+
height: 100px;
|
|
28
|
+
max-width: 80%;
|
|
29
|
+
height: auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.view-on .view-on-option {
|
|
33
|
+
padding: 11px 16px;
|
|
34
|
+
border-radius: 8px;
|
|
35
|
+
background: white;
|
|
36
|
+
color: black;
|
|
37
|
+
text-decoration: none;
|
|
38
|
+
font-family: "Lato";
|
|
39
|
+
font-size: 16px;
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.view-on .view-on-option svg {
|
|
45
|
+
margin-right: 8px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#content {
|
|
49
|
+
max-width: 700px;
|
|
50
|
+
margin: auto;
|
|
51
|
+
margin-top: 30px;
|
|
52
|
+
padding: 0 30px;
|
|
53
|
+
padding-bottom: 30px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#demo #demo-images .image-container {
|
|
57
|
+
width: 150px;
|
|
58
|
+
height: 150px;
|
|
59
|
+
border-radius: 5px;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
margin-bottom: 20px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#demo #demo-images .image-container.selected {
|
|
66
|
+
box-shadow: 0 0 0 6px #1e85ff;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#demo #demo-images .image-container img {
|
|
70
|
+
width: 150px;
|
|
71
|
+
height: 150px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#demo {
|
|
75
|
+
display: flex;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#arrow {
|
|
79
|
+
margin: 20px;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#arrow::before {
|
|
85
|
+
content: "\2192";
|
|
86
|
+
font-size: 35px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#demo-result {
|
|
90
|
+
flex: 1;
|
|
91
|
+
border: 3px solid #ededed;
|
|
92
|
+
border-radius: 5px;
|
|
93
|
+
background-color: #f7f7f7;
|
|
94
|
+
padding: 20px;
|
|
95
|
+
box-sizing: border-box;
|
|
96
|
+
|
|
97
|
+
height: 320px;
|
|
98
|
+
overflow: auto;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#demo-result canvas {
|
|
102
|
+
max-width: 100%;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
code {
|
|
106
|
+
background-color: #f1f1f1 !important;
|
|
107
|
+
border-radius: 5px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* content: 2193 when screen smol */
|
|
111
|
+
|
|
112
|
+
@media only screen and (max-width: 600px) {
|
|
113
|
+
#demo{
|
|
114
|
+
display: block;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#demo-images{
|
|
118
|
+
display: flex;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#demo #demo-images .image-container, #demo #demo-images .image-container img{
|
|
123
|
+
height: 100px;
|
|
124
|
+
width: 100px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#demo #demo-images .image-container:first-of-type{
|
|
128
|
+
margin-right: 50px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#demo-title{
|
|
132
|
+
text-align: center;
|
|
133
|
+
}
|
|
134
|
+
#arrow{
|
|
135
|
+
margin-top: 0;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#arrow::before{
|
|
140
|
+
content: "\2193";
|
|
141
|
+
}
|
|
142
|
+
}
|
package/docs/index.html
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-32CWY3SB1G"></script><script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-32CWY3SB1G")</script>
|
|
6
|
+
<meta charset="UTF-8" />
|
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<title>jscanify - Javascript mobile document scanner</title>
|
|
10
|
+
<meta name="description" content="Open-source pure Javascript implemented mobile document scanner." />
|
|
11
|
+
<meta property="og:title" content="jscanify" />
|
|
12
|
+
<meta property="og:description" content="Open-source pure Javascript implemented mobile document scanner." />
|
|
13
|
+
<meta property="og:url" content="https://colonelparrot.github.io/jscanify/" />
|
|
14
|
+
<meta property="og:image" content="https://colonelparrot.github.io/jscanify/images/logo.png" />
|
|
15
|
+
<meta property="og:locale" content="en_US" />
|
|
16
|
+
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
17
|
+
<link rel="stylesheet" href="index.css" />
|
|
18
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-light.min.css"
|
|
19
|
+
integrity="sha512-WDk6RzwygsN9KecRHAfm9HTN87LQjqdygDmkHSJxVkVI7ErCZ8ZWxP6T8RvBujY1n2/E4Ac+bn2ChXnp5rnnHA=="
|
|
20
|
+
crossorigin="anonymous" referrerpolicy="no-referrer" async />
|
|
21
|
+
</head>
|
|
22
|
+
|
|
23
|
+
<body>
|
|
24
|
+
<div id="hero">
|
|
25
|
+
<img src="images/logo-full-small.png" />
|
|
26
|
+
<h2>Open-source pure Javascript implemented mobile document scanner.</h2>
|
|
27
|
+
<br />
|
|
28
|
+
<div class="view-on">
|
|
29
|
+
<a class="view-on-option" href="https://github.com/ColonelParrot/jscanify" target="_blank" style="margin: 10px">
|
|
30
|
+
<svg width="16" height="16" aria-hidden="true" viewBox="0 0 16 16">
|
|
31
|
+
<path fill-rule="evenodd"
|
|
32
|
+
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z">
|
|
33
|
+
</path>
|
|
34
|
+
</svg>
|
|
35
|
+
View on Github
|
|
36
|
+
</a>
|
|
37
|
+
<a class="view-on-option" href="https://nodei.co/npm/jscanify/" target="_blank">
|
|
38
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="41" height="16" viewBox="0 0 256 100"
|
|
39
|
+
preserveAspectRatio="xMinYMin meet">
|
|
40
|
+
<path d="M0 0v85.498h71.166V99.83H128V85.498h128V0H0z" fill="#CB3837"></path>
|
|
41
|
+
<path
|
|
42
|
+
d="M42.502 14.332h-28.17v56.834h28.17V28.664h14.332v42.502h14.332V14.332H42.502zM85.498 14.332v71.166h28.664V71.166h28.17V14.332H85.498zM128 56.834h-13.838v-28.17H128v28.17zM184.834 14.332h-28.17v56.834h28.17V28.664h14.332v42.502h14.332V28.664h14.332v42.502h14.332V14.332h-57.328z"
|
|
43
|
+
fill="#FFF"></path>
|
|
44
|
+
</svg>
|
|
45
|
+
View on npm
|
|
46
|
+
</a>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div id="content">
|
|
50
|
+
<b>jscanify</b> is an open-source pure Javascript implemented mobile
|
|
51
|
+
document scanner designed to run in any Javascript environment
|
|
52
|
+
<u>for free</u>. <br /><br />
|
|
53
|
+
<b>jscanify</b> is capable of detecting & highlighting documents in an
|
|
54
|
+
image, as well as undistorting it. It is fast and easy to learn.<br /><br />
|
|
55
|
+
It can run in the <b>browser</b> or on a server with <b>NodeJS</b>.
|
|
56
|
+
<br /><br />
|
|
57
|
+
<hr />
|
|
58
|
+
<div id="demo-title">
|
|
59
|
+
<h1 style="margin-bottom: 0">Demo</h1>
|
|
60
|
+
<p style="margin-top: 0">Select an image below to scan</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div id="demo">
|
|
63
|
+
<div id="demo-images">
|
|
64
|
+
<div class="image-container">
|
|
65
|
+
<img src="images/test/test.png" />
|
|
66
|
+
</div>
|
|
67
|
+
<div class="image-container" style="margin-bottom: 0">
|
|
68
|
+
<img src="images/test/test2.avif" />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div id="arrow"></div>
|
|
72
|
+
<div id="demo-result">
|
|
73
|
+
Scan results will appear here
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<br /><br />
|
|
77
|
+
<hr />
|
|
78
|
+
<h1>Installation</h1>
|
|
79
|
+
<pre><code class="language-js">$ npm i jscanify
|
|
80
|
+
import jscanify from 'jscanify'</code></pre>
|
|
81
|
+
OR
|
|
82
|
+
<pre><code class="language-html"><script src="https://docs.opencv.org/4.7.0/opencv.js" async></script>
|
|
83
|
+
<script src="https://cdn.jsdelivr.net/gh/ColonelParrot/jscanify@master/src/jscanify.min.js"></script></code></pre>
|
|
84
|
+
<h1>Usage</h1>
|
|
85
|
+
<pre><code class="language-js">const scanner = new jscanify();
|
|
86
|
+
const paperWidth = 500;
|
|
87
|
+
const paperHeight = 1000;
|
|
88
|
+
image.onload = function () {
|
|
89
|
+
scanner.extractPaper(image, paperWidth, paperHeight, (resultCanvas) => {
|
|
90
|
+
document.body.appendChild(resultCanvas);
|
|
91
|
+
});
|
|
92
|
+
};</code></pre>
|
|
93
|
+
It's that easy! Come check out the <a href="https://github.com/ColonelParrot/jscanify/wiki" target="_blank">documentation</a>!
|
|
94
|
+
</div>
|
|
95
|
+
<script src="../src/jscanify.js"></script>
|
|
96
|
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
|
97
|
+
<script src="script.js"></script>
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"
|
|
101
|
+
integrity="sha512-bgHRAiTjGrzHzLyKOnpFvaEpGzJet3z4tZnXGjpsCcqOnAH6VGUx9frc5bcIhKTVLEiCO6vEhNAgx5jtLUYrfA=="
|
|
102
|
+
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
103
|
+
<script>hljs.highlightAll();</script>
|
|
104
|
+
</body>
|
|
105
|
+
|
|
106
|
+
</html>
|
package/docs/script.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
let loadedOpenCV = false
|
|
2
|
+
|
|
3
|
+
const openCvURL = "https://docs.opencv.org/4.7.0/opencv.js"
|
|
4
|
+
|
|
5
|
+
function loadOpenCV(onComplete) {
|
|
6
|
+
if (loadedOpenCV) {
|
|
7
|
+
onComplete()
|
|
8
|
+
} else {
|
|
9
|
+
$('#demo-result').text('Loading OpenCV...')
|
|
10
|
+
const script = document.createElement("script")
|
|
11
|
+
script.src = openCvURL
|
|
12
|
+
|
|
13
|
+
script.onload = function () {
|
|
14
|
+
setTimeout(function () {
|
|
15
|
+
onComplete()
|
|
16
|
+
}, 1000)
|
|
17
|
+
loadedOpenCV = true
|
|
18
|
+
}
|
|
19
|
+
document.body.appendChild(script)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const scanner = new jscanify()
|
|
24
|
+
$('#demo-images .image-container').click(function () {
|
|
25
|
+
$('.image-container.selected').removeClass('selected')
|
|
26
|
+
$(this).addClass('selected')
|
|
27
|
+
const imageSrc = $(this).find('img')[0].src
|
|
28
|
+
console.log(imageSrc)
|
|
29
|
+
loadOpenCV(function () {
|
|
30
|
+
$('#demo-result').empty()
|
|
31
|
+
|
|
32
|
+
const newImg = document.createElement("img")
|
|
33
|
+
newImg.src = imageSrc
|
|
34
|
+
|
|
35
|
+
scanner.extractPaper(newImg, 386, 500, (resultCanvas) => {
|
|
36
|
+
$('#demo-result').append(resultCanvas);
|
|
37
|
+
|
|
38
|
+
const highlightedCanvas = scanner.highlightPaper(newImg)
|
|
39
|
+
$('#demo-result').append(highlightedCanvas);
|
|
40
|
+
});
|
|
41
|
+
})
|
|
42
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jscanify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Open-source Javascript mobile document scanner.",
|
|
5
|
+
"main": "src/jscanify.js",
|
|
6
|
+
"directories": {
|
|
7
|
+
"doc": "docs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/ColonelParrot/jscanify.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"js",
|
|
18
|
+
"scanner",
|
|
19
|
+
"document-scanner"
|
|
20
|
+
],
|
|
21
|
+
"author": "ColonelParrot",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/ColonelParrot/jscanify/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://colonelparrot.github.io/jscanify/"
|
|
27
|
+
}
|
package/src/jscanify.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/*! jscanify v1.0.0 | (c) ColonelParrot and other contributors | MIT License */
|
|
2
|
+
|
|
3
|
+
(function (global, factory) {
|
|
4
|
+
typeof exports === "object" && typeof module !== "undefined"
|
|
5
|
+
? (module.exports = factory())
|
|
6
|
+
: typeof define === "function" && define.amd
|
|
7
|
+
? define(factory)
|
|
8
|
+
: (global.jscanify = factory());
|
|
9
|
+
})(this, function () {
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Calculates distance between two points. Each point must have `x` and `y` property
|
|
14
|
+
* @param {*} p1 point 1
|
|
15
|
+
* @param {*} p2 point 2
|
|
16
|
+
* @returns distance between two points
|
|
17
|
+
*/
|
|
18
|
+
function distance(p1, p2) {
|
|
19
|
+
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class jscanify {
|
|
23
|
+
constructor() { }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Finds the contour of the paper within the image
|
|
27
|
+
* @param {*} img image to process
|
|
28
|
+
* @returns the biggest contour inside the image
|
|
29
|
+
*/
|
|
30
|
+
findPaperContour(img) {
|
|
31
|
+
const imgGray = new cv.Mat();
|
|
32
|
+
cv.cvtColor(img, imgGray, cv.COLOR_RGBA2GRAY);
|
|
33
|
+
|
|
34
|
+
const imgBlur = new cv.Mat();
|
|
35
|
+
cv.GaussianBlur(
|
|
36
|
+
imgGray,
|
|
37
|
+
imgBlur,
|
|
38
|
+
new cv.Size(5, 5),
|
|
39
|
+
0,
|
|
40
|
+
0,
|
|
41
|
+
cv.BORDER_DEFAULT
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const imgThresh = new cv.Mat();
|
|
45
|
+
cv.threshold(
|
|
46
|
+
imgBlur,
|
|
47
|
+
imgThresh,
|
|
48
|
+
0,
|
|
49
|
+
255,
|
|
50
|
+
cv.THRESH_BINARY + cv.THRESH_OTSU
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let contours = new cv.MatVector();
|
|
54
|
+
let hierarchy = new cv.Mat();
|
|
55
|
+
|
|
56
|
+
cv.findContours(
|
|
57
|
+
imgThresh,
|
|
58
|
+
contours,
|
|
59
|
+
hierarchy,
|
|
60
|
+
cv.RETR_CCOMP,
|
|
61
|
+
cv.CHAIN_APPROX_SIMPLE
|
|
62
|
+
);
|
|
63
|
+
let maxArea = 0;
|
|
64
|
+
let maxContourIndex = -1;
|
|
65
|
+
for (let i = 0; i < contours.size(); ++i) {
|
|
66
|
+
let contourArea = cv.contourArea(contours.get(i));
|
|
67
|
+
if (contourArea > maxArea) {
|
|
68
|
+
maxArea = contourArea;
|
|
69
|
+
maxContourIndex = i;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const maxContour = contours.get(maxContourIndex);
|
|
74
|
+
|
|
75
|
+
imgGray.delete();
|
|
76
|
+
imgBlur.delete();
|
|
77
|
+
imgThresh.delete();
|
|
78
|
+
contours.delete();
|
|
79
|
+
hierarchy.delete();
|
|
80
|
+
return maxContour;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Highlights the paper detected inside the image.
|
|
85
|
+
* @param {*} image image to process
|
|
86
|
+
* @param {*} options options for highlighting. Accepts `color` and `thickness` parameter
|
|
87
|
+
* @returns `HTMLCanvasElement` with original image and paper highlighted
|
|
88
|
+
*/
|
|
89
|
+
highlightPaper(image, options) {
|
|
90
|
+
options = options || {};
|
|
91
|
+
options.color = options.color || "orange";
|
|
92
|
+
options.thickness = options.thickness || 10;
|
|
93
|
+
const canvas = document.createElement("canvas");
|
|
94
|
+
const ctx = canvas.getContext("2d");
|
|
95
|
+
const img = cv.imread(image);
|
|
96
|
+
|
|
97
|
+
const maxContour = this.findPaperContour(img);
|
|
98
|
+
if (maxContour) {
|
|
99
|
+
const {
|
|
100
|
+
topLeftCorner,
|
|
101
|
+
topRightCorner,
|
|
102
|
+
bottomLeftCorner,
|
|
103
|
+
bottomRightCorner,
|
|
104
|
+
} = this.getCornerPoints(maxContour, img);
|
|
105
|
+
|
|
106
|
+
cv.imshow(canvas, img);
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
topLeftCorner &&
|
|
110
|
+
topRightCorner &&
|
|
111
|
+
bottomLeftCorner &&
|
|
112
|
+
bottomRightCorner
|
|
113
|
+
) {
|
|
114
|
+
ctx.strokeStyle = options.color;
|
|
115
|
+
ctx.lineWidth = options.thickness;
|
|
116
|
+
ctx.beginPath();
|
|
117
|
+
ctx.moveTo(...Object.values(topLeftCorner));
|
|
118
|
+
ctx.lineTo(...Object.values(topRightCorner));
|
|
119
|
+
ctx.lineTo(...Object.values(bottomRightCorner));
|
|
120
|
+
ctx.lineTo(...Object.values(bottomLeftCorner));
|
|
121
|
+
ctx.lineTo(...Object.values(topLeftCorner));
|
|
122
|
+
ctx.stroke();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
img.delete();
|
|
127
|
+
return canvas;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extracts and undistorts the image detected within the frame.
|
|
132
|
+
* @param {*} image image to process
|
|
133
|
+
* @param {*} resultWidth desired result paper width
|
|
134
|
+
* @param {*} resultHeight desired result paper height
|
|
135
|
+
* @param {*} onComplete callback with `HTMLCanvasElement` passed - the unwarped paper
|
|
136
|
+
* @param {*} cornerPoints optional custom corner points, in case automatic corner points are incorrect
|
|
137
|
+
*/
|
|
138
|
+
extractPaper(image, resultWidth, resultHeight, onComplete, cornerPoints) {
|
|
139
|
+
const canvas = document.createElement("canvas");
|
|
140
|
+
|
|
141
|
+
const img = cv.imread(image);
|
|
142
|
+
|
|
143
|
+
const maxContour = this.findPaperContour(img);
|
|
144
|
+
|
|
145
|
+
const {
|
|
146
|
+
topLeftCorner,
|
|
147
|
+
topRightCorner,
|
|
148
|
+
bottomLeftCorner,
|
|
149
|
+
bottomRightCorner,
|
|
150
|
+
} = cornerPoints || this.getCornerPoints(maxContour, img);
|
|
151
|
+
let warpedDst = new cv.Mat();
|
|
152
|
+
|
|
153
|
+
let dsize = new cv.Size(resultWidth, resultHeight);
|
|
154
|
+
let srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
155
|
+
topLeftCorner.x,
|
|
156
|
+
topLeftCorner.y,
|
|
157
|
+
topRightCorner.x,
|
|
158
|
+
topRightCorner.y,
|
|
159
|
+
bottomLeftCorner.x,
|
|
160
|
+
bottomLeftCorner.y,
|
|
161
|
+
bottomRightCorner.x,
|
|
162
|
+
bottomRightCorner.y,
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
let dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
166
|
+
0,
|
|
167
|
+
0,
|
|
168
|
+
resultWidth,
|
|
169
|
+
0,
|
|
170
|
+
0,
|
|
171
|
+
resultHeight,
|
|
172
|
+
resultWidth,
|
|
173
|
+
resultHeight,
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
let M = cv.getPerspectiveTransform(srcTri, dstTri);
|
|
177
|
+
cv.warpPerspective(
|
|
178
|
+
img,
|
|
179
|
+
warpedDst,
|
|
180
|
+
M,
|
|
181
|
+
dsize,
|
|
182
|
+
cv.INTER_LINEAR,
|
|
183
|
+
cv.BORDER_CONSTANT,
|
|
184
|
+
new cv.Scalar()
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
cv.imshow(canvas, warpedDst);
|
|
188
|
+
|
|
189
|
+
const newImg = document.createElement("img");
|
|
190
|
+
newImg.src = canvas.toDataURL();
|
|
191
|
+
newImg.onload = function () {
|
|
192
|
+
// flip unwarped image
|
|
193
|
+
|
|
194
|
+
let ctx = canvas.getContext("2d");
|
|
195
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
196
|
+
canvas.width = resultWidth;
|
|
197
|
+
canvas.height = resultHeight;
|
|
198
|
+
ctx.setTransform(1, 0, 0, -1, 0, canvas.height);
|
|
199
|
+
|
|
200
|
+
ctx.drawImage(newImg, 0, 0);
|
|
201
|
+
|
|
202
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
203
|
+
|
|
204
|
+
img.delete();
|
|
205
|
+
warpedDst.delete();
|
|
206
|
+
onComplete(canvas);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Calculates the corner points of a contour.
|
|
212
|
+
* @param {*} contour contour from {@link findPaperContour}
|
|
213
|
+
* @returns object with properties `topLeftCorner`, `topRightCorner`, `bottomLeftCorner`, `bottomRightCorner`, each with `x` and `y` property
|
|
214
|
+
*/
|
|
215
|
+
getCornerPoints(contour) {
|
|
216
|
+
let rect = cv.minAreaRect(contour);
|
|
217
|
+
const center = rect.center;
|
|
218
|
+
|
|
219
|
+
let topLeftCorner;
|
|
220
|
+
let topLeftCornerDist = 0;
|
|
221
|
+
|
|
222
|
+
let topRightCorner;
|
|
223
|
+
let topRightCornerDist = 0;
|
|
224
|
+
|
|
225
|
+
let bottomLeftCorner;
|
|
226
|
+
let bottomLeftCornerDist = 0;
|
|
227
|
+
|
|
228
|
+
let bottomRightCorner;
|
|
229
|
+
let bottomRightCornerDist = 0;
|
|
230
|
+
|
|
231
|
+
for (let i = 0; i < contour.data32S.length; i += 2) {
|
|
232
|
+
const point = { x: contour.data32S[i], y: contour.data32S[i + 1] };
|
|
233
|
+
const dist = distance(point, center);
|
|
234
|
+
if (point.x < center.x && point.y > center.y) {
|
|
235
|
+
// top left
|
|
236
|
+
if (dist > topLeftCornerDist) {
|
|
237
|
+
topLeftCorner = point;
|
|
238
|
+
topLeftCornerDist = dist;
|
|
239
|
+
}
|
|
240
|
+
} else if (point.x > center.x && point.y > center.y) {
|
|
241
|
+
// top right
|
|
242
|
+
if (dist > topRightCornerDist) {
|
|
243
|
+
topRightCorner = point;
|
|
244
|
+
topRightCornerDist = dist;
|
|
245
|
+
}
|
|
246
|
+
} else if (point.x < center.x && point.y < center.y) {
|
|
247
|
+
// bottom left
|
|
248
|
+
if (dist > bottomLeftCornerDist) {
|
|
249
|
+
bottomLeftCorner = point;
|
|
250
|
+
bottomLeftCornerDist = dist;
|
|
251
|
+
}
|
|
252
|
+
} else if (point.x > center.x && point.y < center.y) {
|
|
253
|
+
// bottom right
|
|
254
|
+
if (dist > bottomRightCornerDist) {
|
|
255
|
+
bottomRightCorner = point;
|
|
256
|
+
bottomRightCornerDist = dist;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
topLeftCorner,
|
|
263
|
+
topRightCorner,
|
|
264
|
+
bottomLeftCorner,
|
|
265
|
+
bottomRightCorner,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (typeof module !== "undefined") {
|
|
272
|
+
module.exports = { jscanify };
|
|
273
|
+
}
|
|
274
|
+
return jscanify;
|
|
275
|
+
});
|