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.
- package/PetrusReactNative.podspec +20 -0
- package/android/build.gradle +64 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/petrusreactnative/PDF417ScanSession.kt +86 -0
- package/android/src/main/java/com/petrusreactnative/PassportScanSession.kt +137 -0
- package/android/src/main/java/com/petrusreactnative/PetrusCameraView.kt +146 -0
- package/android/src/main/java/com/petrusreactnative/PetrusCameraViewManager.kt +93 -0
- package/android/src/main/java/com/petrusreactnative/PetrusOverlayView.kt +87 -0
- package/android/src/main/java/com/petrusreactnative/PetrusReactNativeModule.kt +253 -0
- package/android/src/main/java/com/petrusreactnative/PetrusReactNativePackage.kt +38 -0
- package/android/src/main/java/com/petrusreactnative/VehicleRegistrationScanSession.kt +119 -0
- package/android/src/main/java/com/petrusreactnative/events/CapturedEvent.kt +30 -0
- package/android/src/main/java/com/petrusreactnative/events/DocumentDetectedEvent.kt +49 -0
- package/android/src/main/java/com/petrusreactnative/events/FrameMetadataEvent.kt +24 -0
- package/android/src/main/java/com/petrusreactnative/mrz/PassportResultMapping.kt +54 -0
- package/android/src/main/java/com/petrusreactnative/ocr/OCRResultMapping.kt +52 -0
- package/android/src/main/java/com/petrusreactnative/pdf417/SouthAfricanDriversLicenseResultMapping.kt +80 -0
- package/android/src/main/java/com/petrusreactnative/sa/SouthAfricanIDResultMapping.kt +50 -0
- package/android/src/main/java/com/petrusreactnative/vehicle/VehicleRegistrationResultMapping.kt +72 -0
- package/ios/PetrusReactNative.h +5 -0
- package/ios/PetrusReactNative.mm +21 -0
- package/lib/module/NativePetrusReactNative.js +5 -0
- package/lib/module/NativePetrusReactNative.js.map +1 -0
- package/lib/module/PetrusCameraView.js +6 -0
- package/lib/module/PetrusCameraView.js.map +1 -0
- package/lib/module/PetrusCameraView.native.js +24 -0
- package/lib/module/PetrusCameraView.native.js.map +1 -0
- package/lib/module/PetrusCameraViewNativeComponent.ts +47 -0
- package/lib/module/cameraPermissions.js +9 -0
- package/lib/module/cameraPermissions.js.map +1 -0
- package/lib/module/cameraPermissions.native.js +10 -0
- package/lib/module/cameraPermissions.native.js.map +1 -0
- package/lib/module/getPipelineVersion.js +6 -0
- package/lib/module/getPipelineVersion.js.map +1 -0
- package/lib/module/getPipelineVersion.native.js +7 -0
- package/lib/module/getPipelineVersion.native.js.map +1 -0
- package/lib/module/index.js +45 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/parseMRZ.js +6 -0
- package/lib/module/parseMRZ.js.map +1 -0
- package/lib/module/parseMRZ.native.js +12 -0
- package/lib/module/parseMRZ.native.js.map +1 -0
- package/lib/module/parseSouthAfricanDriversLicense.js +6 -0
- package/lib/module/parseSouthAfricanDriversLicense.js.map +1 -0
- package/lib/module/parseSouthAfricanDriversLicense.native.js +12 -0
- package/lib/module/parseSouthAfricanDriversLicense.native.js.map +1 -0
- package/lib/module/parseSouthAfricanID.js +6 -0
- package/lib/module/parseSouthAfricanID.js.map +1 -0
- package/lib/module/parseSouthAfricanID.native.js +8 -0
- package/lib/module/parseSouthAfricanID.native.js.map +1 -0
- package/lib/module/parseVehicleRegistration.js +6 -0
- package/lib/module/parseVehicleRegistration.js.map +1 -0
- package/lib/module/parseVehicleRegistration.native.js +8 -0
- package/lib/module/parseVehicleRegistration.native.js.map +1 -0
- package/lib/module/recognizeText.js +6 -0
- package/lib/module/recognizeText.js.map +1 -0
- package/lib/module/recognizeText.native.js +8 -0
- package/lib/module/recognizeText.native.js.map +1 -0
- package/lib/module/scanPDF417.js +6 -0
- package/lib/module/scanPDF417.js.map +1 -0
- package/lib/module/scanPDF417.native.js +8 -0
- package/lib/module/scanPDF417.native.js.map +1 -0
- package/lib/module/scanPassport.js +6 -0
- package/lib/module/scanPassport.js.map +1 -0
- package/lib/module/scanPassport.native.js +13 -0
- package/lib/module/scanPassport.native.js.map +1 -0
- package/lib/module/scanSouthAfricanDriversLicense.js +6 -0
- package/lib/module/scanSouthAfricanDriversLicense.js.map +1 -0
- package/lib/module/scanSouthAfricanDriversLicense.native.js +12 -0
- package/lib/module/scanSouthAfricanDriversLicense.native.js.map +1 -0
- package/lib/module/scanVehicleRegistration.js +6 -0
- package/lib/module/scanVehicleRegistration.js.map +1 -0
- package/lib/module/scanVehicleRegistration.native.js +13 -0
- package/lib/module/scanVehicleRegistration.native.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativePetrusReactNative.d.ts +271 -0
- package/lib/typescript/src/NativePetrusReactNative.d.ts.map +1 -0
- package/lib/typescript/src/PetrusCameraView.d.ts +10 -0
- package/lib/typescript/src/PetrusCameraView.d.ts.map +1 -0
- package/lib/typescript/src/PetrusCameraView.native.d.ts +4 -0
- package/lib/typescript/src/PetrusCameraView.native.d.ts.map +1 -0
- package/lib/typescript/src/PetrusCameraViewNativeComponent.d.ts +38 -0
- package/lib/typescript/src/PetrusCameraViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/cameraPermissions.d.ts +4 -0
- package/lib/typescript/src/cameraPermissions.d.ts.map +1 -0
- package/lib/typescript/src/cameraPermissions.native.d.ts +4 -0
- package/lib/typescript/src/cameraPermissions.native.d.ts.map +1 -0
- package/lib/typescript/src/getPipelineVersion.d.ts +2 -0
- package/lib/typescript/src/getPipelineVersion.d.ts.map +1 -0
- package/lib/typescript/src/getPipelineVersion.native.d.ts +2 -0
- package/lib/typescript/src/getPipelineVersion.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +45 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/parseMRZ.d.ts +3 -0
- package/lib/typescript/src/parseMRZ.d.ts.map +1 -0
- package/lib/typescript/src/parseMRZ.native.d.ts +8 -0
- package/lib/typescript/src/parseMRZ.native.d.ts.map +1 -0
- package/lib/typescript/src/parseSouthAfricanDriversLicense.d.ts +3 -0
- package/lib/typescript/src/parseSouthAfricanDriversLicense.d.ts.map +1 -0
- package/lib/typescript/src/parseSouthAfricanDriversLicense.native.d.ts +8 -0
- package/lib/typescript/src/parseSouthAfricanDriversLicense.native.d.ts.map +1 -0
- package/lib/typescript/src/parseSouthAfricanID.d.ts +3 -0
- package/lib/typescript/src/parseSouthAfricanID.d.ts.map +1 -0
- package/lib/typescript/src/parseSouthAfricanID.native.d.ts +4 -0
- package/lib/typescript/src/parseSouthAfricanID.native.d.ts.map +1 -0
- package/lib/typescript/src/parseVehicleRegistration.d.ts +3 -0
- package/lib/typescript/src/parseVehicleRegistration.d.ts.map +1 -0
- package/lib/typescript/src/parseVehicleRegistration.native.d.ts +4 -0
- package/lib/typescript/src/parseVehicleRegistration.native.d.ts.map +1 -0
- package/lib/typescript/src/recognizeText.d.ts +3 -0
- package/lib/typescript/src/recognizeText.d.ts.map +1 -0
- package/lib/typescript/src/recognizeText.native.d.ts +4 -0
- package/lib/typescript/src/recognizeText.native.d.ts.map +1 -0
- package/lib/typescript/src/scanPDF417.d.ts +3 -0
- package/lib/typescript/src/scanPDF417.d.ts.map +1 -0
- package/lib/typescript/src/scanPDF417.native.d.ts +4 -0
- package/lib/typescript/src/scanPDF417.native.d.ts.map +1 -0
- package/lib/typescript/src/scanPassport.d.ts +3 -0
- package/lib/typescript/src/scanPassport.d.ts.map +1 -0
- package/lib/typescript/src/scanPassport.native.d.ts +9 -0
- package/lib/typescript/src/scanPassport.native.d.ts.map +1 -0
- package/lib/typescript/src/scanSouthAfricanDriversLicense.d.ts +3 -0
- package/lib/typescript/src/scanSouthAfricanDriversLicense.d.ts.map +1 -0
- package/lib/typescript/src/scanSouthAfricanDriversLicense.native.d.ts +8 -0
- package/lib/typescript/src/scanSouthAfricanDriversLicense.native.d.ts.map +1 -0
- package/lib/typescript/src/scanVehicleRegistration.d.ts +3 -0
- package/lib/typescript/src/scanVehicleRegistration.d.ts.map +1 -0
- package/lib/typescript/src/scanVehicleRegistration.native.d.ts +9 -0
- package/lib/typescript/src/scanVehicleRegistration.native.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +237 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +124 -0
- package/src/NativePetrusReactNative.ts +303 -0
- package/src/PetrusCameraView.native.tsx +44 -0
- package/src/PetrusCameraView.tsx +19 -0
- package/src/PetrusCameraViewNativeComponent.ts +47 -0
- package/src/cameraPermissions.native.tsx +10 -0
- package/src/cameraPermissions.tsx +13 -0
- package/src/getPipelineVersion.native.tsx +5 -0
- package/src/getPipelineVersion.tsx +5 -0
- package/src/index.tsx +83 -0
- package/src/parseMRZ.native.tsx +11 -0
- package/src/parseMRZ.tsx +7 -0
- package/src/parseSouthAfricanDriversLicense.native.tsx +13 -0
- package/src/parseSouthAfricanDriversLicense.tsx +9 -0
- package/src/parseSouthAfricanID.native.tsx +7 -0
- package/src/parseSouthAfricanID.tsx +7 -0
- package/src/parseVehicleRegistration.native.tsx +9 -0
- package/src/parseVehicleRegistration.tsx +9 -0
- package/src/recognizeText.native.tsx +7 -0
- package/src/recognizeText.tsx +7 -0
- package/src/scanPDF417.native.tsx +7 -0
- package/src/scanPDF417.tsx +7 -0
- package/src/scanPassport.native.tsx +12 -0
- package/src/scanPassport.tsx +7 -0
- package/src/scanSouthAfricanDriversLicense.native.tsx +11 -0
- package/src/scanSouthAfricanDriversLicense.tsx +7 -0
- package/src/scanVehicleRegistration.native.tsx +12 -0
- package/src/scanVehicleRegistration.tsx +7 -0
- 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)) } }
|