@sanctum-key/react-native-sdk 1.0.15 → 1.0.17
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/build/package.json +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +113 -119
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +13 -63
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/utils/cropByObb.d.ts +2 -7
- package/build/src/utils/cropByObb.d.ts.map +1 -1
- package/build/src/utils/cropByObb.js +104 -40
- package/build/src/utils/cropByObb.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +566 -573
- package/src/modules/api/CardAuthentification.ts +21 -89
- package/src/utils/cropByObb.ts +134 -41
|
@@ -22,50 +22,6 @@ interface ApiVerificationResponse {
|
|
|
22
22
|
[key: string]: any;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function getDistance(p1: number[], p2: number[]): number {
|
|
26
|
-
return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getPolygonArea(points: number[][]): number {
|
|
30
|
-
const n = points.length;
|
|
31
|
-
let area = 0;
|
|
32
|
-
for (let i = 0; i < n; i++) {
|
|
33
|
-
const j = (i + 1) % n;
|
|
34
|
-
area += points[i][0] * points[j][1];
|
|
35
|
-
area -= points[j][0] * points[i][1];
|
|
36
|
-
}
|
|
37
|
-
return Math.abs(area) / 2;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function assertCardNotTooFar(points: number[][]): void {
|
|
41
|
-
if (!points || points.length < 3) return;
|
|
42
|
-
|
|
43
|
-
const longestCardEdge = Math.max(
|
|
44
|
-
getDistance(points[0], points[1]),
|
|
45
|
-
getDistance(points[1], points[2]),
|
|
46
|
-
getDistance(points[2], points[3] ?? points[0]),
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const cardArea = getPolygonArea(points);
|
|
50
|
-
|
|
51
|
-
logger.log(
|
|
52
|
-
`Card distance check – longest edge: ${Math.round(longestCardEdge)} px, ` +
|
|
53
|
-
`OBB area: ${Math.round(cardArea)} px²`
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
if (longestCardEdge < 700) {
|
|
57
|
-
logger.log(`🛑 Rejected: longest edge too short (${Math.round(longestCardEdge)} px < 700 px)`);
|
|
58
|
-
throw new Error('TOO_FAR_AWAY');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (cardArea < 400_000) {
|
|
62
|
-
logger.log(`🛑 Rejected: card area too small (${Math.round(cardArea)} px² < 400 000 px²)`);
|
|
63
|
-
throw new Error('TOO_FAR_AWAY');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ─── frontVerification ────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
25
|
export async function frontVerification(
|
|
70
26
|
result: {
|
|
71
27
|
path?: string,
|
|
@@ -82,8 +38,8 @@ export async function frontVerification(
|
|
|
82
38
|
console.log("Front verification START", JSON.stringify({ result }, null, 2));
|
|
83
39
|
logger.log("Front verification", JSON.stringify({ result }, null, 2));
|
|
84
40
|
|
|
85
|
-
const authMethods = Array.isArray(result.regionMapping?.authMethod)
|
|
86
|
-
? result.regionMapping.authMethod.map(m => String(m).toUpperCase())
|
|
41
|
+
const authMethods = Array.isArray(result.regionMapping?.authMethod)
|
|
42
|
+
? result.regionMapping.authMethod.map(m => String(m).toUpperCase())
|
|
87
43
|
: [];
|
|
88
44
|
const hasMrz = authMethods.some(m => m.includes('MRZ'));
|
|
89
45
|
|
|
@@ -130,6 +86,7 @@ export async function frontVerification(
|
|
|
130
86
|
let points: number[][] | null = null;
|
|
131
87
|
let isCardInFrame = true;
|
|
132
88
|
let hasCroppedSides = false;
|
|
89
|
+
console.log(points)
|
|
133
90
|
|
|
134
91
|
if (cardData) {
|
|
135
92
|
points = cardData.obb;
|
|
@@ -140,14 +97,10 @@ export async function frontVerification(
|
|
|
140
97
|
}
|
|
141
98
|
|
|
142
99
|
if (!isCardInFrame || hasCroppedSides) {
|
|
143
|
-
logger.log(`Front Framing failed. Cropped sides detected.`);
|
|
100
|
+
logger.log(`Front Framing failed. Cropped sides detected by backend.`);
|
|
144
101
|
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
145
102
|
}
|
|
146
103
|
|
|
147
|
-
if (points && points.length >= 3) {
|
|
148
|
-
assertCardNotTooFar(points);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
104
|
const obbConfidence = getObbConfidence(detected.card_obb);
|
|
152
105
|
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
153
106
|
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
@@ -174,11 +127,11 @@ export async function frontVerification(
|
|
|
174
127
|
postfix: result?.currentSide || 'front',
|
|
175
128
|
token: token,
|
|
176
129
|
template_path: result?.templatePath || '',
|
|
177
|
-
mrz_type: result?.mrzType || 'TD1'
|
|
130
|
+
mrz_type: result?.mrzType || 'TD1'
|
|
178
131
|
},
|
|
179
132
|
env
|
|
180
133
|
);
|
|
181
|
-
|
|
134
|
+
|
|
182
135
|
mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
|
|
183
136
|
|
|
184
137
|
if (mrz?.parsed_data?.status === 'FAILURE' || mrz?.success === false) {
|
|
@@ -186,7 +139,7 @@ export async function frontVerification(
|
|
|
186
139
|
throw new Error(`MRZ illisible: ${mrz?.parsed_data?.status_message || 'Veuillez nettoyer l\'objectif et réessayer'}`);
|
|
187
140
|
}
|
|
188
141
|
} catch (err: any) {
|
|
189
|
-
logger.
|
|
142
|
+
logger.log("MRZ Extraction Error:", err);
|
|
190
143
|
throw new Error(err?.message || "Erreur lors de l'extraction MRZ. Veuillez reprendre la photo.");
|
|
191
144
|
}
|
|
192
145
|
}
|
|
@@ -194,12 +147,11 @@ export async function frontVerification(
|
|
|
194
147
|
return { ...detected, croppedBase64, bbox, ...(mrz ? { mrz } : {}) };
|
|
195
148
|
|
|
196
149
|
} catch (e: any) {
|
|
197
|
-
logger.
|
|
150
|
+
logger.log('Error front verification:', e?.message);
|
|
198
151
|
throw new Error(e?.message || 'Erreur de détection du visage');
|
|
199
152
|
}
|
|
200
153
|
}
|
|
201
154
|
|
|
202
|
-
|
|
203
155
|
export async function backVerification(
|
|
204
156
|
result: {
|
|
205
157
|
path?: string,
|
|
@@ -218,8 +170,8 @@ export async function backVerification(
|
|
|
218
170
|
logger.log("result.regionMapping", result.regionMapping, result.currentSide, result.code);
|
|
219
171
|
logger.log('result object', result)
|
|
220
172
|
|
|
221
|
-
const authMethods = Array.isArray(result.regionMapping?.authMethod)
|
|
222
|
-
? result.regionMapping.authMethod.map(m => String(m).toUpperCase())
|
|
173
|
+
const authMethods = Array.isArray(result.regionMapping?.authMethod)
|
|
174
|
+
? result.regionMapping.authMethod.map(m => String(m).toUpperCase())
|
|
223
175
|
: [];
|
|
224
176
|
|
|
225
177
|
const hasMrz = authMethods.some(m => m.includes('MRZ'))
|
|
@@ -285,6 +237,7 @@ export async function backVerification(
|
|
|
285
237
|
let points: number[][] | null = null;
|
|
286
238
|
let isCardInFrame = false;
|
|
287
239
|
let hasCroppedSides = true;
|
|
240
|
+
console.log(points)
|
|
288
241
|
|
|
289
242
|
if (cardData.obb) {
|
|
290
243
|
points = cardData.obb;
|
|
@@ -296,25 +249,14 @@ export async function backVerification(
|
|
|
296
249
|
points = cardData[0];
|
|
297
250
|
isCardInFrame = true;
|
|
298
251
|
hasCroppedSides = false;
|
|
299
|
-
|
|
300
|
-
const minX = Math.min(...points.map((p: number[]) => p[0]));
|
|
301
|
-
const minY = Math.min(...points.map((p: number[]) => p[1]));
|
|
302
|
-
if (minX <= 15 || minY <= 15) {
|
|
303
|
-
isCardInFrame = false;
|
|
304
|
-
hasCroppedSides = true;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
252
|
+
// 🚨 ARTIFICIAL CHECK REMOVED HERE
|
|
307
253
|
}
|
|
308
254
|
|
|
309
255
|
if (!isCardInFrame || hasCroppedSides) {
|
|
310
|
-
logger.log(`Back Framing failed.
|
|
256
|
+
logger.log(`Back Framing failed. Backend reported document out of bounds.`);
|
|
311
257
|
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
312
258
|
}
|
|
313
259
|
|
|
314
|
-
if (points && points.length >= 3) {
|
|
315
|
-
assertCardNotTooFar(points);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
260
|
const obbConf = getObbConfidence(templateResponse?.card_obb);
|
|
319
261
|
if (obbConf !== null && obbConf < OBB_CONFIDENCE_THRESHOLD) {
|
|
320
262
|
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
@@ -354,9 +296,9 @@ export async function backVerification(
|
|
|
354
296
|
if (mrz?.parsed_data?.status === 'FAILURE' || mrz?.success === false) {
|
|
355
297
|
throw new Error(mrz?.parsed_data?.status_message || 'Lecture MRZ échouée');
|
|
356
298
|
}
|
|
357
|
-
|
|
299
|
+
|
|
358
300
|
extractionResult.mrz = mrz; // Attaching securely to result object
|
|
359
|
-
|
|
301
|
+
|
|
360
302
|
} catch (mrzError: any) {
|
|
361
303
|
logger.log(`MRZ échoué: ${mrzError?.message}`);
|
|
362
304
|
// If MRZ fails, we MUST throw an error so the user is forced to retake it!
|
|
@@ -394,7 +336,7 @@ export async function backVerification(
|
|
|
394
336
|
};
|
|
395
337
|
|
|
396
338
|
} catch (e: any) {
|
|
397
|
-
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME'
|
|
339
|
+
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME') throw e;
|
|
398
340
|
throw new Error(e?.message || 'Erreur de détection des données');
|
|
399
341
|
}
|
|
400
342
|
}
|
|
@@ -439,6 +381,7 @@ export async function checkTemplateType(
|
|
|
439
381
|
let points: number[][] | null = null;
|
|
440
382
|
let isCardInFrame = false;
|
|
441
383
|
let hasCroppedSides = true;
|
|
384
|
+
console.log(points)
|
|
442
385
|
|
|
443
386
|
if (cardData.obb) {
|
|
444
387
|
points = cardData.obb;
|
|
@@ -450,25 +393,14 @@ export async function checkTemplateType(
|
|
|
450
393
|
points = cardData[0];
|
|
451
394
|
isCardInFrame = true;
|
|
452
395
|
hasCroppedSides = false;
|
|
453
|
-
|
|
454
|
-
const minX = Math.min(...points.map((p: number[]) => p[0]));
|
|
455
|
-
const minY = Math.min(...points.map((p: number[]) => p[1]));
|
|
456
|
-
if (minX <= 15 || minY <= 15) {
|
|
457
|
-
isCardInFrame = false;
|
|
458
|
-
hasCroppedSides = true;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
396
|
+
// 🚨 ARTIFICIAL CHECK REMOVED HERE
|
|
461
397
|
}
|
|
462
398
|
|
|
463
399
|
if (!isCardInFrame || hasCroppedSides) {
|
|
464
|
-
logger.log(`Template Framing failed.`);
|
|
400
|
+
logger.log(`Template Framing failed by backend check.`);
|
|
465
401
|
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
466
402
|
}
|
|
467
403
|
|
|
468
|
-
if (points && points.length >= 3) {
|
|
469
|
-
assertCardNotTooFar(points);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
404
|
const LPIPS_THRESHOLD = 0.75;
|
|
473
405
|
if (templateType.lpips_score !== undefined && templateType.lpips_score > LPIPS_THRESHOLD) {
|
|
474
406
|
logger.log(`🛑 Country Mismatch! LPIPS: ${templateType.lpips_score}`);
|
|
@@ -478,8 +410,8 @@ export async function checkTemplateType(
|
|
|
478
410
|
logger.log("templateType result", JSON.stringify(truncateFields(templateType), null, 2));
|
|
479
411
|
return templateType;
|
|
480
412
|
} catch (e: any) {
|
|
481
|
-
logger.
|
|
482
|
-
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message
|
|
413
|
+
logger.log('Errorrr checking template type:', JSON.stringify(errorMessage(e), null, 2));
|
|
414
|
+
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message?.includes('ne correspond pas')) throw e;
|
|
483
415
|
throw new Error(e?.message || 'Erreur de vérification du template');
|
|
484
416
|
}
|
|
485
417
|
}
|
package/src/utils/cropByObb.ts
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
2
|
import * as ImageManipulator from "expo-image-manipulator";
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
type Point = [number, number];
|
|
6
6
|
|
|
7
|
-
/** OBB confidence below this = card not fully in frame; don't crop, give user feedback. */
|
|
8
7
|
export const OBB_CONFIDENCE_THRESHOLD = 0.85;
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
* card_obb format from API:
|
|
12
|
-
* - Legacy: [ [ [p1,p2,p3,p4], confidence ] ]
|
|
13
|
-
* - New: [ { obb: [p1,p2,p3,p4], confidence, card_in_frame?, cropped_sides? } ]
|
|
14
|
-
* Returns confidence in [0,1] or null if not present.
|
|
15
|
-
*/
|
|
9
|
+
|
|
16
10
|
export function getObbConfidence(cardObb: any): number | null {
|
|
17
11
|
if (!cardObb || !Array.isArray(cardObb) || cardObb.length === 0) return null;
|
|
18
12
|
const first = cardObb[0];
|
|
19
13
|
|
|
20
14
|
// New format: first is an object with a confidence field
|
|
21
|
-
if (first && typeof first === 'object' && 'confidence' in first) {
|
|
15
|
+
if (first && typeof first === 'object' && !Array.isArray(first) && 'confidence' in first) {
|
|
22
16
|
const score = (first as any).confidence;
|
|
23
17
|
return typeof score === 'number' ? score : null;
|
|
24
18
|
}
|
|
@@ -36,14 +30,12 @@ export function getObbConfidence(cardObb: any): number | null {
|
|
|
36
30
|
export function getCardInFrame(cardObb: any): boolean | null {
|
|
37
31
|
if (!cardObb) return null;
|
|
38
32
|
|
|
39
|
-
// card_obb can be an array of objects or a single object
|
|
40
33
|
const first = Array.isArray(cardObb) ? cardObb[0] : cardObb;
|
|
41
34
|
|
|
42
35
|
if (first && typeof first === 'object' && 'card_in_frame' in first) {
|
|
43
36
|
const isCardInFrame = first.card_in_frame === true;
|
|
44
37
|
const hasCroppedSides = Array.isArray(first.cropped_sides) && first.cropped_sides.length > 0;
|
|
45
38
|
|
|
46
|
-
// If the API explicitly says it's not in frame, OR if it lists cropped edges, reject it.
|
|
47
39
|
if (!isCardInFrame || hasCroppedSides) {
|
|
48
40
|
if (hasCroppedSides) {
|
|
49
41
|
console.warn(`Card is cropped on sides: ${first.cropped_sides.join(', ')}`);
|
|
@@ -51,11 +43,9 @@ export function getCardInFrame(cardObb: any): boolean | null {
|
|
|
51
43
|
return false;
|
|
52
44
|
}
|
|
53
45
|
|
|
54
|
-
// If we made it here, card_in_frame is true AND cropped_sides is empty.
|
|
55
46
|
return true;
|
|
56
47
|
}
|
|
57
48
|
|
|
58
|
-
// Backward compatibility check
|
|
59
49
|
if (cardObb && typeof cardObb === 'object' && 'card_in_frame' in cardObb) {
|
|
60
50
|
const value = (cardObb as any).card_in_frame;
|
|
61
51
|
return typeof value === 'boolean' ? value : null;
|
|
@@ -64,6 +54,25 @@ export function getCardInFrame(cardObb: any): boolean | null {
|
|
|
64
54
|
return null;
|
|
65
55
|
}
|
|
66
56
|
|
|
57
|
+
|
|
58
|
+
function extractObbPoints(cardObb: any): Point[] | null {
|
|
59
|
+
if (!cardObb) return null;
|
|
60
|
+
const first = Array.isArray(cardObb) ? cardObb[0] : cardObb;
|
|
61
|
+
|
|
62
|
+
// New format: { obb: [...], confidence, ... }
|
|
63
|
+
if (first && typeof first === 'object' && !Array.isArray(first) && 'obb' in first) {
|
|
64
|
+
const pts = (first as any).obb;
|
|
65
|
+
if (Array.isArray(pts) && pts.length === 4) return pts as Point[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Legacy format: [ [p1,p2,p3,p4], confidence ]
|
|
69
|
+
if (Array.isArray(first) && Array.isArray(first[0]) && first[0].length === 4) {
|
|
70
|
+
return first[0] as Point[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
// Compute axis-aligned bounding box from oriented quadrilateral
|
|
68
77
|
function computeAabb(points: Point[]) {
|
|
69
78
|
const xs = points.map(p => p[0]);
|
|
@@ -75,7 +84,6 @@ function computeAabb(points: Point[]) {
|
|
|
75
84
|
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
|
|
76
85
|
}
|
|
77
86
|
|
|
78
|
-
// Web-only crop using Canvas; returns dataURL (base64)
|
|
79
87
|
async function cropWeb(uri: string, points: Point[]): Promise<string> {
|
|
80
88
|
return new Promise((resolve, reject) => {
|
|
81
89
|
const img = new Image();
|
|
@@ -89,7 +97,7 @@ async function cropWeb(uri: string, points: Point[]): Promise<string> {
|
|
|
89
97
|
if (!ctx) return reject(new Error('Canvas context not available'));
|
|
90
98
|
ctx.drawImage(
|
|
91
99
|
img,
|
|
92
|
-
minX, minY, width, height, // Source
|
|
100
|
+
minX, minY, width, height, // Source
|
|
93
101
|
0, 0, canvas.width, canvas.height // Destination
|
|
94
102
|
);
|
|
95
103
|
resolve(canvas.toDataURL('image/jpeg', 0.92));
|
|
@@ -99,15 +107,17 @@ async function cropWeb(uri: string, points: Point[]): Promise<string> {
|
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
// Fallback: return original for native
|
|
103
110
|
export async function cropByObb(uri: string, cardObb: any): Promise<{ base64?: string; bbox?: { minX: number; minY: number; width: number; height: number } }> {
|
|
104
111
|
try {
|
|
105
112
|
const confidence = getObbConfidence(cardObb);
|
|
106
113
|
if (confidence !== null && confidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
107
114
|
return {};
|
|
108
115
|
}
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
|
|
117
|
+
// FIX: Use extractObbPoints() which handles both legacy and new API formats.
|
|
118
|
+
// The old code used `Array.isArray(first?.[0]) ? first[0] : null`, which
|
|
119
|
+
// returned null for new-format responses like [{obb:[...], confidence}].
|
|
120
|
+
const points = extractObbPoints(cardObb);
|
|
111
121
|
if (!points || points.length !== 4) return {};
|
|
112
122
|
|
|
113
123
|
const { minX, minY, width, height } = computeAabb(points);
|
|
@@ -124,28 +134,58 @@ export async function cropByObb(uri: string, cardObb: any): Promise<{ base64?: s
|
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
|
|
137
|
+
async function normalizeOrientationAndGetDimensions(
|
|
138
|
+
uri: string
|
|
139
|
+
): Promise<{ normalizedUri: string; width: number; height: number }> {
|
|
140
|
+
// Web: canvas already handles EXIF; skip normalization
|
|
141
|
+
if (Platform.OS === 'web') {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const img = new Image();
|
|
144
|
+
img.onload = () => resolve({ normalizedUri: uri, width: img.naturalWidth, height: img.naturalHeight });
|
|
145
|
+
img.onerror = reject;
|
|
146
|
+
img.src = uri;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Native: re-encode to bake EXIF rotation into actual pixels
|
|
151
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
152
|
+
uri,
|
|
153
|
+
[], // No transforms — just re-encode to normalize
|
|
154
|
+
{ compress: 1, format: ImageManipulator.SaveFormat.JPEG }
|
|
155
|
+
);
|
|
156
|
+
return { normalizedUri: result.uri, width: result.width, height: result.height };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
127
160
|
export async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tolerance: number = 0.02): Promise<string> {
|
|
128
161
|
return new Promise<string>((resolve) => {
|
|
129
162
|
let isResolved = false;
|
|
130
163
|
|
|
131
|
-
// Safety Timeout (
|
|
164
|
+
// Safety Timeout (15 seconds — slightly longer to account for normalization step)
|
|
132
165
|
const timeout = setTimeout(() => {
|
|
133
166
|
if (!isResolved) {
|
|
134
167
|
isResolved = true;
|
|
135
168
|
console.warn("Crop timeout, returning original.");
|
|
136
169
|
resolve(uri);
|
|
137
170
|
}
|
|
138
|
-
},
|
|
171
|
+
}, 15000);
|
|
172
|
+
|
|
173
|
+
(async () => {
|
|
174
|
+
try {
|
|
175
|
+
// STEP 1: Normalize EXIF orientation.
|
|
176
|
+
// This is the core fix — on Android the raw photo pixels may be stored
|
|
177
|
+
// rotated. After this call, normalizedUri has pixels matching the on-screen
|
|
178
|
+
// orientation and actualWidth/actualHeight are the display-space dimensions.
|
|
179
|
+
const { normalizedUri, width: actualWidth, height: actualHeight } =
|
|
180
|
+
await normalizeOrientationAndGetDimensions(uri);
|
|
139
181
|
|
|
140
|
-
try {
|
|
141
|
-
RNImage.getSize(uri, async (actualWidth, actualHeight) => {
|
|
142
182
|
if (isResolved) return;
|
|
143
183
|
|
|
144
|
-
//
|
|
145
|
-
//
|
|
184
|
+
// STEP 2: Resolve bbox to pixel coordinates.
|
|
185
|
+
// Detect if bbox is percentage (e.g., 0.90) or absolute pixels (e.g., 800).
|
|
146
186
|
const isPercentageBased = bbox.width <= 1.5 && bbox.height <= 1.5;
|
|
147
187
|
|
|
148
|
-
let pixelMinX, pixelMinY, pixelWidth, pixelHeight;
|
|
188
|
+
let pixelMinX: number, pixelMinY: number, pixelWidth: number, pixelHeight: number;
|
|
149
189
|
|
|
150
190
|
if (isPercentageBased) {
|
|
151
191
|
pixelMinX = bbox.minX * actualWidth;
|
|
@@ -153,18 +193,16 @@ export async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tol
|
|
|
153
193
|
pixelWidth = bbox.width * actualWidth;
|
|
154
194
|
pixelHeight = bbox.height * actualHeight;
|
|
155
195
|
} else {
|
|
156
|
-
// Fallback if backend pixels somehow bypass the check
|
|
157
196
|
pixelMinX = bbox.minX;
|
|
158
197
|
pixelMinY = bbox.minY;
|
|
159
198
|
pixelWidth = bbox.width;
|
|
160
199
|
pixelHeight = bbox.height;
|
|
161
200
|
}
|
|
162
201
|
|
|
163
|
-
//
|
|
202
|
+
// STEP 3: Add tolerance and clamp to image bounds.
|
|
164
203
|
const toleranceX = pixelWidth * tolerance;
|
|
165
204
|
const toleranceY = pixelHeight * tolerance;
|
|
166
205
|
|
|
167
|
-
// Clamp values to ensure we don't try to crop outside the image bounds
|
|
168
206
|
const newMinX = Math.max(0, pixelMinX - toleranceX);
|
|
169
207
|
const newMinY = Math.max(0, pixelMinY - toleranceY);
|
|
170
208
|
const newMaxX = Math.min(actualWidth, pixelMinX + pixelWidth + toleranceX);
|
|
@@ -173,19 +211,18 @@ export async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tol
|
|
|
173
211
|
const finalCrop = {
|
|
174
212
|
originX: Math.floor(newMinX),
|
|
175
213
|
originY: Math.floor(newMinY),
|
|
176
|
-
width: Math.floor(newMaxX - newMinX),
|
|
177
|
-
height: Math.floor(newMaxY - newMinY),
|
|
214
|
+
width: Math.max(1, Math.floor(newMaxX - newMinX)),
|
|
215
|
+
height: Math.max(1, Math.floor(newMaxY - newMinY)),
|
|
178
216
|
};
|
|
179
217
|
|
|
180
|
-
//
|
|
218
|
+
// STEP 4: Perform final crop on the already-normalized image.
|
|
181
219
|
try {
|
|
182
|
-
// @ts-ignore - Required for utility functions outside React components
|
|
183
220
|
const result = await ImageManipulator.manipulateAsync(
|
|
184
|
-
|
|
221
|
+
normalizedUri,
|
|
185
222
|
[{ crop: finalCrop }],
|
|
186
223
|
{ compress: 0.95, format: ImageManipulator.SaveFormat.JPEG }
|
|
187
224
|
);
|
|
188
|
-
|
|
225
|
+
|
|
189
226
|
if (!isResolved) {
|
|
190
227
|
isResolved = true;
|
|
191
228
|
clearTimeout(timeout);
|
|
@@ -195,23 +232,79 @@ export async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tol
|
|
|
195
232
|
if (!isResolved) {
|
|
196
233
|
isResolved = true;
|
|
197
234
|
clearTimeout(timeout);
|
|
198
|
-
console.error("ImageManipulator failed, returning
|
|
199
|
-
|
|
235
|
+
console.error("ImageManipulator crop failed, returning normalized image", err);
|
|
236
|
+
|
|
237
|
+
resolve(normalizedUri);
|
|
200
238
|
}
|
|
201
239
|
}
|
|
202
|
-
}
|
|
240
|
+
} catch (e) {
|
|
203
241
|
if (!isResolved) {
|
|
204
242
|
isResolved = true;
|
|
205
243
|
clearTimeout(timeout);
|
|
244
|
+
console.error("normalizeOrientationAndGetDimensions failed, returning original", e);
|
|
206
245
|
resolve(uri);
|
|
207
246
|
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
247
|
+
}
|
|
248
|
+
})();
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function cropToCenterScanArea(
|
|
253
|
+
uri: string,
|
|
254
|
+
scanWidthRatio: number = 0.91, // Match Kotlin's 0.91f
|
|
255
|
+
scanAreaAspectRatio: number = 1.59 // Standard ID Card ratio
|
|
256
|
+
): Promise<string> {
|
|
257
|
+
return new Promise<string>((resolve) => {
|
|
258
|
+
let isResolved = false;
|
|
259
|
+
|
|
260
|
+
const timeout = setTimeout(() => {
|
|
210
261
|
if (!isResolved) {
|
|
211
262
|
isResolved = true;
|
|
212
|
-
clearTimeout(timeout);
|
|
213
263
|
resolve(uri);
|
|
214
264
|
}
|
|
215
|
-
}
|
|
265
|
+
}, 10000);
|
|
266
|
+
|
|
267
|
+
(async () => {
|
|
268
|
+
try {
|
|
269
|
+
// 1. Use Claude's brilliant EXIF normalization first!
|
|
270
|
+
const { normalizedUri, width: actualWidth, height: actualHeight } =
|
|
271
|
+
await normalizeOrientationAndGetDimensions(uri);
|
|
272
|
+
|
|
273
|
+
if (isResolved) return;
|
|
274
|
+
|
|
275
|
+
// 2. Apply the Kotlin mathematical centering
|
|
276
|
+
const scanWidth = actualWidth * scanWidthRatio;
|
|
277
|
+
const scanHeight = scanWidth / scanAreaAspectRatio;
|
|
278
|
+
const left = (actualWidth - scanWidth) / 2;
|
|
279
|
+
const top = (actualHeight - scanHeight) / 2;
|
|
280
|
+
|
|
281
|
+
const finalCrop = {
|
|
282
|
+
originX: Math.max(0, Math.floor(left)),
|
|
283
|
+
originY: Math.max(0, Math.floor(top)),
|
|
284
|
+
width: Math.max(1, Math.floor(scanWidth)),
|
|
285
|
+
height: Math.max(1, Math.floor(scanHeight)),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// 3. Perform the final centered crop
|
|
289
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
290
|
+
normalizedUri,
|
|
291
|
+
[{ crop: finalCrop }],
|
|
292
|
+
{ compress: 0.95, format: ImageManipulator.SaveFormat.JPEG }
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (!isResolved) {
|
|
296
|
+
isResolved = true;
|
|
297
|
+
clearTimeout(timeout);
|
|
298
|
+
resolve(result.uri);
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
if (!isResolved) {
|
|
302
|
+
isResolved = true;
|
|
303
|
+
clearTimeout(timeout);
|
|
304
|
+
console.error("Center crop failed, returning original", err);
|
|
305
|
+
resolve(uri); // Fallback to raw image
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
216
309
|
});
|
|
217
310
|
}
|