@smileid/web-components 11.4.5 → 11.6.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/dist/esm/DocumentCaptureScreens-DjSTdVP-.js +5398 -0
- package/dist/esm/DocumentCaptureScreens-DjSTdVP-.js.map +1 -0
- package/dist/esm/{Navigation-Bb7MPLE8.js → Navigation-6DH3vF4-.js} +28 -22
- package/dist/esm/Navigation-6DH3vF4-.js.map +1 -0
- package/dist/esm/{PoweredBySmileId-CxbaihMu.js → PoweredBySmileId-DoKwoPUd.js} +424 -6
- package/dist/esm/PoweredBySmileId-DoKwoPUd.js.map +1 -0
- package/dist/esm/SelfieCaptureScreens-CtX-4Tco.js +11470 -0
- package/dist/esm/SelfieCaptureScreens-CtX-4Tco.js.map +1 -0
- package/dist/esm/combobox.js +1 -1
- package/dist/esm/document.js +1 -1
- package/dist/esm/end-user-consent.js +713 -2
- package/dist/esm/end-user-consent.js.map +1 -1
- package/dist/esm/index-BqyuTk9f.js +1366 -0
- package/dist/esm/{index-C4RTMbgw.js.map → index-BqyuTk9f.js.map} +1 -1
- package/dist/esm/localisation.js +1 -1
- package/dist/esm/main.js +14 -14
- package/dist/esm/navigation.js +1 -1
- package/dist/esm/package-CjZI-cNQ.js +2540 -0
- package/dist/esm/package-CjZI-cNQ.js.map +1 -0
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +81 -37
- package/dist/esm/smart-camera-web.js.map +1 -1
- package/dist/esm/totp-consent.js +731 -2
- package/dist/esm/totp-consent.js.map +1 -1
- package/dist/esm/validate.js +31 -0
- package/dist/esm/validate.js.map +1 -0
- package/dist/smart-camera-web.js +1513 -383
- package/dist/smart-camera-web.js.map +1 -1
- package/dist/types/main.d.ts +18 -1
- package/dist/types/validate.d.ts +21 -0
- package/lib/components/document/src/DocumentCaptureScreens.js +97 -18
- package/lib/components/document/src/assets/lottie.d.ts +12 -0
- package/lib/components/document/src/assets/svg-inline.d.ts +8 -0
- package/lib/components/document/src/document-auto-capture/DocumentAutoCapture.stories.js +75 -0
- package/lib/components/document/src/document-auto-capture/DocumentAutoCapture.tsx +1458 -0
- package/lib/components/document/src/document-auto-capture/README.md +73 -0
- package/lib/components/document/src/document-auto-capture/assets/Greenbook_Shimmer.svg +42 -0
- package/lib/components/document/src/document-auto-capture/assets/ID_Back_Shimmer.svg +8 -0
- package/lib/components/document/src/document-auto-capture/assets/ID_Front_Shimmer.svg +20 -0
- package/lib/components/document/src/document-auto-capture/assets/Passport-Shimmer.svg +143 -0
- package/lib/components/document/src/document-auto-capture/assets/shimmers.ts +21 -0
- package/lib/components/document/src/document-auto-capture/assets/svg-raw.d.ts +4 -0
- package/lib/components/document/src/document-auto-capture/components/CaptureButton.tsx +122 -0
- package/lib/components/document/src/document-auto-capture/components/Overlay.tsx +167 -0
- package/lib/components/document/src/document-auto-capture/components/TuningPanel.tsx +856 -0
- package/lib/components/document/src/document-auto-capture/constants/captureLayout.ts +58 -0
- package/lib/components/document/src/document-auto-capture/detection/cvErrorRecovery.ts +40 -0
- package/lib/components/document/src/document-auto-capture/detection/documentAspect.ts +20 -0
- package/lib/components/document/src/document-auto-capture/detection/qualityScoring.ts +35 -0
- package/lib/components/document/src/document-auto-capture/detection/seamRejection.ts +209 -0
- package/lib/components/document/src/document-auto-capture/detection/synthesisTiming.ts +10 -0
- package/lib/components/document/src/document-auto-capture/hooks/useCamera.ts +117 -0
- package/lib/components/document/src/document-auto-capture/hooks/useCardDetection.ts +3059 -0
- package/lib/components/document/src/document-auto-capture/index.ts +4 -0
- package/lib/components/document/src/document-auto-capture/theme.ts +40 -0
- package/lib/components/document/src/document-auto-capture/utils/debug.ts +25 -0
- package/lib/components/document/src/document-auto-capture/utils/opencvLoader.ts +86 -0
- package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.tsx +327 -244
- package/lib/components/document/src/document-capture-review/DocumentCaptureReview.js +153 -189
- package/lib/components/document/src/document-capture-submission/DocumentCaptureSubmission.tsx +432 -0
- package/lib/components/document/src/document-capture-submission/index.js +3 -0
- package/lib/components/navigation/src/Navigation.js +27 -8
- package/lib/components/selfie/README.md +13 -0
- package/lib/components/selfie/src/SelfieCaptureScreens.js +56 -8
- package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieCapture.tsx +684 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieConsent.tsx +71 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieSubmission.tsx +181 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/OvalProgress.tsx +87 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/Icon.svg +8 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/accessories.svg +77 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/active_liveness_animation.lottie +0 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/device.svg +12 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/device_orientation.lottie +0 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/good.svg +52 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/id-card.svg +9 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/illustrations.tsx +852 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/instructions-img.svg +3 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/multiple-faces.svg +69 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/person.svg +6 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/phone.svg +8 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/poor-lighting.svg +53 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/too_dark_animation.lottie +0 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/ActiveLivenessOverlay.tsx +226 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/AlertDisplay.tsx +38 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/BackNavigation.tsx +45 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CameraPreview.tsx +96 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CaptureControls.tsx +97 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CaptureGuidelines.tsx +374 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/ConsentView.tsx +460 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/SubmissionView.tsx +426 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/index.ts +3 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/constants.ts +23 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/index.ts +2 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/useCamera.ts +238 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/useFaceCapture.ts +1075 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/index.ts +1 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/alertMessages.ts +20 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/canvas.ts +108 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/faceDetection.ts +545 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/imageCapture.ts +66 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/imageQuality.ts +151 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/index.ts +5 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/mediapipeManager.ts +215 -0
- package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +24 -1
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +2 -2
- package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +15 -7
- package/lib/components/selfie/src/smartselfie-capture/utils/canvas.ts +4 -6
- package/lib/components/signature-pad/package.json +1 -1
- package/lib/components/smart-camera-web/src/README.md +11 -0
- package/lib/components/smart-camera-web/src/SmartCameraWeb.js +89 -8
- package/lib/components/totp-consent/src/TotpConsent.js +1 -1
- package/lib/domain/localisation/index.js +2 -2
- package/package.json +9 -5
- package/dist/esm/DocumentCaptureScreens-D2G0NOQr.js +0 -4147
- package/dist/esm/DocumentCaptureScreens-D2G0NOQr.js.map +0 -1
- package/dist/esm/EndUserConsent-uHfA3txP.js +0 -717
- package/dist/esm/EndUserConsent-uHfA3txP.js.map +0 -1
- package/dist/esm/Navigation-Bb7MPLE8.js.map +0 -1
- package/dist/esm/PoweredBySmileId-CxbaihMu.js.map +0 -1
- package/dist/esm/SelfieCaptureScreens-Dr7VzON7.js +0 -7651
- package/dist/esm/SelfieCaptureScreens-Dr7VzON7.js.map +0 -1
- package/dist/esm/TotpConsent-Depzg0ti.js +0 -734
- package/dist/esm/TotpConsent-Depzg0ti.js.map +0 -1
- package/dist/esm/index-C4RTMbgw.js +0 -1360
- package/dist/esm/package-D6YrpMcO.js +0 -565
- package/dist/esm/package-D6YrpMcO.js.map +0 -1
- package/dist/esm/styles-BTEClL7R.js +0 -419
- package/dist/esm/styles-BTEClL7R.js.map +0 -1
- /package/lib/components/document/src/assets/lottie/{taking photo of green book passport.lottie → greenbook.lottie} +0 -0
- /package/lib/components/document/src/assets/lottie/{taking photo of ID FLIP 2D.lottie → id-card-flip.lottie} +0 -0
- /package/lib/components/document/src/assets/lottie/{taking photo of ID.lottie → id-card.lottie} +0 -0
- /package/lib/components/document/src/assets/lottie/{taking photo of passport 2.lottie → passport.lottie} +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const FULLSCREEN_CAPTURE_LAYOUT = {
|
|
2
|
+
reservedVerticalPx: 90,
|
|
3
|
+
maxGuideWidthPx: 600,
|
|
4
|
+
widthRatio: 1.0,
|
|
5
|
+
minHeightRatio: 0.55,
|
|
6
|
+
minGuideWidthPx: 220,
|
|
7
|
+
defaultHorizontalInsetPx: 4,
|
|
8
|
+
sideControlsInsetPx: 132,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface FullscreenGuideSizeParams {
|
|
12
|
+
aspectRatio?: number;
|
|
13
|
+
displayHeight?: number;
|
|
14
|
+
displayWidth?: number;
|
|
15
|
+
horizontalInsetPx?: number;
|
|
16
|
+
reservedVerticalPx?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getFullscreenGuideSize({
|
|
20
|
+
aspectRatio,
|
|
21
|
+
displayHeight,
|
|
22
|
+
displayWidth,
|
|
23
|
+
horizontalInsetPx = FULLSCREEN_CAPTURE_LAYOUT.defaultHorizontalInsetPx,
|
|
24
|
+
reservedVerticalPx = FULLSCREEN_CAPTURE_LAYOUT.reservedVerticalPx,
|
|
25
|
+
}: FullscreenGuideSizeParams) {
|
|
26
|
+
const width = Math.max(1, displayWidth || 0);
|
|
27
|
+
const height = Math.max(1, displayHeight || 0);
|
|
28
|
+
const ratio = Math.max(0.2, aspectRatio || 1.585);
|
|
29
|
+
|
|
30
|
+
const availableGuideHeight = Math.max(
|
|
31
|
+
height - reservedVerticalPx,
|
|
32
|
+
height * FULLSCREEN_CAPTURE_LAYOUT.minHeightRatio,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const maxWidthFromHeight = availableGuideHeight * ratio;
|
|
36
|
+
const maxWidthFromInsets = Math.max(
|
|
37
|
+
FULLSCREEN_CAPTURE_LAYOUT.minGuideWidthPx,
|
|
38
|
+
width - horizontalInsetPx * 2,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const guideWidth = Math.max(
|
|
42
|
+
FULLSCREEN_CAPTURE_LAYOUT.minGuideWidthPx,
|
|
43
|
+
Math.min(
|
|
44
|
+
width * FULLSCREEN_CAPTURE_LAYOUT.widthRatio,
|
|
45
|
+
FULLSCREEN_CAPTURE_LAYOUT.maxGuideWidthPx,
|
|
46
|
+
maxWidthFromHeight,
|
|
47
|
+
maxWidthFromInsets,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const guideHeight = guideWidth / ratio;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
guideWidth,
|
|
55
|
+
guideHeight,
|
|
56
|
+
reservedVerticalPx,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const CHROMA_DISABLE_ERROR_THRESHOLD = 3;
|
|
2
|
+
export const CV_ERROR_FALLBACK_THRESHOLD = 6;
|
|
3
|
+
|
|
4
|
+
type RecoveryInput = {
|
|
5
|
+
errorStreak: number;
|
|
6
|
+
chromaUnavailable: boolean;
|
|
7
|
+
chromaDisableThreshold?: number;
|
|
8
|
+
fallbackThreshold?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type RecoveryAction = {
|
|
12
|
+
nextErrorStreak: number;
|
|
13
|
+
shouldDisableChroma: boolean;
|
|
14
|
+
shouldClearProcessingError: boolean;
|
|
15
|
+
shouldActivateFallback: boolean;
|
|
16
|
+
shouldSuspendDetection: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function nextCvErrorRecoveryAction({
|
|
20
|
+
errorStreak,
|
|
21
|
+
chromaUnavailable,
|
|
22
|
+
chromaDisableThreshold = CHROMA_DISABLE_ERROR_THRESHOLD,
|
|
23
|
+
fallbackThreshold = CV_ERROR_FALLBACK_THRESHOLD,
|
|
24
|
+
}: RecoveryInput): RecoveryAction {
|
|
25
|
+
const nextErrorStreak = errorStreak + 1;
|
|
26
|
+
const shouldDisableChroma =
|
|
27
|
+
!chromaUnavailable && nextErrorStreak >= chromaDisableThreshold;
|
|
28
|
+
const chromaUnavailableAfterError = chromaUnavailable || shouldDisableChroma;
|
|
29
|
+
|
|
30
|
+
const shouldActivateFallback =
|
|
31
|
+
chromaUnavailableAfterError && nextErrorStreak >= fallbackThreshold;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
nextErrorStreak,
|
|
35
|
+
shouldDisableChroma,
|
|
36
|
+
shouldClearProcessingError: shouldDisableChroma,
|
|
37
|
+
shouldActivateFallback,
|
|
38
|
+
shouldSuspendDetection: shouldActivateFallback,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const ASPECT_RATIOS = {
|
|
2
|
+
'id-card': 1.585,
|
|
3
|
+
passport: 1.42,
|
|
4
|
+
greenbook: 1.42,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type AspectKey = keyof typeof ASPECT_RATIOS;
|
|
8
|
+
export type DiscoveryVote = 'id-card' | 'passport';
|
|
9
|
+
|
|
10
|
+
export const isAspectKey = (v: unknown): v is AspectKey =>
|
|
11
|
+
typeof v === 'string' &&
|
|
12
|
+
Object.prototype.hasOwnProperty.call(ASPECT_RATIOS, v);
|
|
13
|
+
|
|
14
|
+
const ASPECT_RATIO_MIDPOINT =
|
|
15
|
+
(ASPECT_RATIOS['id-card'] + ASPECT_RATIOS.passport) / 2;
|
|
16
|
+
|
|
17
|
+
export const classifyDiscoveryAspect = (
|
|
18
|
+
normalizedAspect: number,
|
|
19
|
+
): DiscoveryVote =>
|
|
20
|
+
normalizedAspect >= ASPECT_RATIO_MIDPOINT ? 'id-card' : 'passport';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const SYNTHETIC_CONTOUR_CONFIDENCE = 0.55;
|
|
2
|
+
|
|
3
|
+
export const clamp01 = (v: number): number => Math.max(0, Math.min(1, v));
|
|
4
|
+
|
|
5
|
+
const QUALITY_WEIGHTS = {
|
|
6
|
+
sharpness: 0.35,
|
|
7
|
+
glare: 0.15,
|
|
8
|
+
fill: 0.2,
|
|
9
|
+
aspect: 0.2,
|
|
10
|
+
contour: 0.1,
|
|
11
|
+
chroma: 0.05,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface FrameQualityParts {
|
|
15
|
+
sharpness?: number | null;
|
|
16
|
+
glare?: number | null;
|
|
17
|
+
fill?: number | null;
|
|
18
|
+
aspect?: number | null;
|
|
19
|
+
contour?: number | null;
|
|
20
|
+
chroma?: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function frameQualityScore(parts: FrameQualityParts): number {
|
|
24
|
+
const keys = Object.keys(QUALITY_WEIGHTS) as Array<
|
|
25
|
+
keyof typeof QUALITY_WEIGHTS
|
|
26
|
+
>;
|
|
27
|
+
const present = keys.filter((key) => parts[key] != null);
|
|
28
|
+
const den = present.reduce((sum, key) => sum + QUALITY_WEIGHTS[key], 0);
|
|
29
|
+
if (den <= 0) return 0;
|
|
30
|
+
const num = present.reduce(
|
|
31
|
+
(sum, key) => sum + clamp01(parts[key] as number) * QUALITY_WEIGHTS[key],
|
|
32
|
+
0,
|
|
33
|
+
);
|
|
34
|
+
return num / den;
|
|
35
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Seam / straight-line rejection for the document contour pass.
|
|
2
|
+
//
|
|
3
|
+
// On surfaces with strong straight linear features — a parquet/plank floor, a
|
|
4
|
+
// slatted table — the seams between planks produce long, high-contrast straight
|
|
5
|
+
// edges that survive the adaptive-Canny high threshold. A card-shaped quad can
|
|
6
|
+
// then be framed by those background lines instead of a real document border,
|
|
7
|
+
// especially for a low-contrast (e.g. dark) card whose own border gradient is
|
|
8
|
+
// weak. The existing geometry gates (aspect / fill / angles / wall-hug) are
|
|
9
|
+
// proxies that such a seam-quad can still pass.
|
|
10
|
+
//
|
|
11
|
+
// Discriminator: a real card edge is a BOUNDED segment ending at two corners
|
|
12
|
+
// where perpendicular edges meet. A seam is a THROUGH-LINE that continues PAST
|
|
13
|
+
// the candidate's corners. For each of the quad's 4 edges we look for a Hough
|
|
14
|
+
// line segment that is collinear with the edge AND overshoots both of its
|
|
15
|
+
// endpoints (or runs to the ROI boundary). If >= minSeamEdges edges sit on such
|
|
16
|
+
// through-lines, the quad is framed by background lines, not a card → reject.
|
|
17
|
+
//
|
|
18
|
+
// Pure module (no OpenCV): the hook runs cv.HoughLinesP and passes plain arrays
|
|
19
|
+
// in, so the geometry is unit-testable in isolation (mirrors qualityScoring.ts).
|
|
20
|
+
|
|
21
|
+
export interface Segment {
|
|
22
|
+
x1: number;
|
|
23
|
+
y1: number;
|
|
24
|
+
x2: number;
|
|
25
|
+
y2: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Corner {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SeamParams {
|
|
34
|
+
// Max angular difference (deg) for a segment to count as parallel to an edge.
|
|
35
|
+
angleTolDeg?: number;
|
|
36
|
+
// Max perpendicular distance (px) from the edge midpoint to the segment's
|
|
37
|
+
// infinite line for the two to be considered collinear (same line).
|
|
38
|
+
distTolPx?: number;
|
|
39
|
+
// A collinear segment must overshoot an edge endpoint by more than this
|
|
40
|
+
// fraction of the edge length to count as "extending past" that corner.
|
|
41
|
+
overshootFrac?: number;
|
|
42
|
+
// Number of edges that must sit on through-lines to reject the quad.
|
|
43
|
+
minSeamEdges?: number;
|
|
44
|
+
// Optional ROI size: when provided, a segment endpoint within boundMarginPx
|
|
45
|
+
// of a ROI wall also satisfies overshoot on that side (a seam running to the
|
|
46
|
+
// ROI edge when the card sits near the frame border).
|
|
47
|
+
roiW?: number;
|
|
48
|
+
roiH?: number;
|
|
49
|
+
boundMarginPx?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Documented defaults. Only the Hough acquisition knobs (threshold, min length,
|
|
53
|
+
// max gap, enable) are exposed to the tuning panel; these geometric tolerances
|
|
54
|
+
// are intentionally fixed constants.
|
|
55
|
+
const DEFAULTS = {
|
|
56
|
+
angleTolDeg: 8,
|
|
57
|
+
distTolPx: 6,
|
|
58
|
+
overshootFrac: 0.15,
|
|
59
|
+
minSeamEdges: 2,
|
|
60
|
+
boundMarginPx: 4,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Orientation of a vector in [0, 180) degrees (lines are undirected).
|
|
64
|
+
function lineAngleDeg(dx: number, dy: number): number {
|
|
65
|
+
let a = (Math.atan2(dy, dx) * 180) / Math.PI;
|
|
66
|
+
if (a < 0) a += 180;
|
|
67
|
+
if (a >= 180) a -= 180;
|
|
68
|
+
return a;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Smallest absolute difference between two [0,180) line angles, in [0, 90].
|
|
72
|
+
function angleDiffDeg(a: number, b: number): number {
|
|
73
|
+
let d = Math.abs(a - b) % 180;
|
|
74
|
+
if (d > 90) d = 180 - d;
|
|
75
|
+
return d;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ResolvedParams {
|
|
79
|
+
angleTol: number;
|
|
80
|
+
distTol: number;
|
|
81
|
+
overshoot: number;
|
|
82
|
+
nearRoiWall: (x: number, y: number) => boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// True when `seg` is collinear with edge A→B and overshoots both endpoints
|
|
86
|
+
// (or runs to a ROI wall). Uses early returns rather than `continue` so the
|
|
87
|
+
// caller can express the per-edge test as a single `Array.some`.
|
|
88
|
+
function segmentIsThroughLine(
|
|
89
|
+
a: Corner,
|
|
90
|
+
b: Corner,
|
|
91
|
+
ux: number,
|
|
92
|
+
uy: number,
|
|
93
|
+
len: number,
|
|
94
|
+
edgeAngle: number,
|
|
95
|
+
mx: number,
|
|
96
|
+
my: number,
|
|
97
|
+
seg: Segment,
|
|
98
|
+
p: ResolvedParams,
|
|
99
|
+
): boolean {
|
|
100
|
+
const sx = seg.x2 - seg.x1;
|
|
101
|
+
const sy = seg.y2 - seg.y1;
|
|
102
|
+
const segLen = Math.sqrt(sx * sx + sy * sy);
|
|
103
|
+
if (segLen === 0) return false;
|
|
104
|
+
|
|
105
|
+
// (a) parallel?
|
|
106
|
+
if (angleDiffDeg(edgeAngle, lineAngleDeg(sx, sy)) > p.angleTol) return false;
|
|
107
|
+
|
|
108
|
+
// (b) collinear? perpendicular distance of edge midpoint to the segment's
|
|
109
|
+
// infinite line = |(M - P) x segDir| / |segDir|.
|
|
110
|
+
const wx = mx - seg.x1;
|
|
111
|
+
const wy = my - seg.y1;
|
|
112
|
+
const perpDist = Math.abs(wx * sy - wy * sx) / segLen;
|
|
113
|
+
if (perpDist > p.distTol) return false;
|
|
114
|
+
|
|
115
|
+
// (c) overshoot: project the segment's endpoints onto the edge axis (origin
|
|
116
|
+
// at A). The edge spans t in [0, len].
|
|
117
|
+
const tP = (seg.x1 - a.x) * ux + (seg.y1 - a.y) * uy;
|
|
118
|
+
const tQ = (seg.x2 - a.x) * ux + (seg.y2 - a.y) * uy;
|
|
119
|
+
const s0 = Math.min(tP, tQ);
|
|
120
|
+
const s1 = Math.max(tP, tQ);
|
|
121
|
+
const overshootBeforeA =
|
|
122
|
+
s0 < -p.overshoot * len ||
|
|
123
|
+
p.nearRoiWall(s0 === tP ? seg.x1 : seg.x2, s0 === tP ? seg.y1 : seg.y2);
|
|
124
|
+
const overshootAfterB =
|
|
125
|
+
s1 > len + p.overshoot * len ||
|
|
126
|
+
p.nearRoiWall(s1 === tP ? seg.x1 : seg.x2, s1 === tP ? seg.y1 : seg.y2);
|
|
127
|
+
return overshootBeforeA && overshootAfterB;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Whether edge A→B sits on a background through-line.
|
|
131
|
+
function edgeOnThroughLine(
|
|
132
|
+
a: Corner,
|
|
133
|
+
b: Corner,
|
|
134
|
+
segments: Segment[],
|
|
135
|
+
p: ResolvedParams,
|
|
136
|
+
): boolean {
|
|
137
|
+
const ex = b.x - a.x;
|
|
138
|
+
const ey = b.y - a.y;
|
|
139
|
+
const len = Math.sqrt(ex * ex + ey * ey);
|
|
140
|
+
if (len === 0) return false;
|
|
141
|
+
const ux = ex / len; // unit direction along the edge
|
|
142
|
+
const uy = ey / len;
|
|
143
|
+
const edgeAngle = lineAngleDeg(ex, ey);
|
|
144
|
+
const mx = (a.x + b.x) / 2; // edge midpoint
|
|
145
|
+
const my = (a.y + b.y) / 2;
|
|
146
|
+
return segments.some((seg) =>
|
|
147
|
+
segmentIsThroughLine(a, b, ux, uy, len, edgeAngle, mx, my, seg, p),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Classify each of the quad's 4 edges as lying on a background "through-line".
|
|
153
|
+
* Returns the per-edge flags and the count of through-line edges.
|
|
154
|
+
*/
|
|
155
|
+
export function classifyEdgesOnThroughLines(
|
|
156
|
+
corners: Corner[],
|
|
157
|
+
segments: Segment[],
|
|
158
|
+
params: SeamParams = {},
|
|
159
|
+
): { seamEdgeCount: number; perEdge: boolean[] } {
|
|
160
|
+
const boundMargin = params.boundMarginPx ?? DEFAULTS.boundMarginPx;
|
|
161
|
+
const { roiW, roiH } = params;
|
|
162
|
+
const resolved: ResolvedParams = {
|
|
163
|
+
angleTol: params.angleTolDeg ?? DEFAULTS.angleTolDeg,
|
|
164
|
+
distTol: params.distTolPx ?? DEFAULTS.distTolPx,
|
|
165
|
+
overshoot: params.overshootFrac ?? DEFAULTS.overshootFrac,
|
|
166
|
+
nearRoiWall: (x: number, y: number): boolean =>
|
|
167
|
+
roiW != null &&
|
|
168
|
+
roiH != null &&
|
|
169
|
+
(x <= boundMargin ||
|
|
170
|
+
y <= boundMargin ||
|
|
171
|
+
x >= roiW - boundMargin ||
|
|
172
|
+
y >= roiH - boundMargin),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const perEdge = [false, false, false, false];
|
|
176
|
+
if (!corners || corners.length !== 4 || !segments || segments.length === 0) {
|
|
177
|
+
return { seamEdgeCount: 0, perEdge };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < 4; i++) {
|
|
181
|
+
perEdge[i] = edgeOnThroughLine(
|
|
182
|
+
corners[i],
|
|
183
|
+
corners[(i + 1) % 4],
|
|
184
|
+
segments,
|
|
185
|
+
resolved,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const seamEdgeCount = perEdge.reduce((n, flag) => n + (flag ? 1 : 0), 0);
|
|
190
|
+
return { seamEdgeCount, perEdge };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* True when the quad is framed by background straight lines (a seam artifact)
|
|
195
|
+
* rather than a real card border.
|
|
196
|
+
*/
|
|
197
|
+
export function isSeamFalseQuad(
|
|
198
|
+
corners: Corner[],
|
|
199
|
+
segments: Segment[],
|
|
200
|
+
params: SeamParams = {},
|
|
201
|
+
): boolean {
|
|
202
|
+
const minSeamEdges = params.minSeamEdges ?? DEFAULTS.minSeamEdges;
|
|
203
|
+
const { seamEdgeCount } = classifyEdgesOnThroughLines(
|
|
204
|
+
corners,
|
|
205
|
+
segments,
|
|
206
|
+
params,
|
|
207
|
+
);
|
|
208
|
+
return seamEdgeCount >= minSeamEdges;
|
|
209
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const SYNTH_BRIDGE_WINDOW_MS = 500;
|
|
2
|
+
|
|
3
|
+
export function isSyntheticBridgeRecent(
|
|
4
|
+
lastRealCardAtMs: number | null,
|
|
5
|
+
nowMs: number,
|
|
6
|
+
): boolean {
|
|
7
|
+
if (lastRealCardAtMs == null) return false;
|
|
8
|
+
const elapsedMs = nowMs - lastRealCardAtMs;
|
|
9
|
+
return elapsedMs >= 0 && elapsedMs <= SYNTH_BRIDGE_WINDOW_MS;
|
|
10
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Acquire and manage the rear-facing camera stream for document capture.
|
|
5
|
+
* Mirrors the id-scanner implementation: tries progressively relaxed
|
|
6
|
+
* constraints (1920×1080 → environment-only → any video) so older devices
|
|
7
|
+
* still get a usable stream.
|
|
8
|
+
*/
|
|
9
|
+
export function useCamera() {
|
|
10
|
+
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
let currentStream: MediaStream | null = null;
|
|
17
|
+
|
|
18
|
+
const startCamera = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const constraintsList: MediaStreamConstraints[] = [
|
|
21
|
+
{
|
|
22
|
+
audio: false,
|
|
23
|
+
video: {
|
|
24
|
+
facingMode: 'environment',
|
|
25
|
+
width: { ideal: 1920 },
|
|
26
|
+
height: { ideal: 1080 },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
audio: false,
|
|
31
|
+
video: { facingMode: 'environment' },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
audio: false,
|
|
35
|
+
video: true,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Sequential fallback: try each constraint set; only attempt the next
|
|
40
|
+
// if the previous one rejects. Implemented as a small recursive helper
|
|
41
|
+
// so we avoid both `await`-in-loop and the contrived reduce/Promise
|
|
42
|
+
// chain that the previous version used.
|
|
43
|
+
const tryConstraints = (
|
|
44
|
+
index: number,
|
|
45
|
+
lastError?: Error,
|
|
46
|
+
): Promise<MediaStream> => {
|
|
47
|
+
if (index >= constraintsList.length) {
|
|
48
|
+
return Promise.reject(
|
|
49
|
+
lastError || new Error('All camera constraints failed'),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return navigator.mediaDevices
|
|
53
|
+
.getUserMedia(constraintsList[index])
|
|
54
|
+
.catch((e: Error) => {
|
|
55
|
+
console.warn(
|
|
56
|
+
'Camera constraint failed, trying next:',
|
|
57
|
+
e?.message,
|
|
58
|
+
);
|
|
59
|
+
return tryConstraints(index + 1, e);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const mediaStream = await tryConstraints(0);
|
|
64
|
+
|
|
65
|
+
// The component may have unmounted while getUserMedia was pending; if
|
|
66
|
+
// so, stop the freshly-acquired stream immediately so we don't leak the
|
|
67
|
+
// camera (the cleanup below ran before currentStream was assigned).
|
|
68
|
+
if (cancelled) {
|
|
69
|
+
mediaStream.getTracks().forEach((track) => track.stop());
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const track = mediaStream.getVideoTracks()[0];
|
|
74
|
+
|
|
75
|
+
// Best-effort continuous autofocus / exposure / white balance.
|
|
76
|
+
// Laptop webcams in particular benefit from this — many ship with
|
|
77
|
+
// continuous AF available but not enabled by default. Each constraint
|
|
78
|
+
// is applied independently so an unsupported one doesn't kill the
|
|
79
|
+
// others.
|
|
80
|
+
const tryApply = async (constraint: MediaTrackConstraints) => {
|
|
81
|
+
try {
|
|
82
|
+
await track.applyConstraints(constraint);
|
|
83
|
+
} catch {
|
|
84
|
+
/* unsupported, ignore */
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
await tryApply({
|
|
88
|
+
advanced: [{ focusMode: 'continuous' } as MediaTrackConstraintSet],
|
|
89
|
+
});
|
|
90
|
+
// await tryApply({ advanced: [{ exposureMode: 'continuous' } as MediaTrackConstraintSet] });
|
|
91
|
+
// await tryApply({ advanced: [{ whiteBalanceMode: 'continuous' } as MediaTrackConstraintSet] });
|
|
92
|
+
|
|
93
|
+
if (videoRef.current) {
|
|
94
|
+
videoRef.current.srcObject = mediaStream;
|
|
95
|
+
await videoRef.current.play();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setStream(mediaStream);
|
|
99
|
+
currentStream = mediaStream;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('Camera access failed:', err);
|
|
102
|
+
setError('Camera access denied or unavailable.');
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
startCamera();
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
if (currentStream) {
|
|
111
|
+
currentStream.getTracks().forEach((track) => track.stop());
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
return { videoRef, error, stream };
|
|
117
|
+
}
|