smartcomply-web-sdk 1.0.80 → 1.0.82

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 (37) hide show
  1. package/dist/camera/ActionDetector.d.ts.map +1 -1
  2. package/dist/camera/ActionDetector.js +7 -5
  3. package/dist/camera/ActionDetector.js.map +1 -1
  4. package/dist/client/Smartcomply.d.ts +1 -1
  5. package/dist/client/Smartcomply.js +1 -1
  6. package/dist/esm/camera/ActionDetector.d.ts.map +1 -1
  7. package/dist/esm/camera/ActionDetector.js +7 -5
  8. package/dist/esm/camera/ActionDetector.js.map +1 -1
  9. package/dist/esm/client/Smartcomply.d.ts +1 -1
  10. package/dist/esm/client/Smartcomply.js +1 -1
  11. package/dist/esm/flow/DocumentCapture.d.ts +42 -5
  12. package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
  13. package/dist/esm/flow/DocumentCapture.js +176 -19
  14. package/dist/esm/flow/DocumentCapture.js.map +1 -1
  15. package/dist/esm/flow/SmartComplyFlow.d.ts +10 -0
  16. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  17. package/dist/esm/flow/SmartComplyFlow.js +53 -0
  18. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  19. package/dist/esm/utils/ImageQuality.d.ts +38 -0
  20. package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
  21. package/dist/esm/utils/ImageQuality.js +98 -0
  22. package/dist/esm/utils/ImageQuality.js.map +1 -1
  23. package/dist/flow/DocumentCapture.d.ts +42 -5
  24. package/dist/flow/DocumentCapture.d.ts.map +1 -1
  25. package/dist/flow/DocumentCapture.js +175 -18
  26. package/dist/flow/DocumentCapture.js.map +1 -1
  27. package/dist/flow/SmartComplyFlow.d.ts +10 -0
  28. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  29. package/dist/flow/SmartComplyFlow.js +53 -0
  30. package/dist/flow/SmartComplyFlow.js.map +1 -1
  31. package/dist/smartcomply.browser.js +256 -11
  32. package/dist/smartcomply.browser.js.map +3 -3
  33. package/dist/utils/ImageQuality.d.ts +38 -0
  34. package/dist/utils/ImageQuality.d.ts.map +1 -1
  35. package/dist/utils/ImageQuality.js +99 -0
  36. package/dist/utils/ImageQuality.js.map +1 -1
  37. package/package.json +1 -1
@@ -163,6 +163,104 @@ export function assessQuality(imageData) {
163
163
  quality: metrics,
164
164
  };
165
165
  }
166
+ /**
167
+ * Lightweight, dependency-free framing heuristic — NOT real document/edge
168
+ * detection (that needs actual computer vision, e.g. OpenCV.js). This is the
169
+ * same class of check as assessQuality above: cheap enough to run every
170
+ * ~300ms on a mid-range phone, good enough to catch the obvious failure
171
+ * (no document in frame, badly cropped, document far outside the guide box),
172
+ * not intended to catch subtle/partial cropping — that's still the backend
173
+ * OCR + registry check's job.
174
+ *
175
+ * Approach: a real physical document produces a sharp intensity edge at its
176
+ * own boundary. If the guide box is roughly aligned with a real document,
177
+ * that edge should sit close to the guide box's own border. Sample a thin
178
+ * ring around the guide box border and check edge strength there (Sobel-style
179
+ * gradient, cheaper than the full Laplacian pass in calculateSharpness), plus
180
+ * confirm the box interior isn't just blank/uniform background (a document
181
+ * held further back than the guide, or no document at all, leaves the
182
+ * interior looking like flat background/desk).
183
+ *
184
+ * guideBoxPct: the guide box's position as a fraction of imageData's full
185
+ * width/height (0-1) — callers pass the SAME percentages used to size the
186
+ * on-screen CSS guide box (see DocumentCapture's guideWrap), computed against
187
+ * the raw video frame's own pixel dimensions so no CSS/object-fit mapping is
188
+ * needed.
189
+ */
190
+ export function assessFraming(imageData, guideBoxPct) {
191
+ const { width: W, height: H, data } = imageData;
192
+ const gray = new Uint8Array(W * H);
193
+ for (let i = 0; i < W * H; i++) {
194
+ const idx = i * 4;
195
+ gray[i] = Math.round(0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]);
196
+ }
197
+ const gx0 = Math.round(guideBoxPct.x * W);
198
+ const gy0 = Math.round(guideBoxPct.y * H);
199
+ const gx1 = Math.round((guideBoxPct.x + guideBoxPct.width) * W);
200
+ const gy1 = Math.round((guideBoxPct.y + guideBoxPct.height) * H);
201
+ const edgeAt = (x, y) => {
202
+ if (x < 1 || y < 1 || x >= W - 1 || y >= H - 1)
203
+ return 0;
204
+ const gxVal = gray[y * W + x + 1] - gray[y * W + x - 1];
205
+ const gyVal = gray[(y + 1) * W + x] - gray[(y - 1) * W + x];
206
+ return Math.sqrt(gxVal * gxVal + gyVal * gyVal);
207
+ };
208
+ const EDGE_THRESHOLD = 40;
209
+ const BORDER_BAND = Math.max(4, Math.round(Math.min(W, H) * 0.015));
210
+ // Sample points along the guide box border ring (top/bottom edges, left/right edges).
211
+ let borderPoints = 0;
212
+ let borderHits = 0;
213
+ const SAMPLES_PER_SIDE = 24;
214
+ const sampleBand = (fixed, isX, start, end) => {
215
+ for (let s = 0; s < SAMPLES_PER_SIDE; s++) {
216
+ const t = start + ((end - start) * s) / (SAMPLES_PER_SIDE - 1);
217
+ let bestEdge = 0;
218
+ for (let b = -BORDER_BAND; b <= BORDER_BAND; b++) {
219
+ const x = isX ? fixed + b : Math.round(t);
220
+ const y = isX ? Math.round(t) : fixed + b;
221
+ bestEdge = Math.max(bestEdge, edgeAt(x, y));
222
+ }
223
+ borderPoints++;
224
+ if (bestEdge >= EDGE_THRESHOLD)
225
+ borderHits++;
226
+ }
227
+ };
228
+ sampleBand(gy0, false, gx0, gx1); // top
229
+ sampleBand(gy1, false, gx0, gx1); // bottom
230
+ sampleBand(gx0, true, gy0, gy1); // left
231
+ sampleBand(gx1, true, gy0, gy1); // right
232
+ const borderEdgeCoverage = borderPoints > 0 ? borderHits / borderPoints : 0;
233
+ // Interior variance — sample a coarse grid inside the box, well clear of
234
+ // the border band, and check the spread of gray values isn't flat.
235
+ let sum = 0, sumSq = 0, n = 0;
236
+ const GRID = 12;
237
+ const insetX = Math.round((gx1 - gx0) * 0.1);
238
+ const insetY = Math.round((gy1 - gy0) * 0.1);
239
+ for (let iy = 0; iy < GRID; iy++) {
240
+ for (let ix = 0; ix < GRID; ix++) {
241
+ const x = gx0 + insetX + Math.round(((gx1 - gx0 - 2 * insetX) * ix) / (GRID - 1));
242
+ const y = gy0 + insetY + Math.round(((gy1 - gy0 - 2 * insetY) * iy) / (GRID - 1));
243
+ if (x < 0 || y < 0 || x >= W || y >= H)
244
+ continue;
245
+ const v = gray[y * W + x];
246
+ sum += v;
247
+ sumSq += v * v;
248
+ n++;
249
+ }
250
+ }
251
+ const mean = n > 0 ? sum / n : 0;
252
+ const variance = n > 0 ? sumSq / n - mean * mean : 0;
253
+ // Normalize against a "reasonable document photo" ballpark std-dev (~35)
254
+ // so the 0-1 scale is comparable across lighting conditions.
255
+ const interiorVariance = Math.min(1, Math.sqrt(Math.max(0, variance)) / 35);
256
+ // Require a solid majority of the border ring to show an edge AND some
257
+ // real content inside — either alone can false-positive (a plain wall has
258
+ // no edges but so does a document photographed at very low contrast; a
259
+ // cluttered background has edges everywhere but nothing document-shaped
260
+ // where the guide box actually is).
261
+ const isFramed = borderEdgeCoverage >= 0.55 && interiorVariance >= 0.25;
262
+ return { isFramed, borderEdgeCoverage, interiorVariance };
263
+ }
166
264
  /**
167
265
  * Get quality color for UI feedback
168
266
  */
@@ -1 +1 @@
1
- {"version":3,"file":"ImageQuality.js","sourceRoot":"","sources":["../../../src/utils/ImageQuality.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ImageQuality.js","sourceRoot":"","sources":["../../../src/utils/ImageQuality.ts"],"names":[],"mappings":"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"}
@@ -27,6 +27,9 @@ export declare class DocumentCapture {
27
27
  private backBlob;
28
28
  private currentSide;
29
29
  private requiresBackOverride;
30
+ private framingTimer;
31
+ private framingStableCount;
32
+ private framingAutoTriggered;
30
33
  /**
31
34
  * Mount the document capture UI and return a promise that resolves
32
35
  * with the captured document image(s).
@@ -58,15 +61,49 @@ export declare class DocumentCapture {
58
61
  */
59
62
  private _watchOrientation;
60
63
  private _unwatchOrientation;
64
+ /**
65
+ * Auto-capture-on-stability — the same pattern the liveness module already
66
+ * uses for face-lock (see liveness.ts's checkFace), applied to documents:
67
+ * poll the raw video frame on an interval, score it against the guide box
68
+ * with assessFraming() (see utils/ImageQuality.ts), and fire the shutter
69
+ * automatically once several consecutive samples score as "framed". This
70
+ * is industry-standard for document capture (Persona/Onfido/Jumio-style
71
+ * SDKs all auto-capture rather than trust a manual tap against a static
72
+ * guide overlay) — the guide box alone never blocked a badly cropped tap.
73
+ *
74
+ * The manual capture button still works throughout and always wins
75
+ * (framingAutoTriggered guards against a race between the two).
76
+ */
77
+ private _startFramingLoop;
78
+ private _stopFramingLoop;
61
79
  private openCamera;
62
80
  private captureFrame;
63
81
  private showPreview;
64
82
  /**
65
- * Runs the shared client-side quality heuristic (blur/glare/contrast/
66
- * brightness — see utils/ImageQuality.ts) against the just-captured photo
67
- * and, if it scores poorly, shows a plain-language nudge on the preview
68
- * screen. Fails silently on decode error — this is an advisory extra, not
69
- * a required step, so a browser that can't run it must not block capture.
83
+ * Runs the shared client-side quality heuristics — blur/glare/contrast/
84
+ * brightness (assessQuality) and document framing/completeness
85
+ * (assessFraming) — against the just-captured or just-uploaded photo, and
86
+ * shows a plain-language nudge on the preview screen if either scores
87
+ * poorly. This is the one chokepoint both the camera and manual-upload
88
+ * paths funnel through (see showPreview), so it's the only place that
89
+ * reliably catches a badly cropped document regardless of how the photo
90
+ * arrived — the camera's own auto-capture framing loop only guards live
91
+ * capture, and does nothing for an uploaded file or a manual shutter tap
92
+ * fired before the loop ever samples.
93
+ *
94
+ * Framing here checks the WHOLE image (guideBoxPct = full frame), not a
95
+ * guide-box region — there's no guide overlay for an arbitrary uploaded
96
+ * photo, so "does the document fill most of the frame" is the correct
97
+ * question, matching the same border-edge + interior-variance heuristic
98
+ * the camera loop already uses.
99
+ *
100
+ * A framing failure disables useBtn — unlike the blur/glare nudge, this
101
+ * one blocks: a cropped ID reads as a confident, wrong identity downstream
102
+ * (partial OCR fields, a registry check against a truncated ID number)
103
+ * with no further chance to catch it, so the user must retake/re-upload
104
+ * rather than being trusted to notice an advisory nudge and act on it
105
+ * voluntarily. Fails open on decode error — this heuristic not running at
106
+ * all must not lock the user out of a photo it never actually scored.
70
107
  */
71
108
  private _checkPhotoQuality;
72
109
  /** Decodes to a canvas, optionally downscaled to maxDim on the long edge (never upscales). */
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentCapture.d.ts","sourceRoot":"","sources":["../../src/flow/DocumentCapture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAgB,MAAM,SAAS,CAAC;AACpD,OAAO,EAAuC,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAQ3F;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,IAAI,CAAC;IACZ,IAAI,CAAC,EAAE,IAAI,CAAC;IAKZ,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAA+B;IAC3C,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,cAAc,CAA0D;IAChF,OAAO,CAAC,aAAa,CAAuC;IAC5D,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,oBAAoB,CAAuC;IAEnE;;;OAGG;IACH,OAAO,CACL,SAAS,EAAE,WAAW,EACtB,KAAK,EAAE,WAAW,EAClB,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,YAAY,GAC1B,OAAO,CAAC,qBAAqB,CAAC;IAiCjC,OAAO,IAAI,IAAI;IAuBf,OAAO,CAAC,eAAe;IAOvB,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB;IAI7B,0EAA0E;IAC1E,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,kBAAkB;IA4L1B,wDAAwD;IACxD,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,gBAAgB;IAyHxB;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IAQpB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,mBAAmB;YAQb,UAAU;IAsExB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,WAAW;IA4JnB;;;;;;OAMG;YACW,kBAAkB;IA4BhC,8FAA8F;IAC9F,OAAO,CAAC,aAAa;IAqBrB;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,qBAAqB;IAyC7B,OAAO,CAAC,wBAAwB;IAShC,OAAO,CAAC,YAAY;IAsBpB;;;;;;;;OAQG;YACW,sBAAsB;IASpC,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,SAAS;IAoCjB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,aAAa;IA4ErB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAuC5B,OAAO,CAAC,gBAAgB;CAazB"}
1
+ {"version":3,"file":"DocumentCapture.d.ts","sourceRoot":"","sources":["../../src/flow/DocumentCapture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAgB,MAAM,SAAS,CAAC;AACpD,OAAO,EAAuC,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAQ3F;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,IAAI,CAAC;IACZ,IAAI,CAAC,EAAE,IAAI,CAAC;IAKZ,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAA+B;IAC3C,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,cAAc,CAA0D;IAChF,OAAO,CAAC,aAAa,CAAuC;IAC5D,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,oBAAoB,CAAuC;IACnE,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,oBAAoB,CAAS;IAErC;;;OAGG;IACH,OAAO,CACL,SAAS,EAAE,WAAW,EACtB,KAAK,EAAE,WAAW,EAClB,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,YAAY,GAC1B,OAAO,CAAC,qBAAqB,CAAC;IAiCjC,OAAO,IAAI,IAAI;IAuBf,OAAO,CAAC,eAAe;IAOvB,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB;IAI7B,0EAA0E;IAC1E,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,kBAAkB;IA4L1B,wDAAwD;IACxD,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,gBAAgB;IA0IxB;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IAQpB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,mBAAmB;IAQ3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,iBAAiB;IA2FzB,OAAO,CAAC,gBAAgB;YAOV,UAAU;IAsExB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,WAAW;IA+JnB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;YACW,kBAAkB;IAsDhC,8FAA8F;IAC9F,OAAO,CAAC,aAAa;IAqBrB;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,qBAAqB;IAyC7B,OAAO,CAAC,wBAAwB;IAShC,OAAO,CAAC,YAAY;IAsBpB;;;;;;;;OAQG;YACW,sBAAsB;IASpC,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,SAAS;IAoCjB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,aAAa;IA4ErB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAuC5B,OAAO,CAAC,gBAAgB;CAazB"}
@@ -23,6 +23,9 @@ class DocumentCapture {
23
23
  this.backBlob = null;
24
24
  this.currentSide = 'front';
25
25
  this.requiresBackOverride = undefined;
26
+ this.framingTimer = null;
27
+ this.framingStableCount = 0;
28
+ this.framingAutoTriggered = false;
26
29
  }
27
30
  /**
28
31
  * Mount the document capture UI and return a promise that resolves
@@ -332,7 +335,7 @@ class DocumentCapture {
332
335
  svg.appendChild(bracketGroup);
333
336
  guideWrap.appendChild(svg);
334
337
  wrapper.appendChild(guideWrap);
335
- // Header — gradient scrim + document label
338
+ // Header — gradient scrim + document label + live framing status
336
339
  const header = document.createElement("div");
337
340
  header.style.cssText = `
338
341
  position:absolute;top:0;left:0;right:0;
@@ -340,6 +343,13 @@ class DocumentCapture {
340
343
  padding:14px 16px 32px;text-align:center;
341
344
  `;
342
345
  header.innerHTML = `<div style="color:#fff;font-size:${theme_1.TYPE.sm}px;font-weight:600;">Align your ${this.getDocumentLabel(documentType)} within the frame</div>`;
346
+ const framingStatus = document.createElement("div");
347
+ framingStatus.style.cssText = `
348
+ margin-top:6px;font-size:${theme_1.TYPE.xs}px;font-weight:600;
349
+ color:rgba(255,255,255,0.7);transition:color 0.2s ease;
350
+ `;
351
+ framingStatus.textContent = "Fit the document inside the frame";
352
+ header.appendChild(framingStatus);
343
353
  wrapper.appendChild(header);
344
354
  // Footer — gradient scrim + tips
345
355
  const footer = document.createElement("div");
@@ -374,11 +384,17 @@ class DocumentCapture {
374
384
  inner.style.transform = "translate(-50%,-50%) scale(1)";
375
385
  });
376
386
  captureBtn.addEventListener("click", () => {
387
+ this.framingAutoTriggered = true; // manual tap always wins, skip any pending auto-trigger
377
388
  this.captureFrame(container, theme, documentType);
378
389
  });
379
390
  container.appendChild(captureBtn);
380
- // Open camera
381
- this.openCamera();
391
+ // Open camera, then start the auto-capture framing loop once the stream
392
+ // has real dimensions to sample against.
393
+ this.openCamera().then(() => {
394
+ if (this.videoEl) {
395
+ this.videoEl.addEventListener("loadedmetadata", () => this._startFramingLoop(container, theme, documentType, framingStatus), { once: true });
396
+ }
397
+ });
382
398
  }
383
399
  /**
384
400
  * Matches the video box's aspect-ratio CSS to the stream's actual
@@ -415,6 +431,103 @@ class DocumentCapture {
415
431
  this.orientationHandler = null;
416
432
  }
417
433
  }
434
+ /**
435
+ * Auto-capture-on-stability — the same pattern the liveness module already
436
+ * uses for face-lock (see liveness.ts's checkFace), applied to documents:
437
+ * poll the raw video frame on an interval, score it against the guide box
438
+ * with assessFraming() (see utils/ImageQuality.ts), and fire the shutter
439
+ * automatically once several consecutive samples score as "framed". This
440
+ * is industry-standard for document capture (Persona/Onfido/Jumio-style
441
+ * SDKs all auto-capture rather than trust a manual tap against a static
442
+ * guide overlay) — the guide box alone never blocked a badly cropped tap.
443
+ *
444
+ * The manual capture button still works throughout and always wins
445
+ * (framingAutoTriggered guards against a race between the two).
446
+ */
447
+ _startFramingLoop(container, theme, documentType, statusEl) {
448
+ if (!this.videoEl)
449
+ return;
450
+ this.framingStableCount = 0;
451
+ this.framingAutoTriggered = false;
452
+ const STABLE_REQUIRED = 3; // consecutive good samples before firing
453
+ const SAMPLE_INTERVAL_MS = 350; // cheap enough to run this often; not every animation frame
454
+ const MAX_WAIT_MS = 12000; // give up auto-capture, leave the manual button as the only path
455
+ const startTime = performance.now();
456
+ // Guide box in fractional coordinates, matching the on-screen CSS guide
457
+ // (width:84%, aspect-ratio:16/10, centered) — computed directly against
458
+ // the video's own pixel dimensions so no CSS/object-fit mapping is needed.
459
+ const boxWidthPct = 0.84;
460
+ const boxAspect = 16 / 10;
461
+ const tick = () => {
462
+ if (!this.videoEl || this.framingAutoTriggered)
463
+ return;
464
+ if (performance.now() - startTime >= MAX_WAIT_MS) {
465
+ statusEl.textContent = "Fit the document inside the frame";
466
+ return; // stop auto-detecting; manual capture button remains available
467
+ }
468
+ const w = this.videoEl.videoWidth;
469
+ const h = this.videoEl.videoHeight;
470
+ if (!w || !h || this.videoEl.readyState < 2) {
471
+ this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
472
+ return;
473
+ }
474
+ try {
475
+ const boxWidthPx = w * boxWidthPct;
476
+ const boxHeightPx = boxWidthPx / boxAspect;
477
+ const boxXPct = (1 - boxWidthPct) / 2;
478
+ const boxHeightPct = boxHeightPx / h;
479
+ const boxYPct = (1 - boxHeightPct) / 2;
480
+ const canvas = document.createElement("canvas");
481
+ // Downscaled sampling copy only — matches _checkPhotoQuality's own
482
+ // reasoning for why this must not run against the full-resolution
483
+ // frame on every tick.
484
+ const scale = Math.min(1, 480 / w);
485
+ canvas.width = Math.round(w * scale);
486
+ canvas.height = Math.round(h * scale);
487
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
488
+ if (!ctx) {
489
+ this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
490
+ return;
491
+ }
492
+ ctx.drawImage(this.videoEl, 0, 0, canvas.width, canvas.height);
493
+ const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
494
+ const result = (0, ImageQuality_1.assessFraming)(pixels, {
495
+ x: boxXPct, y: boxYPct, width: boxWidthPct, height: boxHeightPct,
496
+ });
497
+ if (result.isFramed) {
498
+ this.framingStableCount++;
499
+ statusEl.style.color = theme.primary;
500
+ statusEl.textContent = this.framingStableCount >= STABLE_REQUIRED
501
+ ? "Capturing…"
502
+ : "Hold steady…";
503
+ }
504
+ else {
505
+ this.framingStableCount = 0;
506
+ statusEl.style.color = "rgba(255,255,255,0.7)";
507
+ statusEl.textContent = result.interiorVariance < 0.25
508
+ ? "Move closer — fill the frame with your document"
509
+ : "Align all 4 corners within the guide";
510
+ }
511
+ if (this.framingStableCount >= STABLE_REQUIRED) {
512
+ this.framingAutoTriggered = true;
513
+ this.captureFrame(container, theme, documentType);
514
+ return;
515
+ }
516
+ }
517
+ catch {
518
+ // Advisory/auto-capture only — a scoring failure just falls back to
519
+ // the manual capture button, never blocks the flow.
520
+ }
521
+ this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
522
+ };
523
+ this.framingTimer = setTimeout(tick, SAMPLE_INTERVAL_MS);
524
+ }
525
+ _stopFramingLoop() {
526
+ if (this.framingTimer !== null) {
527
+ clearTimeout(this.framingTimer);
528
+ this.framingTimer = null;
529
+ }
530
+ }
418
531
  async openCamera() {
419
532
  // Check browser compatibility
420
533
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
@@ -576,7 +689,7 @@ class DocumentCapture {
576
689
  URL.revokeObjectURL(currentUrl);
577
690
  currentUrl = URL.createObjectURL(rotated);
578
691
  img.src = currentUrl;
579
- this._checkPhotoQuality(currentBlob, qualityNotice, theme);
692
+ this._checkPhotoQuality(currentBlob, qualityNotice, theme, useBtn);
580
693
  }
581
694
  catch {
582
695
  // Leave the current photo untouched — rotation is a convenience,
@@ -598,12 +711,14 @@ class DocumentCapture {
598
711
  sublabel.style.cssText = `color:${theme.textMuted};font-size:${theme_1.TYPE.xs}px;text-align:center;line-height:1.6;`;
599
712
  sublabel.textContent = "All text must be readable and all four corners must be visible. Sideways or upside-down? Use Rotate on the photo above.";
600
713
  this.root.appendChild(sublabel);
601
- // Advisory-only quality nudge — catches an obviously blurry/dark/glare-
602
- // heavy photo right here instead of only finding out several seconds
603
- // later. Never blocks "Use Photo": this is a heuristic, not a hard
604
- // rule, so a photo that's actually fine but scores oddly must still be
605
- // usable — it just nudges the user toward retaking with a plain-
606
- // language reason instead of silently letting a bad photo through.
714
+ // Quality/framing notice. Blur/glare/contrast issues (assessQuality) stay
715
+ // advisory-only — a heuristic that's off shouldn't block a photo that's
716
+ // actually fine. A framing failure (document not fully in frame —
717
+ // assessFraming) DOES block "Use Photo": a cropped ID reads as a
718
+ // confident, wrong identity downstream (partial OCR fields, a registry
719
+ // check against a truncated ID number) with no further chance to catch
720
+ // it, so this is the one signal worth hard-blocking on rather than
721
+ // trusting the user to notice and retake voluntarily.
607
722
  const qualityNotice = document.createElement("div");
608
723
  qualityNotice.style.cssText = `
609
724
  display:none;width:100%;padding:10px 14px;border-radius:${theme_1.RADIUS.md}px;
@@ -612,7 +727,6 @@ class DocumentCapture {
612
727
  `;
613
728
  qualityNotice.setAttribute("role", "status");
614
729
  this.root.appendChild(qualityNotice);
615
- this._checkPhotoQuality(currentBlob, qualityNotice, theme);
616
730
  // Buttons
617
731
  const btnRow = document.createElement("div");
618
732
  btnRow.style.cssText = "display:flex;gap:12px;width:100%;";
@@ -638,15 +752,35 @@ class DocumentCapture {
638
752
  btnRow.appendChild(retakeBtn);
639
753
  btnRow.appendChild(useBtn);
640
754
  this.root.appendChild(btnRow);
755
+ this._checkPhotoQuality(currentBlob, qualityNotice, theme, useBtn);
641
756
  }
642
757
  /**
643
- * Runs the shared client-side quality heuristic (blur/glare/contrast/
644
- * brightness — see utils/ImageQuality.ts) against the just-captured photo
645
- * and, if it scores poorly, shows a plain-language nudge on the preview
646
- * screen. Fails silently on decode error — this is an advisory extra, not
647
- * a required step, so a browser that can't run it must not block capture.
758
+ * Runs the shared client-side quality heuristics — blur/glare/contrast/
759
+ * brightness (assessQuality) and document framing/completeness
760
+ * (assessFraming) — against the just-captured or just-uploaded photo, and
761
+ * shows a plain-language nudge on the preview screen if either scores
762
+ * poorly. This is the one chokepoint both the camera and manual-upload
763
+ * paths funnel through (see showPreview), so it's the only place that
764
+ * reliably catches a badly cropped document regardless of how the photo
765
+ * arrived — the camera's own auto-capture framing loop only guards live
766
+ * capture, and does nothing for an uploaded file or a manual shutter tap
767
+ * fired before the loop ever samples.
768
+ *
769
+ * Framing here checks the WHOLE image (guideBoxPct = full frame), not a
770
+ * guide-box region — there's no guide overlay for an arbitrary uploaded
771
+ * photo, so "does the document fill most of the frame" is the correct
772
+ * question, matching the same border-edge + interior-variance heuristic
773
+ * the camera loop already uses.
774
+ *
775
+ * A framing failure disables useBtn — unlike the blur/glare nudge, this
776
+ * one blocks: a cropped ID reads as a confident, wrong identity downstream
777
+ * (partial OCR fields, a registry check against a truncated ID number)
778
+ * with no further chance to catch it, so the user must retake/re-upload
779
+ * rather than being trusted to notice an advisory nudge and act on it
780
+ * voluntarily. Fails open on decode error — this heuristic not running at
781
+ * all must not lock the user out of a photo it never actually scored.
648
782
  */
649
- async _checkPhotoQuality(imageData, noticeEl, theme) {
783
+ async _checkPhotoQuality(imageData, noticeEl, theme, useBtn) {
650
784
  try {
651
785
  // Document photos are captured at up to DOC_CAMERA_IDEAL_WIDTH/HEIGHT
652
786
  // (2000x1500) — scoring every pixel across four full-image passes in
@@ -659,6 +793,28 @@ class DocumentCapture {
659
793
  if (!ctx)
660
794
  return;
661
795
  const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
796
+ const framing = (0, ImageQuality_1.assessFraming)(pixels, { x: 0, y: 0, width: 1, height: 1 });
797
+ if (!framing.isFramed) {
798
+ if (noticeEl.isConnected) {
799
+ noticeEl.style.display = "block";
800
+ noticeEl.textContent = framing.interiorVariance < 0.25
801
+ ? "We couldn't detect a document in this photo — make sure the ID is clearly visible and try again."
802
+ : "Part of the document looks cut off — make sure all four corners are visible and try again.";
803
+ }
804
+ if (useBtn.isConnected) {
805
+ useBtn.disabled = true;
806
+ useBtn.style.opacity = "0.5";
807
+ useBtn.style.cursor = "not-allowed";
808
+ }
809
+ return; // framing issue is the more actionable/specific one — don't also show the quality nudge
810
+ }
811
+ // Framing passed — make sure a previous failed attempt (before Retake/
812
+ // Rotate) doesn't leave the button stuck disabled.
813
+ if (useBtn.isConnected) {
814
+ useBtn.disabled = false;
815
+ useBtn.style.opacity = "1";
816
+ useBtn.style.cursor = "pointer";
817
+ }
662
818
  const result = (0, ImageQuality_1.assessQuality)(pixels);
663
819
  if (noticeEl.isConnected && !result.isValid) {
664
820
  noticeEl.style.display = "block";
@@ -668,7 +824,7 @@ class DocumentCapture {
668
824
  }
669
825
  }
670
826
  catch {
671
- // Advisory only — no notice shown if scoring fails for any reason.
827
+ // Fails open — no notice, button stays enabled, if scoring fails for any reason.
672
828
  }
673
829
  }
674
830
  /** Decodes to a canvas, optionally downscaled to maxDim on the long edge (never upscales). */
@@ -805,6 +961,7 @@ class DocumentCapture {
805
961
  input.click();
806
962
  }
807
963
  stopCamera() {
964
+ this._stopFramingLoop();
808
965
  this._unwatchOrientation();
809
966
  this.cameraManager.stop();
810
967
  if (this.videoEl) {