@dile/scanner 0.0.8 → 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/README.md +30 -4
- package/package.json +2 -2
- package/src/DileScanner.js +258 -80
- package/src/default-labels.js +6 -2
- package/src/scanner-engine.js +126 -9
- package/test/scanner-test-helpers.js +4 -1
- package/vite-plugin.js +42 -4
package/README.md
CHANGED
|
@@ -22,9 +22,9 @@ Read the docs and find demos on the [dile-components site](https://dile-componen
|
|
|
22
22
|
|
|
23
23
|
## ML detector files
|
|
24
24
|
|
|
25
|
-
The ML detector is optional and off by default. Without `ml-detector` your users download nothing of it. With `ml-detector` the component uses scanic's neural detector, and the browser downloads its files (3.5 MB, about 2.2 MB with gzip) the first time
|
|
25
|
+
The ML detector is optional and off by default. Without `ml-detector` your users download nothing of it. With `ml-detector` the component uses scanic's neural detector, and the browser downloads its files (3.5 MB, about 2.2 MB with gzip) in the background the first time the scanner is opened.
|
|
26
26
|
|
|
27
|
-
By default the files come from jsDelivr. The package installs the matching `scanic-ml` version (3.5 MB in `node_modules`, only disk space in your project), so you can serve your own copy with the included Vite plugin:
|
|
27
|
+
By default the files come from jsDelivr. The package installs the matching `scanic-ml` version (3.5 MB in `node_modules`, only disk space in your project), so you can serve your own copy with the included Vite plugin. It serves them in a folder with the `scanic-ml` version (`/dile-scanner-ml/0.2.0/`) and exports that URL in a virtual module:
|
|
28
28
|
|
|
29
29
|
```javascript
|
|
30
30
|
// vite.config.js
|
|
@@ -33,10 +33,36 @@ import { scannerMlAssets } from '@dile/scanner/vite-plugin.js';
|
|
|
33
33
|
export default { plugins: [scannerMlAssets()] };
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
```
|
|
37
|
-
|
|
36
|
+
```javascript
|
|
37
|
+
import { mlAssetsUrl } from 'virtual:dile-scanner-ml';
|
|
38
|
+
|
|
39
|
+
scanner.mlDetector = true;
|
|
40
|
+
scanner.mlAssetsUrl = mlAssetsUrl;
|
|
38
41
|
```
|
|
39
42
|
|
|
43
|
+
## Using it in a PWA
|
|
44
|
+
|
|
45
|
+
The ML files can be cached by the service worker so they are downloaded only once, the first time the scanner is opened. Don't precache them (they would be downloaded when installing the app); add a runtime caching rule instead. With vite-plugin-pwa:
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
VitePWA({
|
|
49
|
+
workbox: {
|
|
50
|
+
globIgnores: ['**/dile-scanner-ml/**'],
|
|
51
|
+
runtimeCaching: [{
|
|
52
|
+
urlPattern: ({ url }) => url.pathname.includes('/dile-scanner-ml/'),
|
|
53
|
+
handler: 'CacheFirst',
|
|
54
|
+
options: {
|
|
55
|
+
cacheName: 'dile-scanner-ml',
|
|
56
|
+
cacheableResponse: { statuses: [200] },
|
|
57
|
+
expiration: { maxEntries: 3 },
|
|
58
|
+
},
|
|
59
|
+
}],
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The versioned folder makes sure an update never mixes cached files of different versions. See the [docs](https://dile-components.com/components/dile-scanner/#pwa) for the details.
|
|
65
|
+
|
|
40
66
|
## Maintenance note
|
|
41
67
|
|
|
42
68
|
`scanic` is pinned to an exact version because `src/corner-editor-styles.js` copies the styles of its corner editor (scanic injects them into `document.head`, which does not reach the shadow root). When updating scanic, review that copy against `node_modules/scanic/dist/scanic.js` (search `.scanic-corner-editor`) and update `scanic-ml` (also pinned) to the version that scanic expects (see the jsDelivr URL in `node_modules/scanic/dist/scanic-mlDetector.js`).
|
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
|
@@ -2,6 +2,8 @@ import { LitElement, html, css, nothing } from "lit";
|
|
|
2
2
|
import "@dile/ui/components/modal/modal.js";
|
|
3
3
|
import "@dile/ui/components/button/button.js";
|
|
4
4
|
import "@dile/ui/components/spinner/spinner.js";
|
|
5
|
+
import "@dile/ui/components/progress-bar/progress-bar.js";
|
|
6
|
+
import "@dile/iconlib/dile-iconlib.js";
|
|
5
7
|
import "@dile/iconlib/lucide-icons/camera.js";
|
|
6
8
|
import "@dile/iconlib/lucide-icons/image.js";
|
|
7
9
|
import "@dile/iconlib/lucide-icons/check.js";
|
|
@@ -50,8 +52,10 @@ const SIZE_FALLBACKS = [
|
|
|
50
52
|
// (output="images"). With "multipage" the user can add several pages, reorder
|
|
51
53
|
// them, rotate them, adjust them again and remove them. With "ml-detector"
|
|
52
54
|
// the edges are detected with scanic's neural detector (about 2.2 MB
|
|
53
|
-
// downloaded with gzip the first time) instead of the
|
|
54
|
-
// default.
|
|
55
|
+
// downloaded with gzip the first time the scanner opens) instead of the
|
|
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.
|
|
55
59
|
//
|
|
56
60
|
// It does not upload anything: when the user saves, it dispatches
|
|
57
61
|
// "dile-document-scanned" with the files and closes. Closing it without saving
|
|
@@ -77,6 +81,14 @@ export class DileScanner extends LitElement {
|
|
|
77
81
|
--dile-modal-border-radius: var(--dile-scanner-modal-border-radius, 15px);
|
|
78
82
|
--dile-modal-content-background-color: var(--dile-background-color, #fff);
|
|
79
83
|
--dile-modal-content-text-color: var(--dile-on-background-color, #303030);
|
|
84
|
+
/* Grays mixed from the theme background and text colors, so the
|
|
85
|
+
scanner works with light and dark themes without hardcoded grays. */
|
|
86
|
+
--_fg: var(--dile-on-background-color, #303030);
|
|
87
|
+
--_bg: var(--dile-background-color, #fff);
|
|
88
|
+
--_surface: color-mix(in srgb, var(--_fg) 6%, var(--_bg));
|
|
89
|
+
--_surface-strong: color-mix(in srgb, var(--_fg) 12%, var(--_bg));
|
|
90
|
+
--_border: color-mix(in srgb, var(--_fg) 15%, var(--_bg));
|
|
91
|
+
--_muted: color-mix(in srgb, var(--_fg) 68%, var(--_bg));
|
|
80
92
|
}
|
|
81
93
|
@media (max-width: 600px) {
|
|
82
94
|
:host {
|
|
@@ -127,14 +139,39 @@ export class DileScanner extends LitElement {
|
|
|
127
139
|
}
|
|
128
140
|
@media (max-width: 600px) {
|
|
129
141
|
.footer {
|
|
130
|
-
|
|
142
|
+
display: grid;
|
|
143
|
+
grid-auto-columns: 1fr;
|
|
144
|
+
grid-auto-flow: column;
|
|
131
145
|
--dile-button-padding-x: 0.6rem;
|
|
132
146
|
--dile-button-font-size: 0.9rem;
|
|
133
147
|
}
|
|
148
|
+
.footer dile-button {
|
|
149
|
+
display: grid;
|
|
150
|
+
}
|
|
134
151
|
}
|
|
152
|
+
/* Secondary buttons set their own text color: inheriting
|
|
153
|
+
--dile-on-primary-color can leave text without contrast on their
|
|
154
|
+
background. By default they use the theme background and text colors,
|
|
155
|
+
so they also work with dark themes. Hover keeps the same
|
|
156
|
+
colors so a theme hover color does not ruin the contrast either. */
|
|
135
157
|
.secondary {
|
|
136
|
-
--dile-button-background-color: var(--dile-scanner-secondary-button-color,
|
|
137
|
-
--dile-button-border-color: var(--dile-scanner-secondary-button-color,
|
|
158
|
+
--dile-button-background-color: var(--dile-scanner-secondary-button-color, var(--_surface-strong));
|
|
159
|
+
--dile-button-border-color: var(--dile-scanner-secondary-button-color, var(--_surface-strong));
|
|
160
|
+
--dile-button-text-color: var(--dile-scanner-secondary-button-text-color, var(--_fg));
|
|
161
|
+
--dile-button-hover-background-color: var(--dile-scanner-secondary-button-color, var(--_surface-strong));
|
|
162
|
+
--dile-button-hover-border-color: var(--dile-scanner-secondary-button-color, var(--_surface-strong));
|
|
163
|
+
--dile-button-hover-text-color: var(--dile-scanner-secondary-button-text-color, var(--_fg));
|
|
164
|
+
}
|
|
165
|
+
/* Outlined buttons to add pages, lighter than the main action. */
|
|
166
|
+
.outline {
|
|
167
|
+
--dile-button-background-color: transparent;
|
|
168
|
+
--dile-button-hover-background-color: transparent;
|
|
169
|
+
--dile-button-border-color: var(--dile-scanner-outline-button-color, var(--_fg));
|
|
170
|
+
--dile-button-hover-border-color: var(--dile-scanner-outline-button-color, var(--_fg));
|
|
171
|
+
--dile-button-text-color: var(--dile-scanner-outline-button-color, var(--_fg));
|
|
172
|
+
--dile-button-hover-text-color: var(--dile-scanner-outline-button-color, var(--_fg));
|
|
173
|
+
--dile-button-border-width: 1px;
|
|
174
|
+
--dile-button-font-weight: normal;
|
|
138
175
|
}
|
|
139
176
|
.file-input {
|
|
140
177
|
display: none;
|
|
@@ -152,53 +189,137 @@ export class DileScanner extends LitElement {
|
|
|
152
189
|
height: calc(100dvh - 18rem);
|
|
153
190
|
}
|
|
154
191
|
}
|
|
192
|
+
.subtitle {
|
|
193
|
+
margin-top: -0.75rem;
|
|
194
|
+
font-size: 0.9rem;
|
|
195
|
+
color: var(--dile-scanner-muted-text-color, var(--_muted));
|
|
196
|
+
}
|
|
197
|
+
/* Pages in a horizontal scroll area with snap: on a phone each card
|
|
198
|
+
takes most of the width and the next one peeks out; on wider screens
|
|
199
|
+
several cards fit side by side. */
|
|
155
200
|
.pages {
|
|
156
|
-
display:
|
|
157
|
-
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
|
201
|
+
display: flex;
|
|
158
202
|
gap: 0.75rem;
|
|
203
|
+
overflow-x: auto;
|
|
204
|
+
scroll-snap-type: x mandatory;
|
|
205
|
+
padding: 0.25rem 0.25rem 0.75rem;
|
|
206
|
+
margin: 0 -0.25rem;
|
|
207
|
+
scrollbar-width: thin;
|
|
159
208
|
}
|
|
160
209
|
.page {
|
|
210
|
+
flex: 0 0 var(--dile-scanner-page-width, min(78%, 250px));
|
|
211
|
+
scroll-snap-align: center;
|
|
161
212
|
display: flex;
|
|
162
213
|
flex-direction: column;
|
|
163
|
-
gap: 0.
|
|
214
|
+
gap: 0.5rem;
|
|
164
215
|
padding: 0.5rem;
|
|
165
|
-
border-radius: 0.
|
|
166
|
-
background-color: var(--dile-
|
|
216
|
+
border-radius: 0.75rem;
|
|
217
|
+
background-color: var(--dile-scanner-page-background-color, var(--_bg));
|
|
218
|
+
border: 1px solid var(--dile-scanner-page-border-color, var(--_border));
|
|
219
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
220
|
+
}
|
|
221
|
+
.page-header .page-number {
|
|
222
|
+
color: var(--_fg);
|
|
167
223
|
}
|
|
168
224
|
.page img {
|
|
169
225
|
width: 100%;
|
|
170
|
-
height:
|
|
226
|
+
height: 200px;
|
|
171
227
|
object-fit: contain;
|
|
172
|
-
|
|
173
|
-
|
|
228
|
+
padding: 0.5rem;
|
|
229
|
+
background-color: var(--dile-scanner-page-image-background-color, var(--_surface-strong));
|
|
230
|
+
border-radius: 0.5rem;
|
|
231
|
+
}
|
|
232
|
+
.single .pages {
|
|
233
|
+
justify-content: center;
|
|
174
234
|
}
|
|
175
235
|
.single .page {
|
|
176
|
-
|
|
177
|
-
margin: 0 auto;
|
|
178
|
-
width: 100%;
|
|
236
|
+
flex-basis: min(100%, 420px);
|
|
179
237
|
}
|
|
180
238
|
.single .page img {
|
|
181
239
|
height: min(55dvh, 520px);
|
|
182
240
|
}
|
|
241
|
+
.page-header {
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
justify-content: space-between;
|
|
245
|
+
gap: 0.25rem;
|
|
246
|
+
padding: 0.25rem;
|
|
247
|
+
border-radius: 0.5rem;
|
|
248
|
+
background-color: var(--dile-scanner-page-header-background-color, var(--_surface));
|
|
249
|
+
--dile-button-border-radius: 0.4rem;
|
|
250
|
+
--dile-button-border-width: 0;
|
|
251
|
+
--dile-button-padding-y: 0.3rem;
|
|
252
|
+
--dile-button-padding-x: 0.3rem;
|
|
253
|
+
--dile-button-background-color: var(--dile-scanner-move-button-color, #5f6368);
|
|
254
|
+
--dile-button-hover-background-color: var(--dile-scanner-move-button-color, #5f6368);
|
|
255
|
+
--dile-button-text-color: #fff;
|
|
256
|
+
--dile-button-hover-text-color: #fff;
|
|
257
|
+
--dile-button-disabled-background-color: transparent;
|
|
258
|
+
--dile-button-disabled-text-color: var(--_border);
|
|
259
|
+
}
|
|
183
260
|
.page-number {
|
|
184
|
-
|
|
261
|
+
flex: 1;
|
|
262
|
+
font-size: 0.9rem;
|
|
185
263
|
text-align: center;
|
|
186
264
|
}
|
|
187
265
|
.page-actions {
|
|
188
|
-
display:
|
|
189
|
-
|
|
190
|
-
|
|
266
|
+
display: grid;
|
|
267
|
+
grid-auto-columns: 1fr;
|
|
268
|
+
grid-auto-flow: column;
|
|
191
269
|
gap: 0.25rem;
|
|
192
|
-
--dile-button-padding-y: 0.3rem;
|
|
193
|
-
--dile-button-padding-x: 0.4rem;
|
|
194
270
|
}
|
|
195
|
-
.page-
|
|
196
|
-
|
|
197
|
-
|
|
271
|
+
.page-action {
|
|
272
|
+
display: flex;
|
|
273
|
+
flex-direction: column;
|
|
274
|
+
align-items: center;
|
|
275
|
+
gap: 0.2rem;
|
|
276
|
+
padding: 0.35rem 0.2rem;
|
|
277
|
+
border: none;
|
|
278
|
+
border-radius: 0.4rem;
|
|
279
|
+
background: none;
|
|
280
|
+
font: inherit;
|
|
281
|
+
font-size: 0.7rem;
|
|
282
|
+
text-transform: uppercase;
|
|
283
|
+
letter-spacing: 0.02em;
|
|
284
|
+
cursor: pointer;
|
|
285
|
+
color: var(--dile-scanner-page-action-color, var(--_fg));
|
|
286
|
+
--dile-icon-color: currentColor;
|
|
287
|
+
--dile-icon-size: 22px;
|
|
288
|
+
}
|
|
289
|
+
.page-action:hover {
|
|
290
|
+
background-color: var(--dile-scanner-page-header-background-color, var(--_surface));
|
|
291
|
+
}
|
|
292
|
+
.page-action:focus-visible {
|
|
293
|
+
outline: 2px solid var(--dile-button-ring-color, #12c9e9);
|
|
294
|
+
}
|
|
295
|
+
.page-action.remove {
|
|
296
|
+
color: var(--dile-scanner-remove-button-color, var(--dile-alert-error-color, #c00));
|
|
297
|
+
font-weight: bold;
|
|
198
298
|
}
|
|
199
299
|
.size {
|
|
300
|
+
display: flex;
|
|
301
|
+
flex-direction: column;
|
|
302
|
+
gap: 0.4rem;
|
|
200
303
|
font-size: 0.85rem;
|
|
201
|
-
|
|
304
|
+
--dile-progress-background: var(--dile-scanner-size-track-color, var(--_surface-strong));
|
|
305
|
+
--dile-progress-bar-background: var(--dile-scanner-size-bar-color, var(--dile-scanner-accent-color, var(--_fg)));
|
|
306
|
+
--dile-progress-bar-min-height: 6px;
|
|
307
|
+
--dile-progress-bar-percentage-font-size: 0;
|
|
308
|
+
--dile-progres-bar-percentage-padding-top: 0;
|
|
309
|
+
--dile-progres-bar-percentage-padding-right: 0;
|
|
310
|
+
--dile-progres-bar-percentage-padding-bottom: 0;
|
|
311
|
+
--dile-progres-bar-percentage-padding-left: 0;
|
|
312
|
+
}
|
|
313
|
+
.size.full {
|
|
314
|
+
--dile-progress-bar-background: var(--dile-alert-error-color, #c00);
|
|
315
|
+
}
|
|
316
|
+
.size-row {
|
|
317
|
+
display: flex;
|
|
318
|
+
justify-content: space-between;
|
|
319
|
+
gap: 0.5rem;
|
|
320
|
+
}
|
|
321
|
+
.size-value {
|
|
322
|
+
color: var(--dile-scanner-muted-text-color, var(--_muted));
|
|
202
323
|
}
|
|
203
324
|
.busy {
|
|
204
325
|
display: flex;
|
|
@@ -230,7 +351,7 @@ export class DileScanner extends LitElement {
|
|
|
230
351
|
* Detects the edges with scanic's neural detector (more robust on hard
|
|
231
352
|
* photos), falling back to the classical one when it finds nothing.
|
|
232
353
|
* Downloads about 2.2 MB with gzip (3.5 MB uncompressed) the first
|
|
233
|
-
* time
|
|
354
|
+
* time the scanner is opened.
|
|
234
355
|
*/
|
|
235
356
|
mlDetector: { type: Boolean, attribute: "ml-detector" },
|
|
236
357
|
/**
|
|
@@ -238,6 +359,19 @@ export class DileScanner extends LitElement {
|
|
|
238
359
|
* ONNX runtime). Empty: they are downloaded from jsDelivr.
|
|
239
360
|
*/
|
|
240
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" },
|
|
241
375
|
_step: { type: String, state: true },
|
|
242
376
|
_pages: { type: Array, state: true },
|
|
243
377
|
_busy: { type: Boolean, state: true },
|
|
@@ -257,6 +391,8 @@ export class DileScanner extends LitElement {
|
|
|
257
391
|
this.labels = {};
|
|
258
392
|
this.mlDetector = false;
|
|
259
393
|
this.mlAssetsUrl = "";
|
|
394
|
+
this.mlMaxWait = 5000;
|
|
395
|
+
this.mlFastConnectionOnly = false;
|
|
260
396
|
this._step = "capture";
|
|
261
397
|
this._pages = [];
|
|
262
398
|
this._busy = false;
|
|
@@ -266,6 +402,7 @@ export class DileScanner extends LitElement {
|
|
|
266
402
|
this._adjusting = null;
|
|
267
403
|
this._editor = null;
|
|
268
404
|
this._saved = false;
|
|
405
|
+
this._scrollToPage = null;
|
|
269
406
|
// Changes on every reset: an async task started before closing or
|
|
270
407
|
// reopening the scanner checks it to discard its result.
|
|
271
408
|
this._session = 0;
|
|
@@ -276,6 +413,15 @@ export class DileScanner extends LitElement {
|
|
|
276
413
|
return { ...DEFAULT_LABELS, ...this.labels };
|
|
277
414
|
}
|
|
278
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
|
+
|
|
279
425
|
get _modal() {
|
|
280
426
|
return this.shadowRoot.getElementById("modal");
|
|
281
427
|
}
|
|
@@ -284,7 +430,7 @@ export class DileScanner extends LitElement {
|
|
|
284
430
|
open() {
|
|
285
431
|
this._reset();
|
|
286
432
|
this._saved = false;
|
|
287
|
-
preloadEngine();
|
|
433
|
+
preloadEngine(this._engineOptions);
|
|
288
434
|
this._modal.open();
|
|
289
435
|
}
|
|
290
436
|
|
|
@@ -293,6 +439,18 @@ export class DileScanner extends LitElement {
|
|
|
293
439
|
this._modal.close();
|
|
294
440
|
}
|
|
295
441
|
|
|
442
|
+
updated() {
|
|
443
|
+
// Brings a new or moved page into view in the horizontal page list. It
|
|
444
|
+
// waits while the step is hidden (busy), because a hidden element can't
|
|
445
|
+
// be scrolled.
|
|
446
|
+
if (this._scrollToPage === null || this._busy || this._step !== "pages") {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const page = this.shadowRoot.querySelector(`.page[data-index="${this._scrollToPage}"]`);
|
|
450
|
+
this._scrollToPage = null;
|
|
451
|
+
page?.scrollIntoView({ block: "nearest", inline: "center", behavior: "smooth" });
|
|
452
|
+
}
|
|
453
|
+
|
|
296
454
|
disconnectedCallback() {
|
|
297
455
|
super.disconnectedCallback();
|
|
298
456
|
this._reset();
|
|
@@ -304,6 +462,12 @@ export class DileScanner extends LitElement {
|
|
|
304
462
|
<dile-modal id="modal" blocking @dile-modal-closed=${this._onModalClosed}>
|
|
305
463
|
<div class="scanner">
|
|
306
464
|
<h2>${labels.title}</h2>
|
|
465
|
+
${this._step === "pages" && this.multipage && !this._busy
|
|
466
|
+
? html`<p class="subtitle">
|
|
467
|
+
${this._pages.length}
|
|
468
|
+
${this._pages.length === 1 ? labels.pageScanned : labels.pagesScanned}
|
|
469
|
+
</p>`
|
|
470
|
+
: nothing}
|
|
307
471
|
${this._busy
|
|
308
472
|
? html`<div class="busy">
|
|
309
473
|
<dile-spinner active></dile-spinner>
|
|
@@ -362,10 +526,11 @@ export class DileScanner extends LitElement {
|
|
|
362
526
|
`;
|
|
363
527
|
}
|
|
364
528
|
|
|
365
|
-
_captureButtons(photoLabel, imageLabel, disabled = false) {
|
|
529
|
+
_captureButtons(photoLabel, imageLabel, disabled = false, className = "") {
|
|
366
530
|
return html`
|
|
367
531
|
${this._isTouchDevice
|
|
368
532
|
? html`<dile-button
|
|
533
|
+
class=${className}
|
|
369
534
|
icon="lucide.camera"
|
|
370
535
|
?disabled=${disabled}
|
|
371
536
|
@click=${this._takePhoto}
|
|
@@ -373,6 +538,7 @@ export class DileScanner extends LitElement {
|
|
|
373
538
|
>`
|
|
374
539
|
: nothing}
|
|
375
540
|
<dile-button
|
|
541
|
+
class=${className}
|
|
376
542
|
icon="lucide.image"
|
|
377
543
|
?disabled=${disabled}
|
|
378
544
|
@click=${this._chooseImage}
|
|
@@ -411,87 +577,100 @@ export class DileScanner extends LitElement {
|
|
|
411
577
|
|
|
412
578
|
_pagesTemplate(labels) {
|
|
413
579
|
const limitReached = this._limitReached;
|
|
580
|
+
const count = this._pages.length;
|
|
414
581
|
return html`
|
|
415
582
|
<div class="${this.multipage ? "" : "single"}">
|
|
416
583
|
<div class="pages">
|
|
417
584
|
${this._pages.map((page, index) => this._pageTemplate(page, index, labels))}
|
|
418
585
|
</div>
|
|
419
586
|
</div>
|
|
420
|
-
${this.maxBytes
|
|
421
|
-
? html`<p class="size">
|
|
422
|
-
${this._formatSize(this._pagesSize)} ${labels.sizeOf}
|
|
423
|
-
${this._formatSize(this.maxBytes)}
|
|
424
|
-
</p>`
|
|
425
|
-
: nothing}
|
|
587
|
+
${this.maxBytes ? this._sizeTemplate(labels, limitReached) : nothing}
|
|
426
588
|
${this.multipage && limitReached
|
|
427
589
|
? html`<p class="message">${labels.sizeLimitReached}</p>`
|
|
428
590
|
: nothing}
|
|
429
591
|
<div class="actions">
|
|
430
592
|
${this.multipage
|
|
431
|
-
? this._captureButtons(labels.addPhoto, labels.addImage, limitReached)
|
|
432
|
-
: this._captureButtons(labels.retakePhoto, labels.replaceImage)}
|
|
593
|
+
? this._captureButtons(labels.addPhoto, labels.addImage, limitReached, "outline")
|
|
594
|
+
: this._captureButtons(labels.retakePhoto, labels.replaceImage, false, "outline")}
|
|
433
595
|
</div>
|
|
434
596
|
<div class="footer">
|
|
435
597
|
<dile-button class="secondary" icon="lucide.x" @click=${this.close}
|
|
436
598
|
>${labels.cancel}</dile-button
|
|
437
599
|
>
|
|
438
600
|
<dile-button icon="lucide.check" @click=${this._save}
|
|
439
|
-
>${labels.confirm}</dile-button
|
|
601
|
+
>${labels.confirm}${this.multipage ? ` (${count})` : ""}</dile-button
|
|
440
602
|
>
|
|
441
603
|
</div>
|
|
442
604
|
`;
|
|
443
605
|
}
|
|
444
606
|
|
|
607
|
+
_sizeTemplate(labels, limitReached) {
|
|
608
|
+
const percent = Math.min(100, Math.round((this._pagesSize / this.maxBytes) * 100));
|
|
609
|
+
return html`
|
|
610
|
+
<div class="size ${limitReached ? "full" : ""}">
|
|
611
|
+
<div class="size-row">
|
|
612
|
+
<span>${labels.documentSize}</span>
|
|
613
|
+
<span class="size-value"
|
|
614
|
+
>${this._formatSize(this._pagesSize)} ${labels.sizeOf}
|
|
615
|
+
${this._formatSize(this.maxBytes)}</span
|
|
616
|
+
>
|
|
617
|
+
</div>
|
|
618
|
+
<dile-progress-bar value=${percent} aria-hidden="true"></dile-progress-bar>
|
|
619
|
+
</div>
|
|
620
|
+
`;
|
|
621
|
+
}
|
|
622
|
+
|
|
445
623
|
_pageTemplate(page, index, labels) {
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
const showOrder = this.multipage && this._pages.length > 1;
|
|
624
|
+
const count = this._pages.length;
|
|
625
|
+
const showOrder = this.multipage && count > 1;
|
|
449
626
|
return html`
|
|
450
|
-
<div class="page">
|
|
451
|
-
<img src=${page.url} alt="${labels.page} ${index + 1}" />
|
|
627
|
+
<div class="page" data-index=${index}>
|
|
452
628
|
${this.multipage
|
|
453
|
-
? html`<div class="page-
|
|
629
|
+
? html`<div class="page-header">
|
|
630
|
+
${showOrder
|
|
631
|
+
? html`<dile-button
|
|
632
|
+
icon="lucide.chevron-left"
|
|
633
|
+
label=${labels.moveBefore}
|
|
634
|
+
title=${labels.moveBefore}
|
|
635
|
+
?disabled=${index === 0}
|
|
636
|
+
@click=${() => this._movePage(index, -1)}
|
|
637
|
+
></dile-button>`
|
|
638
|
+
: nothing}
|
|
639
|
+
<span class="page-number"
|
|
640
|
+
>${labels.page} ${index + 1} ${labels.pageOf} ${count}</span
|
|
641
|
+
>
|
|
642
|
+
${showOrder
|
|
643
|
+
? html`<dile-button
|
|
644
|
+
icon="lucide.chevron-right"
|
|
645
|
+
label=${labels.moveAfter}
|
|
646
|
+
title=${labels.moveAfter}
|
|
647
|
+
?disabled=${index === count - 1}
|
|
648
|
+
@click=${() => this._movePage(index, 1)}
|
|
649
|
+
></dile-button>`
|
|
650
|
+
: nothing}
|
|
651
|
+
</div>`
|
|
454
652
|
: nothing}
|
|
653
|
+
<img src=${page.url} alt="${labels.page} ${index + 1}" />
|
|
455
654
|
<div class="page-actions">
|
|
456
|
-
${
|
|
457
|
-
|
|
458
|
-
icon="lucide.chevron-left"
|
|
459
|
-
label=${labels.moveBefore}
|
|
460
|
-
?disabled=${isFirst}
|
|
461
|
-
@click=${() => this._movePage(index, -1)}
|
|
462
|
-
></dile-button>`
|
|
463
|
-
: nothing}
|
|
464
|
-
<dile-button
|
|
465
|
-
icon="lucide.rotate-cw"
|
|
466
|
-
label=${labels.rotate}
|
|
467
|
-
@click=${() => this._rotatePage(index)}
|
|
468
|
-
></dile-button>
|
|
469
|
-
<dile-button
|
|
470
|
-
icon="lucide.crop"
|
|
471
|
-
label=${labels.adjust}
|
|
472
|
-
@click=${() => this._adjustPage(index)}
|
|
473
|
-
></dile-button>
|
|
655
|
+
${this._pageAction("lucide.rotate-cw", labels.rotate, () => this._rotatePage(index))}
|
|
656
|
+
${this._pageAction("lucide.crop", labels.adjust, () => this._adjustPage(index))}
|
|
474
657
|
${this.multipage
|
|
475
|
-
?
|
|
476
|
-
class="remove"
|
|
477
|
-
icon="lucide.trash-2"
|
|
478
|
-
label=${labels.remove}
|
|
479
|
-
@click=${() => this._removePage(index)}
|
|
480
|
-
></dile-button>`
|
|
481
|
-
: nothing}
|
|
482
|
-
${showOrder
|
|
483
|
-
? html`<dile-button
|
|
484
|
-
icon="lucide.chevron-right"
|
|
485
|
-
label=${labels.moveAfter}
|
|
486
|
-
?disabled=${isLast}
|
|
487
|
-
@click=${() => this._movePage(index, 1)}
|
|
488
|
-
></dile-button>`
|
|
658
|
+
? this._pageAction("lucide.trash-2", labels.remove, () => this._removePage(index), "remove")
|
|
489
659
|
: nothing}
|
|
490
660
|
</div>
|
|
491
661
|
</div>
|
|
492
662
|
`;
|
|
493
663
|
}
|
|
494
664
|
|
|
665
|
+
_pageAction(icon, label, onClick, className = "") {
|
|
666
|
+
return html`
|
|
667
|
+
<button type="button" class="page-action ${className}" data-icon=${icon} @click=${onClick}>
|
|
668
|
+
<dile-iconlib icon=${icon} aria-hidden="true"></dile-iconlib>
|
|
669
|
+
<span>${label}</span>
|
|
670
|
+
</button>
|
|
671
|
+
`;
|
|
672
|
+
}
|
|
673
|
+
|
|
495
674
|
// ---- Capture ----
|
|
496
675
|
|
|
497
676
|
_takePhoto() {
|
|
@@ -544,10 +723,7 @@ export class DileScanner extends LitElement {
|
|
|
544
723
|
}
|
|
545
724
|
try {
|
|
546
725
|
const [corners, sourceBlob] = await Promise.all([
|
|
547
|
-
detectCorners(canvas,
|
|
548
|
-
ml: this.mlDetector,
|
|
549
|
-
mlAssetsUrl: this.mlAssetsUrl,
|
|
550
|
-
}).catch(() => null),
|
|
726
|
+
detectCorners(canvas, this._engineOptions).catch(() => null),
|
|
551
727
|
canvasToBlob(canvas, "image/jpeg", SOURCE_QUALITY),
|
|
552
728
|
]);
|
|
553
729
|
if (session !== this._session) {
|
|
@@ -667,6 +843,7 @@ export class DileScanner extends LitElement {
|
|
|
667
843
|
const pages = [...this._pages];
|
|
668
844
|
if (index === null || index === undefined) {
|
|
669
845
|
pages.push(page);
|
|
846
|
+
this._scrollToPage = pages.length - 1;
|
|
670
847
|
} else {
|
|
671
848
|
URL.revokeObjectURL(pages[index].url);
|
|
672
849
|
pages[index] = page;
|
|
@@ -729,6 +906,7 @@ export class DileScanner extends LitElement {
|
|
|
729
906
|
const pages = [...this._pages];
|
|
730
907
|
[pages[index], pages[target]] = [pages[target], pages[index]];
|
|
731
908
|
this._pages = pages;
|
|
909
|
+
this._scrollToPage = target;
|
|
732
910
|
}
|
|
733
911
|
|
|
734
912
|
_removePage(index) {
|
package/src/default-labels.js
CHANGED
|
@@ -19,11 +19,15 @@ export const DEFAULT_LABELS = {
|
|
|
19
19
|
cancel: "Cancel",
|
|
20
20
|
confirm: "Save document",
|
|
21
21
|
page: "Page",
|
|
22
|
+
pageOf: "of",
|
|
23
|
+
pageScanned: "page scanned",
|
|
24
|
+
pagesScanned: "pages scanned",
|
|
25
|
+
documentSize: "Document size",
|
|
22
26
|
rotate: "Rotate",
|
|
23
|
-
adjust: "
|
|
27
|
+
adjust: "Crop",
|
|
24
28
|
moveBefore: "Move before",
|
|
25
29
|
moveAfter: "Move after",
|
|
26
|
-
remove: "
|
|
30
|
+
remove: "Delete",
|
|
27
31
|
processing: "Processing…",
|
|
28
32
|
readError:
|
|
29
33
|
"The image could not be read. Try with another image (JPG, PNG or WebP).",
|
package/src/scanner-engine.js
CHANGED
|
@@ -12,12 +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
|
-
// so it is ready by the time the first photo arrives.
|
|
17
|
-
|
|
42
|
+
// so it is ready by the time the first photo arrives. With "ml" it also
|
|
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 } = {}) {
|
|
18
46
|
loadScanic().catch(() => {
|
|
19
47
|
// Ignored here: the error shows up again when the engine is really used.
|
|
20
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;
|
|
68
|
+
}
|
|
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
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function mlOptions(mlAssetsUrl) {
|
|
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));
|
|
21
130
|
}
|
|
22
131
|
|
|
23
132
|
// Returns the corners of the document or null when none is found.
|
|
@@ -28,19 +137,27 @@ export function preloadEngine() {
|
|
|
28
137
|
// not as a retry, because on hard photos the classical detector does not
|
|
29
138
|
// always fail: it can return a wrong shape (e.g. a line of text) as if it
|
|
30
139
|
// were the document. The neural detector downloads about 2.2 MB with gzip
|
|
31
|
-
// (3.5 MB uncompressed: model + ONNX runtime) the first time
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
140
|
+
// (3.5 MB uncompressed: model + ONNX runtime) the first time (when the
|
|
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
|
+
) {
|
|
36
153
|
const { Scanner } = await loadScanic();
|
|
37
154
|
scanner ??= new Scanner();
|
|
38
|
-
if (ml) {
|
|
155
|
+
if (ml && (await withTimeout(loadMl({ mlAssetsUrl, mlFastConnectionOnly }), mlMaxWait))) {
|
|
39
156
|
try {
|
|
40
157
|
const mlResult = await scanner.scan(canvas, {
|
|
41
158
|
mode: "detect",
|
|
42
159
|
detector: "ml",
|
|
43
|
-
ml: mlAssetsUrl
|
|
160
|
+
ml: mlOptions(mlAssetsUrl),
|
|
44
161
|
});
|
|
45
162
|
if (mlResult.success) {
|
|
46
163
|
return mlResult.corners;
|
|
@@ -135,7 +135,10 @@ export async function openScanner(attrs) {
|
|
|
135
135
|
export const query = (el, selector) => el.shadowRoot.querySelector(selector);
|
|
136
136
|
export const queryAll = (el, selector) => [...el.shadowRoot.querySelectorAll(selector)];
|
|
137
137
|
export const button = (el, icon) => query(el, `.step:not([hidden]) dile-button[icon="lucide.${icon}"]`);
|
|
138
|
-
|
|
138
|
+
// Buttons of the page cards: the move arrows (dile-button in the header) and
|
|
139
|
+
// the actions (native buttons with data-icon).
|
|
140
|
+
export const pageButtons = (el, icon) =>
|
|
141
|
+
queryAll(el, `.page dile-button[icon="lucide.${icon}"], .page button[data-icon="lucide.${icon}"]`);
|
|
139
142
|
export const pages = (el) => queryAll(el, '.page');
|
|
140
143
|
export const errorMessage = (el) => query(el, '.message.error')?.textContent.trim();
|
|
141
144
|
|
package/vite-plugin.js
CHANGED
|
@@ -6,11 +6,19 @@
|
|
|
6
6
|
// import { scannerMlAssets } from '@dile/scanner/vite-plugin.js';
|
|
7
7
|
// export default { plugins: [scannerMlAssets()] };
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// // app code
|
|
10
|
+
// import { mlAssetsUrl } from 'virtual:dile-scanner-ml';
|
|
11
|
+
// scanner.mlAssetsUrl = mlAssetsUrl; // "/dile-scanner-ml/0.2.0/"
|
|
10
12
|
//
|
|
11
13
|
// The files are served by the dev server and copied to the build with their
|
|
12
14
|
// original names (scanic expects them together in one folder). They are only
|
|
13
15
|
// downloaded by the browser when the ML detector is actually used.
|
|
16
|
+
//
|
|
17
|
+
// The default folder includes the scanic-ml version, so updating
|
|
18
|
+
// @dile/scanner changes the URL of the files. That way a cache (HTTP or a PWA
|
|
19
|
+
// service worker) never mixes the model of one version with the runtime of
|
|
20
|
+
// another. The "virtual:dile-scanner-ml" module exports the current URL, so
|
|
21
|
+
// the app does not have to write the version by hand.
|
|
14
22
|
import { createRequire } from "node:module";
|
|
15
23
|
import { readFileSync } from "node:fs";
|
|
16
24
|
import { dirname, extname, join } from "node:path";
|
|
@@ -21,24 +29,54 @@ export const ML_ASSET_FILES = [
|
|
|
21
29
|
"ort-wasm-simd-threaded.wasm",
|
|
22
30
|
];
|
|
23
31
|
|
|
32
|
+
export const ML_VIRTUAL_MODULE = "virtual:dile-scanner-ml";
|
|
33
|
+
const RESOLVED_VIRTUAL_MODULE = `\0${ML_VIRTUAL_MODULE}`;
|
|
34
|
+
|
|
24
35
|
const CONTENT_TYPES = {
|
|
25
36
|
".ort": "application/octet-stream",
|
|
26
37
|
".mjs": "text/javascript",
|
|
27
38
|
".wasm": "application/wasm",
|
|
28
39
|
};
|
|
29
40
|
|
|
41
|
+
function mlPackageJson() {
|
|
42
|
+
const require = createRequire(import.meta.url);
|
|
43
|
+
return require.resolve("scanic-ml/package.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
// Folder of the "scanic-ml" files, resolved from this package so it is the
|
|
31
47
|
// version pinned by @dile/scanner.
|
|
32
48
|
export function mlAssetsDir() {
|
|
33
|
-
|
|
34
|
-
return join(dirname(require.resolve("scanic-ml/package.json")), "dist");
|
|
49
|
+
return join(dirname(mlPackageJson()), "dist");
|
|
35
50
|
}
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
// Version of the "scanic-ml" files (e.g. "0.2.0").
|
|
53
|
+
export function mlAssetsVersion() {
|
|
54
|
+
return JSON.parse(readFileSync(mlPackageJson(), "utf8")).version;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function scannerMlAssets({
|
|
58
|
+
path = `dile-scanner-ml/${mlAssetsVersion()}`,
|
|
59
|
+
} = {}) {
|
|
38
60
|
const folder = path.replace(/^\/+|\/+$/g, "");
|
|
39
61
|
const assetsDir = mlAssetsDir();
|
|
62
|
+
let base = "/";
|
|
40
63
|
return {
|
|
41
64
|
name: "dile-scanner-ml-assets",
|
|
65
|
+
configResolved(config) {
|
|
66
|
+
base = config.base.endsWith("/") ? config.base : `${config.base}/`;
|
|
67
|
+
},
|
|
68
|
+
resolveId(id) {
|
|
69
|
+
return id === ML_VIRTUAL_MODULE ? RESOLVED_VIRTUAL_MODULE : null;
|
|
70
|
+
},
|
|
71
|
+
load(id) {
|
|
72
|
+
if (id !== RESOLVED_VIRTUAL_MODULE) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return [
|
|
76
|
+
`export const mlAssetsUrl = ${JSON.stringify(`${base}${folder}/`)};`,
|
|
77
|
+
`export const mlAssetsVersion = ${JSON.stringify(mlAssetsVersion())};`,
|
|
78
|
+
].join("\n");
|
|
79
|
+
},
|
|
42
80
|
configureServer(server) {
|
|
43
81
|
server.middlewares.use((req, res, next) => {
|
|
44
82
|
const prefix = `${server.config.base}${folder}/`;
|