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,20 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "PetrusReactNative"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/petrus/petrus-react-native.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ install_modules_dependencies(s)
20
+ end
@@ -0,0 +1,64 @@
1
+ buildscript {
2
+ ext.PetrusReactNative = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36
6
+ ]
7
+
8
+ ext.getExtOrDefault = { prop ->
9
+ if (rootProject.ext.has(prop)) {
10
+ return rootProject.ext.get(prop)
11
+ }
12
+
13
+ return PetrusReactNative[prop]
14
+ }
15
+
16
+ repositories {
17
+ google()
18
+ mavenCentral()
19
+ }
20
+
21
+ dependencies {
22
+ classpath "com.android.tools.build:gradle:8.7.2"
23
+ // noinspection DifferentKotlinGradleVersion
24
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
25
+ }
26
+ }
27
+
28
+
29
+ apply plugin: "com.android.library"
30
+ apply plugin: "kotlin-android"
31
+
32
+ apply plugin: "com.facebook.react"
33
+
34
+ android {
35
+ namespace "com.petrusreactnative"
36
+
37
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
38
+
39
+ defaultConfig {
40
+ minSdkVersion getExtOrDefault("minSdkVersion")
41
+ }
42
+
43
+ compileOptions {
44
+ sourceCompatibility JavaVersion.VERSION_17
45
+ targetCompatibility JavaVersion.VERSION_17
46
+ }
47
+
48
+ // Explicit to avoid "Inconsistent JVM-target compatibility" failures under JDK 17 --
49
+ // flagged in the Phase 0 review as an implicit-inference risk.
50
+ kotlinOptions {
51
+ jvmTarget = "17"
52
+ }
53
+ }
54
+
55
+ dependencies {
56
+ implementation "com.facebook.react:react-android"
57
+ // petrus-core exposes CameraX as `api`, so PreviewView etc. resolve on *our* compile
58
+ // classpath via this `implementation` dependency -- we don't need `api` here since
59
+ // nothing downstream of petrus-react-native touches CameraX types directly.
60
+ implementation project(":petrus-core")
61
+ // For launching a CoroutineScope around OCRProcessor's suspend API from the
62
+ // (non-suspend) TurboModule method.
63
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0"
64
+ }
@@ -0,0 +1,4 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.CAMERA" />
3
+ <uses-feature android:name="android.hardware.camera" android:required="true" />
4
+ </manifest>
@@ -0,0 +1,86 @@
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.ImageAnalysis
7
+ import androidx.camera.lifecycle.ProcessCameraProvider
8
+ import androidx.core.content.ContextCompat
9
+ import androidx.lifecycle.LifecycleOwner
10
+ import com.facebook.react.bridge.ReactApplicationContext
11
+ import com.petrus.vision.pdf417.PDF417Result
12
+ import com.petrus.vision.pdf417.PDF417Scanner
13
+ import java.util.concurrent.Executors
14
+ import java.util.concurrent.atomic.AtomicBoolean
15
+
16
+ /**
17
+ * One-shot headless PDF417 scan: binds just an `ImageAnalysis` use case (no
18
+ * `Preview`/`PreviewView` -- nothing is displayed) to the current Activity's
19
+ * lifecycle, scans frames until one decodes or [timeoutMs] elapses, then unbinds.
20
+ *
21
+ * Deliberately separate from `CameraController` (petrus-core) rather than reusing
22
+ * it: `CameraController`'s API requires a `PreviewView`, which a headless scan has
23
+ * no use for. Needs `Activity`/`LifecycleOwner` access, same reasoning as why
24
+ * `PetrusCameraView` lives here rather than in petrus-core.
25
+ */
26
+ class PDF417ScanSession(private val reactContext: ReactApplicationContext) {
27
+
28
+ private val scanner = PDF417Scanner()
29
+ private val executor = Executors.newSingleThreadExecutor()
30
+ private val finished = AtomicBoolean(false)
31
+ private var cameraProvider: ProcessCameraProvider? = null
32
+
33
+ fun start(timeoutMs: Long, onResult: (PDF417Result?) -> Unit) {
34
+ val lifecycleOwner = reactContext.currentActivity as? LifecycleOwner
35
+ if (lifecycleOwner == null) {
36
+ onResult(null)
37
+ return
38
+ }
39
+
40
+ val timeoutHandler = Handler(Looper.getMainLooper())
41
+
42
+ fun finish(result: PDF417Result?) {
43
+ if (!finished.compareAndSet(false, true)) return
44
+ timeoutHandler.removeCallbacksAndMessages(null)
45
+ cameraProvider?.unbindAll()
46
+ executor.shutdown()
47
+ onResult(result)
48
+ }
49
+
50
+ timeoutHandler.postDelayed({ finish(null) }, timeoutMs)
51
+
52
+ val providerFuture = ProcessCameraProvider.getInstance(reactContext)
53
+ providerFuture.addListener(
54
+ {
55
+ if (finished.get()) return@addListener
56
+
57
+ val provider = providerFuture.get()
58
+ cameraProvider = provider
59
+
60
+ val analysis =
61
+ ImageAnalysis.Builder()
62
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
63
+ .build()
64
+ .also {
65
+ it.setAnalyzer(executor) { image ->
66
+ try {
67
+ if (!finished.get()) {
68
+ scanner.scan(image)?.let { result -> finish(result) }
69
+ }
70
+ } finally {
71
+ image.close()
72
+ }
73
+ }
74
+ }
75
+
76
+ try {
77
+ provider.unbindAll()
78
+ provider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, analysis)
79
+ } catch (e: Exception) {
80
+ finish(null)
81
+ }
82
+ },
83
+ ContextCompat.getMainExecutor(reactContext),
84
+ )
85
+ }
86
+ }
@@ -0,0 +1,137 @@
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.ImageAnalysis
7
+ import androidx.camera.lifecycle.ProcessCameraProvider
8
+ import androidx.core.content.ContextCompat
9
+ import androidx.lifecycle.LifecycleOwner
10
+ import com.facebook.react.bridge.ReactApplicationContext
11
+ import com.petrus.core.detection.DocumentDetector
12
+ import com.petrus.vision.mrz.MRZDetector
13
+ import com.petrus.vision.mrz.PassportParser
14
+ import com.petrus.vision.mrz.PassportResult
15
+ import com.petrus.vision.ocr.OCRProcessor
16
+ import java.util.concurrent.Executors
17
+ import java.util.concurrent.atomic.AtomicBoolean
18
+ import kotlinx.coroutines.CoroutineScope
19
+ import kotlinx.coroutines.Dispatchers
20
+ import kotlinx.coroutines.SupervisorJob
21
+ import kotlinx.coroutines.cancel
22
+ import kotlinx.coroutines.launch
23
+
24
+ /**
25
+ * One-shot headless passport scan: binds just an `ImageAnalysis` use case (no
26
+ * `Preview`/`PreviewView`) to the current Activity's lifecycle. Per frame:
27
+ * detects the document boundary ([DocumentDetector], Phase 1/2, reused not
28
+ * re-implemented), crops the MRZ band ([MRZDetector], ICAO 9303 ERZ geometry),
29
+ * runs OCR on *just that crop* ([OCRProcessor.processBitmap]), and parses it
30
+ * ([PassportParser]) -- stopping once a frame yields a result with a non-empty
31
+ * passport number, or [start]'s timeout elapses. Structurally mirrors
32
+ * `VehicleRegistrationScanSession`/`PDF417ScanSession`.
33
+ *
34
+ * Frames with no confidently-detected document boundary are skipped entirely (no
35
+ * crop, no OCR) rather than falling back to whole-frame OCR -- unlike
36
+ * `VehicleRegistrationScanSession` (Phase 6), which OCRs the whole frame every
37
+ * time since it has no separate region to isolate first, this scan session's
38
+ * whole point is running OCR on *only the MRZ crop*, so a missing/low-confidence
39
+ * detection has nothing meaningful to crop yet.
40
+ */
41
+ class PassportScanSession(private val reactContext: ReactApplicationContext) {
42
+
43
+ private val documentDetector = DocumentDetector()
44
+ private val ocrProcessor = OCRProcessor()
45
+ private val executor = Executors.newSingleThreadExecutor()
46
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
47
+ private val finished = AtomicBoolean(false)
48
+ private var cameraProvider: ProcessCameraProvider? = null
49
+
50
+ fun start(timeoutMs: Long, onResult: (PassportResult?) -> Unit) {
51
+ val lifecycleOwner = reactContext.currentActivity as? LifecycleOwner
52
+ if (lifecycleOwner == null) {
53
+ onResult(null)
54
+ return
55
+ }
56
+
57
+ val timeoutHandler = Handler(Looper.getMainLooper())
58
+
59
+ fun finish(result: PassportResult?) {
60
+ if (!finished.compareAndSet(false, true)) return
61
+ timeoutHandler.removeCallbacksAndMessages(null)
62
+ cameraProvider?.unbindAll()
63
+ ocrProcessor.close()
64
+ scope.cancel()
65
+ executor.shutdown()
66
+ onResult(result)
67
+ }
68
+
69
+ timeoutHandler.postDelayed({ finish(null) }, timeoutMs)
70
+
71
+ val providerFuture = ProcessCameraProvider.getInstance(reactContext)
72
+ providerFuture.addListener(
73
+ {
74
+ if (finished.get()) return@addListener
75
+
76
+ val provider = providerFuture.get()
77
+ cameraProvider = provider
78
+
79
+ val analysis =
80
+ ImageAnalysis.Builder()
81
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
82
+ .build()
83
+ .also {
84
+ it.setAnalyzer(executor) { image ->
85
+ if (finished.get()) {
86
+ image.close()
87
+ return@setAnalyzer
88
+ }
89
+
90
+ // Synchronous, same as FrameAnalyzer's own per-frame detection call --
91
+ // fast CPU work (OpenCV edge/contour detection on the Y plane only).
92
+ val corners = documentDetector.detect(image)
93
+ if (corners == null || corners.confidence < MIN_DETECTION_CONFIDENCE) {
94
+ image.close()
95
+ return@setAnalyzer
96
+ }
97
+
98
+ scope.launch {
99
+ try {
100
+ val mrzCrop = MRZDetector.crop(image, corners)
101
+ val ocrResult =
102
+ try {
103
+ ocrProcessor.processBitmap(mrzCrop, image.imageInfo.rotationDegrees)
104
+ } finally {
105
+ mrzCrop.recycle()
106
+ }
107
+ if (!finished.get()) {
108
+ val parsed = PassportParser.parse(ocrResult.fullText)
109
+ if (parsed.passportNumber.isNotEmpty()) {
110
+ finish(parsed)
111
+ }
112
+ }
113
+ } catch (e: Exception) {
114
+ // Swallow -- a single failed frame shouldn't end the scan session.
115
+ } finally {
116
+ image.close()
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ try {
123
+ provider.unbindAll()
124
+ provider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, analysis)
125
+ } catch (e: Exception) {
126
+ finish(null)
127
+ }
128
+ },
129
+ ContextCompat.getMainExecutor(reactContext),
130
+ )
131
+ }
132
+
133
+ companion object {
134
+ /** Below this, DocumentDetector's own quad is too unreliable to crop an MRZ band from confidently. */
135
+ private const val MIN_DETECTION_CONFIDENCE = 0.5f
136
+ }
137
+ }
@@ -0,0 +1,146 @@
1
+ package com.petrusreactnative
2
+
3
+ import android.util.Log
4
+ import android.widget.FrameLayout
5
+ import androidx.camera.view.PreviewView
6
+ import androidx.lifecycle.LifecycleOwner
7
+ import com.facebook.react.bridge.LifecycleEventListener
8
+ import com.facebook.react.uimanager.ThemedReactContext
9
+ import com.petrus.core.camera.CameraController
10
+ import com.petrus.core.camera.FrameMetadata
11
+ import com.petrus.core.detection.DetectionUpdate
12
+ import com.petrus.core.detection.DocumentDetectionPipeline
13
+ import java.util.concurrent.Executors
14
+ import java.util.concurrent.atomic.AtomicLong
15
+
16
+ /**
17
+ * Hosts a CameraX [PreviewView] (+ a [PetrusOverlayView] drawn on top of it) and drives
18
+ * [CameraController] off the RN view/host lifecycle. Document detection runs on every
19
+ * analyzed frame via [DocumentDetectionPipeline]; the overlay updates natively (no
20
+ * bridge cost) while [onDocumentDetectedListener]/[onCapturedListener] cross to JS,
21
+ * throttled/one-shot respectively.
22
+ */
23
+ class PetrusCameraView(private val reactContext: ThemedReactContext) :
24
+ FrameLayout(reactContext), LifecycleEventListener {
25
+
26
+ private val previewView: PreviewView = PreviewView(reactContext)
27
+ private val overlayView: PetrusOverlayView = PetrusOverlayView(reactContext)
28
+ private val cameraController = CameraController()
29
+ private val detectionPipeline = DocumentDetectionPipeline()
30
+ private val analysisExecutor = Executors.newSingleThreadExecutor()
31
+ private val lastFrameEmitMs = AtomicLong(0L)
32
+ private val lastDetectionEmitMs = AtomicLong(0L)
33
+
34
+ var isActive: Boolean = false
35
+ set(value) {
36
+ if (field == value) return
37
+ field = value
38
+ updateCameraBinding()
39
+ }
40
+
41
+ /** Set by the ViewManager in addEventEmitters(); already throttled to 1/sec. */
42
+ var onFrameMetadataListener: ((FrameMetadata) -> Unit)? = null
43
+
44
+ /** Set by the ViewManager; already throttled (see [DETECTION_EVENT_THROTTLE_MS]). */
45
+ var onDocumentDetectedListener: ((DetectionUpdate) -> Unit)? = null
46
+
47
+ /** Set by the ViewManager; fires once per auto-capture trigger, not throttled. */
48
+ var onCapturedListener: ((DetectionUpdate) -> Unit)? = null
49
+
50
+ init {
51
+ addView(previewView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
52
+ addView(overlayView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
53
+ reactContext.addLifecycleEventListener(this)
54
+ }
55
+
56
+ override fun onAttachedToWindow() {
57
+ super.onAttachedToWindow()
58
+ updateCameraBinding()
59
+ }
60
+
61
+ override fun onDetachedFromWindow() {
62
+ super.onDetachedFromWindow()
63
+ cameraController.stop()
64
+ }
65
+
66
+ override fun onHostResume() {
67
+ updateCameraBinding()
68
+ }
69
+
70
+ override fun onHostPause() {
71
+ cameraController.stop()
72
+ }
73
+
74
+ override fun onHostDestroy() {
75
+ cameraController.stop()
76
+ }
77
+
78
+ /** Called by the ViewManager when RN drops this view instance. */
79
+ fun release() {
80
+ cameraController.stop()
81
+ reactContext.removeLifecycleEventListener(this)
82
+ analysisExecutor.shutdown()
83
+ }
84
+
85
+ private fun updateCameraBinding() {
86
+ if (!isActive || !isAttachedToWindow) {
87
+ cameraController.stop()
88
+ return
89
+ }
90
+
91
+ val lifecycleOwner = reactContext.currentActivity as? LifecycleOwner
92
+ if (lifecycleOwner == null) {
93
+ Log.w(TAG, "no LifecycleOwner available on currentActivity; cannot bind camera")
94
+ return
95
+ }
96
+
97
+ cameraController.start(
98
+ context = reactContext,
99
+ lifecycleOwner = lifecycleOwner,
100
+ previewView = previewView,
101
+ analysisExecutor = analysisExecutor,
102
+ onFrame = { metadata -> emitFrameThrottled(metadata) },
103
+ detectionPipeline = detectionPipeline,
104
+ onDetection = { update -> handleDetection(update) },
105
+ )
106
+ }
107
+
108
+ private fun handleDetection(update: DetectionUpdate) {
109
+ // Native, every frame -- no bridge cost, so the overlay stays smooth regardless of
110
+ // how the JS-facing event below is throttled.
111
+ overlayView.post { overlayView.update(update.corners, update.isStable) }
112
+
113
+ emitDetectionThrottled(update)
114
+
115
+ if (update.shouldCapture) {
116
+ onCapturedListener?.invoke(update)
117
+ detectionPipeline.resetAfterCapture()
118
+ }
119
+ }
120
+
121
+ private fun emitFrameThrottled(metadata: FrameMetadata) {
122
+ val now = metadata.timestampMs
123
+ val last = lastFrameEmitMs.get()
124
+ if (now - last >= FRAME_EVENT_THROTTLE_MS && lastFrameEmitMs.compareAndSet(last, now)) {
125
+ onFrameMetadataListener?.invoke(metadata)
126
+ }
127
+ }
128
+
129
+ private fun emitDetectionThrottled(update: DetectionUpdate) {
130
+ val now = System.currentTimeMillis()
131
+ val last = lastDetectionEmitMs.get()
132
+ if (now - last >= DETECTION_EVENT_THROTTLE_MS && lastDetectionEmitMs.compareAndSet(last, now)) {
133
+ onDocumentDetectedListener?.invoke(update)
134
+ }
135
+ }
136
+
137
+ companion object {
138
+ private const val TAG = "PetrusCameraView"
139
+ private const val FRAME_EVENT_THROTTLE_MS = 1000L
140
+
141
+ // Faster than the frame-metadata throttle: onDocumentDetected drives live
142
+ // stability/confidence feedback in JS, where 1/sec would feel laggy. The overlay
143
+ // itself already renders at full frame rate natively, independent of this.
144
+ private const val DETECTION_EVENT_THROTTLE_MS = 150L
145
+ }
146
+ }
@@ -0,0 +1,93 @@
1
+ package com.petrusreactnative
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.SimpleViewManager
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.UIManagerHelper
7
+ import com.facebook.react.uimanager.ViewManagerDelegate
8
+ import com.facebook.react.uimanager.annotations.ReactProp
9
+ import com.facebook.react.viewmanagers.PetrusCameraViewManagerDelegate
10
+ import com.facebook.react.viewmanagers.PetrusCameraViewManagerInterface
11
+ import com.petrusreactnative.events.CapturedEvent
12
+ import com.petrusreactnative.events.DocumentDetectedEvent
13
+ import com.petrusreactnative.events.FrameMetadataEvent
14
+
15
+ /**
16
+ * Fabric ViewManager for PetrusCameraView. [PetrusCameraViewManagerInterface] and
17
+ * [PetrusCameraViewManagerDelegate] are codegen-generated at build time from
18
+ * src/PetrusCameraViewNativeComponent.ts -- they don't exist in source form.
19
+ */
20
+ @ReactModule(name = PetrusCameraViewManager.NAME)
21
+ class PetrusCameraViewManager :
22
+ SimpleViewManager<PetrusCameraView>(), PetrusCameraViewManagerInterface<PetrusCameraView> {
23
+
24
+ private val delegate: ViewManagerDelegate<PetrusCameraView> =
25
+ PetrusCameraViewManagerDelegate(this)
26
+
27
+ override fun getDelegate(): ViewManagerDelegate<PetrusCameraView> = delegate
28
+
29
+ override fun getName(): String = NAME
30
+
31
+ override fun createViewInstance(context: ThemedReactContext): PetrusCameraView =
32
+ PetrusCameraView(context)
33
+
34
+ @ReactProp(name = "isActive", defaultBoolean = false)
35
+ override fun setIsActive(view: PetrusCameraView, value: Boolean) {
36
+ view.isActive = value
37
+ }
38
+
39
+ override fun addEventEmitters(reactContext: ThemedReactContext, view: PetrusCameraView) {
40
+ view.onFrameMetadataListener = { metadata ->
41
+ UIManagerHelper.getEventDispatcher(reactContext)
42
+ ?.dispatchEvent(FrameMetadataEvent(UIManagerHelper.getSurfaceId(view), view.id, metadata))
43
+ }
44
+
45
+ view.onDocumentDetectedListener = { update ->
46
+ UIManagerHelper.getEventDispatcher(reactContext)
47
+ ?.dispatchEvent(
48
+ DocumentDetectedEvent(UIManagerHelper.getSurfaceId(view), view.id, update)
49
+ )
50
+ }
51
+
52
+ view.onCapturedListener = { update ->
53
+ val corners = update.corners
54
+ if (corners != null) {
55
+ UIManagerHelper.getEventDispatcher(reactContext)
56
+ ?.dispatchEvent(
57
+ CapturedEvent(
58
+ UIManagerHelper.getSurfaceId(view),
59
+ view.id,
60
+ corners,
61
+ update.confidence,
62
+ System.currentTimeMillis(),
63
+ )
64
+ )
65
+ }
66
+ }
67
+ }
68
+
69
+ // The generated Delegate handles prop dispatch, but event registration still has to
70
+ // be exported manually -- matches RN's own built-in ViewManagers (e.g.
71
+ // SwipeRefreshLayoutManager), which do the same even where a generated
72
+ // Interface/Delegate is also in use.
73
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
74
+ val eventTypeConstants = super.getExportedCustomDirectEventTypeConstants() ?: HashMap()
75
+ eventTypeConstants["topFrameMetadata"] = mutableMapOf("registrationName" to "onFrameMetadata")
76
+ eventTypeConstants["topDocumentDetected"] =
77
+ mutableMapOf("registrationName" to "onDocumentDetected")
78
+ eventTypeConstants["topCaptured"] = mutableMapOf("registrationName" to "onCaptured")
79
+ return eventTypeConstants
80
+ }
81
+
82
+ override fun onDropViewInstance(view: PetrusCameraView) {
83
+ view.onFrameMetadataListener = null
84
+ view.onDocumentDetectedListener = null
85
+ view.onCapturedListener = null
86
+ view.release()
87
+ super.onDropViewInstance(view)
88
+ }
89
+
90
+ companion object {
91
+ const val NAME = "PetrusCameraView"
92
+ }
93
+ }
@@ -0,0 +1,87 @@
1
+ package com.petrusreactnative
2
+
3
+ import android.content.Context
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.Paint
7
+ import android.graphics.Path
8
+ import android.view.View
9
+ import com.petrus.core.detection.DocumentCorners
10
+ import com.petrus.core.detection.Point2D
11
+
12
+ /**
13
+ * Transparent overlay drawing the detected document boundary on top of the camera
14
+ * preview. Pure rendering -- receives already-computed [DocumentCorners], does no CV
15
+ * itself. Owned and updated by [PetrusCameraView]; not a separately RN-exposed view.
16
+ *
17
+ * Coordinate mapping assumes this view's aspect ratio approximates the (rotated)
18
+ * analysis frame's aspect ratio -- PreviewView's exact scale-type crop isn't accounted
19
+ * for. See the README's known limitations.
20
+ */
21
+ class PetrusOverlayView(context: Context) : View(context) {
22
+
23
+ private var corners: DocumentCorners? = null
24
+ private var isStable: Boolean = false
25
+
26
+ private val strokePaint =
27
+ Paint(Paint.ANTI_ALIAS_FLAG).apply {
28
+ style = Paint.Style.STROKE
29
+ strokeWidth = STROKE_WIDTH_PX
30
+ }
31
+ private val cornerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
32
+ private val path = Path()
33
+
34
+ init {
35
+ setWillNotDraw(false)
36
+ }
37
+
38
+ fun update(corners: DocumentCorners?, isStable: Boolean) {
39
+ this.corners = corners
40
+ this.isStable = isStable
41
+ invalidate()
42
+ }
43
+
44
+ override fun onDraw(canvas: Canvas) {
45
+ super.onDraw(canvas)
46
+ val current = corners ?: return
47
+ if (width == 0 || height == 0) return
48
+
49
+ val color = if (isStable) STABLE_COLOR else DETECTING_COLOR
50
+ strokePaint.color = color
51
+ cornerPaint.color = color
52
+
53
+ val points = mapToViewSpace(current)
54
+
55
+ path.reset()
56
+ path.moveTo(points[0].first, points[0].second)
57
+ for (i in 1 until points.size) {
58
+ path.lineTo(points[i].first, points[i].second)
59
+ }
60
+ path.close()
61
+ canvas.drawPath(path, strokePaint)
62
+
63
+ for ((x, y) in points) {
64
+ canvas.drawCircle(x, y, CORNER_RADIUS_PX, cornerPaint)
65
+ }
66
+ }
67
+
68
+ private fun mapToViewSpace(corners: DocumentCorners): List<Pair<Float, Float>> =
69
+ listOf(corners.topLeft, corners.topRight, corners.bottomRight, corners.bottomLeft)
70
+ .map { rotateNormalized(it, corners.rotationDegrees) }
71
+ .map { (x, y) -> x * width to y * height }
72
+
73
+ private fun rotateNormalized(point: Point2D, rotationDegrees: Int): Pair<Float, Float> =
74
+ when (((rotationDegrees % 360) + 360) % 360) {
75
+ 90 -> (1f - point.y) to point.x
76
+ 180 -> (1f - point.x) to (1f - point.y)
77
+ 270 -> point.y to (1f - point.x)
78
+ else -> point.x to point.y
79
+ }
80
+
81
+ companion object {
82
+ private val STABLE_COLOR = Color.parseColor("#4CD964")
83
+ private val DETECTING_COLOR = Color.parseColor("#FFCC00")
84
+ private const val CORNER_RADIUS_PX = 10f
85
+ private const val STROKE_WIDTH_PX = 6f
86
+ }
87
+ }