smartcomply-web-sdk 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/esm/camera/ActionDetector.d.ts +26 -0
- package/dist/esm/camera/ActionDetector.d.ts.map +1 -0
- package/dist/esm/camera/ActionDetector.js +110 -0
- package/dist/esm/camera/ActionDetector.js.map +1 -0
- package/dist/esm/camera/CameraManager.d.ts +7 -0
- package/dist/esm/camera/CameraManager.d.ts.map +1 -0
- package/dist/esm/camera/CameraManager.js +27 -0
- package/dist/esm/camera/CameraManager.js.map +1 -0
- package/dist/esm/camera/FaceDetector.d.ts +12 -0
- package/dist/esm/camera/FaceDetector.d.ts.map +1 -0
- package/dist/esm/camera/FaceDetector.js +51 -0
- package/dist/esm/camera/FaceDetector.js.map +1 -0
- package/dist/esm/camera/Permission.d.ts +42 -0
- package/dist/esm/camera/Permission.d.ts.map +1 -0
- package/dist/esm/camera/Permission.js +101 -0
- package/dist/esm/camera/Permission.js.map +1 -0
- package/dist/esm/camera/VideoRecorder.d.ts +12 -0
- package/dist/esm/camera/VideoRecorder.d.ts.map +1 -0
- package/dist/esm/camera/VideoRecorder.js +47 -0
- package/dist/esm/camera/VideoRecorder.js.map +1 -0
- package/dist/esm/client/Config.d.ts +9 -0
- package/dist/esm/client/Config.d.ts.map +1 -0
- package/dist/esm/client/Config.js +5 -0
- package/dist/esm/client/Config.js.map +1 -0
- package/dist/esm/client/HttpClient.d.ts +44 -0
- package/dist/esm/client/HttpClient.d.ts.map +1 -0
- package/dist/esm/client/HttpClient.js +145 -0
- package/dist/esm/client/HttpClient.js.map +1 -0
- package/dist/esm/client/Smartcomply.d.ts +50 -0
- package/dist/esm/client/Smartcomply.d.ts.map +1 -0
- package/dist/esm/client/Smartcomply.js +73 -0
- package/dist/esm/client/Smartcomply.js.map +1 -0
- package/dist/esm/components/CameraView.d.ts +59 -0
- package/dist/esm/components/CameraView.d.ts.map +1 -0
- package/dist/esm/components/CameraView.js +238 -0
- package/dist/esm/components/CameraView.js.map +1 -0
- package/dist/esm/components/DocumentUpload.d.ts +21 -0
- package/dist/esm/components/DocumentUpload.d.ts.map +1 -0
- package/dist/esm/components/DocumentUpload.js +216 -0
- package/dist/esm/components/DocumentUpload.js.map +1 -0
- package/dist/esm/errors/AuthError.d.ts +8 -0
- package/dist/esm/errors/AuthError.d.ts.map +1 -0
- package/dist/esm/errors/AuthError.js +11 -0
- package/dist/esm/errors/AuthError.js.map +1 -0
- package/dist/esm/errors/NetworkError.d.ts +8 -0
- package/dist/esm/errors/NetworkError.d.ts.map +1 -0
- package/dist/esm/errors/NetworkError.js +11 -0
- package/dist/esm/errors/NetworkError.js.map +1 -0
- package/dist/esm/errors/SDKError.d.ts +7 -0
- package/dist/esm/errors/SDKError.d.ts.map +1 -0
- package/dist/esm/errors/SDKError.js +10 -0
- package/dist/esm/errors/SDKError.js.map +1 -0
- package/dist/esm/flow/DocumentCapture.d.ts +54 -0
- package/dist/esm/flow/DocumentCapture.d.ts.map +1 -0
- package/dist/esm/flow/DocumentCapture.js +797 -0
- package/dist/esm/flow/DocumentCapture.js.map +1 -0
- package/dist/esm/flow/SmartComplyFlow.d.ts +104 -0
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -0
- package/dist/esm/flow/SmartComplyFlow.js +1722 -0
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -0
- package/dist/esm/flow/theme.d.ts +30 -0
- package/dist/esm/flow/theme.d.ts.map +1 -0
- package/dist/esm/flow/theme.js +99 -0
- package/dist/esm/flow/theme.js.map +1 -0
- package/dist/esm/index.d.ts +20 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/modules/liveness/LivenessUI.d.ts +46 -0
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -0
- package/dist/esm/modules/liveness/LivenessUI.js +605 -0
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -0
- package/dist/esm/modules/liveness/index.d.ts +5 -0
- package/dist/esm/modules/liveness/index.d.ts.map +1 -0
- package/dist/esm/modules/liveness/index.js +4 -0
- package/dist/esm/modules/liveness/index.js.map +1 -0
- package/dist/esm/modules/liveness/liveness.d.ts +85 -0
- package/dist/esm/modules/liveness/liveness.d.ts.map +1 -0
- package/dist/esm/modules/liveness/liveness.js +421 -0
- package/dist/esm/modules/liveness/liveness.js.map +1 -0
- package/dist/esm/modules/liveness/uploader.d.ts +34 -0
- package/dist/esm/modules/liveness/uploader.d.ts.map +1 -0
- package/dist/esm/modules/liveness/uploader.js +102 -0
- package/dist/esm/modules/liveness/uploader.js.map +1 -0
- package/dist/esm/modules/onboarding/index.d.ts +2 -0
- package/dist/esm/modules/onboarding/index.d.ts.map +1 -0
- package/dist/esm/modules/onboarding/index.js +2 -0
- package/dist/esm/modules/onboarding/index.js.map +1 -0
- package/dist/esm/modules/onboarding/onboarding.d.ts +25 -0
- package/dist/esm/modules/onboarding/onboarding.d.ts.map +1 -0
- package/dist/esm/modules/onboarding/onboarding.js +29 -0
- package/dist/esm/modules/onboarding/onboarding.js.map +1 -0
- package/dist/esm/types/common.d.ts +27 -0
- package/dist/esm/types/common.d.ts.map +1 -0
- package/dist/esm/types/common.js +2 -0
- package/dist/esm/types/common.js.map +1 -0
- package/dist/esm/types/liveness.d.ts +142 -0
- package/dist/esm/types/liveness.d.ts.map +1 -0
- package/dist/esm/types/liveness.js +2 -0
- package/dist/esm/types/liveness.js.map +1 -0
- package/dist/esm/types/onboarding.d.ts +53 -0
- package/dist/esm/types/onboarding.d.ts.map +1 -0
- package/dist/esm/types/onboarding.js +2 -0
- package/dist/esm/types/onboarding.js.map +1 -0
- package/dist/esm/types/quality.d.ts +23 -0
- package/dist/esm/types/quality.d.ts.map +1 -0
- package/dist/esm/types/quality.js +5 -0
- package/dist/esm/types/quality.js.map +1 -0
- package/dist/esm/utils/ImageQuality.d.ts +48 -0
- package/dist/esm/utils/ImageQuality.d.ts.map +1 -0
- package/dist/esm/utils/ImageQuality.js +177 -0
- package/dist/esm/utils/ImageQuality.js.map +1 -0
- package/dist/esm/utils/constants.d.ts +59 -0
- package/dist/esm/utils/constants.d.ts.map +1 -0
- package/dist/esm/utils/constants.js +67 -0
- package/dist/esm/utils/constants.js.map +1 -0
- package/dist/esm/utils/device.d.ts +14 -0
- package/dist/esm/utils/device.d.ts.map +1 -0
- package/dist/esm/utils/device.js +52 -0
- package/dist/esm/utils/device.js.map +1 -0
- package/dist/esm/utils/logger.d.ts +28 -0
- package/dist/esm/utils/logger.d.ts.map +1 -0
- package/dist/esm/utils/logger.js +64 -0
- package/dist/esm/utils/logger.js.map +1 -0
- package/dist/esm/utils/validator.d.ts +35 -0
- package/dist/esm/utils/validator.d.ts.map +1 -0
- package/dist/esm/utils/validator.js +102 -0
- package/dist/esm/utils/validator.js.map +1 -0
- package/package.json +13 -5
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
export class DocumentCapture {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.root = null;
|
|
4
|
+
this.videoEl = null;
|
|
5
|
+
this.stream = null;
|
|
6
|
+
this.resolveCapture = null;
|
|
7
|
+
this.rejectCapture = null;
|
|
8
|
+
this.frontBlob = null;
|
|
9
|
+
this.backBlob = null;
|
|
10
|
+
this.currentSide = 'front';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mount the document capture UI and return a promise that resolves
|
|
14
|
+
* with the captured document image(s).
|
|
15
|
+
*/
|
|
16
|
+
capture(container, theme, documentType) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
// Validate input parameters
|
|
19
|
+
if (!container) {
|
|
20
|
+
reject(new Error("Container element is required"));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!theme) {
|
|
24
|
+
reject(new Error("Theme configuration is required"));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!documentType || typeof documentType !== 'string') {
|
|
28
|
+
reject(new Error("Document type is required and must be a string"));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Check if already capturing
|
|
32
|
+
if (this.resolveCapture || this.rejectCapture) {
|
|
33
|
+
reject(new Error("Document capture is already in progress"));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.resolveCapture = resolve;
|
|
37
|
+
this.rejectCapture = reject;
|
|
38
|
+
this.frontBlob = null;
|
|
39
|
+
this.backBlob = null;
|
|
40
|
+
this.currentSide = 'front';
|
|
41
|
+
this.renderChoiceScreen(container, theme, documentType);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
destroy() {
|
|
45
|
+
// Stop any ongoing camera streams
|
|
46
|
+
this.stopCamera();
|
|
47
|
+
// Clean up DOM elements
|
|
48
|
+
if (this.root && this.root.parentElement) {
|
|
49
|
+
this.root.parentElement.removeChild(this.root);
|
|
50
|
+
}
|
|
51
|
+
this.root = null;
|
|
52
|
+
// Clean up any pending promises
|
|
53
|
+
if (this.rejectCapture) {
|
|
54
|
+
this.rejectCapture(new Error("Document capture was cancelled"));
|
|
55
|
+
this.rejectCapture = null;
|
|
56
|
+
}
|
|
57
|
+
this.resolveCapture = null;
|
|
58
|
+
// Clean up blob data
|
|
59
|
+
this.frontBlob = null;
|
|
60
|
+
this.backBlob = null;
|
|
61
|
+
this.currentSide = 'front';
|
|
62
|
+
}
|
|
63
|
+
requiresBackSide(documentType) {
|
|
64
|
+
const type = documentType.toLowerCase();
|
|
65
|
+
return type.includes("voter") ||
|
|
66
|
+
type.includes("nin") ||
|
|
67
|
+
type.includes("driver") ||
|
|
68
|
+
type.includes("license") ||
|
|
69
|
+
type.includes("national id") ||
|
|
70
|
+
type.includes("national id card") ||
|
|
71
|
+
type.includes("ghana");
|
|
72
|
+
}
|
|
73
|
+
renderChoiceScreen(container, theme, documentType) {
|
|
74
|
+
this.root = document.createElement("div");
|
|
75
|
+
this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:24px;padding:24px 20px;max-width:100%;";
|
|
76
|
+
const docLabel = this.getDocumentLabel(documentType);
|
|
77
|
+
const requiresBack = this.requiresBackSide(documentType);
|
|
78
|
+
const isCapturingFront = this.currentSide === 'front';
|
|
79
|
+
// Header with enhanced illustration
|
|
80
|
+
const header = document.createElement("div");
|
|
81
|
+
header.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;text-align:center;";
|
|
82
|
+
// Enhanced illustration with gradient background
|
|
83
|
+
const iconContainer = document.createElement("div");
|
|
84
|
+
iconContainer.style.cssText = `
|
|
85
|
+
width:120px;height:120px;border-radius:24px;
|
|
86
|
+
display:flex;align-items:center;justify-content:center;
|
|
87
|
+
background:linear-gradient(135deg, ${theme.primary}15, ${theme.primary}05);
|
|
88
|
+
border:3px solid ${theme.primary}30;
|
|
89
|
+
position:relative;overflow:hidden;
|
|
90
|
+
box-shadow:0 8px 32px ${theme.primary}20;
|
|
91
|
+
`;
|
|
92
|
+
const icon = document.createElement("div");
|
|
93
|
+
icon.style.cssText = "font-size:48px;animation:float 3s ease-in-out infinite;";
|
|
94
|
+
icon.textContent = "🪪";
|
|
95
|
+
iconContainer.appendChild(icon);
|
|
96
|
+
// Add floating animation
|
|
97
|
+
if (!document.head.querySelector('style[data-float-animation]')) {
|
|
98
|
+
const style = document.createElement('style');
|
|
99
|
+
style.setAttribute('data-float-animation', 'true');
|
|
100
|
+
style.textContent = `
|
|
101
|
+
@keyframes float {
|
|
102
|
+
0%, 100% { transform: translateY(0px); }
|
|
103
|
+
50% { transform: translateY(-10px); }
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
document.head.appendChild(style);
|
|
107
|
+
}
|
|
108
|
+
header.appendChild(iconContainer);
|
|
109
|
+
// Enhanced title and subtitle
|
|
110
|
+
const title = document.createElement("div");
|
|
111
|
+
title.style.cssText = `color:${theme.text};font-size:20px;font-weight:700;text-align:center;line-height:1.4;`;
|
|
112
|
+
let labelText = `Capture your ${docLabel}`;
|
|
113
|
+
if (requiresBack) {
|
|
114
|
+
labelText += ` (${isCapturingFront ? 'Front Side' : 'Back Side'})`;
|
|
115
|
+
}
|
|
116
|
+
title.textContent = labelText;
|
|
117
|
+
header.appendChild(title);
|
|
118
|
+
const subtitle = document.createElement("div");
|
|
119
|
+
subtitle.style.cssText = `color:${theme.textSecondary};font-size:14px;text-align:center;line-height:1.5;max-width:280px;`;
|
|
120
|
+
subtitle.textContent = "Choose how you'd like to provide your document";
|
|
121
|
+
header.appendChild(subtitle);
|
|
122
|
+
this.root.appendChild(header);
|
|
123
|
+
// Enhanced progress indicator for multi-sided documents
|
|
124
|
+
if (requiresBack) {
|
|
125
|
+
const progress = document.createElement("div");
|
|
126
|
+
progress.style.cssText = `
|
|
127
|
+
display:flex;flex-direction:column;align-items:center;gap:12px;
|
|
128
|
+
padding:16px 20px;background:${theme.primary}08;border-radius:16px;
|
|
129
|
+
border:1px solid ${theme.primary}20;width:100%;max-width:320px;
|
|
130
|
+
`;
|
|
131
|
+
progress.innerHTML = `
|
|
132
|
+
<div style="display:flex;gap:8px;align-items:center;">
|
|
133
|
+
<div style="width:12px;height:12px;border-radius:50%;background:${isCapturingFront ? theme.primary : theme.border};transition:all 0.3s ease;"></div>
|
|
134
|
+
<div style="width:32px;height:2px;background:${theme.border};border-radius:1px;"></div>
|
|
135
|
+
<div style="width:12px;height:12px;border-radius:50%;background:${!isCapturingFront ? theme.primary : theme.border};transition:all 0.3s ease;"></div>
|
|
136
|
+
</div>
|
|
137
|
+
<span style="color:${theme.text};font-size:13px;font-weight:600;">${isCapturingFront ? 'Step 1 of 2' : 'Step 2 of 2'}</span>
|
|
138
|
+
`;
|
|
139
|
+
this.root.appendChild(progress);
|
|
140
|
+
}
|
|
141
|
+
// Enhanced action buttons container
|
|
142
|
+
const actionsContainer = document.createElement("div");
|
|
143
|
+
actionsContainer.style.cssText = "display:flex;flex-direction:column;gap:16px;width:100%;max-width:320px;";
|
|
144
|
+
// Camera button with enhanced design
|
|
145
|
+
const cameraBtn = this.createEnhancedButton("📷 Take Photo with Camera", theme, true, "Open camera to capture document");
|
|
146
|
+
cameraBtn.addEventListener("click", () => {
|
|
147
|
+
this.showLoadingState("Opening camera...", theme);
|
|
148
|
+
this.root.innerHTML = "";
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
this.renderCameraView(this.root, theme, documentType);
|
|
151
|
+
}, 500);
|
|
152
|
+
});
|
|
153
|
+
actionsContainer.appendChild(cameraBtn);
|
|
154
|
+
// Upload button with enhanced design
|
|
155
|
+
const uploadBtn = this.createEnhancedButton("📁 Choose from Gallery", theme, false, "Upload document image from device");
|
|
156
|
+
uploadBtn.addEventListener("click", () => {
|
|
157
|
+
this.handleFileUpload(container, theme);
|
|
158
|
+
});
|
|
159
|
+
actionsContainer.appendChild(uploadBtn);
|
|
160
|
+
this.root.appendChild(actionsContainer);
|
|
161
|
+
// Enhanced tips section
|
|
162
|
+
const tips = document.createElement("div");
|
|
163
|
+
tips.style.cssText = `
|
|
164
|
+
width:100%;padding:16px 20px;border-radius:16px;
|
|
165
|
+
background:${theme.shimmer};border:1px solid ${theme.border};
|
|
166
|
+
margin-top:8px;
|
|
167
|
+
`;
|
|
168
|
+
const tipsTitle = document.createElement("div");
|
|
169
|
+
tipsTitle.style.cssText = `font-weight:700;color:${theme.text};margin-bottom:12px;font-size:14px;display:flex;align-items:center;gap:8px;`;
|
|
170
|
+
tipsTitle.innerHTML = `💡 Tips for best results`;
|
|
171
|
+
tips.appendChild(tipsTitle);
|
|
172
|
+
const tipsList = document.createElement("div");
|
|
173
|
+
tipsList.style.cssText = `display:grid;gap:8px;font-size:13px;color:${theme.textSecondary};line-height:1.6;`;
|
|
174
|
+
tipsList.innerHTML = `
|
|
175
|
+
<div style="display:flex;gap:8px;align-items:flex-start;">
|
|
176
|
+
<span style="color:${theme.primary};font-weight:bold;">✓</span>
|
|
177
|
+
<span>Lay document flat on a solid surface</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div style="display:flex;gap:8px;align-items:flex-start;">
|
|
180
|
+
<span style="color:${theme.primary};font-weight:bold;">✓</span>
|
|
181
|
+
<span>Ensure all four corners are visible</span>
|
|
182
|
+
</div>
|
|
183
|
+
<div style="display:flex;gap:8px;align-items:flex-start;">
|
|
184
|
+
<span style="color:${theme.primary};font-weight:bold;">✓</span>
|
|
185
|
+
<span>Use good lighting, avoid glare</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div style="display:flex;gap:8px;align-items:flex-start;">
|
|
188
|
+
<span style="color:${theme.primary};font-weight:bold;">✓</span>
|
|
189
|
+
<span>Make sure all text is clearly readable</span>
|
|
190
|
+
</div>
|
|
191
|
+
`;
|
|
192
|
+
tips.appendChild(tipsList);
|
|
193
|
+
this.root.appendChild(tips);
|
|
194
|
+
// File input (hidden)
|
|
195
|
+
const fileInput = document.createElement("input");
|
|
196
|
+
fileInput.type = "file";
|
|
197
|
+
fileInput.accept = "image/jpeg,image/png";
|
|
198
|
+
fileInput.style.display = "none";
|
|
199
|
+
fileInput.id = "sc-doc-file-input";
|
|
200
|
+
fileInput.addEventListener("change", (e) => {
|
|
201
|
+
const file = e.target.files?.[0];
|
|
202
|
+
if (file) {
|
|
203
|
+
// Validate file
|
|
204
|
+
const validationError = this.validateFile(file);
|
|
205
|
+
if (validationError) {
|
|
206
|
+
this.showError(validationError);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.showLoadingState("Processing image...", theme);
|
|
210
|
+
this.compressImage(file, 1280, 960, 0.85)
|
|
211
|
+
.then((compressed) => this.handleFileUploadComplete(compressed, container, theme, documentType))
|
|
212
|
+
.catch(() => this.handleFileUploadComplete(file, container, theme, documentType)); // fallback to original if compress fails
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
this.root.appendChild(fileInput);
|
|
216
|
+
container.appendChild(this.root);
|
|
217
|
+
}
|
|
218
|
+
renderCameraView(container, theme, documentType) {
|
|
219
|
+
const wrapper = document.createElement("div");
|
|
220
|
+
wrapper.style.cssText = "position:relative;width:100%;border-radius:12px;overflow:hidden;background:#000;";
|
|
221
|
+
this.videoEl = document.createElement("video");
|
|
222
|
+
this.videoEl.autoplay = true;
|
|
223
|
+
this.videoEl.playsInline = true;
|
|
224
|
+
this.videoEl.muted = true;
|
|
225
|
+
this.videoEl.style.cssText = "width:100%;display:block;aspect-ratio:4/3;object-fit:cover;";
|
|
226
|
+
wrapper.appendChild(this.videoEl);
|
|
227
|
+
// Guide overlay — rectangular
|
|
228
|
+
const guide = document.createElement("div");
|
|
229
|
+
guide.style.cssText = `
|
|
230
|
+
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
|
231
|
+
pointer-events:none;
|
|
232
|
+
`;
|
|
233
|
+
const rect = document.createElement("div");
|
|
234
|
+
rect.style.cssText = `
|
|
235
|
+
width:85%;height:60%;border:2px solid rgba(255,255,255,0.6);
|
|
236
|
+
border-radius:12px;box-shadow:0 0 0 9999px rgba(0,0,0,0.4);
|
|
237
|
+
`;
|
|
238
|
+
guide.appendChild(rect);
|
|
239
|
+
wrapper.appendChild(guide);
|
|
240
|
+
// Guide label
|
|
241
|
+
const guideLbl = document.createElement("div");
|
|
242
|
+
guideLbl.style.cssText = `
|
|
243
|
+
position:absolute;bottom:60px;left:0;right:0;text-align:center;
|
|
244
|
+
color:#fff;font-size:13px;font-weight:600;
|
|
245
|
+
text-shadow:0 1px 3px rgba(0,0,0,0.5);
|
|
246
|
+
`;
|
|
247
|
+
guideLbl.textContent = `Align your ${this.getDocumentLabel(documentType)} within the frame`;
|
|
248
|
+
wrapper.appendChild(guideLbl);
|
|
249
|
+
container.appendChild(wrapper);
|
|
250
|
+
// Capture button
|
|
251
|
+
const captureBtn = document.createElement("button");
|
|
252
|
+
captureBtn.style.cssText = `
|
|
253
|
+
display:block;margin:16px auto 0;width:64px;height:64px;
|
|
254
|
+
border-radius:50%;border:4px solid ${theme.primary};
|
|
255
|
+
background:transparent;cursor:pointer;position:relative;
|
|
256
|
+
transition:all 0.2s ease;
|
|
257
|
+
`;
|
|
258
|
+
const inner = document.createElement("div");
|
|
259
|
+
inner.style.cssText = `
|
|
260
|
+
width:48px;height:48px;border-radius:50%;background:${theme.primary};
|
|
261
|
+
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
|
|
262
|
+
transition:all 0.15s ease;
|
|
263
|
+
`;
|
|
264
|
+
captureBtn.appendChild(inner);
|
|
265
|
+
captureBtn.addEventListener("mousedown", () => {
|
|
266
|
+
inner.style.transform = "translate(-50%,-50%) scale(0.9)";
|
|
267
|
+
});
|
|
268
|
+
captureBtn.addEventListener("mouseup", () => {
|
|
269
|
+
inner.style.transform = "translate(-50%,-50%) scale(1)";
|
|
270
|
+
});
|
|
271
|
+
captureBtn.addEventListener("click", () => {
|
|
272
|
+
this.captureFrame(container, theme, documentType);
|
|
273
|
+
});
|
|
274
|
+
container.appendChild(captureBtn);
|
|
275
|
+
// Open camera
|
|
276
|
+
this.openCamera();
|
|
277
|
+
}
|
|
278
|
+
async openCamera() {
|
|
279
|
+
// Check browser compatibility
|
|
280
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
281
|
+
this.rejectCapture?.(new Error("Camera access is not supported in this browser. Please use a modern browser like Chrome, Firefox, or Safari."));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const maxRetries = 3;
|
|
285
|
+
let retryCount = 0;
|
|
286
|
+
const attemptCameraAccess = async (constraints) => {
|
|
287
|
+
try {
|
|
288
|
+
return await navigator.mediaDevices.getUserMedia(constraints);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
console.warn(`Camera access attempt ${retryCount + 1} failed:`, err);
|
|
292
|
+
if (retryCount < maxRetries - 1) {
|
|
293
|
+
retryCount++;
|
|
294
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); // Exponential backoff
|
|
295
|
+
return attemptCameraAccess(constraints);
|
|
296
|
+
}
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
try {
|
|
301
|
+
// Try rear camera first
|
|
302
|
+
this.stream = await attemptCameraAccess({
|
|
303
|
+
video: {
|
|
304
|
+
facingMode: { ideal: "environment" },
|
|
305
|
+
width: { ideal: 1280 },
|
|
306
|
+
height: { ideal: 960 },
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
console.warn("Rear camera failed, trying any camera:", err);
|
|
312
|
+
// Fallback to any camera
|
|
313
|
+
try {
|
|
314
|
+
this.stream = await attemptCameraAccess({
|
|
315
|
+
video: { width: { ideal: 1280 } },
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
catch (fallbackErr) {
|
|
319
|
+
const errorMessage = this.getCameraErrorMessage(fallbackErr);
|
|
320
|
+
this.rejectCapture?.(new Error(errorMessage));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (this.videoEl) {
|
|
325
|
+
this.videoEl.srcObject = this.stream;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
getCameraErrorMessage(error) {
|
|
329
|
+
if (error.name === 'NotAllowedError') {
|
|
330
|
+
return "Camera access denied. Please allow camera permissions and try again.";
|
|
331
|
+
}
|
|
332
|
+
else if (error.name === 'NotFoundError') {
|
|
333
|
+
return "No camera found. Please ensure your device has a working camera.";
|
|
334
|
+
}
|
|
335
|
+
else if (error.name === 'NotReadableError') {
|
|
336
|
+
return "Camera is already in use by another application. Please close other apps using the camera and try again.";
|
|
337
|
+
}
|
|
338
|
+
else if (error.name === 'OverconstrainedError') {
|
|
339
|
+
return "Camera constraints not supported. Trying with different settings...";
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
return "Camera access failed. Please check your camera permissions and try again.";
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
captureFrame(container, theme, documentType) {
|
|
346
|
+
if (!this.videoEl)
|
|
347
|
+
return;
|
|
348
|
+
const MAX_W = 1280;
|
|
349
|
+
const MAX_H = 960;
|
|
350
|
+
const vw = this.videoEl.videoWidth || MAX_W;
|
|
351
|
+
const vh = this.videoEl.videoHeight || MAX_H;
|
|
352
|
+
const ratio = Math.min(MAX_W / vw, MAX_H / vh, 1);
|
|
353
|
+
const canvas = document.createElement("canvas");
|
|
354
|
+
canvas.width = Math.round(vw * ratio);
|
|
355
|
+
canvas.height = Math.round(vh * ratio);
|
|
356
|
+
const ctx = canvas.getContext("2d");
|
|
357
|
+
if (!ctx)
|
|
358
|
+
return;
|
|
359
|
+
ctx.drawImage(this.videoEl, 0, 0, canvas.width, canvas.height);
|
|
360
|
+
canvas.toBlob((blob) => {
|
|
361
|
+
if (blob) {
|
|
362
|
+
this.stopCamera();
|
|
363
|
+
this.showPreview(container, theme, blob, documentType);
|
|
364
|
+
}
|
|
365
|
+
}, "image/jpeg", 0.85);
|
|
366
|
+
}
|
|
367
|
+
showPreview(container, theme, imageData, documentType) {
|
|
368
|
+
if (this.root)
|
|
369
|
+
this.root.innerHTML = "";
|
|
370
|
+
else {
|
|
371
|
+
this.root = document.createElement("div");
|
|
372
|
+
container.appendChild(this.root);
|
|
373
|
+
}
|
|
374
|
+
this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;";
|
|
375
|
+
// Preview image
|
|
376
|
+
const img = document.createElement("img");
|
|
377
|
+
const imageUrl = URL.createObjectURL(imageData);
|
|
378
|
+
img.src = imageUrl;
|
|
379
|
+
img.style.cssText = `
|
|
380
|
+
width:100%;max-height:300px;object-fit:contain;border-radius:12px;
|
|
381
|
+
border:2px solid ${theme.border};
|
|
382
|
+
`;
|
|
383
|
+
this.root.appendChild(img);
|
|
384
|
+
// Store URL for cleanup
|
|
385
|
+
img._blobUrl = imageUrl;
|
|
386
|
+
const label = document.createElement("div");
|
|
387
|
+
label.style.cssText = `color:${theme.textSecondary};font-size:13px;text-align:center;font-weight:500;`;
|
|
388
|
+
label.textContent = "Does the photo look clear?";
|
|
389
|
+
this.root.appendChild(label);
|
|
390
|
+
const sublabel = document.createElement("div");
|
|
391
|
+
sublabel.style.cssText = `color:${theme.textMuted};font-size:11px;text-align:center;line-height:1.6;`;
|
|
392
|
+
sublabel.textContent = "All text must be readable and all four corners must be visible.";
|
|
393
|
+
this.root.appendChild(sublabel);
|
|
394
|
+
// Buttons
|
|
395
|
+
const btnRow = document.createElement("div");
|
|
396
|
+
btnRow.style.cssText = "display:flex;gap:12px;width:100%;";
|
|
397
|
+
const retakeBtn = this.createButton("Retake", theme, false);
|
|
398
|
+
retakeBtn.style.flex = "1";
|
|
399
|
+
retakeBtn.addEventListener("click", () => {
|
|
400
|
+
if (img._blobUrl) {
|
|
401
|
+
URL.revokeObjectURL(img._blobUrl);
|
|
402
|
+
}
|
|
403
|
+
this.root.innerHTML = "";
|
|
404
|
+
this.renderChoiceScreen(container, theme, documentType);
|
|
405
|
+
});
|
|
406
|
+
const useBtn = this.createButton("Use Photo ✓", theme, true);
|
|
407
|
+
useBtn.style.flex = "1";
|
|
408
|
+
useBtn.addEventListener("click", () => {
|
|
409
|
+
if (img._blobUrl) {
|
|
410
|
+
URL.revokeObjectURL(img._blobUrl);
|
|
411
|
+
}
|
|
412
|
+
useBtn.textContent = "Processing...";
|
|
413
|
+
useBtn.disabled = true;
|
|
414
|
+
this.compressImage(imageData, 1280, 960, 0.85)
|
|
415
|
+
.then((compressed) => this.handleCaptureComplete(compressed, container, theme, documentType))
|
|
416
|
+
.catch(() => this.handleCaptureComplete(imageData, container, theme, documentType)); // fallback to original
|
|
417
|
+
});
|
|
418
|
+
btnRow.appendChild(retakeBtn);
|
|
419
|
+
btnRow.appendChild(useBtn);
|
|
420
|
+
this.root.appendChild(btnRow);
|
|
421
|
+
}
|
|
422
|
+
handleCaptureComplete(imageData, container, theme, documentType) {
|
|
423
|
+
const requiresBack = this.requiresBackSide(documentType);
|
|
424
|
+
const wasCapturingFront = this.currentSide === 'front';
|
|
425
|
+
// Store the captured image
|
|
426
|
+
if (wasCapturingFront) {
|
|
427
|
+
this.frontBlob = imageData;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
this.backBlob = imageData;
|
|
431
|
+
}
|
|
432
|
+
// If we need back side and haven't captured it yet, move to back side
|
|
433
|
+
if (requiresBack && wasCapturingFront) {
|
|
434
|
+
this.currentSide = 'back';
|
|
435
|
+
this.root.innerHTML = "";
|
|
436
|
+
this.renderChoiceScreen(container, theme, documentType);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// All captures complete, resolve with result
|
|
440
|
+
if (!this.frontBlob) {
|
|
441
|
+
this.rejectCapture?.(new Error("No front image captured. Please try again."));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const result = {
|
|
445
|
+
front: this.frontBlob,
|
|
446
|
+
back: this.backBlob || undefined
|
|
447
|
+
};
|
|
448
|
+
this.resolveCapture?.(result);
|
|
449
|
+
}
|
|
450
|
+
handleFileUploadComplete(imageData, container, theme, documentType) {
|
|
451
|
+
// Show preview first, then handle completion
|
|
452
|
+
this.showPreview(container, theme, imageData, documentType);
|
|
453
|
+
}
|
|
454
|
+
validateFile(file) {
|
|
455
|
+
// Check file type
|
|
456
|
+
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
|
|
457
|
+
if (!allowedTypes.includes(file.type)) {
|
|
458
|
+
return "Invalid file type. Please upload a JPEG or PNG image.";
|
|
459
|
+
}
|
|
460
|
+
// Check file size (max 10MB)
|
|
461
|
+
const maxSize = 10 * 1024 * 1024;
|
|
462
|
+
if (file.size > maxSize) {
|
|
463
|
+
return "File is too large. Please upload an image smaller than 10MB.";
|
|
464
|
+
}
|
|
465
|
+
// Check minimum file size (at least 1KB to avoid empty files)
|
|
466
|
+
if (file.size < 1024) {
|
|
467
|
+
return "File is too small. Please upload a valid image file.";
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
handleFileUpload(container, theme) {
|
|
472
|
+
const input = this.root?.querySelector("#sc-doc-file-input");
|
|
473
|
+
if (input)
|
|
474
|
+
input.click();
|
|
475
|
+
}
|
|
476
|
+
stopCamera() {
|
|
477
|
+
if (this.stream) {
|
|
478
|
+
this.stream.getTracks().forEach((t) => t.stop());
|
|
479
|
+
this.stream = null;
|
|
480
|
+
}
|
|
481
|
+
if (this.videoEl) {
|
|
482
|
+
this.videoEl.srcObject = null;
|
|
483
|
+
this.videoEl = null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
createEnhancedButton(label, theme, isPrimary, ariaLabel) {
|
|
487
|
+
const btn = document.createElement("button");
|
|
488
|
+
btn.textContent = label;
|
|
489
|
+
btn.setAttribute("aria-label", ariaLabel);
|
|
490
|
+
btn.style.cssText = `
|
|
491
|
+
padding:18px 24px;border-radius:16px;font-size:16px;font-weight:600;
|
|
492
|
+
cursor:pointer;transition:all 0.3s ease;border:none;width:100%;
|
|
493
|
+
min-height:56px;touch-action:manipulation;-webkit-tap-highlight-color:transparent;
|
|
494
|
+
position:relative;overflow:hidden;
|
|
495
|
+
${isPrimary
|
|
496
|
+
? `background:linear-gradient(135deg, ${theme.primary}, ${theme.primary}DD);color:#fff;box-shadow:0 8px 24px ${theme.primary}25, 0 4px 12px ${theme.primary}15;`
|
|
497
|
+
: `background:${theme.inputBg};color:${theme.text};border:2px solid ${theme.border};box-shadow:0 2px 8px ${theme.border}20;`}
|
|
498
|
+
`;
|
|
499
|
+
// Add ripple effect
|
|
500
|
+
btn.addEventListener("click", (e) => {
|
|
501
|
+
const ripple = document.createElement("span");
|
|
502
|
+
const rect = btn.getBoundingClientRect();
|
|
503
|
+
const size = Math.max(rect.width, rect.height);
|
|
504
|
+
const x = e.clientX - rect.left - size / 2;
|
|
505
|
+
const y = e.clientY - rect.top - size / 2;
|
|
506
|
+
ripple.style.cssText = `
|
|
507
|
+
position:absolute;width:${size}px;height:${size}px;
|
|
508
|
+
left:${x}px;top:${y}px;border-radius:50%;
|
|
509
|
+
background:${isPrimary ? 'rgba(255,255,255,0.3)' : theme.primary + '20'};
|
|
510
|
+
transform:scale(0);animation:ripple 0.6s ease-out;
|
|
511
|
+
pointer-events:none;
|
|
512
|
+
`;
|
|
513
|
+
btn.appendChild(ripple);
|
|
514
|
+
setTimeout(() => ripple.remove(), 600);
|
|
515
|
+
});
|
|
516
|
+
btn.addEventListener("mouseenter", () => {
|
|
517
|
+
btn.style.transform = "translateY(-2px)";
|
|
518
|
+
if (isPrimary) {
|
|
519
|
+
btn.style.boxShadow = `0 12px 32px ${theme.primary}30, 0 6px 16px ${theme.primary}20`;
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
btn.style.boxShadow = `0 4px 16px ${theme.border}30, 0 2px 8px ${theme.border}20`;
|
|
523
|
+
btn.style.borderColor = theme.primary;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
btn.addEventListener("mouseleave", () => {
|
|
527
|
+
btn.style.transform = "translateY(0)";
|
|
528
|
+
if (isPrimary) {
|
|
529
|
+
btn.style.boxShadow = `0 8px 24px ${theme.primary}25, 0 4px 12px ${theme.primary}15`;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
btn.style.boxShadow = `0 2px 8px ${theme.border}20`;
|
|
533
|
+
btn.style.borderColor = theme.border;
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
// Add ripple animation
|
|
537
|
+
if (!document.head.querySelector('style[data-ripple-animation]')) {
|
|
538
|
+
const style = document.createElement('style');
|
|
539
|
+
style.setAttribute('data-ripple-animation', 'true');
|
|
540
|
+
style.textContent = `
|
|
541
|
+
@keyframes ripple {
|
|
542
|
+
to { transform: scale(4); opacity: 0; }
|
|
543
|
+
}
|
|
544
|
+
`;
|
|
545
|
+
document.head.appendChild(style);
|
|
546
|
+
}
|
|
547
|
+
return btn;
|
|
548
|
+
}
|
|
549
|
+
showLoadingState(message, theme) {
|
|
550
|
+
if (!this.root)
|
|
551
|
+
return;
|
|
552
|
+
this.root.innerHTML = "";
|
|
553
|
+
this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;padding:40px 20px;min-height:300px;";
|
|
554
|
+
// Enhanced loading spinner
|
|
555
|
+
const spinnerContainer = document.createElement("div");
|
|
556
|
+
spinnerContainer.style.cssText = "position:relative;width:80px;height:80px;";
|
|
557
|
+
const spinner = document.createElement("div");
|
|
558
|
+
spinner.style.cssText = `
|
|
559
|
+
position:absolute;inset:0;border-radius:50%;
|
|
560
|
+
border:4px solid ${theme.border};
|
|
561
|
+
border-top-color:${theme.primary};
|
|
562
|
+
animation:sc-spin 1s linear infinite;
|
|
563
|
+
`;
|
|
564
|
+
spinnerContainer.appendChild(spinner);
|
|
565
|
+
const centerIcon = document.createElement("div");
|
|
566
|
+
centerIcon.style.cssText = `
|
|
567
|
+
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
|
568
|
+
font-size:28px;animation:pulse 2s ease-in-out infinite;
|
|
569
|
+
`;
|
|
570
|
+
centerIcon.textContent = "📄";
|
|
571
|
+
spinnerContainer.appendChild(centerIcon);
|
|
572
|
+
this.root.appendChild(spinnerContainer);
|
|
573
|
+
// Loading message
|
|
574
|
+
const loadingText = document.createElement("div");
|
|
575
|
+
loadingText.style.cssText = `
|
|
576
|
+
color:${theme.text};font-size:16px;font-weight:600;text-align:center;
|
|
577
|
+
animation:fadeIn 0.5s ease-out;
|
|
578
|
+
`;
|
|
579
|
+
loadingText.textContent = message;
|
|
580
|
+
this.root.appendChild(loadingText);
|
|
581
|
+
// Add pulse animation
|
|
582
|
+
if (!document.head.querySelector('style[data-pulse-animation]')) {
|
|
583
|
+
const style = document.createElement('style');
|
|
584
|
+
style.setAttribute('data-pulse-animation', 'true');
|
|
585
|
+
style.textContent = `
|
|
586
|
+
@keyframes pulse {
|
|
587
|
+
0%, 100% { transform: scale(1); opacity: 1; }
|
|
588
|
+
50% { transform: scale(1.1); opacity: 0.8; }
|
|
589
|
+
}
|
|
590
|
+
@keyframes fadeIn {
|
|
591
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
592
|
+
to { opacity: 1; transform: translateY(0); }
|
|
593
|
+
}
|
|
594
|
+
`;
|
|
595
|
+
document.head.appendChild(style);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
showError(message) {
|
|
599
|
+
if (!this.root)
|
|
600
|
+
return;
|
|
601
|
+
const errorContainer = document.createElement("div");
|
|
602
|
+
errorContainer.style.cssText = `
|
|
603
|
+
display:flex;align-items:center;gap:12px;padding:16px;
|
|
604
|
+
background:#ff444415;border:1px solid #ff444430;border-radius:12px;
|
|
605
|
+
color:#ff4444;font-size:14px;margin-top:16px;animation:shake 0.5s ease-in-out;
|
|
606
|
+
`;
|
|
607
|
+
errorContainer.innerHTML = `
|
|
608
|
+
<span style="font-size:18px;">⚠️</span>
|
|
609
|
+
<span>${message}</span>
|
|
610
|
+
`;
|
|
611
|
+
// Add shake animation
|
|
612
|
+
if (!document.head.querySelector('style[data-shake-animation]')) {
|
|
613
|
+
const style = document.createElement('style');
|
|
614
|
+
style.setAttribute('data-shake-animation', 'true');
|
|
615
|
+
style.textContent = `
|
|
616
|
+
@keyframes shake {
|
|
617
|
+
0%, 100% { transform: translateX(0); }
|
|
618
|
+
25% { transform: translateX(-5px); }
|
|
619
|
+
75% { transform: translateX(5px); }
|
|
620
|
+
}
|
|
621
|
+
`;
|
|
622
|
+
document.head.appendChild(style);
|
|
623
|
+
}
|
|
624
|
+
this.root.appendChild(errorContainer);
|
|
625
|
+
setTimeout(() => errorContainer.remove(), 5000);
|
|
626
|
+
}
|
|
627
|
+
createButton(label, theme, isPrimary) {
|
|
628
|
+
const btn = document.createElement("button");
|
|
629
|
+
btn.textContent = label;
|
|
630
|
+
btn.style.cssText = `
|
|
631
|
+
padding:16px 24px;border-radius:12px;font-size:16px;font-weight:600;
|
|
632
|
+
cursor:pointer;transition:all 0.2s ease;border:none;width:100%;
|
|
633
|
+
min-height:52px;touch-action:manipulation;-webkit-tap-highlight-color:transparent;
|
|
634
|
+
${isPrimary
|
|
635
|
+
? `background:${theme.primary};color:#fff;box-shadow:0 4px 12px ${theme.primary}30;`
|
|
636
|
+
: `background:${theme.inputBg};color:${theme.text};border:2px solid ${theme.border};`}
|
|
637
|
+
`;
|
|
638
|
+
btn.addEventListener("mouseenter", () => {
|
|
639
|
+
btn.style.transform = "translateY(-1px)";
|
|
640
|
+
if (isPrimary)
|
|
641
|
+
btn.style.background = theme.primaryHover;
|
|
642
|
+
});
|
|
643
|
+
btn.addEventListener("mouseleave", () => {
|
|
644
|
+
btn.style.transform = "translateY(0)";
|
|
645
|
+
if (isPrimary)
|
|
646
|
+
btn.style.background = theme.primary;
|
|
647
|
+
});
|
|
648
|
+
return btn;
|
|
649
|
+
}
|
|
650
|
+
compressImage(source, maxWidth, maxHeight, quality) {
|
|
651
|
+
return new Promise((resolve, reject) => {
|
|
652
|
+
// Read EXIF orientation FIRST so we can correct rotation before canvas draw.
|
|
653
|
+
// Canvas ignores EXIF metadata — without this, phone uploads arrive rotated.
|
|
654
|
+
this._readExifOrientation(source).then((orientation) => {
|
|
655
|
+
const url = URL.createObjectURL(source);
|
|
656
|
+
const imgEl = document.createElement("img");
|
|
657
|
+
imgEl.onload = () => {
|
|
658
|
+
URL.revokeObjectURL(url);
|
|
659
|
+
let { naturalWidth: w, naturalHeight: h } = imgEl;
|
|
660
|
+
// For orientations 5-8 the image is rotated 90°/270° — swap dimensions
|
|
661
|
+
const swapped = orientation >= 5 && orientation <= 8;
|
|
662
|
+
const srcW = swapped ? h : w;
|
|
663
|
+
const srcH = swapped ? w : h;
|
|
664
|
+
const ratio = Math.min(maxWidth / srcW, maxHeight / srcH, 1);
|
|
665
|
+
const outW = Math.round(srcW * ratio);
|
|
666
|
+
const outH = Math.round(srcH * ratio);
|
|
667
|
+
const canvas = document.createElement("canvas");
|
|
668
|
+
canvas.width = outW;
|
|
669
|
+
canvas.height = outH;
|
|
670
|
+
const ctx = canvas.getContext("2d");
|
|
671
|
+
if (!ctx) {
|
|
672
|
+
reject(new Error("Canvas unavailable"));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
// Apply EXIF rotation transform before drawing
|
|
676
|
+
ctx.save();
|
|
677
|
+
if (orientation === 2) {
|
|
678
|
+
ctx.transform(-1, 0, 0, 1, outW, 0);
|
|
679
|
+
}
|
|
680
|
+
else if (orientation === 3) {
|
|
681
|
+
ctx.transform(-1, 0, 0, -1, outW, outH);
|
|
682
|
+
}
|
|
683
|
+
else if (orientation === 4) {
|
|
684
|
+
ctx.transform(1, 0, 0, -1, 0, outH);
|
|
685
|
+
}
|
|
686
|
+
else if (orientation === 5) {
|
|
687
|
+
ctx.transform(0, 1, 1, 0, 0, 0);
|
|
688
|
+
}
|
|
689
|
+
else if (orientation === 6) {
|
|
690
|
+
ctx.transform(0, 1, -1, 0, outH, 0);
|
|
691
|
+
}
|
|
692
|
+
else if (orientation === 7) {
|
|
693
|
+
ctx.transform(0, -1, -1, 0, outH, outW);
|
|
694
|
+
}
|
|
695
|
+
else if (orientation === 8) {
|
|
696
|
+
ctx.transform(0, -1, 1, 0, 0, outW);
|
|
697
|
+
}
|
|
698
|
+
ctx.drawImage(imgEl, 0, 0, swapped ? outH : outW, swapped ? outW : outH);
|
|
699
|
+
ctx.restore();
|
|
700
|
+
canvas.toBlob((blob) => { blob ? resolve(blob) : reject(new Error("Compression failed")); }, "image/jpeg", quality);
|
|
701
|
+
};
|
|
702
|
+
imgEl.onerror = () => { URL.revokeObjectURL(url); reject(new Error("Image load failed")); };
|
|
703
|
+
imgEl.src = url;
|
|
704
|
+
}).catch(() => {
|
|
705
|
+
// If EXIF read fails, fall back to simple compression without rotation
|
|
706
|
+
const url = URL.createObjectURL(source);
|
|
707
|
+
const imgEl = document.createElement("img");
|
|
708
|
+
imgEl.onload = () => {
|
|
709
|
+
URL.revokeObjectURL(url);
|
|
710
|
+
let { naturalWidth: w, naturalHeight: h } = imgEl;
|
|
711
|
+
const ratio = Math.min(maxWidth / w, maxHeight / h, 1);
|
|
712
|
+
const canvas = document.createElement("canvas");
|
|
713
|
+
canvas.width = Math.round(w * ratio);
|
|
714
|
+
canvas.height = Math.round(h * ratio);
|
|
715
|
+
const ctx = canvas.getContext("2d");
|
|
716
|
+
if (!ctx) {
|
|
717
|
+
reject(new Error("Canvas unavailable"));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
ctx.drawImage(imgEl, 0, 0, canvas.width, canvas.height);
|
|
721
|
+
canvas.toBlob((blob) => { blob ? resolve(blob) : reject(new Error("Compression failed")); }, "image/jpeg", quality);
|
|
722
|
+
};
|
|
723
|
+
imgEl.onerror = () => { URL.revokeObjectURL(url); reject(new Error("Image load failed")); };
|
|
724
|
+
imgEl.src = url;
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Read JPEG EXIF orientation tag (1–8) from a Blob/File.
|
|
730
|
+
* Returns 1 (no rotation) if EXIF is absent or unreadable.
|
|
731
|
+
*/
|
|
732
|
+
_readExifOrientation(source) {
|
|
733
|
+
return new Promise((resolve) => {
|
|
734
|
+
const reader = new FileReader();
|
|
735
|
+
reader.onload = (e) => {
|
|
736
|
+
try {
|
|
737
|
+
const buf = e.target?.result;
|
|
738
|
+
const view = new DataView(buf);
|
|
739
|
+
if (view.getUint16(0, false) !== 0xFFD8) {
|
|
740
|
+
resolve(1);
|
|
741
|
+
return;
|
|
742
|
+
} // not JPEG
|
|
743
|
+
let offset = 2;
|
|
744
|
+
while (offset < view.byteLength) {
|
|
745
|
+
const marker = view.getUint16(offset, false);
|
|
746
|
+
offset += 2;
|
|
747
|
+
if (marker === 0xFFE1) { // APP1 — may contain EXIF
|
|
748
|
+
if (view.getUint32(offset + 2, false) !== 0x45786966) {
|
|
749
|
+
resolve(1);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const little = view.getUint16(offset + 8, false) === 0x4949;
|
|
753
|
+
const ifdOff = offset + 8 + view.getUint32(offset + 12, little);
|
|
754
|
+
const tags = view.getUint16(ifdOff, little);
|
|
755
|
+
for (let i = 0; i < tags; i++) {
|
|
756
|
+
if (view.getUint16(ifdOff + 2 + 12 * i, little) === 0x0112) {
|
|
757
|
+
resolve(view.getUint16(ifdOff + 2 + 12 * i + 8, little));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
resolve(1);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
else if ((marker & 0xFF00) !== 0xFF00) {
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
offset += view.getUint16(offset, false);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
resolve(1);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
resolve(1);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
reader.onerror = () => resolve(1);
|
|
778
|
+
// Only need the first 64KB — EXIF is always in the first APP1 segment
|
|
779
|
+
reader.readAsArrayBuffer(source.slice(0, 65536));
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
getDocumentLabel(type) {
|
|
783
|
+
const t = type.toLowerCase();
|
|
784
|
+
if (t.includes("passport"))
|
|
785
|
+
return "passport";
|
|
786
|
+
if (t.includes("driver") || t.includes("licen"))
|
|
787
|
+
return "driver's license";
|
|
788
|
+
if (t.includes("voter") || t.includes("pvc"))
|
|
789
|
+
return "voter's card";
|
|
790
|
+
if (t.includes("national") || t.includes("nin") || t.includes("national id"))
|
|
791
|
+
return "national ID card";
|
|
792
|
+
if (t.includes("bvn"))
|
|
793
|
+
return "BVN card";
|
|
794
|
+
return "identity document";
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
//# sourceMappingURL=DocumentCapture.js.map
|