@exige/web-scanner-sdk 0.2.9

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.
Files changed (57) hide show
  1. package/README.md +330 -0
  2. package/dist/assets/document-worker.js +2 -0
  3. package/dist/assets/document-worker.js.map +1 -0
  4. package/dist/assets/ort-wasm-simd-threaded.jsep.mjs +108 -0
  5. package/dist/assets/ort-wasm-simd-threaded.jsep.wasm +0 -0
  6. package/dist/assets/ort.min.js +2937 -0
  7. package/dist/assets/ort.min.js.map +1 -0
  8. package/dist/assets/tesseract/eng.traineddata.LICENSE +201 -0
  9. package/dist/assets/tesseract/eng.traineddata.NOTICE +9 -0
  10. package/dist/assets/tesseract/eng.traineddata.gz +0 -0
  11. package/dist/assets/tesseract/tesseract-core-lstm.wasm.js +191 -0
  12. package/dist/assets/tesseract/tesseract-core-relaxedsimd-lstm.wasm.js +191 -0
  13. package/dist/assets/tesseract/tesseract-core-simd-lstm.wasm.js +191 -0
  14. package/dist/assets/tesseract/tesseract.js-core.LICENSE +202 -0
  15. package/dist/assets/tesseract/tesseract.js.LICENSE +201 -0
  16. package/dist/assets/tesseract/worker.min.js +3 -0
  17. package/dist/assets/tesseract/worker.min.js.LICENSE.txt +12 -0
  18. package/dist/assets-manifest.json +11 -0
  19. package/dist/components/ModelDownloadRing.d.ts +4 -0
  20. package/dist/components/scanner-camera-overlay.css +2 -0
  21. package/dist/finder-DFueuWuS.js +1225 -0
  22. package/dist/finder-DFueuWuS.js.map +1 -0
  23. package/dist/index.d.ts +24 -0
  24. package/dist/live-overlay-geometry.d.ts +11 -0
  25. package/dist/react/context.d.ts +2 -0
  26. package/dist/react/hooks.d.ts +3 -0
  27. package/dist/react/parts.d.ts +16 -0
  28. package/dist/react/scanner-context.d.ts +5 -0
  29. package/dist/react/types.d.ts +113 -0
  30. package/dist/react.d.ts +4 -0
  31. package/dist/react.js +594 -0
  32. package/dist/react.js.map +1 -0
  33. package/dist/scanner-sdk.js +1001 -0
  34. package/dist/scanner-sdk.js.map +1 -0
  35. package/dist/sdk/camera.d.ts +150 -0
  36. package/dist/sdk/display-quad.d.ts +23 -0
  37. package/dist/sdk/errors.d.ts +14 -0
  38. package/dist/sdk/face-model.d.ts +22 -0
  39. package/dist/sdk/field-model.d.ts +29 -0
  40. package/dist/sdk/finder-simulator.d.ts +17 -0
  41. package/dist/sdk/finder.d.ts +174 -0
  42. package/dist/sdk/frame-geometry.d.ts +6 -0
  43. package/dist/sdk/image.d.ts +18 -0
  44. package/dist/sdk/license-codec.d.ts +2 -0
  45. package/dist/sdk/license-public-key.d.ts +9 -0
  46. package/dist/sdk/license-runtime.d.ts +2 -0
  47. package/dist/sdk/license.d.ts +84 -0
  48. package/dist/sdk/lighting.d.ts +22 -0
  49. package/dist/sdk/model-download.d.ts +25 -0
  50. package/dist/sdk/ocr-runtime.d.ts +27 -0
  51. package/dist/sdk/ocr.d.ts +104 -0
  52. package/dist/sdk/ort-runtime.d.ts +23 -0
  53. package/dist/sdk/programmatic-redaction.d.ts +151 -0
  54. package/dist/sdk/redact.d.ts +17 -0
  55. package/dist/sdk/scan.d.ts +61 -0
  56. package/dist/sdk/types.d.ts +80 -0
  57. package/package.json +93 -0
package/README.md ADDED
@@ -0,0 +1,330 @@
1
+ # Scanner SDK for Web
2
+
3
+ `@exige/web-scanner-sdk` is a client-side web document scanner SDK. ML document finding, OCR, and redaction run in the browser. The public demo is the only path that does not need an account. A hostname you control needs a signed license key.
4
+
5
+ - [Product](https://exige-scanner-sdk-docs.vercel.app/product)
6
+ - [Pricing](https://exige-scanner-sdk-docs.vercel.app/pricing)
7
+ - [Scanbot comparison](https://exige-scanner-sdk-docs.vercel.app/compare/scanbot)
8
+ - [Dynamsoft comparison](https://exige-scanner-sdk-docs.vercel.app/compare/dynamsoft)
9
+ - [FAQ](https://exige-scanner-sdk-docs.vercel.app/faq)
10
+
11
+ All image processing happens in the browser. Camera, finder, scan, OCR, redaction, and torch require a signed license key (`setLicenseKey`). Issuing keys is a separate Next.js app in [`license-portal/`](license-portal/README.md), not part of the npm package.
12
+
13
+ ## Public API
14
+
15
+ ```ts
16
+ import {
17
+ cropDocument,
18
+ mapDocumentCornersToFrame,
19
+ recognizeText,
20
+ redactProgrammatically,
21
+ setLicenseKey,
22
+ startCamera,
23
+ startFinder,
24
+ } from '@exige/web-scanner-sdk'
25
+
26
+ const licenseKey = '<signed key from the license desk>'
27
+ await setLicenseKey(licenseKey)
28
+ const video = document.querySelector('video')!
29
+ const session = await startCamera(video)
30
+ const finder = await startFinder({
31
+ source: video,
32
+ autoCapture: 'defaultOn',
33
+ onCapture: async ({ frame, detection }) => {
34
+ if (!detection?.corners) return
35
+ const corners = mapDocumentCornersToFrame(
36
+ detection.corners,
37
+ { width: detection.sourceWidth, height: detection.sourceHeight },
38
+ { width: frame.width, height: frame.height },
39
+ )
40
+ const scan = await cropDocument(frame, corners)
41
+ if (scan.status !== 'success' || !scan.canvas) {
42
+ console.warn(scan.message)
43
+ return
44
+ }
45
+ const ocr = await recognizeText(scan.canvas, {
46
+ language: 'eng',
47
+ minConfidence: 0.5,
48
+ includeWords: true,
49
+ })
50
+ const redaction = await redactProgrammatically(scan.canvas, {
51
+ ocr,
52
+ rules: [
53
+ { id: 'name', strategy: 'string', mode: 'case-insensitive', values: ['Taylor Morgan'] },
54
+ ],
55
+ style: { mode: 'blackout' },
56
+ })
57
+ console.log(redaction.status, ocr.words?.[0])
58
+ },
59
+ })
60
+ ```
61
+
62
+ Every API accepts browser image sources (`Blob`, `HTMLImageElement`, `HTMLCanvasElement`, or `ImageBitmap`) and has exported TypeScript result types.
63
+
64
+ `minConfidence` uses the normalized `0–1` scale and defaults to `0.5`. Words and characters are filtered independently; `text` is rebuilt from retained words and overall `confidence` is their arithmetic mean. When every word is below the threshold, text is empty and confidence is `0`. OCR box extraction is opt-in: `includeWords` and `includeCharacters` default to `false` and can be enabled independently. Returned bounding boxes use pixel coordinates in the exact input canvas passed to `recognizeText`; after scanning, pass `scan.canvas` so boxes map directly to the perspective-corrected crop. `OcrResult` always includes filtered `text`, `confidence`, `language`, `imageWidth`, and `imageHeight`, with filtered `words` and/or `characters` only when requested.
65
+
66
+ Recognition merges Tesseract Auto on the original crop with Sparse Text on a
67
+ bounded Otsu-binarized copy. Boxes are mapped back to original coordinates,
68
+ overlap-deduplicated with higher confidence preferred, and spatially reordered.
69
+ Standalone punctuation and geometrically or visually implausible boxes are
70
+ rejected before the confidence filter, preventing graphic fragments from
71
+ becoming text while recovering isolated display words.
72
+
73
+ `shadowPass` defaults to `true`. A 12×12 grid finds connected dark paper
74
+ regions in any direction, including the central or offset blob commonly cast
75
+ by a phone and hand under an overhead light. Thin rules and isolated artwork
76
+ do not meet the area/printed-interior test. When accepted OCR density inside
77
+ the blob is low relative to the rest of the page, the SDK estimates local
78
+ illumination with a large box blur, divides it out, and lets Tesseract
79
+ re-binarize a padded Sparse Text tile (maximum 2× enlargement and 1800px
80
+ source dimension). Boxes map back into original crop coordinates before the
81
+ normal merge and rejection pipeline. Worst case is one bounded tile
82
+ recognition; pages without a qualifying low-yield blob pay only a 72×72
83
+ analysis sample. `OcrResult.diagnostics` reports whether the pass ran, its
84
+ strength, and the original-pixel shadow region.
85
+
86
+ ## Programmatic redaction
87
+
88
+ Developers can combine string, nearby-label, normalized template rectangle, and template anchor rules. Pass prior word-level OCR to avoid a second OCR pass:
89
+
90
+ ```ts
91
+ const result = await redactProgrammatically(scan.canvas, {
92
+ ocr,
93
+ rules: [
94
+ { id: 'name', strategy: 'string', mode: 'case-insensitive', values: ['Taylor Morgan'] },
95
+ { id: 'dob', strategy: 'label', label: 'DOB', direction: 'any' },
96
+ ],
97
+ template: {
98
+ id: 'badge-v2',
99
+ fields: [
100
+ { id: 'photo', strategy: 'rect', rect: { x: 0.05, y: 0.12, width: 0.28, height: 0.52 } },
101
+ ],
102
+ },
103
+ style: { mode: 'blackout' },
104
+ requireAll: true,
105
+ })
106
+
107
+ if (result.status === 'unmatched') {
108
+ console.warn(result.unmatchedRuleIds) // no canvas/blob in fail-closed mode
109
+ }
110
+ ```
111
+
112
+ Use `matchRedactionRules` to preview match geometry without changing pixels. `applyRedactions` is the pixel painter for trusted regions you already have. Pass those rectangles yourself. There is no public `redact()` for an end-user drawing tool.
113
+
114
+ Unlabeled and coded IDs can use semantic rules. Logical finding is the default. ML finding is a separate rule with `detector: 'model'`; loading a field model does not replace logical rules.
115
+
116
+ ```ts
117
+ const result = await redactProgrammatically(scan.canvas, {
118
+ ocr,
119
+ rules: [
120
+ { id: 'name', strategy: 'semantic', field: 'person_name' },
121
+ { id: 'ml-name', strategy: 'semantic', field: 'person_name', detector: 'model' },
122
+ { id: 'dob', strategy: 'semantic', field: 'date_of_birth' },
123
+ { id: 'address', strategy: 'semantic', field: 'address' },
124
+ ],
125
+ models: { fields: '/models/id-fields-yolox-nano.onnx', fieldLayout: 'yolox' },
126
+ requireAll: true,
127
+ })
128
+ ```
129
+
130
+ Logical rules use on-device AAMVA aliases, ICAO MRZ evidence, token validation, and conservative layout priors. `detector` is `'logical' | 'model'` and defaults to `'logical'`. Model rules use Apache-2.0 YOLOX-Nano (`models.fieldLayout: 'yolox'`, decoded output `[1, 8400, 12]`). A supplied model is lazy-loaded, and its boxes select OCR words that pass the field's token check. If that file fails to load, `allowHeuristicFallback` (default true) lets model rules use logical finding and reports `modelStatus: 'fallback'`. Omitting `fieldLayout` still decodes YOLOv8 `[1, classes+4, anchors]`.
131
+
132
+ The demo hosts a trained checkpoint at `/models/id-fields-yolox-nano.onnx`. **Semantic field redaction** runs logical finding and does not download a model. **ML field redaction** loads that file with `fieldLayout: 'yolox'`. Either control can be on alone, or both. `openCamera` does not fetch field weights, and the npm package does not download them. Callers pass `models.fields` themselves. The file was fine-tuned on official MIDV-500 images and then on official MIDV-2020 templates. The MIDV-2020 holdout is template sample ids 90–99 (labeled mAP@0.5 0.949). That is separate from the synthetic val mAP@0.5 of 0.998 and from the earlier MIDV-500 documents 41–50 score of 0.117. These weights score 0.075 on that same MIDV-500 split. `postal_code` and `state` had no MIDV-2020 boxes. Photo and scan archives do not label SDK fields.
133
+
134
+ **Face redaction** is separate. Pass `models.face` to `redactProgrammatically`. That option points at `/models/id-face-yolox-nano.onnx`, a one-class YOLOX-Nano (`[1, 8400, 6]`). Detected face rectangles are blacked out on the image. They are not OCR words, and this control does not change the seven-class field model. `openCamera` does not download it. The weights were trained on official MIDV-2020 face rectangles from templates, photos, and scans. Sample ids 90–99 are the holdout in `training/yolox-fields/face-holdout-metrics.json` (mAP@0.5 0.934, chance 0.0099). That is this mock-document split, not a claim about other identity documents.
135
+
136
+ `detectDocument` runs DocAligner and returns an ordered `DocumentQuad` with `topLeft`, `topRight`, `bottomRight`, `bottomLeft`, plus typed `top`, `right`, `bottom`, and `left` line segments. Outcomes are `success`, `no-document`, `low-confidence`, or `low-quality`. `scanDocument` uses that same quad for a projective perspective warp and includes the quad in its result.
137
+
138
+ On touch/mobile-class devices, analysis defaults to a 560px working image and runs in a module Worker when supported. Perspective output is capped at 2048px by default to avoid large transient allocations. Override `maxAnalysisDimension`, `maxOutputDimension`, or `useWorker` in `ScanOptions` when needed.
139
+
140
+ ## Live finder and auto-capture
141
+
142
+ Use a real video/stream or the camera-free canvas simulator:
143
+
144
+ ```ts
145
+ const simulator = createFinderSimulator(canvas)
146
+ simulator.start()
147
+
148
+ const finder = await startFinder({
149
+ source: canvas,
150
+ autoCapture: 'defaultOn',
151
+ onStatus: ({ state, quad, guidance }) => {
152
+ renderOverlay(state, quad)
153
+ instruction.textContent = guidance.hint
154
+ },
155
+ onCapture: async ({ frame, detection }) => {
156
+ if (!detection?.corners) return
157
+ const corners = mapDocumentCornersToFrame(
158
+ detection.corners,
159
+ { width: detection.sourceWidth, height: detection.sourceHeight },
160
+ { width: frame.width, height: frame.height },
161
+ )
162
+ const scan = await cropDocument(frame, corners)
163
+ },
164
+ })
165
+ ```
166
+
167
+ Auto-capture modes are exactly `defaultOn`, `defaultOff`, `alwaysOn`, `alwaysOff`, and `only`. Default modes expose a mutable end-user checkbox; always modes hide it. `only` forces auto-capture on, hides that checkbox, and hides the manual capture button. `alwaysOn` still shows the manual capture button. `setAutoCapture()` returns false for `only`. High confidence, stable raw corners, 72% fill on the limiting axis, 1.5% edge clearance, and acceptable perspective are required before an automatic snap. Fill is the larger of the quad's width and height fill, so a tall receipt or wide boarding pass that already fills its long axis is ready instead of being told to move closer; high-aspect documents are asked for 80% of that long side. Separate, lower exit thresholds keep ready stable through ordinary hand motion. `FinderStatus.guidance` exposes one end-user hint—place the document, move closer, move farther away, hold the camera straight, or hold still—plus its typed reason, limiting-axis fill ratio, and the per-axis `fill` breakdown. Manual capture is always available through `finder.capture()`.
168
+
169
+ ### ML corner detection
170
+
171
+ Document finding always uses DocAligner LCNet100. `detectDocument`, `scanDocument`, and `startFinder(...).detection` load Exige's immutable, CORS-enabled CDN model by default. Omit `modelUrl`. Inference or geometry failure stays visible as `modelStatus: "failed"`. Worker startup throws `ScannerError` with code `worker`. A failed DocAligner HTTP response throws `ScannerError` with code `model-http`. The finder catches both and still reports `modelStatus: "failed"` so the camera stays up. A bad image is `no-document`, `low-confidence`, or `low-quality` and does not throw.
172
+
173
+ The worker lazy-loads `onnxruntime-web` (kept external; it is not concatenated into `scanner-sdk.js`), tries WebGPU, then WASM, reuses sessions, and caches model bytes in IndexedDB plus the browser cache. The JSEP wasm and its glue (`ort-wasm-simd-threaded.jsep.wasm` and `ort-wasm-simd-threaded.jsep.mjs`) ship in the package. Document finding loads them with literal `new URL` calls, so a host app does not copy `/ort/` or set `ortWasmPaths`. Omit `ortWasmPaths`. Pass it only to replace those files: a string is a directory that contains both, or an object `{ wasm, mjs }` sets each URL. The DocAligner weights stay on the CDN, with neighboring Apache-2.0 attribution, and are copied only into the demo build. See the [VitePress ML detector guide](site/guide/ml-document-detector.md).
174
+
175
+ ### Full-screen React camera
176
+
177
+ The demo composes the React camera from a provider and view parts:
178
+
179
+ ```tsx
180
+ import { setLicenseKey } from '@exige/web-scanner-sdk'
181
+ import {
182
+ ScannerControls,
183
+ ScannerFrame,
184
+ ScannerNotice,
185
+ ScannerPreview,
186
+ ScannerProvider,
187
+ ScannerQuad,
188
+ ScannerStatus,
189
+ ScannerTopBar,
190
+ } from '@exige/web-scanner-sdk/react'
191
+ import '@exige/web-scanner-sdk/camera-overlay.css'
192
+
193
+ await setLicenseKey(licenseKey)
194
+
195
+ <ScannerProvider
196
+ open={cameraOpen}
197
+ autoCapture="defaultOn"
198
+ onCancel={() => setCameraOpen(false)}
199
+ onCapture={({ frame, method, settings, displayedQuad }) => {
200
+ console.log(method, settings.width, settings.height)
201
+ return processFullResolutionFrame(frame, displayedQuad)
202
+ }}
203
+ >
204
+ <ScannerFrame style={{ position: 'fixed', inset: 0 }} title="Scan a document">
205
+ <ScannerPreview />
206
+ <ScannerQuad />
207
+ <ScannerTopBar title="Scan a document" />
208
+ <ScannerNotice />
209
+ <ScannerStatus />
210
+ <ScannerControls />
211
+ </ScannerFrame>
212
+ </ScannerProvider>
213
+ ```
214
+
215
+ `useScanner()` reads the session inside the provider. `useScannerRef()` gives the parent an imperative handle, passed as `apiRef`, the same idea as MUI's `useGridApiRef`. The camera covers the viewport, respects safe areas, shows finder/permission/error states, and keeps the existing auto-capture policies. It requests an ideal 4K rear-camera stream, exposes actual track settings, continuously mirrors the last non-black frame, and synchronously copies the unscaled live frame before shutter/flash/track teardown. Capture never calls `ImageCapture.takePhoto()`, which returns black stills on WebKit: the live copy is used when it has visible pixels, the mirrored frame otherwise, and a genuinely blank capture fails loudly instead of exporting black pixels. Render `ScannerDiagnostics` for a live diagnostics panel with engine/browser, video and track state, luminance, and the chosen capture path; **Copy report** pastes a shutter-time dump for support. Pass `theme` to `ScannerFrame` to recolor the camera. `primaryColorHex` replaces the yellowish-green accents (`#c8ff45`). `seekingColorHex` and `detectingColorHex` are the waiting box colors (both `#ef6a5b` unless set apart), `unstableColorHex` is `#ffbe40`, `readyColorHex` is `#8af25a`, and `capturedColorHex` is `#ffffff`. Omit `theme` or any field to keep that default. The [React camera guide](site/guide/react-camera-overlay.md#theme) lists what each field paints. The demo source screen chooses Default, Ocean, Violet, or Signal before the camera starts and passes that palette as `theme` on `ScannerFrame`. Capture results include the displayed quad and its bounded-analysis dimensions; use `mapDocumentCornersToFrame()` plus `cropDocument()` so the final crop matches what was on screen. The framework-free imperative APIs remain available from the root package.
216
+
217
+ ## Run the demo
218
+
219
+ ```bash
220
+ npm install
221
+ npm run dev
222
+ ```
223
+
224
+ Open [http://localhost:43127](http://localhost:43127). Upload a PNG/JPEG/WebP or use a mobile camera, then detect and crop, run OCR, inspect detection suggestions, apply developer redaction rules, and export the result. There is no draw-your-own box tool.
225
+
226
+ ```bash
227
+ npm test
228
+ npm run typecheck
229
+ npm run build
230
+ ```
231
+
232
+ ## Two builds: library and demo
233
+
234
+ The repository produces two independent bundles from the same source. The demo imports `src/index.ts`, the exact public entry published as `@exige/web-scanner-sdk`, so the deployed app exercises the developer-facing API.
235
+
236
+ | Command | Config | Output | Purpose |
237
+ |---|---|---|---|
238
+ | `npm run build:lib` | `vite.config.ts` | `dist/` | Vite library mode: ESM bundle `dist/scanner-sdk.js` plus `.d.ts` declarations. `tesseract.js` and `onnxruntime-web` stay external. The package copies the Tesseract worker, the three LSTM cores, English traineddata, and the ONNX Runtime JSEP wasm into `dist/assets/`. Those bytes are not inside `scanner-sdk.js`. ONNX model weights are not copied into `dist/`. This is what `files`, `exports`, and npm publish use. |
239
+ | `npm run build:demo` | `vite.config.app.ts` | `dist-demo/` | Standard Vite application build: real `index.html`, hashed JS/CSS assets, and `public/` files. This is what static hosting serves. |
240
+ | `npm run build` | both | `dist/` and `dist-demo/` | Convenience wrapper that runs the library build then the demo build. |
241
+
242
+ The library build has no `index.html`, so serving `dist/` from a web host makes the browser download the package entry instead of rendering a page. Deploy `dist-demo/`.
243
+
244
+ Serve the production demo locally to confirm it renders:
245
+
246
+ ```bash
247
+ npm run build:demo
248
+ npm run preview
249
+ ```
250
+
251
+ Open [http://localhost:43129](http://localhost:43129) and confirm an HTML page loads with the scan, finder, OCR, and redaction UI.
252
+
253
+ ## Vercel deployment
254
+
255
+ `vercel.json` pins the demo build so the deployment cannot fall back to the library output:
256
+
257
+ ```json
258
+ {
259
+ "framework": "vite",
260
+ "buildCommand": "npm run build:demo",
261
+ "outputDirectory": "dist-demo",
262
+ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
263
+ }
264
+ ```
265
+
266
+ Matching Vercel project settings (Settings → Build and Deployment):
267
+
268
+ | Setting | Value |
269
+ |---|---|
270
+ | Framework preset | Vite |
271
+ | Root directory | `.` (repository root) |
272
+ | Build command | `npm run build:demo` |
273
+ | Output directory | `dist-demo` |
274
+ | Install command | `npm install` (default) |
275
+
276
+ Leave the project-level build command and output directory overrides off so `vercel.json` applies; if an override is already set in the dashboard, set it to the values above. A `vercel-build` script is also defined as a fallback for zero-config builds. No environment variables or secrets are needed.
277
+
278
+ ## Run the public docs
279
+
280
+ The VitePress site includes integration guides and a TypeDoc-generated API reference. API pages are regenerated from `src/index.ts` before development and production builds, so published signatures track the package exports.
281
+
282
+ ```bash
283
+ npm run docs:dev
284
+ ```
285
+
286
+ Open [http://localhost:43261](http://localhost:43261). To verify the production site:
287
+
288
+ ```bash
289
+ npm run docs:build
290
+ npm run docs:preview
291
+ ```
292
+
293
+ [`vitepress-plugin-llms`](https://github.com/okineadev/vitepress-plugin-llms) emits each page as markdown (`/guide/quickstart.md`, `/api.md`) and writes `/llms.txt` from that same source. The plugin's **Copy as markdown** button copies that file. A docs MCP (`list_pages`, `get_page`, `search_docs`) reads those URLs. The docs site page `/guide/docs-mcp` has the Add to Cursor button and the other client snippets. A Cursor plugin that installs the same remote MCP is in [`plugins/exige-scanner-docs`](plugins/exige-scanner-docs). Deploy the site as a third Vercel project with root directory `site`. Settings, the markdown URL pattern, and the MCP are in [`site/README.md`](site/README.md). The registry manifest is [`docs-mcp/server.json`](docs-mcp/server.json).
294
+
295
+ The docs cover the camera/finder-to-export flow, auto-capture policies, programmatic redaction, and mobile/Safari integration. The generated API reference reflects the current public surface and does not include a draw-boxes `redact()` function.
296
+
297
+ ## Publish to npm
298
+
299
+ `@exige/web-scanner-sdk` publishes from Origin CI at `.depot/workflows/publish.yml` when a `vX.Y.Z` tag is pushed. A matching GitHub Actions copy lives at `.github/workflows/publish.yml`; Origin runs the Depot path. The job does not run on ordinary pushes to `main`. It installs with `npm ci`, checks that `package.json` `"version"` matches the tag (`v0.1.0` ↔ `"0.1.0"`), runs `npm test`, builds the library with `npm run build:lib`, then `npm publish --access public`. The library tarball does not include ONNX weights. Auth uses Depot secret `NPM_TOKEN` through `actions/setup-node` (`registry-url: https://registry.npmjs.org`) and `NODE_AUTH_TOKEN`. Do not put a token in `.npmrc` in git, and do not print the secret.
300
+
301
+ Do not auto-bump the version in CI. To release:
302
+
303
+ 1. Set `"version"` in `package.json` to the new semver (for example `0.1.0`).
304
+ 2. Commit that change on `main` (and any other files the release needs).
305
+ 3. Tag the commit and push the tag:
306
+
307
+ ```bash
308
+ git checkout main
309
+ git pull
310
+ git tag v0.1.0
311
+ git push origin v0.1.0
312
+ ```
313
+
314
+ Replace `0.1.0` with the version you just committed. A `workflow_dispatch` run is available as a fallback; type the same `vX.Y.Z` tag in the confirmation input. The workflow still refuses to publish if that value does not match `package.json`.
315
+
316
+ ## Document models
317
+
318
+ Document finding uses DocAligner LCNet100. The default URL is the Exige CDN file. The demo can also load `/models/docaligner-lcnet100-f4117b78.onnx`. ID-field and face ONNX files under `public/models/` exist for redaction: semantic and ML field rules, and face rectangles passed to `applyRedactions`. The npm tarball does not include those weights. Callers pass `modelUrl` or `models.fields` / `models.face`.
319
+
320
+ `recognizeText()` loads the packaged Tesseract worker, the LSTM core selected the same way `createWorker` does (relaxed SIMD, SIMD, then the baseline build), and English `eng.traineddata.gz` from the package. It does not download them from jsDelivr. For any `language` other than `eng`, pass `langPath`: a directory URL whose files are named `${language}.traineddata.gz` (the same path tesseract.js appends). Set `langPath` with `language: 'eng'` only when replacing the packaged English model. Apache-2.0 notices for Tesseract and the English model are next to those files in `dist/assets/tesseract/`.
321
+
322
+ The previous scanner line is on branch `archive/full-scanner-v0.2.6`.
323
+
324
+ ## Native core
325
+
326
+ [`native/`](native/README.md) still contains the C decoder sources. This SDK line does not copy ONNX weights into the npm tarball, the iOS package in this repository, or the Android AAR, and the web SDK does not call that decoder.
327
+
328
+ ## Browser support and privacy
329
+
330
+ The SDK needs Canvas, `createImageBitmap`, Web Workers, and WebAssembly. Current Chromium, Firefox, and Safari releases support these APIs. Images remain local unless the host application explicitly sends them elsewhere.
@@ -0,0 +1,2 @@
1
+ function e(e,t,n){return(t.x-e.x)*(n.y-e.y)-(t.y-e.y)*(n.x-e.x)}function t(e){let t=0;for(let n=0;n<e.length;n+=1){let r=e[n],i=e[(n+1)%e.length];t+=r.x*i.y-i.x*r.y}return Math.abs(t)/2}function n(e){if(e.length<4)return;let t=e.reduce((e,t)=>t.x+t.y<e.x+e.y?t:e),n=e.reduce((e,t)=>t.x+t.y>e.x+e.y?t:e),r=e.reduce((e,t)=>t.x-t.y>e.x-e.y?t:e),i=e.reduce((e,t)=>t.x-t.y<e.x-e.y?t:e);if((/* @__PURE__ */ new Set([t,r,n,i])).size===4)return{topLeft:t,topRight:r,bottomRight:n,bottomLeft:i}}function r(e){return Math.max(0,Math.min(1,e))}function i(e,t){return Math.hypot(e.x-t.x,e.y-t.y)}function a(e,t,n){let r=e.x-t.x,i=e.y-t.y,a=n.x-t.x,o=n.y-t.y,s=Math.max(1e-6,Math.hypot(r,i)*Math.hypot(a,o));return Math.acos(Math.max(-1,Math.min(1,(r*a+i*o)/s)))*180/Math.PI}function o(e,t,n,r){let i=Math.atan2(t.y-e.y,t.x-e.x),a=Math.atan2(r.y-n.y,r.x-n.x),o=Math.abs(i-a)%Math.PI;return o>Math.PI/2&&(o=Math.PI-o),o}function s(e){let{topLeft:t,topRight:n,bottomRight:r,bottomLeft:i}=e,a=n.x-r.x,o=i.x-r.x,s=n.y-r.y,c=i.y-r.y,l=t.x-n.x+r.x-i.x,u=t.y-n.y+r.y-i.y,d=a*c-o*s,f=Math.abs(d)<1e-6?0:(l*c-o*u)/d,p=Math.abs(d)<1e-6?0:(a*u-l*s)/d;return{a:n.x-t.x+f*n.x,b:i.x-t.x+p*i.x,c:t.x,d:n.y-t.y+f*n.y,e:i.y-t.y+p*i.y,f:t.y,g:f,h:p}}function c(e,t){let{a:n,b:r,c:i,d:a,e:o,f:s,g:c,h:l}=t,u=n-e.x*c,d=r-e.x*l,f=a-e.y*c,p=o-e.y*l,m=e.x-i,h=e.y-s,g=u*p-d*f;if(!(Math.abs(g)<1e-8))return{x:(m*p-d*h)/g,y:(u*h-m*f)/g}}function l(e,t){if(!t.length)return 0;let n=s(e),r=0,i=0,a=Math.max(1,Math.floor(t.length/80));for(let e=0;e<t.length;e+=a){let a=c(t[e],n);if(!a)return 0;let o=Math.min(Math.abs(a.x),Math.abs(1-a.x),Math.abs(a.y),Math.abs(1-a.y))+Math.max(0,-a.x,a.x-1,-a.y,a.y-1)*2;r+=o*o,i+=1}let o=Math.sqrt(r/Math.max(1,i));return Math.exp(-o/.08)}function u(n,s,c,u=[n.topLeft,n.topRight,n.bottomRight,n.bottomLeft]){let d=[n.topLeft,n.topRight,n.bottomRight,n.bottomLeft],f=Math.max(1,Math.min(s,c)),p=Math.max(0,...d.map(e=>Math.max(-e.x,-e.y,e.x-s,e.y-c)/f)),m=d.map((t,n)=>e(t,d[(n+1)%4],d[(n+2)%4])),h=m.every(e=>e>0)||m.every(e=>e<0),g=t(d)/Math.max(1,s*c);if(!h||g<.04||g>.995||p>.12)return{total:0,areaRatio:g,angleScore:0,oppositeAngleScore:0,parallelScore:0,homographyScore:0,aspectBoost:0};let _=d.map((e,t)=>a(d[(t+3)%4],e,d[(t+1)%4])),v=d.map((e,t)=>i(e,d[(t+1)%4])),y=Math.max(v[0],v[2])/Math.max(1,Math.min(v[0],v[2])),b=Math.max(v[1],v[3])/Math.max(1,Math.min(v[1],v[3]));if(_.some(e=>e<18||e>162)||y>4||b>4)return{total:0,areaRatio:g,angleScore:0,oppositeAngleScore:0,parallelScore:0,homographyScore:0,aspectBoost:0};let x=_.reduce((e,t)=>e+Math.exp(-Math.abs(t-90)/48),0)/4,S=(Math.exp(-Math.abs(_[0]+_[2]-180)/55)+Math.exp(-Math.abs(_[1]+_[3]-180)/55))/2,C=(Math.exp(-o(d[0],d[1],d[3],d[2])/.62)+Math.exp(-o(d[0],d[3],d[1],d[2])/.62))/2,w=l(n,u),T=r((g-.04)/.5)*(g>.94?r((.995-g)/.055):1),E=Math.exp(-Math.max(0,Math.max(y,b)-1.6)/1.5),D=p?Math.exp(-p/.07):1,O=Math.max(i(d[0],d[1]),i(d[3],d[2])),k=Math.max(i(d[0],d[3]),i(d[1],d[2])),A=Math.max(O,k)/Math.max(1,Math.min(O,k)),j=Math.exp(-Math.abs(A-1.58)/.18),M=Math.exp(-Math.abs(A-1.29)/.18),N=A>=2?r((A-2)/2):0,P=Math.max(j,M,N)*.08;return{total:r(T*.18+x*.2+S*.14+C*.18+w*.3+P)*(.72+E*.28)*D,areaRatio:g,angleScore:x,oppositeAngleScore:S,parallelScore:C,homographyScore:w,aspectBoost:P}}function d(e,t,n,r=256){let i=r*r,a=new Float32Array(i*3);for(let o=0;o<r;o+=1){let s=Math.min(n-1,Math.round((o+.5)*n/r-.5));for(let n=0;n<r;n+=1){let c=Math.min(t-1,Math.round((n+.5)*t/r-.5)),l=(s*t+c)*4,u=o*r+n;a[u]=e[l]/255,a[i+u]=e[l+1]/255,a[i*2+u]=e[l+2]/255}}return{data:a,dimensions:[1,3,r,r]}}function f(e,t,n,r,i){let a=new Uint8Array(n*r),o=new Int32Array(n*r),s=0,c,l=0;for(let u=0;u<n*r;u+=1){if(a[u]||e[t+u]<i)continue;let d=0,f=0,p=0,m=0,h=0,g=0;for(o[f++]=u,a[u]=1;d<f;){let s=o[d++],c=s%n,l=Math.floor(s/n),u=Math.max(0,e[t+s]);p+=(c+.5)*u,m+=(l+.5)*u,h+=u,g=Math.max(g,u);let _=[s-1,s+1,s-n,s+n];for(let s of _){if(s<0||s>=n*r||a[s]||e[t+s]<i)continue;let l=s%n;Math.abs(l-c)>1||(a[s]=1,o[f++]=s)}}f>s&&h>0&&(s=f,l=g,c={x:p/h,y:m/h})}return c?{point:c,peak:l}:void 0}function p(e,t,r,i,a=.3){if(t.length!==4||Number(t[0])!==1||Number(t[1])!==4)throw Error(`Expected document heatmaps shaped [1,4,H,W], got [${t.join(`,`)}].`);let o=Number(t[2]),s=Number(t[3]),c=s*o;if(e.length<c*4||!s||!o)throw Error(`Document heatmap tensor is incomplete.`);let l=[],d=[];for(let t=0;t<4;t+=1){let n=f(e,t*c,s,o,a);if(!n)return{cornerConfidences:d};l.push({x:Math.max(0,Math.min(r-1,n.point.x/s*r)),y:Math.max(0,Math.min(i-1,n.point.y/o*i))}),d.push(n.peak)}let p=n(l);if(!p)return{cornerConfidences:d};let m=u(p,r,i);if(m.total<.35)return{cornerConfidences:d};let h=d.reduce((e,t)=>e+t,0)/4;return{candidate:{corners:p,areaRatio:m.areaRatio,confidence:Math.min(.99,h*.55+m.total*.45)},cornerConfidences:d}}var m=class extends Error{code;constructor(e,t){super(t),this.name=`ScannerError`,this.code=e}};function h(e){let t=Number(e.headers.get(`content-length`));return Number.isFinite(t)&&t>0?t:null}async function g(e,t){let n=h(e);if(!e.body){let r=await e.arrayBuffer();return t?.({loaded:r.byteLength,total:n??r.byteLength}),r}let r=e.body.getReader(),i=[],a=0;for(;;){let{done:e,value:o}=await r.read();if(e)break;o?.byteLength&&(i.push(o),a+=o.byteLength,t?.({loaded:a,total:n}))}let o=new Uint8Array(a),s=0;for(let e of i)o.set(e,s),s+=e.byteLength;return a===0&&t?.({loaded:0,total:n}),o.buffer}const _=globalThis,v=/* @__PURE__ */ new Map,y=/* @__PURE__ */ new Map,b=`model-bytes`;function x(){return new Promise((e,t)=>{let n=indexedDB.open(`exige-scanner-sdk-models`,1);n.onupgradeneeded=()=>{n.result.objectStoreNames.contains(b)||n.result.createObjectStore(b)},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)})}async function S(e){if(typeof indexedDB>`u`)return;let t=await x();try{return await new Promise((n,r)=>{let i=t.transaction(b).objectStore(b).get(e);i.onsuccess=()=>n(i.result instanceof ArrayBuffer?i.result:void 0),i.onerror=()=>r(i.error)})}finally{t.close()}}async function C(e,t){if(typeof indexedDB>`u`)return;let n=await x();try{await new Promise((r,i)=>{let a=n.transaction(b,`readwrite`);a.objectStore(b).put(t,e),a.oncomplete=()=>r(),a.onerror=()=>i(a.error)})}finally{n.close()}}async function w(e,t,n){let r=v.get(e);return r||(r=(async()=>{if(t)try{let t=await S(e);if(t)return t}catch{}n?.({loaded:0,total:null});let r;try{let i=await fetch(e,{cache:t?`force-cache`:`no-store`});if(!i.ok)throw new m(`model-http`,`Document model request failed with HTTP ${i.status}.`);r=await g(i,n)}catch(e){throw n?.({loaded:0,total:null,done:!0}),e}if(n?.({loaded:r.byteLength,total:r.byteLength,done:!0}),t)try{await C(e,r)}catch{}return r})(),v.set(e,r)),r}function T(e){return e?typeof e==`string`?e:`${e.wasm}|${e.mjs}`:``}async function E(e,t,n,r){let i=`${e}|${T(n)}`,a=y.get(i);return a||(a=(async()=>{let[i,a]=await Promise.all([import("./ort.min.js"),w(e,t,r)]);if(n&&(i.env.wasm.wasmPaths=n),typeof navigator<`u`&&`gpu`in navigator)try{return{session:await i.InferenceSession.create(a,{executionProviders:[`webgpu`],graphOptimizationLevel:`all`}),ort:i,provider:`webgpu`}}catch{}return{session:await i.InferenceSession.create(a,{executionProviders:[`wasm`],graphOptimizationLevel:`all`}),ort:i,provider:`wasm`}})(),y.set(i,a)),a}async function D(e){if(!e.modelUrl)throw Error(`A DocAligner modelUrl is required.`);let t=d(new Uint8ClampedArray(e.pixels),e.width,e.height),{session:n,ort:r,provider:i}=await E(e.modelUrl,e.cacheModel!==!1,e.ortWasmPaths,t=>{_.postMessage({id:e.id,download:{url:e.modelUrl??``,...t}})}),a=performance.now(),o=(await n.run({[n.inputNames[0]]:new r.Tensor(`float32`,t.data,t.dimensions)}))[n.outputNames[0]];if(!o)throw Error(`Document model returned no output tensor.`);return{sharpness:20,candidate:p(o.data,o.dims.map(Number),e.width,e.height,e.heatmapThreshold??.3).candidate,detector:`ml`,modelStatus:`ready`,modelProvider:i,inferenceMs:performance.now()-a}}async function O(e){try{let t=await D(e);return t.candidate?t:{...t,modelStatus:`failed`,modelError:`ML detector returned no geometrically valid corners.`}}catch(e){if(e instanceof m&&e.code===`model-http`)throw e;return{sharpness:20,detector:`ml`,modelStatus:`failed`,modelError:e instanceof Error?e.message:`Document model failed.`}}}let k=Promise.resolve();_.onmessage=e=>{let t=e.data;k=k.then(async()=>{try{_.postMessage({id:t.id,result:await O(t)})}catch(e){_.postMessage({id:t.id,error:e instanceof Error?e.message:`Document analysis failed.`,errorCode:e instanceof m?e.code:void 0})}})};
2
+ //# sourceMappingURL=document-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-worker.js","names":[],"sources":["../../src/sdk/document-geometry.ts","../../src/sdk/document-ml.ts","../../src/sdk/errors.ts","../../src/sdk/model-download.ts","../../src/sdk/document-worker.ts"],"sourcesContent":["import type { DocumentCorners, Point } from './types'\n\nexport interface QuadCandidate {\n corners: DocumentCorners\n confidence: number\n areaRatio: number\n}\n\nexport interface PerspectiveQuadScore {\n total: number\n areaRatio: number\n angleScore: number\n oppositeAngleScore: number\n parallelScore: number\n homographyScore: number\n aspectBoost: number\n}\n\nfunction cross(origin: Point, a: Point, b: Point) {\n return (a.x - origin.x) * (b.y - origin.y) - (a.y - origin.y) * (b.x - origin.x)\n}\n\nexport function polygonArea(points: Point[]) {\n let sum = 0\n for (let index = 0; index < points.length; index += 1) {\n const current = points[index]\n const next = points[(index + 1) % points.length]\n sum += current.x * next.y - next.x * current.y\n }\n return Math.abs(sum) / 2\n}\n\nexport function orderCorners(points: Point[]): DocumentCorners | undefined {\n if (points.length < 4) return undefined\n const topLeft = points.reduce((best, point) => point.x + point.y < best.x + best.y ? point : best)\n const bottomRight = points.reduce((best, point) => point.x + point.y > best.x + best.y ? point : best)\n const topRight = points.reduce((best, point) => point.x - point.y > best.x - best.y ? point : best)\n const bottomLeft = points.reduce((best, point) => point.x - point.y < best.x - best.y ? point : best)\n const unique = new Set([topLeft, topRight, bottomRight, bottomLeft])\n if (unique.size !== 4) return undefined\n return { topLeft, topRight, bottomRight, bottomLeft }\n}\n\nfunction clamp01(value: number) {\n return Math.max(0, Math.min(1, value))\n}\n\nfunction distance(a: Point, b: Point) {\n return Math.hypot(a.x - b.x, a.y - b.y)\n}\n\nfunction interiorAngle(previous: Point, vertex: Point, next: Point) {\n const ax = previous.x - vertex.x\n const ay = previous.y - vertex.y\n const bx = next.x - vertex.x\n const by = next.y - vertex.y\n const denominator = Math.max(1e-6, Math.hypot(ax, ay) * Math.hypot(bx, by))\n return Math.acos(Math.max(-1, Math.min(1, (ax * bx + ay * by) / denominator))) * 180 / Math.PI\n}\n\nfunction directionDifference(a: Point, b: Point, c: Point, d: Point) {\n const first = Math.atan2(b.y - a.y, b.x - a.x)\n const second = Math.atan2(d.y - c.y, d.x - c.x)\n let difference = Math.abs(first - second) % Math.PI\n if (difference > Math.PI / 2) difference = Math.PI - difference\n return difference\n}\n\nfunction unitToQuadCoefficients(corners: DocumentCorners) {\n const { topLeft: tl, topRight: tr, bottomRight: br, bottomLeft: bl } = corners\n const dx1 = tr.x - br.x\n const dx2 = bl.x - br.x\n const dy1 = tr.y - br.y\n const dy2 = bl.y - br.y\n const sx = tl.x - tr.x + br.x - bl.x\n const sy = tl.y - tr.y + br.y - bl.y\n const denominator = dx1 * dy2 - dx2 * dy1\n const g = Math.abs(denominator) < 1e-6 ? 0 : (sx * dy2 - dx2 * sy) / denominator\n const h = Math.abs(denominator) < 1e-6 ? 0 : (dx1 * sy - sx * dy1) / denominator\n return {\n a: tr.x - tl.x + g * tr.x,\n b: bl.x - tl.x + h * bl.x,\n c: tl.x,\n d: tr.y - tl.y + g * tr.y,\n e: bl.y - tl.y + h * bl.y,\n f: tl.y,\n g,\n h,\n }\n}\n\nfunction quadToUnit(point: Point, coefficients: ReturnType<typeof unitToQuadCoefficients>) {\n const { a, b, c, d, e, f, g, h } = coefficients\n const aa = a - point.x * g\n const bb = b - point.x * h\n const dd = d - point.y * g\n const ee = e - point.y * h\n const xx = point.x - c\n const yy = point.y - f\n const determinant = aa * ee - bb * dd\n if (Math.abs(determinant) < 1e-8) return undefined\n return {\n x: (xx * ee - bb * yy) / determinant,\n y: (aa * yy - xx * dd) / determinant,\n }\n}\n\nfunction homographyBoundaryScore(corners: DocumentCorners, hull: Point[]) {\n if (!hull.length) return 0\n const coefficients = unitToQuadCoefficients(corners)\n let squaredResidual = 0\n let samples = 0\n const stride = Math.max(1, Math.floor(hull.length / 80))\n for (let index = 0; index < hull.length; index += stride) {\n const unit = quadToUnit(hull[index], coefficients)\n if (!unit) return 0\n const boundaryDistance = Math.min(\n Math.abs(unit.x),\n Math.abs(1 - unit.x),\n Math.abs(unit.y),\n Math.abs(1 - unit.y),\n )\n const outside = Math.max(0, -unit.x, unit.x - 1, -unit.y, unit.y - 1)\n const residual = boundaryDistance + outside * 2\n squaredResidual += residual * residual\n samples += 1\n }\n const rms = Math.sqrt(squaredResidual / Math.max(1, samples))\n return Math.exp(-rms / 0.08)\n}\n\nexport function scorePerspectiveQuad(\n corners: DocumentCorners,\n frameWidth: number,\n frameHeight: number,\n hull: Point[] = [corners.topLeft, corners.topRight, corners.bottomRight, corners.bottomLeft],\n): PerspectiveQuadScore {\n const ordered = [corners.topLeft, corners.topRight, corners.bottomRight, corners.bottomLeft]\n const frameScale = Math.max(1, Math.min(frameWidth, frameHeight))\n const outsideRatio = Math.max(\n 0,\n ...ordered.map((point) => Math.max(\n -point.x,\n -point.y,\n point.x - frameWidth,\n point.y - frameHeight,\n ) / frameScale),\n )\n const signedCrosses = ordered.map((point, index) =>\n cross(point, ordered[(index + 1) % 4], ordered[(index + 2) % 4]),\n )\n const convex = signedCrosses.every((value) => value > 0) ||\n signedCrosses.every((value) => value < 0)\n const areaRatio = polygonArea(ordered) / Math.max(1, frameWidth * frameHeight)\n if (\n !convex ||\n areaRatio < 0.04 ||\n areaRatio > 0.995 ||\n outsideRatio > 0.12\n ) {\n return {\n total: 0,\n areaRatio,\n angleScore: 0,\n oppositeAngleScore: 0,\n parallelScore: 0,\n homographyScore: 0,\n aspectBoost: 0,\n }\n }\n\n const angles = ordered.map((point, index) =>\n interiorAngle(ordered[(index + 3) % 4], point, ordered[(index + 1) % 4]),\n )\n const edgeLengths = ordered.map((point, index) => distance(point, ordered[(index + 1) % 4]))\n const horizontalScaleRatio = Math.max(edgeLengths[0], edgeLengths[2]) /\n Math.max(1, Math.min(edgeLengths[0], edgeLengths[2]))\n const verticalScaleRatio = Math.max(edgeLengths[1], edgeLengths[3]) /\n Math.max(1, Math.min(edgeLengths[1], edgeLengths[3]))\n if (\n angles.some((angle) => angle < 18 || angle > 162) ||\n horizontalScaleRatio > 4 ||\n verticalScaleRatio > 4\n ) {\n return {\n total: 0,\n areaRatio,\n angleScore: 0,\n oppositeAngleScore: 0,\n parallelScore: 0,\n homographyScore: 0,\n aspectBoost: 0,\n }\n }\n const angleScore = angles.reduce(\n (sum, angle) => sum + Math.exp(-Math.abs(angle - 90) / 48),\n 0,\n ) / 4\n const oppositeAngleScore = (\n Math.exp(-Math.abs(angles[0] + angles[2] - 180) / 55) +\n Math.exp(-Math.abs(angles[1] + angles[3] - 180) / 55)\n ) / 2\n const parallelScore = (\n Math.exp(-directionDifference(ordered[0], ordered[1], ordered[3], ordered[2]) / 0.62) +\n Math.exp(-directionDifference(ordered[0], ordered[3], ordered[1], ordered[2]) / 0.62)\n ) / 2\n const homographyScore = homographyBoundaryScore(corners, hull)\n const areaScore = clamp01((areaRatio - 0.04) / 0.5) *\n (areaRatio > 0.94 ? clamp01((0.995 - areaRatio) / 0.055) : 1)\n const perspectiveScaleScore = Math.exp(\n -Math.max(0, Math.max(horizontalScaleRatio, verticalScaleRatio) - 1.6) / 1.5,\n )\n const frameVisibilityScore = outsideRatio ? Math.exp(-outsideRatio / 0.07) : 1\n const width = Math.max(distance(ordered[0], ordered[1]), distance(ordered[3], ordered[2]))\n const height = Math.max(distance(ordered[0], ordered[3]), distance(ordered[1], ordered[2]))\n const aspect = Math.max(width, height) / Math.max(1, Math.min(width, height))\n const idBoost = Math.exp(-Math.abs(aspect - 1.58) / 0.18)\n const letterBoost = Math.exp(-Math.abs(aspect - 1.29) / 0.18)\n const receiptBoost = aspect >= 2 ? clamp01((aspect - 2) / 2) : 0\n const aspectBoost = Math.max(idBoost, letterBoost, receiptBoost) * 0.08\n const geometricScore = clamp01(\n areaScore * 0.18 +\n angleScore * 0.2 +\n oppositeAngleScore * 0.14 +\n parallelScore * 0.18 +\n homographyScore * 0.3 +\n aspectBoost\n )\n const total = geometricScore * (0.72 + perspectiveScaleScore * 0.28) * frameVisibilityScore\n return {\n total,\n areaRatio,\n angleScore,\n oppositeAngleScore,\n parallelScore,\n homographyScore,\n aspectBoost,\n }\n}\n","import { orderCorners, scorePerspectiveQuad, type QuadCandidate } from './document-geometry'\nimport type { Point } from './types'\n\nexport interface DocumentModelInput {\n data: Float32Array\n dimensions: [1, 3, number, number]\n}\n\nexport interface DecodedDocumentHeatmaps {\n candidate?: QuadCandidate\n cornerConfidences: number[]\n}\n\nexport function preprocessDocumentModel(\n pixels: Uint8ClampedArray,\n width: number,\n height: number,\n inputSize = 256,\n): DocumentModelInput {\n const planeSize = inputSize * inputSize\n const data = new Float32Array(planeSize * 3)\n for (let y = 0; y < inputSize; y += 1) {\n const sourceY = Math.min(height - 1, Math.round((y + 0.5) * height / inputSize - 0.5))\n for (let x = 0; x < inputSize; x += 1) {\n const sourceX = Math.min(width - 1, Math.round((x + 0.5) * width / inputSize - 0.5))\n const sourceOffset = (sourceY * width + sourceX) * 4\n const targetOffset = y * inputSize + x\n data[targetOffset] = pixels[sourceOffset] / 255\n data[planeSize + targetOffset] = pixels[sourceOffset + 1] / 255\n data[planeSize * 2 + targetOffset] = pixels[sourceOffset + 2] / 255\n }\n }\n return { data, dimensions: [1, 3, inputSize, inputSize] }\n}\n\nfunction largestHeatmapComponent(\n values: Float32Array,\n base: number,\n width: number,\n height: number,\n threshold: number,\n) {\n const visited = new Uint8Array(width * height)\n const queue = new Int32Array(width * height)\n let bestCount = 0\n let bestPoint: Point | undefined\n let bestPeak = 0\n for (let seed = 0; seed < width * height; seed += 1) {\n if (visited[seed] || values[base + seed] < threshold) continue\n let read = 0\n let write = 0\n let weightedX = 0\n let weightedY = 0\n let totalWeight = 0\n let peak = 0\n queue[write++] = seed\n visited[seed] = 1\n while (read < write) {\n const index = queue[read++]\n const x = index % width\n const y = Math.floor(index / width)\n const weight = Math.max(0, values[base + index])\n weightedX += (x + 0.5) * weight\n weightedY += (y + 0.5) * weight\n totalWeight += weight\n peak = Math.max(peak, weight)\n const neighbors = [index - 1, index + 1, index - width, index + width]\n for (const neighbor of neighbors) {\n if (\n neighbor < 0 ||\n neighbor >= width * height ||\n visited[neighbor] ||\n values[base + neighbor] < threshold\n ) continue\n const neighborX = neighbor % width\n if (Math.abs(neighborX - x) > 1) continue\n visited[neighbor] = 1\n queue[write++] = neighbor\n }\n }\n if (write > bestCount && totalWeight > 0) {\n bestCount = write\n bestPeak = peak\n bestPoint = {\n x: weightedX / totalWeight,\n y: weightedY / totalWeight,\n }\n }\n }\n return bestPoint ? { point: bestPoint, peak: bestPeak } : undefined\n}\n\nexport function decodeDocumentHeatmaps(\n heatmaps: Float32Array,\n dimensions: readonly number[],\n imageWidth: number,\n imageHeight: number,\n threshold = 0.3,\n): DecodedDocumentHeatmaps {\n if (\n dimensions.length !== 4 ||\n Number(dimensions[0]) !== 1 ||\n Number(dimensions[1]) !== 4\n ) {\n throw new Error(`Expected document heatmaps shaped [1,4,H,W], got [${dimensions.join(',')}].`)\n }\n const heatmapHeight = Number(dimensions[2])\n const heatmapWidth = Number(dimensions[3])\n const planeSize = heatmapWidth * heatmapHeight\n if (heatmaps.length < planeSize * 4 || !heatmapWidth || !heatmapHeight) {\n throw new Error('Document heatmap tensor is incomplete.')\n }\n const points: Point[] = []\n const cornerConfidences: number[] = []\n for (let channel = 0; channel < 4; channel += 1) {\n const component = largestHeatmapComponent(\n heatmaps,\n channel * planeSize,\n heatmapWidth,\n heatmapHeight,\n threshold,\n )\n if (!component) return { cornerConfidences }\n points.push({\n x: Math.max(0, Math.min(imageWidth - 1, component.point.x / heatmapWidth * imageWidth)),\n y: Math.max(0, Math.min(imageHeight - 1, component.point.y / heatmapHeight * imageHeight)),\n })\n cornerConfidences.push(component.peak)\n }\n const corners = orderCorners(points)\n if (!corners) return { cornerConfidences }\n const geometry = scorePerspectiveQuad(corners, imageWidth, imageHeight)\n if (geometry.total < 0.35) return { cornerConfidences }\n const heatmapConfidence = cornerConfidences.reduce((sum, value) => sum + value, 0) / 4\n return {\n candidate: {\n corners,\n areaRatio: geometry.areaRatio,\n confidence: Math.min(0.99, heatmapConfidence * 0.55 + geometry.total * 0.45),\n },\n cornerConfidences,\n }\n}\n","/**\n * Operational failure from the scanner runtime.\n *\n * Scan quality stays a returned status (`success`, `no-document`,\n * `low-confidence`, or `low-quality`). Redaction match quality stays\n * `success`, `partial`, or `unmatched`. Neither of those outcomes uses this error.\n * A bad image does not throw.\n */\nexport type ScannerErrorCode =\n | 'secure-context'\n | 'worker'\n | 'model-http'\n | 'language'\n | 'canvas'\n\nexport class ScannerError extends Error {\n readonly code: ScannerErrorCode\n\n constructor(code: ScannerErrorCode, message: string) {\n super(message)\n this.name = 'ScannerError'\n this.code = code\n }\n}\n\nexport function canvasUnavailable(): ScannerError {\n return new ScannerError('canvas', 'Canvas 2D is unavailable in this browser.')\n}\n","import { ScannerError } from './errors'\n\nexport interface ModelDownloadProgress {\n loaded: number\n /** Byte length from Content-Length. Null when that header is absent. */\n total: number | null\n done?: boolean\n}\n\nexport interface ModelDownloadSnapshot {\n downloading: boolean\n loaded: number\n /** Sum of Content-Length values when every active transfer has one. */\n total: number | null\n}\n\nconst active = new Map<string, { loaded: number; total: number | null }>()\nconst listeners = new Set<(snapshot: ModelDownloadSnapshot) => void>()\nconst cache = new Map<string, ArrayBuffer>()\nconst inflight = new Map<string, Promise<ArrayBuffer>>()\n\nfunction contentLength(response: Response) {\n const parsed = Number(response.headers.get('content-length'))\n return Number.isFinite(parsed) && parsed > 0 ? parsed : null\n}\n\nexport function modelDownloadSnapshot(): ModelDownloadSnapshot {\n if (active.size === 0) return { downloading: false, loaded: 0, total: null }\n let loaded = 0\n let total = 0\n let known = true\n for (const item of active.values()) {\n loaded += item.loaded\n if (item.total == null) known = false\n else total += item.total\n }\n return { downloading: true, loaded, total: known ? total : null }\n}\n\nfunction emit() {\n const snapshot = modelDownloadSnapshot()\n for (const listener of listeners) listener(snapshot)\n}\n\n/** Subscribing does not fetch. Downloads start only from `downloadModelBytes`. */\nexport function subscribeModelDownloads(listener: (snapshot: ModelDownloadSnapshot) => void) {\n listener(modelDownloadSnapshot())\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/** Progress reported from another realm, such as the document worker. */\nexport function reportModelDownload(url: string, progress: ModelDownloadProgress) {\n if (progress.done) active.delete(url)\n else active.set(url, { loaded: progress.loaded, total: progress.total })\n emit()\n}\n\nexport async function readResponseBytes(\n response: Response,\n onProgress?: (progress: ModelDownloadProgress) => void,\n) {\n const total = contentLength(response)\n if (!response.body) {\n const bytes = await response.arrayBuffer()\n onProgress?.({ loaded: bytes.byteLength, total: total ?? bytes.byteLength })\n return bytes\n }\n const reader = response.body.getReader()\n const chunks: Uint8Array[] = []\n let loaded = 0\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!value?.byteLength) continue\n chunks.push(value)\n loaded += value.byteLength\n onProgress?.({ loaded, total })\n }\n const bytes = new Uint8Array(loaded)\n let offset = 0\n for (const chunk of chunks) {\n bytes.set(chunk, offset)\n offset += chunk.byteLength\n }\n if (loaded === 0) onProgress?.({ loaded: 0, total })\n return bytes.buffer\n}\n\n/**\n * Fetch a model file and report loaded/total while the body transfers.\n * A second call for the same URL reuses the cached bytes and does not fetch.\n */\nexport async function downloadModelBytes(url: string, init?: { cache?: RequestCache }) {\n const cached = cache.get(url)\n if (cached) return cached.slice(0)\n const existing = inflight.get(url)\n if (existing) return existing.then((bytes) => bytes.slice(0))\n const request = (async () => {\n reportModelDownload(url, { loaded: 0, total: null })\n const response = await fetch(url, { cache: init?.cache })\n if (!response.ok) {\n throw new ScannerError('model-http', `Model request failed with HTTP ${response.status}.`)\n }\n return readResponseBytes(response, (progress) => reportModelDownload(url, progress))\n })()\n inflight.set(url, request)\n try {\n const bytes = await request\n cache.set(url, bytes)\n return bytes.slice(0)\n } finally {\n inflight.delete(url)\n if (active.has(url)) {\n active.delete(url)\n emit()\n }\n }\n}\n","import {\n decodeDocumentHeatmaps,\n preprocessDocumentModel,\n} from './document-ml'\nimport { ScannerError, type ScannerErrorCode } from './errors'\nimport { readResponseBytes, type ModelDownloadProgress } from './model-download'\nimport type { DocumentModelStatus } from './types'\nimport type { QuadCandidate } from './document-geometry'\n\ninterface PixelAnalysis {\n sharpness: number\n candidate?: QuadCandidate\n detector?: 'ml'\n modelStatus?: DocumentModelStatus\n modelProvider?: 'webgpu' | 'wasm'\n modelError?: string\n inferenceMs?: number\n}\n\ninterface AnalysisRequest {\n id: number\n pixels: ArrayBuffer\n width: number\n height: number\n modelUrl?: string\n heatmapThreshold?: number\n cacheModel?: boolean\n ortWasmPaths?: string | { wasm: string; mjs: string }\n}\n\ninterface AnalysisResponse {\n id: number\n result?: PixelAnalysis\n error?: string\n errorCode?: ScannerErrorCode\n download?: {\n url: string\n loaded: number\n total: number | null\n done?: boolean\n }\n}\n\ninterface WorkerScope {\n onmessage: ((event: MessageEvent<AnalysisRequest>) => void) | null\n postMessage: (message: AnalysisResponse) => void\n}\n\nconst workerScope = globalThis as unknown as WorkerScope\nconst modelBytes = new Map<string, Promise<ArrayBuffer>>()\nconst sessions = new Map<string, Promise<{\n session: import('onnxruntime-web').InferenceSession\n ort: typeof import('onnxruntime-web')\n provider: 'webgpu' | 'wasm'\n}>>()\n\nconst MODEL_DATABASE = 'exige-scanner-sdk-models'\nconst MODEL_STORE = 'model-bytes'\n\nfunction openModelDatabase() {\n return new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(MODEL_DATABASE, 1)\n request.onupgradeneeded = () => {\n if (!request.result.objectStoreNames.contains(MODEL_STORE)) {\n request.result.createObjectStore(MODEL_STORE)\n }\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nasync function readCachedModel(modelUrl: string) {\n if (typeof indexedDB === 'undefined') return undefined\n const database = await openModelDatabase()\n try {\n return await new Promise<ArrayBuffer | undefined>((resolve, reject) => {\n const request = database.transaction(MODEL_STORE).objectStore(MODEL_STORE).get(modelUrl)\n request.onsuccess = () => resolve(\n request.result instanceof ArrayBuffer ? request.result : undefined,\n )\n request.onerror = () => reject(request.error)\n })\n } finally {\n database.close()\n }\n}\n\nasync function writeCachedModel(modelUrl: string, bytes: ArrayBuffer) {\n if (typeof indexedDB === 'undefined') return\n const database = await openModelDatabase()\n try {\n await new Promise<void>((resolve, reject) => {\n const transaction = database.transaction(MODEL_STORE, 'readwrite')\n transaction.objectStore(MODEL_STORE).put(bytes, modelUrl)\n transaction.oncomplete = () => resolve()\n transaction.onerror = () => reject(transaction.error)\n })\n } finally {\n database.close()\n }\n}\n\nasync function fetchModel(\n modelUrl: string,\n cacheModel: boolean,\n onProgress?: (progress: ModelDownloadProgress) => void,\n) {\n let request = modelBytes.get(modelUrl)\n if (!request) {\n request = (async () => {\n if (cacheModel) {\n try {\n const cached = await readCachedModel(modelUrl)\n if (cached) return cached\n } catch {\n // IndexedDB may be unavailable in private/embedded contexts.\n }\n }\n onProgress?.({ loaded: 0, total: null })\n let bytes: ArrayBuffer\n try {\n const response = await fetch(modelUrl, {\n cache: cacheModel ? 'force-cache' : 'no-store',\n })\n if (!response.ok) {\n throw new ScannerError(\n 'model-http',\n `Document model request failed with HTTP ${response.status}.`,\n )\n }\n bytes = await readResponseBytes(response, onProgress)\n } catch (error) {\n onProgress?.({ loaded: 0, total: null, done: true })\n throw error\n }\n onProgress?.({ loaded: bytes.byteLength, total: bytes.byteLength, done: true })\n if (cacheModel) {\n try {\n await writeCachedModel(modelUrl, bytes)\n } catch {\n // HTTP caching and the in-worker promise cache still apply.\n }\n }\n return bytes\n })()\n modelBytes.set(modelUrl, request)\n }\n return request\n}\n\nfunction ortWasmKey(ortWasmPaths?: string | { wasm: string; mjs: string }) {\n if (!ortWasmPaths) return ''\n if (typeof ortWasmPaths === 'string') return ortWasmPaths\n return `${ortWasmPaths.wasm}|${ortWasmPaths.mjs}`\n}\n\nasync function loadSession(\n modelUrl: string,\n cacheModel: boolean,\n ortWasmPaths?: string | { wasm: string; mjs: string },\n onProgress?: (progress: ModelDownloadProgress) => void,\n) {\n const sessionKey = `${modelUrl}|${ortWasmKey(ortWasmPaths)}`\n let request = sessions.get(sessionKey)\n if (!request) {\n request = (async () => {\n const [ort, bytes] = await Promise.all([\n import('onnxruntime-web'),\n fetchModel(modelUrl, cacheModel, onProgress),\n ])\n if (ortWasmPaths) ort.env.wasm.wasmPaths = ortWasmPaths\n const hasWebGpu = typeof navigator !== 'undefined' &&\n 'gpu' in (navigator as Navigator & { gpu?: unknown })\n if (hasWebGpu) {\n try {\n const session = await ort.InferenceSession.create(bytes, {\n executionProviders: ['webgpu'],\n graphOptimizationLevel: 'all',\n })\n return { session, ort, provider: 'webgpu' as const }\n } catch {\n // Fall through to portable WASM.\n }\n }\n const session = await ort.InferenceSession.create(bytes, {\n executionProviders: ['wasm'],\n graphOptimizationLevel: 'all',\n })\n return { session, ort, provider: 'wasm' as const }\n })()\n sessions.set(sessionKey, request)\n }\n return request\n}\n\nasync function analyzeWithModel(request: AnalysisRequest): Promise<PixelAnalysis> {\n if (!request.modelUrl) {\n throw new Error('A DocAligner modelUrl is required.')\n }\n const pixels = new Uint8ClampedArray(request.pixels)\n const input = preprocessDocumentModel(pixels, request.width, request.height)\n const { session, ort, provider } = await loadSession(\n request.modelUrl,\n request.cacheModel !== false,\n request.ortWasmPaths,\n (progress) => {\n workerScope.postMessage({\n id: request.id,\n download: { url: request.modelUrl ?? '', ...progress },\n })\n },\n )\n const startedAt = performance.now()\n const outputs = await session.run({\n [session.inputNames[0]]: new ort.Tensor('float32', input.data, input.dimensions),\n })\n const tensor = outputs[session.outputNames[0]]\n if (!tensor) throw new Error('Document model returned no output tensor.')\n const decoded = decodeDocumentHeatmaps(\n tensor.data as Float32Array,\n tensor.dims.map(Number),\n request.width,\n request.height,\n request.heatmapThreshold ?? 0.3,\n )\n return {\n sharpness: 20,\n candidate: decoded.candidate,\n detector: 'ml',\n modelStatus: 'ready',\n modelProvider: provider,\n inferenceMs: performance.now() - startedAt,\n }\n}\n\nasync function analyze(request: AnalysisRequest): Promise<PixelAnalysis> {\n try {\n const ml = await analyzeWithModel(request)\n if (!ml.candidate) {\n return {\n ...ml,\n modelStatus: 'failed',\n modelError: 'ML detector returned no geometrically valid corners.',\n }\n }\n return ml\n } catch (error) {\n if (error instanceof ScannerError && error.code === 'model-http') throw error\n const message = error instanceof Error ? error.message : 'Document model failed.'\n return {\n sharpness: 20,\n detector: 'ml',\n modelStatus: 'failed',\n modelError: message,\n }\n }\n}\n\nlet analysisQueue = Promise.resolve()\nworkerScope.onmessage = (event) => {\n const request = event.data\n analysisQueue = analysisQueue.then(async () => {\n try {\n workerScope.postMessage({ id: request.id, result: await analyze(request) })\n } catch (error) {\n workerScope.postMessage({\n id: request.id,\n error: error instanceof Error ? error.message : 'Document analysis failed.',\n errorCode: error instanceof ScannerError ? error.code : undefined,\n })\n }\n })\n}\n"],"mappings":"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"}