@sanctum-key/react-native-sdk 1.0.14 → 1.0.16
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/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +148 -8
- package/build/src/components/EnhancedCameraView.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +43 -15
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/utils/cropByObb.d.ts +2 -12
- package/build/src/utils/cropByObb.d.ts.map +1 -1
- package/build/src/utils/cropByObb.js +151 -104
- package/build/src/utils/cropByObb.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EnhancedCameraView.tsx +190 -33
- package/src/components/KYCElements/IDCardCapture.tsx +50 -15
- package/src/utils/cropByObb.ts +188 -125
package/src/utils/cropByObb.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
2
|
import * as ImageManipulator from "expo-image-manipulator";
|
|
3
|
-
|
|
4
|
-
import { logger } from './logger';
|
|
3
|
+
|
|
5
4
|
|
|
6
5
|
type Point = [number, number];
|
|
7
6
|
|
|
@@ -19,7 +18,7 @@ export function getObbConfidence(cardObb: any): number | null {
|
|
|
19
18
|
const first = cardObb[0];
|
|
20
19
|
|
|
21
20
|
// New format: first is an object with a confidence field
|
|
22
|
-
if (first && typeof first === 'object' && 'confidence' in first) {
|
|
21
|
+
if (first && typeof first === 'object' && !Array.isArray(first) && 'confidence' in first) {
|
|
23
22
|
const score = (first as any).confidence;
|
|
24
23
|
return typeof score === 'number' ? score : null;
|
|
25
24
|
}
|
|
@@ -30,44 +29,29 @@ export function getObbConfidence(cardObb: any): number | null {
|
|
|
30
29
|
return typeof score === 'number' ? score : null;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
/**
|
|
34
|
-
* Extracts the card_in_frame flag from card_obb when present.
|
|
35
|
-
* Returns:
|
|
36
|
-
* - true => card fully in frame
|
|
37
|
-
* - false => card not fully in frame
|
|
38
|
-
* - null => field not present (backward compatibility)
|
|
39
|
-
*/
|
|
40
32
|
/**
|
|
41
33
|
* Extracts the framing data from card_obb.
|
|
42
34
|
* STRICT CHECK: Returns true ONLY if card_in_frame is true AND cropped_sides is empty.
|
|
43
|
-
* Returns:
|
|
44
|
-
* - true => card fully in frame (no sides cropped)
|
|
45
|
-
* - false => card not fully in frame OR edges are cropped
|
|
46
|
-
* - null => field not present (backward compatibility)
|
|
47
35
|
*/
|
|
48
36
|
export function getCardInFrame(cardObb: any): boolean | null {
|
|
49
37
|
if (!cardObb) return null;
|
|
50
|
-
|
|
51
|
-
// card_obb can be an array of objects or a single object
|
|
38
|
+
|
|
52
39
|
const first = Array.isArray(cardObb) ? cardObb[0] : cardObb;
|
|
53
40
|
|
|
54
41
|
if (first && typeof first === 'object' && 'card_in_frame' in first) {
|
|
55
42
|
const isCardInFrame = first.card_in_frame === true;
|
|
56
43
|
const hasCroppedSides = Array.isArray(first.cropped_sides) && first.cropped_sides.length > 0;
|
|
57
44
|
|
|
58
|
-
// If the API explicitly says it's not in frame, OR if it lists cropped edges, reject it.
|
|
59
45
|
if (!isCardInFrame || hasCroppedSides) {
|
|
60
46
|
if (hasCroppedSides) {
|
|
61
47
|
console.warn(`Card is cropped on sides: ${first.cropped_sides.join(', ')}`);
|
|
62
48
|
}
|
|
63
|
-
return false;
|
|
49
|
+
return false;
|
|
64
50
|
}
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
return true;
|
|
52
|
+
return true;
|
|
68
53
|
}
|
|
69
54
|
|
|
70
|
-
// Backward compatibility check
|
|
71
55
|
if (cardObb && typeof cardObb === 'object' && 'card_in_frame' in cardObb) {
|
|
72
56
|
const value = (cardObb as any).card_in_frame;
|
|
73
57
|
return typeof value === 'boolean' ? value : null;
|
|
@@ -76,6 +60,25 @@ export function getCardInFrame(cardObb: any): boolean | null {
|
|
|
76
60
|
return null;
|
|
77
61
|
}
|
|
78
62
|
|
|
63
|
+
|
|
64
|
+
function extractObbPoints(cardObb: any): Point[] | null {
|
|
65
|
+
if (!cardObb) return null;
|
|
66
|
+
const first = Array.isArray(cardObb) ? cardObb[0] : cardObb;
|
|
67
|
+
|
|
68
|
+
// New format: { obb: [...], confidence, ... }
|
|
69
|
+
if (first && typeof first === 'object' && !Array.isArray(first) && 'obb' in first) {
|
|
70
|
+
const pts = (first as any).obb;
|
|
71
|
+
if (Array.isArray(pts) && pts.length === 4) return pts as Point[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Legacy format: [ [p1,p2,p3,p4], confidence ]
|
|
75
|
+
if (Array.isArray(first) && Array.isArray(first[0]) && first[0].length === 4) {
|
|
76
|
+
return first[0] as Point[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
79
82
|
// Compute axis-aligned bounding box from oriented quadrilateral
|
|
80
83
|
function computeAabb(points: Point[]) {
|
|
81
84
|
const xs = points.map(p => p[0]);
|
|
@@ -101,14 +104,8 @@ async function cropWeb(uri: string, points: Point[]): Promise<string> {
|
|
|
101
104
|
if (!ctx) return reject(new Error('Canvas context not available'));
|
|
102
105
|
ctx.drawImage(
|
|
103
106
|
img,
|
|
104
|
-
minX, //
|
|
105
|
-
|
|
106
|
-
width, // sw
|
|
107
|
-
height, // sh
|
|
108
|
-
0,
|
|
109
|
-
0,
|
|
110
|
-
canvas.width,
|
|
111
|
-
canvas.height
|
|
107
|
+
minX, minY, width, height, // Source
|
|
108
|
+
0, 0, canvas.width, canvas.height // Destination
|
|
112
109
|
);
|
|
113
110
|
resolve(canvas.toDataURL('image/jpeg', 0.92));
|
|
114
111
|
};
|
|
@@ -117,16 +114,17 @@ async function cropWeb(uri: string, points: Point[]): Promise<string> {
|
|
|
117
114
|
});
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
// Fallback: return original for native (no dependency added); caller can still use bbox to draw overlay
|
|
121
117
|
export async function cropByObb(uri: string, cardObb: any): Promise<{ base64?: string; bbox?: { minX: number; minY: number; width: number; height: number } }> {
|
|
122
118
|
try {
|
|
123
|
-
// card_obb format: [ [ [p1,p2,p3,p4], score ] ] — if confidence too low, don't crop (send full image)
|
|
124
119
|
const confidence = getObbConfidence(cardObb);
|
|
125
120
|
if (confidence !== null && confidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
126
121
|
return {};
|
|
127
122
|
}
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
|
|
124
|
+
// FIX: Use extractObbPoints() which handles both legacy and new API formats.
|
|
125
|
+
// The old code used `Array.isArray(first?.[0]) ? first[0] : null`, which
|
|
126
|
+
// returned null for new-format responses like [{obb:[...], confidence}].
|
|
127
|
+
const points = extractObbPoints(cardObb);
|
|
130
128
|
if (!points || points.length !== 4) return {};
|
|
131
129
|
|
|
132
130
|
const { minX, minY, width, height } = computeAabb(points);
|
|
@@ -135,7 +133,7 @@ export async function cropByObb(uri: string, cardObb: any): Promise<{ base64?: s
|
|
|
135
133
|
const base64 = await cropWeb(uri, points);
|
|
136
134
|
return { base64, bbox: { minX, minY, width, height } };
|
|
137
135
|
}
|
|
138
|
-
|
|
136
|
+
|
|
139
137
|
return { bbox: { minX, minY, width, height } };
|
|
140
138
|
} catch (e) {
|
|
141
139
|
return {};
|
|
@@ -143,112 +141,177 @@ export async function cropByObb(uri: string, cardObb: any): Promise<{ base64?: s
|
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
|
|
144
|
+
async function normalizeOrientationAndGetDimensions(
|
|
145
|
+
uri: string
|
|
146
|
+
): Promise<{ normalizedUri: string; width: number; height: number }> {
|
|
147
|
+
// Web: canvas already handles EXIF; skip normalization
|
|
148
|
+
if (Platform.OS === 'web') {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const img = new Image();
|
|
151
|
+
img.onload = () => resolve({ normalizedUri: uri, width: img.naturalWidth, height: img.naturalHeight });
|
|
152
|
+
img.onerror = reject;
|
|
153
|
+
img.src = uri;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
146
156
|
|
|
147
|
-
|
|
148
|
-
// exemple d'appel :
|
|
149
|
-
export async function cropImageWithBBox(uri: string, bbox: any) {
|
|
150
|
-
// 1️⃣ Récupère la taille originale de l'image
|
|
151
|
-
await RNImage.getSize(uri, (width, height) => {
|
|
152
|
-
console.log("Image originale:", width, height);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// // 2️⃣ Suppose que ton bbox vient d'une image affichée dans `displayedSize`
|
|
156
|
-
// const scaleX = originalWidth / displayedSize.width;
|
|
157
|
-
// const scaleY = originalHeight / displayedSize.height;
|
|
158
|
-
|
|
159
|
-
// 3️⃣ Convertir le bbox à la taille réelle scale 0.10 = 10%
|
|
160
|
-
const crop = {
|
|
161
|
-
originX: bbox.minX,
|
|
162
|
-
originY: bbox.minY,
|
|
163
|
-
width: bbox.width,
|
|
164
|
-
height: bbox.height,
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// 4️⃣ Appliquer le crop
|
|
168
|
-
// @ts-ignore - manipulateAsync is deprecated but still functional, new API (useImageManipulator) is a React hook and not suitable for utility functions
|
|
157
|
+
// Native: re-encode to bake EXIF rotation into actual pixels
|
|
169
158
|
const result = await ImageManipulator.manipulateAsync(
|
|
170
159
|
uri,
|
|
171
|
-
[
|
|
172
|
-
{ compress: 1, format: ImageManipulator.SaveFormat.
|
|
160
|
+
[], // No transforms — just re-encode to normalize
|
|
161
|
+
{ compress: 1, format: ImageManipulator.SaveFormat.JPEG }
|
|
173
162
|
);
|
|
174
|
-
|
|
175
|
-
console.log("Image recadrée:", result.uri);
|
|
176
|
-
return result.uri;
|
|
163
|
+
return { normalizedUri: result.uri, width: result.width, height: result.height };
|
|
177
164
|
}
|
|
178
165
|
|
|
179
|
-
|
|
180
|
-
export async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tolerance: number = 0.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return new Promise<string>((resolve, reject) => {
|
|
166
|
+
|
|
167
|
+
export async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tolerance: number = 0.02): Promise<string> {
|
|
168
|
+
return new Promise<string>((resolve) => {
|
|
184
169
|
let isResolved = false;
|
|
185
|
-
|
|
186
|
-
// Timeout
|
|
170
|
+
|
|
171
|
+
// Safety Timeout (15 seconds — slightly longer to account for normalization step)
|
|
187
172
|
const timeout = setTimeout(() => {
|
|
188
173
|
if (!isResolved) {
|
|
189
174
|
isResolved = true;
|
|
190
|
-
console.warn("
|
|
191
|
-
resolve(uri);
|
|
175
|
+
console.warn("Crop timeout, returning original.");
|
|
176
|
+
resolve(uri);
|
|
192
177
|
}
|
|
193
178
|
}, 15000);
|
|
194
179
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
.
|
|
231
|
-
|
|
180
|
+
(async () => {
|
|
181
|
+
try {
|
|
182
|
+
// STEP 1: Normalize EXIF orientation.
|
|
183
|
+
// This is the core fix — on Android the raw photo pixels may be stored
|
|
184
|
+
// rotated. After this call, normalizedUri has pixels matching the on-screen
|
|
185
|
+
// orientation and actualWidth/actualHeight are the display-space dimensions.
|
|
186
|
+
const { normalizedUri, width: actualWidth, height: actualHeight } =
|
|
187
|
+
await normalizeOrientationAndGetDimensions(uri);
|
|
188
|
+
|
|
189
|
+
if (isResolved) return;
|
|
190
|
+
|
|
191
|
+
// STEP 2: Resolve bbox to pixel coordinates.
|
|
192
|
+
// Detect if bbox is percentage (e.g., 0.90) or absolute pixels (e.g., 800).
|
|
193
|
+
const isPercentageBased = bbox.width <= 1.5 && bbox.height <= 1.5;
|
|
194
|
+
|
|
195
|
+
let pixelMinX: number, pixelMinY: number, pixelWidth: number, pixelHeight: number;
|
|
196
|
+
|
|
197
|
+
if (isPercentageBased) {
|
|
198
|
+
pixelMinX = bbox.minX * actualWidth;
|
|
199
|
+
pixelMinY = bbox.minY * actualHeight;
|
|
200
|
+
pixelWidth = bbox.width * actualWidth;
|
|
201
|
+
pixelHeight = bbox.height * actualHeight;
|
|
202
|
+
} else {
|
|
203
|
+
pixelMinX = bbox.minX;
|
|
204
|
+
pixelMinY = bbox.minY;
|
|
205
|
+
pixelWidth = bbox.width;
|
|
206
|
+
pixelHeight = bbox.height;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// STEP 3: Add tolerance and clamp to image bounds.
|
|
210
|
+
const toleranceX = pixelWidth * tolerance;
|
|
211
|
+
const toleranceY = pixelHeight * tolerance;
|
|
212
|
+
|
|
213
|
+
const newMinX = Math.max(0, pixelMinX - toleranceX);
|
|
214
|
+
const newMinY = Math.max(0, pixelMinY - toleranceY);
|
|
215
|
+
const newMaxX = Math.min(actualWidth, pixelMinX + pixelWidth + toleranceX);
|
|
216
|
+
const newMaxY = Math.min(actualHeight, pixelMinY + pixelHeight + toleranceY);
|
|
217
|
+
|
|
218
|
+
const finalCrop = {
|
|
219
|
+
originX: Math.floor(newMinX),
|
|
220
|
+
originY: Math.floor(newMinY),
|
|
221
|
+
width: Math.max(1, Math.floor(newMaxX - newMinX)),
|
|
222
|
+
height: Math.max(1, Math.floor(newMaxY - newMinY)),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// STEP 4: Perform final crop on the already-normalized image.
|
|
226
|
+
try {
|
|
227
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
228
|
+
normalizedUri,
|
|
229
|
+
[{ crop: finalCrop }],
|
|
230
|
+
{ compress: 0.95, format: ImageManipulator.SaveFormat.JPEG }
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (!isResolved) {
|
|
234
|
+
isResolved = true;
|
|
235
|
+
clearTimeout(timeout);
|
|
236
|
+
resolve(result.uri);
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (!isResolved) {
|
|
240
|
+
isResolved = true;
|
|
241
|
+
clearTimeout(timeout);
|
|
242
|
+
console.error("ImageManipulator crop failed, returning normalized image", err);
|
|
243
|
+
|
|
244
|
+
resolve(normalizedUri);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {
|
|
248
|
+
if (!isResolved) {
|
|
249
|
+
isResolved = true;
|
|
250
|
+
clearTimeout(timeout);
|
|
251
|
+
console.error("normalizeOrientationAndGetDimensions failed, returning original", e);
|
|
252
|
+
resolve(uri);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
})();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function cropToCenterScanArea(
|
|
260
|
+
uri: string,
|
|
261
|
+
scanWidthRatio: number = 0.91, // Match Kotlin's 0.91f
|
|
262
|
+
scanAreaAspectRatio: number = 1.59 // Standard ID Card ratio
|
|
263
|
+
): Promise<string> {
|
|
264
|
+
return new Promise<string>((resolve) => {
|
|
265
|
+
let isResolved = false;
|
|
266
|
+
|
|
267
|
+
const timeout = setTimeout(() => {
|
|
268
|
+
if (!isResolved) {
|
|
269
|
+
isResolved = true;
|
|
270
|
+
resolve(uri);
|
|
271
|
+
}
|
|
272
|
+
}, 10000);
|
|
273
|
+
|
|
274
|
+
(async () => {
|
|
275
|
+
try {
|
|
276
|
+
// 1. Use Claude's brilliant EXIF normalization first!
|
|
277
|
+
const { normalizedUri, width: actualWidth, height: actualHeight } =
|
|
278
|
+
await normalizeOrientationAndGetDimensions(uri);
|
|
279
|
+
|
|
280
|
+
if (isResolved) return;
|
|
281
|
+
|
|
282
|
+
// 2. Apply the Kotlin mathematical centering
|
|
283
|
+
const scanWidth = actualWidth * scanWidthRatio;
|
|
284
|
+
const scanHeight = scanWidth / scanAreaAspectRatio;
|
|
285
|
+
const left = (actualWidth - scanWidth) / 2;
|
|
286
|
+
const top = (actualHeight - scanHeight) / 2;
|
|
287
|
+
|
|
288
|
+
const finalCrop = {
|
|
289
|
+
originX: Math.max(0, Math.floor(left)),
|
|
290
|
+
originY: Math.max(0, Math.floor(top)),
|
|
291
|
+
width: Math.max(1, Math.floor(scanWidth)),
|
|
292
|
+
height: Math.max(1, Math.floor(scanHeight)),
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// 3. Perform the final centered crop
|
|
296
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
297
|
+
normalizedUri,
|
|
298
|
+
[{ crop: finalCrop }],
|
|
299
|
+
{ compress: 0.95, format: ImageManipulator.SaveFormat.JPEG }
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if (!isResolved) {
|
|
232
303
|
isResolved = true;
|
|
233
304
|
clearTimeout(timeout);
|
|
234
|
-
console.log("Image recadrée avec tolérance:", result.uri);
|
|
235
305
|
resolve(result.uri);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (!isResolved) {
|
|
239
309
|
isResolved = true;
|
|
240
310
|
clearTimeout(timeout);
|
|
241
|
-
console.error("
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
if (isResolved) return;
|
|
247
|
-
isResolved = true;
|
|
248
|
-
clearTimeout(timeout);
|
|
249
|
-
console.error("Erreur lors de la récupération de la taille de l'image:", error);
|
|
250
|
-
// En cas d'erreur, retourner l'URI original au lieu de rejeter
|
|
251
|
-
resolve(uri);
|
|
252
|
-
});
|
|
311
|
+
console.error("Center crop failed, returning original", err);
|
|
312
|
+
resolve(uri); // Fallback to raw image
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
})();
|
|
253
316
|
});
|
|
254
|
-
}
|
|
317
|
+
}
|