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,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,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
|
+
}
|