@trustchex/react-native-sdk 1.464.0 → 1.475.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/android/build.gradle +3 -3
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +190 -12
- package/ios/Camera/TrustchexCameraView.swift +244 -46
- package/ios/Permission/PermissionModule.m +22 -0
- package/ios/Permission/PermissionModule.swift +67 -0
- package/lib/module/Screens/Debug/MRZTestScreen.js +121 -147
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +24 -3
- package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1 -1
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +7 -12
- package/lib/module/Screens/Static/ResultScreen.js +9 -1
- package/lib/module/Shared/Components/EIDScanner.js +166 -163
- package/lib/module/Shared/Components/FaceCamera.js +14 -8
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +45 -10
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
- package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/lib/module/Shared/Libs/index.js +20 -0
- package/lib/module/Shared/Libs/mrz.utils.js +570 -16
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +301 -0
- package/lib/module/Shared/Libs/mrzOcrIntegration.js +109 -0
- package/lib/module/Shared/Libs/permissions.utils.js +199 -0
- package/lib/module/Translation/Resources/en.js +1 -0
- package/lib/module/Translation/Resources/tr.js +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
- package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +76 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts +73 -0
- package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
- package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +14 -10
- package/src/Screens/Debug/MRZTestScreen.tsx +137 -166
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +45 -5
- package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1 -1
- package/src/Screens/Dynamic/VideoCallScreen.tsx +8 -19
- package/src/Screens/Static/ResultScreen.tsx +18 -9
- package/src/Shared/Components/EIDScanner.tsx +210 -160
- package/src/Shared/Components/FaceCamera.tsx +19 -16
- package/src/Shared/Components/IdentityDocumentCamera.tsx +53 -13
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
- package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/src/Shared/Libs/index.ts +63 -0
- package/src/Shared/Libs/mrz.utils.ts +639 -11
- package/src/Shared/Libs/mrzFrameAggregator.ts +370 -0
- package/src/Shared/Libs/mrzOcrIntegration.ts +175 -0
- package/src/Shared/Libs/permissions.utils.ts +251 -0
- package/src/Translation/Resources/en.ts +1 -0
- package/src/Translation/Resources/tr.ts +1 -0
- package/src/version.ts +1 -1
package/android/build.gradle
CHANGED
|
@@ -83,9 +83,9 @@ dependencies {
|
|
|
83
83
|
implementation "androidx.camera:camera-video:1.4.0"
|
|
84
84
|
|
|
85
85
|
// Google ML Kit dependencies
|
|
86
|
-
implementation 'com.google.mlkit:text-recognition:16.0.
|
|
87
|
-
implementation 'com.google.mlkit:face-detection:16.1.
|
|
88
|
-
implementation 'com.google.mlkit:barcode-scanning:17.
|
|
86
|
+
implementation 'com.google.mlkit:text-recognition:16.0.1'
|
|
87
|
+
implementation 'com.google.mlkit:face-detection:16.1.7'
|
|
88
|
+
implementation 'com.google.mlkit:barcode-scanning:17.3.0'
|
|
89
89
|
|
|
90
90
|
// Google Play Services dependency for Tasks
|
|
91
91
|
implementation 'com.google.android.gms:play-services-tasks:18.2.0'
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
package com.trustchex.reactnativesdk.camera
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.Manifest
|
|
5
|
+
import android.content.pm.PackageManager
|
|
4
6
|
import android.graphics.Bitmap
|
|
5
7
|
import android.graphics.BitmapFactory
|
|
6
8
|
import android.graphics.ImageFormat
|
|
7
9
|
import android.graphics.Matrix
|
|
10
|
+
import android.media.MediaMetadataRetriever
|
|
8
11
|
import android.graphics.Rect
|
|
9
12
|
import android.graphics.YuvImage
|
|
10
13
|
import android.util.Base64
|
|
@@ -14,6 +17,8 @@ import android.util.Size
|
|
|
14
17
|
import android.widget.FrameLayout
|
|
15
18
|
import androidx.annotation.OptIn
|
|
16
19
|
import androidx.camera.core.*
|
|
20
|
+
import androidx.camera.core.resolutionselector.ResolutionSelector
|
|
21
|
+
import androidx.camera.core.resolutionselector.ResolutionStrategy
|
|
17
22
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
18
23
|
import androidx.camera.video.*
|
|
19
24
|
import androidx.camera.view.PreviewView
|
|
@@ -303,10 +308,18 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
303
308
|
"hd" -> Size(720, 1280) // Portrait HD
|
|
304
309
|
else -> Size(1080, 1920) // Portrait Full HD (default)
|
|
305
310
|
}
|
|
311
|
+
val resolutionSelector = ResolutionSelector.Builder()
|
|
312
|
+
.setResolutionStrategy(
|
|
313
|
+
ResolutionStrategy(
|
|
314
|
+
targetResolution,
|
|
315
|
+
ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
.build()
|
|
306
319
|
|
|
307
320
|
// Preview use case
|
|
308
321
|
preview = Preview.Builder()
|
|
309
|
-
.
|
|
322
|
+
.setResolutionSelector(resolutionSelector)
|
|
310
323
|
.setTargetRotation(android.view.Surface.ROTATION_0) // Portrait
|
|
311
324
|
.build()
|
|
312
325
|
.also {
|
|
@@ -315,7 +328,7 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
315
328
|
|
|
316
329
|
// Image analysis use case
|
|
317
330
|
imageAnalyzer = ImageAnalysis.Builder()
|
|
318
|
-
.
|
|
331
|
+
.setResolutionSelector(resolutionSelector)
|
|
319
332
|
.setTargetRotation(android.view.Surface.ROTATION_0) // Portrait
|
|
320
333
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
321
334
|
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
|
|
@@ -439,9 +452,20 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
439
452
|
// Brightness calculation restricted to scanning frame area (between 36% from top and 36% from bottom, 5% margins on sides)
|
|
440
453
|
val averageBrightness = computeYPlaneBrightness(imageProxy, reportedWidth, reportedHeight)
|
|
441
454
|
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
455
|
+
// Text recognition: process ONLY the data inside the on-screen scan
|
|
456
|
+
// frame. We capture the full image but crop to the scan-frame ROI
|
|
457
|
+
// (top 36% / bottom 36% / 5% side margins — the same rectangle the UI
|
|
458
|
+
// guide and brightness sampling use) and UPSCALE it before OCR. This
|
|
459
|
+
// (a) excludes front-of-card text and surrounding noise (screen glare,
|
|
460
|
+
// IDE/browser chrome) that pollute the MRZ read, and (b) gives ML Kit
|
|
461
|
+
// more pixels per character — ML Kit needs ≥16px/char, and a full-frame
|
|
462
|
+
// capture leaves a 30–44-char MRZ line below that floor. Block
|
|
463
|
+
// coordinates from the ROI pass are mapped back to the full portrait
|
|
464
|
+
// frame so the JS overlay/filters stay correct.
|
|
465
|
+
val roi = if (textRecognitionEnabled) {
|
|
466
|
+
buildScanFrameRoiImage(imageProxy, rotationDegrees, reportedWidth, reportedHeight)
|
|
467
|
+
} else null
|
|
468
|
+
val textInputImage = roi?.image ?: inputImage
|
|
445
469
|
|
|
446
470
|
// Generate JPEG base64 only when JS side explicitly needs the image
|
|
447
471
|
// NOTE: Do NOT auto-generate for face detection - too expensive, causes frame drops
|
|
@@ -526,13 +550,23 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
526
550
|
blockMap.putString("text", block.text)
|
|
527
551
|
val bb = block.boundingBox
|
|
528
552
|
if (bb != null) {
|
|
553
|
+
// When OCR ran on the upscaled scan-frame ROI, map
|
|
554
|
+
// the box back to full portrait-frame coordinates:
|
|
555
|
+
// divide by the ROI upscale, then add the ROI origin.
|
|
556
|
+
val s = roi?.scale ?: 1f
|
|
557
|
+
val ox = roi?.offsetX ?: 0
|
|
558
|
+
val oy = roi?.offsetY ?: 0
|
|
559
|
+
val left = (bb.left / s).toInt() + ox
|
|
560
|
+
val top = (bb.top / s).toInt() + oy
|
|
561
|
+
val w = (bb.width() / s).toInt()
|
|
562
|
+
val h = (bb.height() / s).toInt()
|
|
529
563
|
val boxMap = Arguments.createMap()
|
|
530
|
-
boxMap.putInt("x",
|
|
531
|
-
boxMap.putInt("y",
|
|
532
|
-
boxMap.putInt("width",
|
|
533
|
-
boxMap.putInt("height",
|
|
534
|
-
boxMap.putInt("boundingCenterX",
|
|
535
|
-
boxMap.putInt("boundingCenterY",
|
|
564
|
+
boxMap.putInt("x", left)
|
|
565
|
+
boxMap.putInt("y", top)
|
|
566
|
+
boxMap.putInt("width", w)
|
|
567
|
+
boxMap.putInt("height", h)
|
|
568
|
+
boxMap.putInt("boundingCenterX", left + w / 2)
|
|
569
|
+
boxMap.putInt("boundingCenterY", top + h / 2)
|
|
536
570
|
blockMap.putMap("blockFrame", boxMap)
|
|
537
571
|
}
|
|
538
572
|
blocksArray.pushMap(blockMap)
|
|
@@ -777,6 +811,16 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
777
811
|
return
|
|
778
812
|
}
|
|
779
813
|
|
|
814
|
+
val hasMicPermission = ContextCompat.checkSelfPermission(
|
|
815
|
+
reactContext,
|
|
816
|
+
Manifest.permission.RECORD_AUDIO
|
|
817
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
818
|
+
|
|
819
|
+
if (!hasMicPermission) {
|
|
820
|
+
sendRecordingErrorEvent("Microphone permission is required for verbal consent recording")
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
|
|
780
824
|
val videoCapture = videoCapture ?: run {
|
|
781
825
|
sendRecordingErrorEvent("VideoCapture not initialized")
|
|
782
826
|
return
|
|
@@ -813,7 +857,24 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
813
857
|
sendRecordingErrorEvent("Recording error: ${event.cause?.message}")
|
|
814
858
|
} else {
|
|
815
859
|
val filePath = videoFile.absolutePath
|
|
816
|
-
|
|
860
|
+
val hasAudioTrack = try {
|
|
861
|
+
val retriever = MediaMetadataRetriever()
|
|
862
|
+
retriever.setDataSource(filePath)
|
|
863
|
+
val hasAudio = retriever
|
|
864
|
+
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
|
|
865
|
+
retriever.release()
|
|
866
|
+
hasAudio == "yes"
|
|
867
|
+
} catch (_: Exception) {
|
|
868
|
+
false
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (!hasAudioTrack) {
|
|
872
|
+
videoFile.delete()
|
|
873
|
+
currentRecordingFile = null
|
|
874
|
+
sendRecordingErrorEvent("Recorded video has no audio track")
|
|
875
|
+
} else {
|
|
876
|
+
sendRecordingFinishedEvent(filePath)
|
|
877
|
+
}
|
|
817
878
|
}
|
|
818
879
|
}
|
|
819
880
|
else -> {}
|
|
@@ -977,6 +1038,123 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
977
1038
|
}
|
|
978
1039
|
}
|
|
979
1040
|
|
|
1041
|
+
/** A scan-frame ROI prepared for OCR, with the transform back to full-frame coords. */
|
|
1042
|
+
private data class ScanFrameRoi(
|
|
1043
|
+
val image: InputImage,
|
|
1044
|
+
val offsetX: Int,
|
|
1045
|
+
val offsetY: Int,
|
|
1046
|
+
val scale: Float
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Builds an [InputImage] cropped to the on-screen scan frame (top 36% /
|
|
1051
|
+
* bottom 36% / 5% side margins — the same rectangle the UI guide draws) and
|
|
1052
|
+
* UPSCALED so ML Kit gets more pixels per character. We capture the full frame
|
|
1053
|
+
* but OCR only this region, which removes front-of-card text and surrounding
|
|
1054
|
+
* noise and lifts a dense MRZ line above ML Kit's ≥16px/char floor. Returns
|
|
1055
|
+
* null on any failure so the caller falls back to full-frame OCR.
|
|
1056
|
+
*/
|
|
1057
|
+
private fun buildScanFrameRoiImage(
|
|
1058
|
+
imageProxy: ImageProxy,
|
|
1059
|
+
rotationDegrees: Int,
|
|
1060
|
+
portraitWidth: Int,
|
|
1061
|
+
portraitHeight: Int
|
|
1062
|
+
): ScanFrameRoi? {
|
|
1063
|
+
return try {
|
|
1064
|
+
val full = yuvImageToBitmap(imageProxy, rotationDegrees) ?: return null
|
|
1065
|
+
|
|
1066
|
+
// Scan-frame rectangle in the rotated (portrait) bitmap. Covers the
|
|
1067
|
+
// lower-middle band where the MRZ lands, with generous vertical
|
|
1068
|
+
// tolerance (top 25% → bottom 12%) so the band doesn't have to be
|
|
1069
|
+
// perfectly centered to be OCR'd. Keep it large enough for tolerance
|
|
1070
|
+
// but not the whole frame (front text/glare would creep back in).
|
|
1071
|
+
val scanTopPercent = 0.25
|
|
1072
|
+
val scanBottomPercent = 0.12
|
|
1073
|
+
val scanLeftPercent = 0.04
|
|
1074
|
+
val scanRightPercent = 0.04
|
|
1075
|
+
val left = (full.width * scanLeftPercent).toInt().coerceIn(0, full.width - 1)
|
|
1076
|
+
val right = (full.width * (1.0 - scanRightPercent)).toInt().coerceIn(left + 1, full.width)
|
|
1077
|
+
val top = (full.height * scanTopPercent).toInt().coerceIn(0, full.height - 1)
|
|
1078
|
+
val bottom = (full.height * (1.0 - scanBottomPercent)).toInt().coerceIn(top + 1, full.height)
|
|
1079
|
+
val cropW = right - left
|
|
1080
|
+
val cropH = bottom - top
|
|
1081
|
+
if (cropW < 8 || cropH < 8) {
|
|
1082
|
+
full.recycle()
|
|
1083
|
+
return null
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
val cropped = Bitmap.createBitmap(full, left, top, cropW, cropH)
|
|
1087
|
+
full.recycle()
|
|
1088
|
+
|
|
1089
|
+
// Upscale so a ~30–44-char MRZ line clears the per-character pixel
|
|
1090
|
+
// minimum. Cap the long edge to keep OCR latency bounded.
|
|
1091
|
+
val maxLongEdge = 2200
|
|
1092
|
+
val longEdge = max(cropW, cropH)
|
|
1093
|
+
val rawScale = if (longEdge > 0) (maxLongEdge.toFloat() / longEdge) else 1f
|
|
1094
|
+
val scale = rawScale.coerceIn(1f, 2.5f)
|
|
1095
|
+
val outW = (cropW * scale).toInt()
|
|
1096
|
+
val outH = (cropH * scale).toInt()
|
|
1097
|
+
val scaled = if (scale != 1f) {
|
|
1098
|
+
val s = Bitmap.createScaledBitmap(cropped, outW, outH, true)
|
|
1099
|
+
cropped.recycle()
|
|
1100
|
+
s
|
|
1101
|
+
} else {
|
|
1102
|
+
cropped
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Grayscale + contrast boost: the MRZ is black OCR-B on a light
|
|
1106
|
+
// background, so desaturating and stretching contrast makes glyphs
|
|
1107
|
+
// crisp and suppresses the dominant filler confusion (the "<" angle
|
|
1108
|
+
// bracket misread as C/E/K under glare). Done with a ColorMatrix on a
|
|
1109
|
+
// Canvas — no OpenCV dependency.
|
|
1110
|
+
val enhanced = enhanceForOcr(scaled)
|
|
1111
|
+
if (enhanced !== scaled) scaled.recycle()
|
|
1112
|
+
|
|
1113
|
+
// The bitmap is already rotated to portrait, so rotation = 0 here.
|
|
1114
|
+
ScanFrameRoi(
|
|
1115
|
+
image = InputImage.fromBitmap(enhanced, 0),
|
|
1116
|
+
offsetX = left,
|
|
1117
|
+
offsetY = top,
|
|
1118
|
+
scale = scale
|
|
1119
|
+
)
|
|
1120
|
+
} catch (e: Exception) {
|
|
1121
|
+
null
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Desaturate + boost contrast to make dark OCR-B glyphs pop against the light
|
|
1127
|
+
* MRZ background, which suppresses the "<" filler being misread as C/E/K under
|
|
1128
|
+
* glare. Uses a single ColorMatrix (saturation 0, then a contrast scale about
|
|
1129
|
+
* mid-grey). Returns the original bitmap unchanged on failure.
|
|
1130
|
+
*/
|
|
1131
|
+
private fun enhanceForOcr(src: Bitmap): Bitmap {
|
|
1132
|
+
return try {
|
|
1133
|
+
val out = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
|
|
1134
|
+
val canvas = android.graphics.Canvas(out)
|
|
1135
|
+
val paint = android.graphics.Paint()
|
|
1136
|
+
|
|
1137
|
+
val gray = android.graphics.ColorMatrix().apply { setSaturation(0f) }
|
|
1138
|
+
// Contrast about mid-grey (128): out = c*in + (1-c)*128.
|
|
1139
|
+
val c = 1.6f
|
|
1140
|
+
val t = (1f - c) * 128f
|
|
1141
|
+
val contrast = android.graphics.ColorMatrix(
|
|
1142
|
+
floatArrayOf(
|
|
1143
|
+
c, 0f, 0f, 0f, t,
|
|
1144
|
+
0f, c, 0f, 0f, t,
|
|
1145
|
+
0f, 0f, c, 0f, t,
|
|
1146
|
+
0f, 0f, 0f, 1f, 0f
|
|
1147
|
+
)
|
|
1148
|
+
)
|
|
1149
|
+
gray.postConcat(contrast)
|
|
1150
|
+
paint.colorFilter = android.graphics.ColorMatrixColorFilter(gray)
|
|
1151
|
+
canvas.drawBitmap(src, 0f, 0f, paint)
|
|
1152
|
+
out
|
|
1153
|
+
} catch (e: Exception) {
|
|
1154
|
+
src
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
980
1158
|
fun cleanup() {
|
|
981
1159
|
// Cancel any active recording and delete file
|
|
982
1160
|
if (activeRecording != null) {
|