@dile/scanner 0.0.9 → 0.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 +2 -2
- package/src/DileScanner.js +29 -6
- package/src/scanner-engine.js +122 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/scanner",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Web Component to scan documents in the browser: edge detection, perspective correction and multipage PDF or JPEG output, powered by scanic and jsPDF",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"scanner",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"scanic": "1.7.1",
|
|
37
37
|
"scanic-ml": "0.2.0"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "e48f49078bcae4a1c3f879c0c0d03e5445d1a535"
|
|
40
40
|
}
|
package/src/DileScanner.js
CHANGED
|
@@ -53,7 +53,9 @@ const SIZE_FALLBACKS = [
|
|
|
53
53
|
// them, rotate them, adjust them again and remove them. With "ml-detector"
|
|
54
54
|
// the edges are detected with scanic's neural detector (about 2.2 MB
|
|
55
55
|
// downloaded with gzip the first time the scanner opens) instead of the
|
|
56
|
-
// classical one; off by default.
|
|
56
|
+
// classical one; off by default. A photo waits for that download at most
|
|
57
|
+
// "ml-max-wait" ms, and "ml-fast-connection-only" skips it on slow
|
|
58
|
+
// connections.
|
|
57
59
|
//
|
|
58
60
|
// It does not upload anything: when the user saves, it dispatches
|
|
59
61
|
// "dile-document-scanned" with the files and closes. Closing it without saving
|
|
@@ -357,6 +359,19 @@ export class DileScanner extends LitElement {
|
|
|
357
359
|
* ONNX runtime). Empty: they are downloaded from jsDelivr.
|
|
358
360
|
*/
|
|
359
361
|
mlAssetsUrl: { type: String, attribute: "ml-assets-url" },
|
|
362
|
+
/**
|
|
363
|
+
* Maximum time in milliseconds a photo waits for the ML detector while
|
|
364
|
+
* it downloads (0 means no limit). When it is not ready, the classical
|
|
365
|
+
* detector is used for that photo and the download goes on in the
|
|
366
|
+
* background.
|
|
367
|
+
*/
|
|
368
|
+
mlMaxWait: { type: Number, attribute: "ml-max-wait" },
|
|
369
|
+
/**
|
|
370
|
+
* Does not download the ML detector when the browser reports a slow
|
|
371
|
+
* connection (2G, 3G or data saver), unless it is already in the cache
|
|
372
|
+
* (Cache Storage). The classical detector is used instead.
|
|
373
|
+
*/
|
|
374
|
+
mlFastConnectionOnly: { type: Boolean, attribute: "ml-fast-connection-only" },
|
|
360
375
|
_step: { type: String, state: true },
|
|
361
376
|
_pages: { type: Array, state: true },
|
|
362
377
|
_busy: { type: Boolean, state: true },
|
|
@@ -376,6 +391,8 @@ export class DileScanner extends LitElement {
|
|
|
376
391
|
this.labels = {};
|
|
377
392
|
this.mlDetector = false;
|
|
378
393
|
this.mlAssetsUrl = "";
|
|
394
|
+
this.mlMaxWait = 5000;
|
|
395
|
+
this.mlFastConnectionOnly = false;
|
|
379
396
|
this._step = "capture";
|
|
380
397
|
this._pages = [];
|
|
381
398
|
this._busy = false;
|
|
@@ -396,6 +413,15 @@ export class DileScanner extends LitElement {
|
|
|
396
413
|
return { ...DEFAULT_LABELS, ...this.labels };
|
|
397
414
|
}
|
|
398
415
|
|
|
416
|
+
get _engineOptions() {
|
|
417
|
+
return {
|
|
418
|
+
ml: this.mlDetector,
|
|
419
|
+
mlAssetsUrl: this.mlAssetsUrl,
|
|
420
|
+
mlMaxWait: this.mlMaxWait,
|
|
421
|
+
mlFastConnectionOnly: this.mlFastConnectionOnly,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
399
425
|
get _modal() {
|
|
400
426
|
return this.shadowRoot.getElementById("modal");
|
|
401
427
|
}
|
|
@@ -404,7 +430,7 @@ export class DileScanner extends LitElement {
|
|
|
404
430
|
open() {
|
|
405
431
|
this._reset();
|
|
406
432
|
this._saved = false;
|
|
407
|
-
preloadEngine(
|
|
433
|
+
preloadEngine(this._engineOptions);
|
|
408
434
|
this._modal.open();
|
|
409
435
|
}
|
|
410
436
|
|
|
@@ -697,10 +723,7 @@ export class DileScanner extends LitElement {
|
|
|
697
723
|
}
|
|
698
724
|
try {
|
|
699
725
|
const [corners, sourceBlob] = await Promise.all([
|
|
700
|
-
detectCorners(canvas,
|
|
701
|
-
ml: this.mlDetector,
|
|
702
|
-
mlAssetsUrl: this.mlAssetsUrl,
|
|
703
|
-
}).catch(() => null),
|
|
726
|
+
detectCorners(canvas, this._engineOptions).catch(() => null),
|
|
704
727
|
canvasToBlob(canvas, "image/jpeg", SOURCE_QUALITY),
|
|
705
728
|
]);
|
|
706
729
|
if (session !== this._session) {
|
package/src/scanner-engine.js
CHANGED
|
@@ -12,28 +12,121 @@ function loadScanic() {
|
|
|
12
12
|
return scanicModule;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Base URL of the "scanic-ml" files when no mlAssetsUrl is given. scanic uses
|
|
16
|
+
// the same one internally; it is repeated here to look for the files in the
|
|
17
|
+
// cache (see isMlCached). scanner-ml.spec.js checks that its version matches
|
|
18
|
+
// the installed "scanic-ml".
|
|
19
|
+
export const CDN_ML_ASSETS_URL = "https://cdn.jsdelivr.net/npm/scanic-ml@0.2.0/dist/";
|
|
20
|
+
|
|
21
|
+
// The two big files of the ML detector (the .mjs loader is tiny).
|
|
22
|
+
const ML_CACHE_CHECK_FILES = ["doccornernet_lean.ort", "ort-wasm-simd-threaded.wasm"];
|
|
23
|
+
|
|
24
|
+
// Connections where downloading about 2.2 MB takes too long (see
|
|
25
|
+
// isSlowConnection).
|
|
26
|
+
const SLOW_CONNECTION_TYPES = ["slow-2g", "2g", "3g"];
|
|
27
|
+
|
|
28
|
+
// scanic aborts the download of the model after 30 s by default. The download
|
|
29
|
+
// goes on in the background even when a photo stops waiting for it (see
|
|
30
|
+
// mlMaxWait), so it gets a much longer limit: on a slow connection it would
|
|
31
|
+
// never finish (nor reach the cache) with 30 s, and with no limit at all a
|
|
32
|
+
// stalled connection would keep it pending until the page is reloaded.
|
|
33
|
+
const ML_MODEL_FETCH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
// Loading of the ML detector: { key, promise }. The promise resolves to true
|
|
36
|
+
// when it is ready and to false when it fails or is skipped (slow connection),
|
|
37
|
+
// and never rejects. It is forgotten when it does not end ready, so the next
|
|
38
|
+
// open() or photo decides again (e.g. the network is back or got faster).
|
|
39
|
+
let mlLoad = null;
|
|
40
|
+
|
|
15
41
|
// Starts downloading scanic in the background (e.g. when the scanner opens),
|
|
16
42
|
// so it is ready by the time the first photo arrives. With "ml" it also
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
// prepares the neural detector (see loadMl), so the first photo does not wait
|
|
44
|
+
// for about 2.2 MB. Calling it on every open() downloads nothing again.
|
|
45
|
+
export function preloadEngine({ ml = false, mlAssetsUrl = "", mlFastConnectionOnly = false } = {}) {
|
|
46
|
+
loadScanic().catch(() => {
|
|
47
|
+
// Ignored here: the error shows up again when the engine is really used.
|
|
48
|
+
});
|
|
49
|
+
if (ml) {
|
|
50
|
+
loadMl({ mlAssetsUrl, mlFastConnectionOnly });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Downloads the ML detector files and creates its session. scanic keeps the
|
|
55
|
+
// session cached by URL, so detectCorners() reuses it. With
|
|
56
|
+
// "mlFastConnectionOnly" nothing is downloaded on a slow connection, unless the
|
|
57
|
+
// files are already in the cache.
|
|
58
|
+
function loadMl({ mlAssetsUrl, mlFastConnectionOnly }) {
|
|
59
|
+
const key = mlAssetsUrl;
|
|
60
|
+
if (mlLoad?.key === key) {
|
|
61
|
+
return mlLoad.promise;
|
|
62
|
+
}
|
|
63
|
+
const current = {
|
|
64
|
+
key,
|
|
65
|
+
promise: (async () => {
|
|
66
|
+
if (mlFastConnectionOnly && isSlowConnection() && !(await isMlCached(mlAssetsUrl))) {
|
|
67
|
+
return false;
|
|
28
68
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
})
|
|
69
|
+
const { Scanner } = await loadScanic();
|
|
70
|
+
await new Scanner({ detector: "ml", ml: mlOptions(mlAssetsUrl) }).initialize();
|
|
71
|
+
return true;
|
|
72
|
+
})().catch(() => false),
|
|
73
|
+
};
|
|
74
|
+
mlLoad = current;
|
|
75
|
+
current.promise.then((ready) => {
|
|
76
|
+
if (!ready && mlLoad === current) {
|
|
77
|
+
mlLoad = null;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return current.promise;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// True when the browser says the connection is slow or the user asked to save
|
|
84
|
+
// data. Only Chromium browsers have navigator.connection: elsewhere (Safari,
|
|
85
|
+
// Firefox) the connection is unknown and this returns false.
|
|
86
|
+
export function isSlowConnection() {
|
|
87
|
+
const connection = globalThis.navigator?.connection;
|
|
88
|
+
if (!connection) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return connection.saveData === true || SLOW_CONNECTION_TYPES.includes(connection.effectiveType);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// True when the ML files are in the Cache Storage (e.g. cached by the service
|
|
95
|
+
// worker of a PWA). The HTTP cache of the browser can't be checked.
|
|
96
|
+
export async function isMlCached(mlAssetsUrl = "") {
|
|
97
|
+
if (!globalThis.caches) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const base = new URL(mlAssetsUrl || CDN_ML_ASSETS_URL, document.baseURI);
|
|
102
|
+
const folder = base.href.endsWith("/") ? base.href : `${base.href}/`;
|
|
103
|
+
const matches = await Promise.all(
|
|
104
|
+
ML_CACHE_CHECK_FILES.map((file) => caches.match(new URL(file, folder).href)),
|
|
105
|
+
);
|
|
106
|
+
return matches.every(Boolean);
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
33
110
|
}
|
|
34
111
|
|
|
35
112
|
function mlOptions(mlAssetsUrl) {
|
|
36
|
-
return
|
|
113
|
+
return {
|
|
114
|
+
...(mlAssetsUrl ? { assetBaseUrl: mlAssetsUrl } : {}),
|
|
115
|
+
modelFetchTimeoutMs: ML_MODEL_FETCH_TIMEOUT_MS,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Resolves to the value of the promise, or to false after "ms" milliseconds
|
|
120
|
+
// (0 means no limit). The promise itself goes on.
|
|
121
|
+
function withTimeout(promise, ms) {
|
|
122
|
+
if (!ms || ms <= 0) {
|
|
123
|
+
return promise;
|
|
124
|
+
}
|
|
125
|
+
let timer;
|
|
126
|
+
const timeout = new Promise((resolve) => {
|
|
127
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
128
|
+
});
|
|
129
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
37
130
|
}
|
|
38
131
|
|
|
39
132
|
// Returns the corners of the document or null when none is found.
|
|
@@ -45,14 +138,21 @@ function mlOptions(mlAssetsUrl) {
|
|
|
45
138
|
// always fail: it can return a wrong shape (e.g. a line of text) as if it
|
|
46
139
|
// were the document. The neural detector downloads about 2.2 MB with gzip
|
|
47
140
|
// (3.5 MB uncompressed: model + ONNX runtime) the first time (when the
|
|
48
|
-
// scanner opens, see preloadEngine), from jsDelivr
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// detector
|
|
52
|
-
|
|
141
|
+
// scanner opens, see preloadEngine), from jsDelivr unless mlAssetsUrl points
|
|
142
|
+
// to a self-hosted copy of the "scanic-ml" package files.
|
|
143
|
+
//
|
|
144
|
+
// The photo waits for the ML detector at most "mlMaxWait" milliseconds (0
|
|
145
|
+
// means no limit). When it is not ready by then, it was skipped (see
|
|
146
|
+
// mlFastConnectionOnly), it finds nothing or fails (e.g. no network), the
|
|
147
|
+
// classical detector is used. The download goes on in the background, so a
|
|
148
|
+
// later photo or the next open() can use it.
|
|
149
|
+
export async function detectCorners(
|
|
150
|
+
canvas,
|
|
151
|
+
{ ml = false, mlAssetsUrl = "", mlMaxWait = 0, mlFastConnectionOnly = false } = {},
|
|
152
|
+
) {
|
|
53
153
|
const { Scanner } = await loadScanic();
|
|
54
154
|
scanner ??= new Scanner();
|
|
55
|
-
if (ml) {
|
|
155
|
+
if (ml && (await withTimeout(loadMl({ mlAssetsUrl, mlFastConnectionOnly }), mlMaxWait))) {
|
|
56
156
|
try {
|
|
57
157
|
const mlResult = await scanner.scan(canvas, {
|
|
58
158
|
mode: "detect",
|