petrus-react-native 0.1.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.
Files changed (163) hide show
  1. package/PetrusReactNative.podspec +20 -0
  2. package/android/build.gradle +64 -0
  3. package/android/src/main/AndroidManifest.xml +4 -0
  4. package/android/src/main/java/com/petrusreactnative/PDF417ScanSession.kt +86 -0
  5. package/android/src/main/java/com/petrusreactnative/PassportScanSession.kt +137 -0
  6. package/android/src/main/java/com/petrusreactnative/PetrusCameraView.kt +146 -0
  7. package/android/src/main/java/com/petrusreactnative/PetrusCameraViewManager.kt +93 -0
  8. package/android/src/main/java/com/petrusreactnative/PetrusOverlayView.kt +87 -0
  9. package/android/src/main/java/com/petrusreactnative/PetrusReactNativeModule.kt +253 -0
  10. package/android/src/main/java/com/petrusreactnative/PetrusReactNativePackage.kt +38 -0
  11. package/android/src/main/java/com/petrusreactnative/VehicleRegistrationScanSession.kt +119 -0
  12. package/android/src/main/java/com/petrusreactnative/events/CapturedEvent.kt +30 -0
  13. package/android/src/main/java/com/petrusreactnative/events/DocumentDetectedEvent.kt +49 -0
  14. package/android/src/main/java/com/petrusreactnative/events/FrameMetadataEvent.kt +24 -0
  15. package/android/src/main/java/com/petrusreactnative/mrz/PassportResultMapping.kt +54 -0
  16. package/android/src/main/java/com/petrusreactnative/ocr/OCRResultMapping.kt +52 -0
  17. package/android/src/main/java/com/petrusreactnative/pdf417/SouthAfricanDriversLicenseResultMapping.kt +80 -0
  18. package/android/src/main/java/com/petrusreactnative/sa/SouthAfricanIDResultMapping.kt +50 -0
  19. package/android/src/main/java/com/petrusreactnative/vehicle/VehicleRegistrationResultMapping.kt +72 -0
  20. package/ios/PetrusReactNative.h +5 -0
  21. package/ios/PetrusReactNative.mm +21 -0
  22. package/lib/module/NativePetrusReactNative.js +5 -0
  23. package/lib/module/NativePetrusReactNative.js.map +1 -0
  24. package/lib/module/PetrusCameraView.js +6 -0
  25. package/lib/module/PetrusCameraView.js.map +1 -0
  26. package/lib/module/PetrusCameraView.native.js +24 -0
  27. package/lib/module/PetrusCameraView.native.js.map +1 -0
  28. package/lib/module/PetrusCameraViewNativeComponent.ts +47 -0
  29. package/lib/module/cameraPermissions.js +9 -0
  30. package/lib/module/cameraPermissions.js.map +1 -0
  31. package/lib/module/cameraPermissions.native.js +10 -0
  32. package/lib/module/cameraPermissions.native.js.map +1 -0
  33. package/lib/module/getPipelineVersion.js +6 -0
  34. package/lib/module/getPipelineVersion.js.map +1 -0
  35. package/lib/module/getPipelineVersion.native.js +7 -0
  36. package/lib/module/getPipelineVersion.native.js.map +1 -0
  37. package/lib/module/index.js +45 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/package.json +1 -0
  40. package/lib/module/parseMRZ.js +6 -0
  41. package/lib/module/parseMRZ.js.map +1 -0
  42. package/lib/module/parseMRZ.native.js +12 -0
  43. package/lib/module/parseMRZ.native.js.map +1 -0
  44. package/lib/module/parseSouthAfricanDriversLicense.js +6 -0
  45. package/lib/module/parseSouthAfricanDriversLicense.js.map +1 -0
  46. package/lib/module/parseSouthAfricanDriversLicense.native.js +12 -0
  47. package/lib/module/parseSouthAfricanDriversLicense.native.js.map +1 -0
  48. package/lib/module/parseSouthAfricanID.js +6 -0
  49. package/lib/module/parseSouthAfricanID.js.map +1 -0
  50. package/lib/module/parseSouthAfricanID.native.js +8 -0
  51. package/lib/module/parseSouthAfricanID.native.js.map +1 -0
  52. package/lib/module/parseVehicleRegistration.js +6 -0
  53. package/lib/module/parseVehicleRegistration.js.map +1 -0
  54. package/lib/module/parseVehicleRegistration.native.js +8 -0
  55. package/lib/module/parseVehicleRegistration.native.js.map +1 -0
  56. package/lib/module/recognizeText.js +6 -0
  57. package/lib/module/recognizeText.js.map +1 -0
  58. package/lib/module/recognizeText.native.js +8 -0
  59. package/lib/module/recognizeText.native.js.map +1 -0
  60. package/lib/module/scanPDF417.js +6 -0
  61. package/lib/module/scanPDF417.js.map +1 -0
  62. package/lib/module/scanPDF417.native.js +8 -0
  63. package/lib/module/scanPDF417.native.js.map +1 -0
  64. package/lib/module/scanPassport.js +6 -0
  65. package/lib/module/scanPassport.js.map +1 -0
  66. package/lib/module/scanPassport.native.js +13 -0
  67. package/lib/module/scanPassport.native.js.map +1 -0
  68. package/lib/module/scanSouthAfricanDriversLicense.js +6 -0
  69. package/lib/module/scanSouthAfricanDriversLicense.js.map +1 -0
  70. package/lib/module/scanSouthAfricanDriversLicense.native.js +12 -0
  71. package/lib/module/scanSouthAfricanDriversLicense.native.js.map +1 -0
  72. package/lib/module/scanVehicleRegistration.js +6 -0
  73. package/lib/module/scanVehicleRegistration.js.map +1 -0
  74. package/lib/module/scanVehicleRegistration.native.js +13 -0
  75. package/lib/module/scanVehicleRegistration.native.js.map +1 -0
  76. package/lib/module/types.js +2 -0
  77. package/lib/module/types.js.map +1 -0
  78. package/lib/typescript/package.json +1 -0
  79. package/lib/typescript/src/NativePetrusReactNative.d.ts +271 -0
  80. package/lib/typescript/src/NativePetrusReactNative.d.ts.map +1 -0
  81. package/lib/typescript/src/PetrusCameraView.d.ts +10 -0
  82. package/lib/typescript/src/PetrusCameraView.d.ts.map +1 -0
  83. package/lib/typescript/src/PetrusCameraView.native.d.ts +4 -0
  84. package/lib/typescript/src/PetrusCameraView.native.d.ts.map +1 -0
  85. package/lib/typescript/src/PetrusCameraViewNativeComponent.d.ts +38 -0
  86. package/lib/typescript/src/PetrusCameraViewNativeComponent.d.ts.map +1 -0
  87. package/lib/typescript/src/cameraPermissions.d.ts +4 -0
  88. package/lib/typescript/src/cameraPermissions.d.ts.map +1 -0
  89. package/lib/typescript/src/cameraPermissions.native.d.ts +4 -0
  90. package/lib/typescript/src/cameraPermissions.native.d.ts.map +1 -0
  91. package/lib/typescript/src/getPipelineVersion.d.ts +2 -0
  92. package/lib/typescript/src/getPipelineVersion.d.ts.map +1 -0
  93. package/lib/typescript/src/getPipelineVersion.native.d.ts +2 -0
  94. package/lib/typescript/src/getPipelineVersion.native.d.ts.map +1 -0
  95. package/lib/typescript/src/index.d.ts +45 -0
  96. package/lib/typescript/src/index.d.ts.map +1 -0
  97. package/lib/typescript/src/parseMRZ.d.ts +3 -0
  98. package/lib/typescript/src/parseMRZ.d.ts.map +1 -0
  99. package/lib/typescript/src/parseMRZ.native.d.ts +8 -0
  100. package/lib/typescript/src/parseMRZ.native.d.ts.map +1 -0
  101. package/lib/typescript/src/parseSouthAfricanDriversLicense.d.ts +3 -0
  102. package/lib/typescript/src/parseSouthAfricanDriversLicense.d.ts.map +1 -0
  103. package/lib/typescript/src/parseSouthAfricanDriversLicense.native.d.ts +8 -0
  104. package/lib/typescript/src/parseSouthAfricanDriversLicense.native.d.ts.map +1 -0
  105. package/lib/typescript/src/parseSouthAfricanID.d.ts +3 -0
  106. package/lib/typescript/src/parseSouthAfricanID.d.ts.map +1 -0
  107. package/lib/typescript/src/parseSouthAfricanID.native.d.ts +4 -0
  108. package/lib/typescript/src/parseSouthAfricanID.native.d.ts.map +1 -0
  109. package/lib/typescript/src/parseVehicleRegistration.d.ts +3 -0
  110. package/lib/typescript/src/parseVehicleRegistration.d.ts.map +1 -0
  111. package/lib/typescript/src/parseVehicleRegistration.native.d.ts +4 -0
  112. package/lib/typescript/src/parseVehicleRegistration.native.d.ts.map +1 -0
  113. package/lib/typescript/src/recognizeText.d.ts +3 -0
  114. package/lib/typescript/src/recognizeText.d.ts.map +1 -0
  115. package/lib/typescript/src/recognizeText.native.d.ts +4 -0
  116. package/lib/typescript/src/recognizeText.native.d.ts.map +1 -0
  117. package/lib/typescript/src/scanPDF417.d.ts +3 -0
  118. package/lib/typescript/src/scanPDF417.d.ts.map +1 -0
  119. package/lib/typescript/src/scanPDF417.native.d.ts +4 -0
  120. package/lib/typescript/src/scanPDF417.native.d.ts.map +1 -0
  121. package/lib/typescript/src/scanPassport.d.ts +3 -0
  122. package/lib/typescript/src/scanPassport.d.ts.map +1 -0
  123. package/lib/typescript/src/scanPassport.native.d.ts +9 -0
  124. package/lib/typescript/src/scanPassport.native.d.ts.map +1 -0
  125. package/lib/typescript/src/scanSouthAfricanDriversLicense.d.ts +3 -0
  126. package/lib/typescript/src/scanSouthAfricanDriversLicense.d.ts.map +1 -0
  127. package/lib/typescript/src/scanSouthAfricanDriversLicense.native.d.ts +8 -0
  128. package/lib/typescript/src/scanSouthAfricanDriversLicense.native.d.ts.map +1 -0
  129. package/lib/typescript/src/scanVehicleRegistration.d.ts +3 -0
  130. package/lib/typescript/src/scanVehicleRegistration.d.ts.map +1 -0
  131. package/lib/typescript/src/scanVehicleRegistration.native.d.ts +9 -0
  132. package/lib/typescript/src/scanVehicleRegistration.native.d.ts.map +1 -0
  133. package/lib/typescript/src/types.d.ts +237 -0
  134. package/lib/typescript/src/types.d.ts.map +1 -0
  135. package/package.json +124 -0
  136. package/src/NativePetrusReactNative.ts +303 -0
  137. package/src/PetrusCameraView.native.tsx +44 -0
  138. package/src/PetrusCameraView.tsx +19 -0
  139. package/src/PetrusCameraViewNativeComponent.ts +47 -0
  140. package/src/cameraPermissions.native.tsx +10 -0
  141. package/src/cameraPermissions.tsx +13 -0
  142. package/src/getPipelineVersion.native.tsx +5 -0
  143. package/src/getPipelineVersion.tsx +5 -0
  144. package/src/index.tsx +83 -0
  145. package/src/parseMRZ.native.tsx +11 -0
  146. package/src/parseMRZ.tsx +7 -0
  147. package/src/parseSouthAfricanDriversLicense.native.tsx +13 -0
  148. package/src/parseSouthAfricanDriversLicense.tsx +9 -0
  149. package/src/parseSouthAfricanID.native.tsx +7 -0
  150. package/src/parseSouthAfricanID.tsx +7 -0
  151. package/src/parseVehicleRegistration.native.tsx +9 -0
  152. package/src/parseVehicleRegistration.tsx +9 -0
  153. package/src/recognizeText.native.tsx +7 -0
  154. package/src/recognizeText.tsx +7 -0
  155. package/src/scanPDF417.native.tsx +7 -0
  156. package/src/scanPDF417.tsx +7 -0
  157. package/src/scanPassport.native.tsx +12 -0
  158. package/src/scanPassport.tsx +7 -0
  159. package/src/scanSouthAfricanDriversLicense.native.tsx +11 -0
  160. package/src/scanSouthAfricanDriversLicense.tsx +7 -0
  161. package/src/scanVehicleRegistration.native.tsx +12 -0
  162. package/src/scanVehicleRegistration.tsx +7 -0
  163. package/src/types.ts +272 -0
@@ -0,0 +1,253 @@
1
+ package com.petrusreactnative
2
+
3
+ import android.Manifest
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.net.Uri
9
+ import android.util.Base64
10
+ import androidx.core.content.ContextCompat
11
+ import com.facebook.react.bridge.Promise
12
+ import com.facebook.react.bridge.ReactApplicationContext
13
+ import com.facebook.react.modules.core.PermissionAwareActivity
14
+ import com.facebook.react.bridge.WritableMap
15
+ import com.petrus.core.PetrusCore
16
+ import com.petrus.vision.ocr.OCRProcessor
17
+ import com.petrus.vision.pdf417.PDF417Decoder
18
+ import com.petrus.vision.pdf417.PDF417Scanner
19
+ import com.petrus.vision.sa.SouthAfricanIDParser
20
+ import com.petrus.vision.pdf417.SouthAfricanDriversLicenseParser
21
+ import com.petrus.vision.mrz.PassportParser
22
+ import com.petrus.vision.vehicle.VehicleRegistrationParser
23
+ import com.petrusreactnative.mrz.toWritableMap
24
+ import com.petrusreactnative.ocr.toWritableMap
25
+ import com.petrusreactnative.pdf417.toWritableMap
26
+ import com.petrusreactnative.sa.toWritableMap
27
+ import com.petrusreactnative.vehicle.toWritableMap
28
+ import androidx.camera.core.ExperimentalGetImage
29
+ import java.io.File
30
+ import kotlinx.coroutines.CoroutineScope
31
+ import kotlinx.coroutines.Dispatchers
32
+ import kotlinx.coroutines.SupervisorJob
33
+ import kotlinx.coroutines.cancel
34
+ import kotlinx.coroutines.launch
35
+
36
+ class PetrusReactNativeModule(reactContext: ReactApplicationContext) :
37
+ NativePetrusReactNativeSpec(reactContext) {
38
+
39
+ private val ocrProcessor = OCRProcessor()
40
+ private val pdf417Scanner = PDF417Scanner()
41
+ private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
42
+
43
+ // Phase 0 placeholder: proves the JS <-> TurboModule <-> petrus-core wiring
44
+ // works end to end. Replaced by the real capture/detect/OCR/parse API surface
45
+ // in later phases.
46
+ override fun getPipelineVersion(): String {
47
+ return "petrus-core@${PetrusCore.VERSION}"
48
+ }
49
+
50
+ override fun recognizeText(imagePath: String, promise: Promise) {
51
+ moduleScope.launch {
52
+ try {
53
+ val result = ocrProcessor.process(reactApplicationContext, imagePath)
54
+ promise.resolve(result.toWritableMap())
55
+ } catch (e: Exception) {
56
+ promise.reject("OCR_FAILED", e.message ?: "Text recognition failed", e)
57
+ }
58
+ }
59
+ }
60
+
61
+ override fun invalidate() {
62
+ ocrProcessor.close()
63
+ moduleScope.cancel()
64
+ super.invalidate()
65
+ }
66
+
67
+ // Synchronous -- pure CPU work (regex + arithmetic on a short string), no I/O,
68
+ // no coroutine needed. Same reasoning as getPipelineVersion.
69
+ override fun parseSouthAfricanID(text: String): WritableMap {
70
+ return SouthAfricanIDParser.parse(text).toWritableMap()
71
+ }
72
+
73
+ override fun scanPDF417(imagePath: String, promise: Promise) {
74
+ moduleScope.launch {
75
+ try {
76
+ val bitmap = loadBitmap(imagePath)
77
+ if (bitmap == null) {
78
+ promise.reject("PDF417_IMAGE_LOAD_FAILED", "Could not load image at $imagePath")
79
+ return@launch
80
+ }
81
+ val result =
82
+ try {
83
+ pdf417Scanner.scan(bitmap)
84
+ } finally {
85
+ bitmap.recycle()
86
+ }
87
+ if (result == null) {
88
+ promise.reject("PDF417_NOT_FOUND", "No PDF417 barcode found in the image")
89
+ } else {
90
+ promise.resolve(result.toWritableMap())
91
+ }
92
+ } catch (e: Exception) {
93
+ promise.reject("PDF417_SCAN_FAILED", e.message ?: "PDF417 scan failed", e)
94
+ }
95
+ }
96
+ }
97
+
98
+ // Synchronous -- pure CPU work (RSA modPow over a few hundred bytes + nibble
99
+ // parsing), no I/O. Throws (rather than returning a null/empty result) when
100
+ // rawDataBase64 doesn't decode to a recognized SA driver's licence format -- see
101
+ // NativePetrusReactNative.ts for why this differs from parseSouthAfricanID.
102
+ override fun parseSouthAfricanDriversLicense(rawDataBase64: String): WritableMap {
103
+ val bytes = Base64.decode(rawDataBase64, Base64.DEFAULT)
104
+ val decoded =
105
+ PDF417Decoder.decode(bytes)
106
+ ?: throw IllegalArgumentException(
107
+ "Data does not decode to a recognized SA driver's licence barcode format"
108
+ )
109
+ return SouthAfricanDriversLicenseParser.parse(decoded).toWritableMap()
110
+ }
111
+
112
+ override fun scanSouthAfricanDriversLicense(promise: Promise) {
113
+ PDF417ScanSession(reactApplicationContext).start(timeoutMs = SCAN_TIMEOUT_MS) { result ->
114
+ if (result == null) {
115
+ promise.reject("PDF417_SCAN_TIMEOUT", "No PDF417 barcode found within the scan window")
116
+ return@start
117
+ }
118
+ val decoded = PDF417Decoder.decode(result.rawBytes)
119
+ if (decoded == null) {
120
+ promise.reject(
121
+ "PDF417_DECODE_FAILED",
122
+ "Scanned barcode did not decode to a recognized SA driver's licence format",
123
+ )
124
+ return@start
125
+ }
126
+ promise.resolve(SouthAfricanDriversLicenseParser.parse(decoded).toWritableMap())
127
+ }
128
+ }
129
+
130
+ // Synchronous -- pure CPU work (regex-based field extraction over a short OCR
131
+ // string), no I/O, same reasoning as parseSouthAfricanID.
132
+ override fun parseVehicleRegistration(text: String): WritableMap {
133
+ return VehicleRegistrationParser.parse(text).toWritableMap()
134
+ }
135
+
136
+ // Constructing VehicleRegistrationScanSession touches ImageProxy.getImage()
137
+ // (via OCRProcessor.processFrame), which CameraX marks @ExperimentalGetImage --
138
+ // opted into here rather than propagated, since this is the leaf call site.
139
+ @OptIn(ExperimentalGetImage::class)
140
+ override fun scanVehicleRegistration(promise: Promise) {
141
+ VehicleRegistrationScanSession(reactApplicationContext).start(timeoutMs = SCAN_TIMEOUT_MS) { result ->
142
+ if (result == null) {
143
+ promise.reject("VEHICLE_REGISTRATION_SCAN_TIMEOUT", "No registration number or VIN found within the scan window")
144
+ return@start
145
+ }
146
+ promise.resolve(result.toWritableMap())
147
+ }
148
+ }
149
+
150
+ // Synchronous -- pure CPU work (regex + checksum arithmetic over two
151
+ // 44-character lines), same reasoning as parseSouthAfricanID. Never throws --
152
+ // PassportParser.parse never returns null (see its doc comment).
153
+ override fun parseMRZ(text: String): WritableMap {
154
+ return PassportParser.parse(text).toWritableMap()
155
+ }
156
+
157
+ // Unlike scanVehicleRegistration, no @ExperimentalGetImage opt-in is needed --
158
+ // PassportScanSession never touches ImageProxy.getImage() (MRZDetector uses
159
+ // the non-experimental ImageProxy.toBitmap(), and DocumentDetector uses
160
+ // ImageProxy.planes directly).
161
+ override fun scanPassport(promise: Promise) {
162
+ PassportScanSession(reactApplicationContext).start(timeoutMs = SCAN_TIMEOUT_MS) { result ->
163
+ if (result == null) {
164
+ promise.reject("PASSPORT_SCAN_TIMEOUT", "No passport MRZ found within the scan window")
165
+ return@start
166
+ }
167
+ promise.resolve(result.toWritableMap())
168
+ }
169
+ }
170
+
171
+ /** [imagePath] may be a bare filesystem path or a URI (file://, content://, ...). */
172
+ private fun loadBitmap(imagePath: String): Bitmap? {
173
+ val uri = if (imagePath.contains("://")) Uri.parse(imagePath) else Uri.fromFile(File(imagePath))
174
+ return reactApplicationContext.contentResolver.openInputStream(uri)?.use { stream ->
175
+ BitmapFactory.decodeStream(stream)
176
+ }
177
+ }
178
+
179
+ override fun checkPermission(promise: Promise) {
180
+ promise.resolve(currentPermissionStatus())
181
+ }
182
+
183
+ override fun requestPermission(promise: Promise) {
184
+ val activity = reactApplicationContext.currentActivity
185
+ if (activity !is PermissionAwareActivity) {
186
+ promise.reject("NO_ACTIVITY", "No activity available to request camera permission")
187
+ return
188
+ }
189
+
190
+ if (isGranted()) {
191
+ promise.resolve("granted")
192
+ return
193
+ }
194
+
195
+ markHasRequestedBefore()
196
+ activity.requestPermissions(
197
+ arrayOf(Manifest.permission.CAMERA),
198
+ CAMERA_PERMISSION_REQUEST_CODE,
199
+ ) { requestCode, _, grantResults ->
200
+ if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
201
+ return@requestPermissions false
202
+ }
203
+ val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
204
+ promise.resolve(if (granted) "granted" else currentPermissionStatus())
205
+ true
206
+ }
207
+ }
208
+
209
+ private fun isGranted(): Boolean =
210
+ ContextCompat.checkSelfPermission(
211
+ reactApplicationContext,
212
+ Manifest.permission.CAMERA,
213
+ ) == PackageManager.PERMISSION_GRANTED
214
+
215
+ /**
216
+ * Android has no direct "permanently blocked" signal. The standard heuristic (used by
217
+ * most permission libraries): if the permission isn't granted, rationale can no longer
218
+ * be shown, AND we know we've asked before, the user must have picked "don't ask
219
+ * again" -- otherwise it's just "not asked/denied yet, can still ask."
220
+ */
221
+ private fun currentPermissionStatus(): String {
222
+ if (isGranted()) return "granted"
223
+
224
+ val activity = reactApplicationContext.currentActivity
225
+ val canAskAgain =
226
+ activity is PermissionAwareActivity &&
227
+ activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)
228
+
229
+ return when {
230
+ canAskAgain -> "denied"
231
+ hasRequestedBefore() -> "blocked"
232
+ else -> "denied"
233
+ }
234
+ }
235
+
236
+ private fun hasRequestedBefore(): Boolean =
237
+ prefs().getBoolean(KEY_HAS_REQUESTED, false)
238
+
239
+ private fun markHasRequestedBefore() {
240
+ prefs().edit().putBoolean(KEY_HAS_REQUESTED, true).apply()
241
+ }
242
+
243
+ private fun prefs() =
244
+ reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
245
+
246
+ companion object {
247
+ const val NAME = NativePetrusReactNativeSpec.NAME
248
+ private const val PREFS_NAME = "petrus_camera_permission"
249
+ private const val KEY_HAS_REQUESTED = "has_requested_camera_permission"
250
+ private const val CAMERA_PERMISSION_REQUEST_CODE = 5417
251
+ private const val SCAN_TIMEOUT_MS = 20_000L
252
+ }
253
+ }
@@ -0,0 +1,38 @@
1
+ package com.petrusreactnative
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+ import java.util.HashMap
10
+
11
+ class PetrusReactNativePackage : BaseReactPackage() {
12
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
13
+ return if (name == PetrusReactNativeModule.NAME) {
14
+ PetrusReactNativeModule(reactContext)
15
+ } else {
16
+ null
17
+ }
18
+ }
19
+
20
+ override fun createViewManagers(
21
+ reactContext: ReactApplicationContext
22
+ ): List<ViewManager<*, *>> {
23
+ return listOf(PetrusCameraViewManager())
24
+ }
25
+
26
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
27
+ mapOf(
28
+ PetrusReactNativeModule.NAME to ReactModuleInfo(
29
+ name = PetrusReactNativeModule.NAME,
30
+ className = PetrusReactNativeModule.NAME,
31
+ canOverrideExistingModule = false,
32
+ needsEagerInit = false,
33
+ isCxxModule = false,
34
+ isTurboModule = true
35
+ )
36
+ )
37
+ }
38
+ }
@@ -0,0 +1,119 @@
1
+ package com.petrusreactnative
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import androidx.camera.core.CameraSelector
6
+ import androidx.camera.core.ExperimentalGetImage
7
+ import androidx.camera.core.ImageAnalysis
8
+ import androidx.camera.lifecycle.ProcessCameraProvider
9
+ import androidx.core.content.ContextCompat
10
+ import androidx.lifecycle.LifecycleOwner
11
+ import com.facebook.react.bridge.ReactApplicationContext
12
+ import com.petrus.vision.ocr.OCRProcessor
13
+ import com.petrus.vision.vehicle.VehicleRegistrationParser
14
+ import com.petrus.vision.vehicle.VehicleRegistrationResult
15
+ import kotlinx.coroutines.CoroutineScope
16
+ import kotlinx.coroutines.Dispatchers
17
+ import kotlinx.coroutines.SupervisorJob
18
+ import kotlinx.coroutines.cancel
19
+ import kotlinx.coroutines.launch
20
+ import java.util.concurrent.Executors
21
+ import java.util.concurrent.atomic.AtomicBoolean
22
+
23
+ /**
24
+ * One-shot headless vehicle-registration scan: binds just an `ImageAnalysis` use
25
+ * case (no `Preview`/`PreviewView`) to the current Activity's lifecycle, runs OCR
26
+ * on frames via [OCRProcessor.processFrame] (petrus-core -- keeps ML Kit's
27
+ * `InputImage`/`Text` types out of this module entirely) and parses each result
28
+ * with [VehicleRegistrationParser], stopping once a frame yields a registration
29
+ * number or VIN, or [start]'s timeout elapses. Structurally mirrors
30
+ * `PDF417ScanSession`; needs `Activity`/`LifecycleOwner` access, same reasoning as
31
+ * why that class (and not petrus-core's `CameraController`) owns this plumbing.
32
+ *
33
+ * Unlike PDF417 decoding, OCR always returns *some* text even from an irrelevant
34
+ * frame, so "OCR succeeded" can't be the stopping condition. Stops once
35
+ * `registrationNumber` or `vin` is non-null -- the two most identifying,
36
+ * structurally-checkable fields -- rather than requiring full
37
+ * `validation.isValid`, which would be too strict for a single handheld-camera
38
+ * frame. Analysis frames are typically downscaled for performance, so OCR accuracy
39
+ * here is lower than a full-resolution still photo; adding a real `ImageCapture`
40
+ * use case for a higher-quality final frame is out of scope here, same boundary
41
+ * `PDF417ScanSession` draws for barcode scanning.
42
+ */
43
+ @ExperimentalGetImage
44
+ class VehicleRegistrationScanSession(private val reactContext: ReactApplicationContext) {
45
+
46
+ private val ocrProcessor = OCRProcessor()
47
+ private val executor = Executors.newSingleThreadExecutor()
48
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
49
+ private val finished = AtomicBoolean(false)
50
+ private var cameraProvider: ProcessCameraProvider? = null
51
+
52
+ fun start(timeoutMs: Long, onResult: (VehicleRegistrationResult?) -> Unit) {
53
+ val lifecycleOwner = reactContext.currentActivity as? LifecycleOwner
54
+ if (lifecycleOwner == null) {
55
+ onResult(null)
56
+ return
57
+ }
58
+
59
+ val timeoutHandler = Handler(Looper.getMainLooper())
60
+
61
+ fun finish(result: VehicleRegistrationResult?) {
62
+ if (!finished.compareAndSet(false, true)) return
63
+ timeoutHandler.removeCallbacksAndMessages(null)
64
+ cameraProvider?.unbindAll()
65
+ ocrProcessor.close()
66
+ scope.cancel()
67
+ executor.shutdown()
68
+ onResult(result)
69
+ }
70
+
71
+ timeoutHandler.postDelayed({ finish(null) }, timeoutMs)
72
+
73
+ val providerFuture = ProcessCameraProvider.getInstance(reactContext)
74
+ providerFuture.addListener(
75
+ {
76
+ if (finished.get()) return@addListener
77
+
78
+ val provider = providerFuture.get()
79
+ cameraProvider = provider
80
+
81
+ val analysis =
82
+ ImageAnalysis.Builder()
83
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
84
+ .build()
85
+ .also {
86
+ it.setAnalyzer(executor) { image ->
87
+ if (finished.get()) {
88
+ image.close()
89
+ return@setAnalyzer
90
+ }
91
+ scope.launch {
92
+ try {
93
+ val ocrResult = ocrProcessor.processFrame(image)
94
+ if (!finished.get()) {
95
+ val parsed = VehicleRegistrationParser.parse(ocrResult.fullText)
96
+ if (parsed.registrationNumber != null || parsed.vin != null) {
97
+ finish(parsed)
98
+ }
99
+ }
100
+ } catch (e: Exception) {
101
+ // Swallow -- a single failed frame shouldn't end the scan session.
102
+ } finally {
103
+ image.close()
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ try {
110
+ provider.unbindAll()
111
+ provider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, analysis)
112
+ } catch (e: Exception) {
113
+ finish(null)
114
+ }
115
+ },
116
+ ContextCompat.getMainExecutor(reactContext),
117
+ )
118
+ }
119
+ }
@@ -0,0 +1,30 @@
1
+ package com.petrusreactnative.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+ import com.petrus.core.detection.DocumentCorners
7
+ import com.petrusreactnative.events.DocumentDetectedEvent.Companion.toWritableMap
8
+
9
+ /**
10
+ * Fires once when AutoCaptureManager's threshold (confidence > 0.9, stable 750ms) is
11
+ * crossed. Signal only -- carries the corners/confidence at the moment of capture, not
12
+ * a photo. Full-resolution capture is a later phase (see README).
13
+ */
14
+ internal class CapturedEvent(
15
+ surfaceId: Int,
16
+ viewId: Int,
17
+ private val corners: DocumentCorners,
18
+ private val confidence: Float,
19
+ private val timestampMs: Long,
20
+ ) : Event<CapturedEvent>(surfaceId, viewId) {
21
+
22
+ override fun getEventName(): String = "topCaptured"
23
+
24
+ override fun getEventData(): WritableMap =
25
+ Arguments.createMap().apply {
26
+ putMap("corners", corners.toWritableMap())
27
+ putDouble("confidence", confidence.toDouble())
28
+ putDouble("timestamp", timestampMs.toDouble())
29
+ }
30
+ }
@@ -0,0 +1,49 @@
1
+ package com.petrusreactnative.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+ import com.petrus.core.detection.DetectionUpdate
7
+ import com.petrus.core.detection.DocumentCorners
8
+ import com.petrus.core.detection.Point2D
9
+
10
+ /** Fabric event carrying a document detection update to JS. Already throttled by the caller. */
11
+ internal class DocumentDetectedEvent(
12
+ surfaceId: Int,
13
+ viewId: Int,
14
+ private val update: DetectionUpdate,
15
+ ) : Event<DocumentDetectedEvent>(surfaceId, viewId) {
16
+
17
+ override fun getEventName(): String = "topDocumentDetected"
18
+
19
+ override fun getEventData(): WritableMap =
20
+ Arguments.createMap().apply {
21
+ putBoolean("detected", update.corners != null)
22
+ putBoolean("isStable", update.isStable)
23
+ putDouble("stableDurationMs", update.stableDurationMs.toDouble())
24
+ putDouble("confidence", update.confidence.toDouble())
25
+ // Always a well-formed object (zeroed when nothing's detected) rather than
26
+ // null/omitted -- keeps the codegen'd event schema a plain nested object, no
27
+ // optional/nullable object type. `detected` is the authoritative signal.
28
+ putMap("corners", (update.corners ?: DocumentCorners.EMPTY).toWritableMap())
29
+ }
30
+
31
+ companion object {
32
+ fun DocumentCorners.toWritableMap(): WritableMap =
33
+ Arguments.createMap().apply {
34
+ putMap("topLeft", topLeft.toWritableMap())
35
+ putMap("topRight", topRight.toWritableMap())
36
+ putMap("bottomRight", bottomRight.toWritableMap())
37
+ putMap("bottomLeft", bottomLeft.toWritableMap())
38
+ putInt("frameWidth", frameWidth)
39
+ putInt("frameHeight", frameHeight)
40
+ putInt("rotationDegrees", rotationDegrees)
41
+ }
42
+
43
+ fun Point2D.toWritableMap(): WritableMap =
44
+ Arguments.createMap().apply {
45
+ putDouble("x", x.toDouble())
46
+ putDouble("y", y.toDouble())
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,24 @@
1
+ package com.petrusreactnative.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+ import com.petrus.core.camera.FrameMetadata
7
+
8
+ /** Fabric event carrying frame metadata to JS. Already throttled by the time this fires. */
9
+ internal class FrameMetadataEvent(
10
+ surfaceId: Int,
11
+ viewId: Int,
12
+ private val metadata: FrameMetadata,
13
+ ) : Event<FrameMetadataEvent>(surfaceId, viewId) {
14
+
15
+ override fun getEventName(): String = "topFrameMetadata"
16
+
17
+ override fun getEventData(): WritableMap =
18
+ Arguments.createMap().apply {
19
+ putInt("width", metadata.width)
20
+ putInt("height", metadata.height)
21
+ putInt("rotation", metadata.rotationDegrees)
22
+ putDouble("timestamp", metadata.timestampMs.toDouble())
23
+ }
24
+ }
@@ -0,0 +1,54 @@
1
+ package com.petrusreactnative.mrz
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.petrus.vision.mrz.MRZDate
6
+ import com.petrus.vision.mrz.MRZSex
7
+ import com.petrus.vision.mrz.MRZValidator
8
+ import com.petrus.vision.mrz.PassportResult
9
+
10
+ /** Converts petrus-core's plain-Kotlin MRZ/passport model into RN bridge (WritableMap) types. */
11
+
12
+ fun PassportResult.toWritableMap(): WritableMap =
13
+ Arguments.createMap().apply {
14
+ putString("documentCode", documentCode)
15
+ putString("issuingCountry", issuingCountry)
16
+ putString("surname", surname)
17
+ putString("givenNames", givenNames)
18
+ putString("passportNumber", passportNumber)
19
+ putString("nationality", nationality)
20
+ if (dateOfBirth != null) putMap("dateOfBirth", dateOfBirth.toWritableMap()) else putNull("dateOfBirth")
21
+ putString("sex", sex.toWireValue())
22
+ if (dateOfExpiry != null) putMap("dateOfExpiry", dateOfExpiry.toWritableMap()) else putNull("dateOfExpiry")
23
+ if (personalNumber != null) putString("personalNumber", personalNumber) else putNull("personalNumber")
24
+ putMap("validation", validation.toWritableMap())
25
+ putArray("parseWarnings", parseWarnings.toWritableStringArray())
26
+ }
27
+
28
+ private fun MRZDate.toWritableMap(): WritableMap =
29
+ Arguments.createMap().apply {
30
+ putInt("year", year)
31
+ putInt("month", month)
32
+ putInt("day", day)
33
+ putInt("twoDigitYear", twoDigitYear)
34
+ }
35
+
36
+ private fun MRZSex.toWireValue(): String =
37
+ when (this) {
38
+ MRZSex.MALE -> "male"
39
+ MRZSex.FEMALE -> "female"
40
+ MRZSex.UNSPECIFIED -> "unspecified"
41
+ }
42
+
43
+ private fun MRZValidator.ValidationResult.toWritableMap(): WritableMap =
44
+ Arguments.createMap().apply {
45
+ putBoolean("isValid", isValid)
46
+ putBoolean("passportNumberCheckValid", passportNumberCheckValid)
47
+ putBoolean("dateOfBirthCheckValid", dateOfBirthCheckValid)
48
+ putBoolean("expiryDateCheckValid", expiryDateCheckValid)
49
+ putBoolean("personalNumberCheckValid", personalNumberCheckValid)
50
+ putBoolean("compositeCheckValid", compositeCheckValid)
51
+ }
52
+
53
+ private fun List<String>.toWritableStringArray() =
54
+ Arguments.createArray().apply { forEach { pushString(it) } }
@@ -0,0 +1,52 @@
1
+ package com.petrusreactnative.ocr
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableArray
5
+ import com.facebook.react.bridge.WritableMap
6
+ import com.petrus.vision.ocr.OCRBlock
7
+ import com.petrus.vision.ocr.OCRBoundingBox
8
+ import com.petrus.vision.ocr.OCRLine
9
+ import com.petrus.vision.ocr.OCRResult
10
+ import com.petrus.vision.ocr.OCRWord
11
+
12
+ /** Converts petrus-core's plain-Kotlin OCR model into RN bridge (WritableMap) types. */
13
+
14
+ fun OCRResult.toWritableMap(): WritableMap =
15
+ Arguments.createMap().apply {
16
+ putString("fullText", fullText)
17
+ putDouble("confidence", confidence.toDouble())
18
+ putArray("blocks", blocks.toWritableArray { it.toWritableMap() })
19
+ }
20
+
21
+ private fun OCRBlock.toWritableMap(): WritableMap =
22
+ Arguments.createMap().apply {
23
+ putString("text", text)
24
+ putMap("boundingBox", boundingBox.toWritableMap())
25
+ putArray("lines", lines.toWritableArray { it.toWritableMap() })
26
+ }
27
+
28
+ private fun OCRLine.toWritableMap(): WritableMap =
29
+ Arguments.createMap().apply {
30
+ putString("text", text)
31
+ putMap("boundingBox", boundingBox.toWritableMap())
32
+ putDouble("confidence", confidence.toDouble())
33
+ putArray("words", words.toWritableArray { it.toWritableMap() })
34
+ }
35
+
36
+ private fun OCRWord.toWritableMap(): WritableMap =
37
+ Arguments.createMap().apply {
38
+ putString("text", text)
39
+ putMap("boundingBox", boundingBox.toWritableMap())
40
+ putDouble("confidence", confidence.toDouble())
41
+ }
42
+
43
+ private fun OCRBoundingBox.toWritableMap(): WritableMap =
44
+ Arguments.createMap().apply {
45
+ putInt("left", left)
46
+ putInt("top", top)
47
+ putInt("right", right)
48
+ putInt("bottom", bottom)
49
+ }
50
+
51
+ private inline fun <T> List<T>.toWritableArray(transform: (T) -> WritableMap): WritableArray =
52
+ Arguments.createArray().apply { forEach { pushMap(transform(it)) } }