react-native-neuroscan 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 (51) hide show
  1. package/LICENSE +20 -0
  2. package/Neuroscan.podspec +29 -0
  3. package/README.md +233 -0
  4. package/android/app/build/generated/source/codegen/RCTAppDependencyProvider.h +25 -0
  5. package/android/app/build/generated/source/codegen/RCTAppDependencyProvider.mm +35 -0
  6. package/android/app/build/generated/source/codegen/RCTModuleProviders.h +16 -0
  7. package/android/app/build/generated/source/codegen/RCTModuleProviders.mm +51 -0
  8. package/android/app/build/generated/source/codegen/RCTModulesConformingToProtocolsProvider.h +18 -0
  9. package/android/app/build/generated/source/codegen/RCTModulesConformingToProtocolsProvider.mm +54 -0
  10. package/android/app/build/generated/source/codegen/RCTThirdPartyComponentsProvider.h +16 -0
  11. package/android/app/build/generated/source/codegen/RCTThirdPartyComponentsProvider.mm +30 -0
  12. package/android/app/build/generated/source/codegen/ReactAppDependencyProvider.podspec +34 -0
  13. package/android/app/build/generated/source/codegen/java/com/facebook/fbreact/specs/NativeNeuroscanSpec.java +67 -0
  14. package/android/app/build/generated/source/codegen/java/com/facebook/react/viewmanagers/NeuroScanCameraViewManagerDelegate.java +72 -0
  15. package/android/app/build/generated/source/codegen/java/com/facebook/react/viewmanagers/NeuroScanCameraViewManagerInterface.java +29 -0
  16. package/android/app/build/generated/source/codegen/jni/CMakeLists.txt +36 -0
  17. package/android/app/build/generated/source/codegen/jni/RNNeuroScanSpec-generated.cpp +74 -0
  18. package/android/app/build/generated/source/codegen/jni/RNNeuroScanSpec.h +31 -0
  19. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/ComponentDescriptors.cpp +22 -0
  20. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/ComponentDescriptors.h +24 -0
  21. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/EventEmitters.cpp +62 -0
  22. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/EventEmitters.h +54 -0
  23. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/Props.cpp +32 -0
  24. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/Props.h +34 -0
  25. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/RNNeuroScanSpecJSI-generated.cpp +80 -0
  26. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/RNNeuroScanSpecJSI.h +134 -0
  27. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/ShadowNodes.cpp +17 -0
  28. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/ShadowNodes.h +32 -0
  29. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/States.cpp +16 -0
  30. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNNeuroScanSpec/States.h +29 -0
  31. package/android/build.gradle +73 -0
  32. package/android/src/main/AndroidManifest.xml +4 -0
  33. package/android/src/main/java/com/neuroscan/NeuroscanModule.kt +342 -0
  34. package/android/src/main/java/com/neuroscan/NeuroscanPackage.kt +36 -0
  35. package/ios/DocumentScannerController.swift +115 -0
  36. package/ios/NeuroScanImpl.swift +226 -0
  37. package/ios/Neuroscan.h +5 -0
  38. package/ios/Neuroscan.mm +118 -0
  39. package/lib/module/NativeNeuroscan.js +5 -0
  40. package/lib/module/NativeNeuroscan.js.map +1 -0
  41. package/lib/module/index.js +4 -0
  42. package/lib/module/index.js.map +1 -0
  43. package/lib/module/package.json +1 -0
  44. package/lib/typescript/package.json +1 -0
  45. package/lib/typescript/src/NativeNeuroscan.d.ts +47 -0
  46. package/lib/typescript/src/NativeNeuroscan.d.ts.map +1 -0
  47. package/lib/typescript/src/index.d.ts +3 -0
  48. package/lib/typescript/src/index.d.ts.map +1 -0
  49. package/package.json +126 -0
  50. package/src/NativeNeuroscan.ts +52 -0
  51. package/src/index.tsx +2 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
3
+ *
4
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
5
+ * once the code is regenerated.
6
+ *
7
+ * @generated by codegen project: GenerateModuleH.js
8
+ */
9
+
10
+ #pragma once
11
+
12
+ #include <ReactCommon/TurboModule.h>
13
+ #include <react/bridging/Bridging.h>
14
+
15
+ namespace facebook::react {
16
+
17
+
18
+ class JSI_EXPORT NativeNeuroscanCxxSpecJSI : public TurboModule {
19
+ protected:
20
+ NativeNeuroscanCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker);
21
+
22
+ public:
23
+ virtual jsi::Value scanDocument(jsi::Runtime &rt, jsi::Object options) = 0;
24
+ virtual jsi::Value detectEdges(jsi::Runtime &rt, jsi::String imageUri) = 0;
25
+ virtual jsi::Value cropDocument(jsi::Runtime &rt, jsi::String imageUri, jsi::Object corners) = 0;
26
+ virtual jsi::Value processImage(jsi::Runtime &rt, jsi::String imageUri, jsi::Object options) = 0;
27
+ virtual jsi::Value scanAndProcess(jsi::Runtime &rt, jsi::String imageUri, jsi::Object processOptions) = 0;
28
+ virtual jsi::Value cleanupTempFiles(jsi::Runtime &rt) = 0;
29
+ virtual void addListener(jsi::Runtime &rt, jsi::String eventType) = 0;
30
+ virtual void removeListeners(jsi::Runtime &rt, double count) = 0;
31
+
32
+ };
33
+
34
+ template <typename T>
35
+ class JSI_EXPORT NativeNeuroscanCxxSpec : public TurboModule {
36
+ public:
37
+ jsi::Value create(jsi::Runtime &rt, const jsi::PropNameID &propName) override {
38
+ return delegate_.create(rt, propName);
39
+ }
40
+
41
+ std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& runtime) override {
42
+ return delegate_.getPropertyNames(runtime);
43
+ }
44
+
45
+ static constexpr std::string_view kModuleName = "Neuroscan";
46
+
47
+ protected:
48
+ NativeNeuroscanCxxSpec(std::shared_ptr<CallInvoker> jsInvoker)
49
+ : TurboModule(std::string{NativeNeuroscanCxxSpec::kModuleName}, jsInvoker),
50
+ delegate_(reinterpret_cast<T*>(this), jsInvoker) {}
51
+
52
+
53
+ private:
54
+ class Delegate : public NativeNeuroscanCxxSpecJSI {
55
+ public:
56
+ Delegate(T *instance, std::shared_ptr<CallInvoker> jsInvoker) :
57
+ NativeNeuroscanCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {
58
+
59
+ }
60
+
61
+ jsi::Value scanDocument(jsi::Runtime &rt, jsi::Object options) override {
62
+ static_assert(
63
+ bridging::getParameterCount(&T::scanDocument) == 2,
64
+ "Expected scanDocument(...) to have 2 parameters");
65
+
66
+ return bridging::callFromJs<jsi::Value>(
67
+ rt, &T::scanDocument, jsInvoker_, instance_, std::move(options));
68
+ }
69
+ jsi::Value detectEdges(jsi::Runtime &rt, jsi::String imageUri) override {
70
+ static_assert(
71
+ bridging::getParameterCount(&T::detectEdges) == 2,
72
+ "Expected detectEdges(...) to have 2 parameters");
73
+
74
+ return bridging::callFromJs<jsi::Value>(
75
+ rt, &T::detectEdges, jsInvoker_, instance_, std::move(imageUri));
76
+ }
77
+ jsi::Value cropDocument(jsi::Runtime &rt, jsi::String imageUri, jsi::Object corners) override {
78
+ static_assert(
79
+ bridging::getParameterCount(&T::cropDocument) == 3,
80
+ "Expected cropDocument(...) to have 3 parameters");
81
+
82
+ return bridging::callFromJs<jsi::Value>(
83
+ rt, &T::cropDocument, jsInvoker_, instance_, std::move(imageUri), std::move(corners));
84
+ }
85
+ jsi::Value processImage(jsi::Runtime &rt, jsi::String imageUri, jsi::Object options) override {
86
+ static_assert(
87
+ bridging::getParameterCount(&T::processImage) == 3,
88
+ "Expected processImage(...) to have 3 parameters");
89
+
90
+ return bridging::callFromJs<jsi::Value>(
91
+ rt, &T::processImage, jsInvoker_, instance_, std::move(imageUri), std::move(options));
92
+ }
93
+ jsi::Value scanAndProcess(jsi::Runtime &rt, jsi::String imageUri, jsi::Object processOptions) override {
94
+ static_assert(
95
+ bridging::getParameterCount(&T::scanAndProcess) == 3,
96
+ "Expected scanAndProcess(...) to have 3 parameters");
97
+
98
+ return bridging::callFromJs<jsi::Value>(
99
+ rt, &T::scanAndProcess, jsInvoker_, instance_, std::move(imageUri), std::move(processOptions));
100
+ }
101
+ jsi::Value cleanupTempFiles(jsi::Runtime &rt) override {
102
+ static_assert(
103
+ bridging::getParameterCount(&T::cleanupTempFiles) == 1,
104
+ "Expected cleanupTempFiles(...) to have 1 parameters");
105
+
106
+ return bridging::callFromJs<jsi::Value>(
107
+ rt, &T::cleanupTempFiles, jsInvoker_, instance_);
108
+ }
109
+ void addListener(jsi::Runtime &rt, jsi::String eventType) override {
110
+ static_assert(
111
+ bridging::getParameterCount(&T::addListener) == 2,
112
+ "Expected addListener(...) to have 2 parameters");
113
+
114
+ return bridging::callFromJs<void>(
115
+ rt, &T::addListener, jsInvoker_, instance_, std::move(eventType));
116
+ }
117
+ void removeListeners(jsi::Runtime &rt, double count) override {
118
+ static_assert(
119
+ bridging::getParameterCount(&T::removeListeners) == 2,
120
+ "Expected removeListeners(...) to have 2 parameters");
121
+
122
+ return bridging::callFromJs<void>(
123
+ rt, &T::removeListeners, jsInvoker_, instance_, std::move(count));
124
+ }
125
+
126
+ private:
127
+ friend class NativeNeuroscanCxxSpec;
128
+ T *instance_;
129
+ };
130
+
131
+ Delegate delegate_;
132
+ };
133
+
134
+ } // namespace facebook::react
@@ -0,0 +1,17 @@
1
+
2
+ /**
3
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
4
+ *
5
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
6
+ * once the code is regenerated.
7
+ *
8
+ * @generated by codegen project: GenerateShadowNodeCpp.js
9
+ */
10
+
11
+ #include <react/renderer/components/RNNeuroScanSpec/ShadowNodes.h>
12
+
13
+ namespace facebook::react {
14
+
15
+ extern const char NeuroScanCameraViewComponentName[] = "NeuroScanCameraView";
16
+
17
+ } // namespace facebook::react
@@ -0,0 +1,32 @@
1
+
2
+ /**
3
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
4
+ *
5
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
6
+ * once the code is regenerated.
7
+ *
8
+ * @generated by codegen project: GenerateShadowNodeH.js
9
+ */
10
+
11
+ #pragma once
12
+
13
+ #include <react/renderer/components/RNNeuroScanSpec/EventEmitters.h>
14
+ #include <react/renderer/components/RNNeuroScanSpec/Props.h>
15
+ #include <react/renderer/components/RNNeuroScanSpec/States.h>
16
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
17
+ #include <jsi/jsi.h>
18
+
19
+ namespace facebook::react {
20
+
21
+ JSI_EXPORT extern const char NeuroScanCameraViewComponentName[];
22
+
23
+ /*
24
+ * `ShadowNode` for <NeuroScanCameraView> component.
25
+ */
26
+ using NeuroScanCameraViewShadowNode = ConcreteViewShadowNode<
27
+ NeuroScanCameraViewComponentName,
28
+ NeuroScanCameraViewProps,
29
+ NeuroScanCameraViewEventEmitter,
30
+ NeuroScanCameraViewState>;
31
+
32
+ } // namespace facebook::react
@@ -0,0 +1,16 @@
1
+
2
+ /**
3
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
4
+ *
5
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
6
+ * once the code is regenerated.
7
+ *
8
+ * @generated by codegen project: GenerateStateCpp.js
9
+ */
10
+ #include <react/renderer/components/RNNeuroScanSpec/States.h>
11
+
12
+ namespace facebook::react {
13
+
14
+
15
+
16
+ } // namespace facebook::react
@@ -0,0 +1,29 @@
1
+ /**
2
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
3
+ *
4
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
5
+ * once the code is regenerated.
6
+ *
7
+ * @generated by codegen project: GenerateStateH.js
8
+ */
9
+ #pragma once
10
+
11
+ #ifdef ANDROID
12
+ #include <folly/dynamic.h>
13
+ #endif
14
+
15
+ namespace facebook::react {
16
+
17
+ class NeuroScanCameraViewState {
18
+ public:
19
+ NeuroScanCameraViewState() = default;
20
+
21
+ #ifdef ANDROID
22
+ NeuroScanCameraViewState(NeuroScanCameraViewState const &previousState, folly::dynamic data){};
23
+ folly::dynamic getDynamic() const {
24
+ return {};
25
+ };
26
+ #endif
27
+ };
28
+
29
+ } // namespace facebook::react
@@ -0,0 +1,73 @@
1
+ buildscript {
2
+ ext.Neuroscan = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return Neuroscan[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ apply plugin: "com.android.library"
31
+ apply plugin: "kotlin-android"
32
+
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.neuroscan"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+ }
44
+
45
+ buildFeatures {
46
+ buildConfig true
47
+ }
48
+
49
+ buildTypes {
50
+ release {
51
+ minifyEnabled false
52
+ }
53
+ }
54
+
55
+ lint {
56
+ disable "GradleCompatible"
57
+ }
58
+
59
+ compileOptions {
60
+ sourceCompatibility JavaVersion.VERSION_1_8
61
+ targetCompatibility JavaVersion.VERSION_1_8
62
+ }
63
+ }
64
+
65
+ dependencies {
66
+ implementation "com.facebook.react:react-android"
67
+
68
+ // ML Kit Document Scanner
69
+ implementation "com.google.android.gms:play-services-mlkit-document-scanner:16.0.0-beta1"
70
+
71
+ // Coroutines
72
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
73
+ }
@@ -0,0 +1,4 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-feature android:name="android.hardware.camera" android:required="false" />
3
+ <uses-permission android:name="android.permission.CAMERA" />
4
+ </manifest>
@@ -0,0 +1,342 @@
1
+ package com.neuroscan
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.graphics.Bitmap
6
+ import android.graphics.BitmapFactory
7
+ import android.content.IntentSender
8
+ import com.facebook.react.bridge.*
9
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanning
10
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions
11
+ import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult
12
+ import kotlinx.coroutines.*
13
+ import java.io.File
14
+ import java.io.FileOutputStream
15
+ import java.util.UUID
16
+
17
+ class NeuroscanModule(reactContext: ReactApplicationContext) :
18
+ NativeNeuroscanSpec(reactContext) {
19
+
20
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
21
+
22
+ private var scanPromise: Promise? = null
23
+ private val activityListener = ScanActivityListener()
24
+
25
+ private val tempDir: File
26
+ get() {
27
+ val dir = File(reactApplicationContext.cacheDir, "neuroscan")
28
+ if (!dir.exists()) dir.mkdirs()
29
+ return dir
30
+ }
31
+
32
+ init {
33
+ reactContext.addActivityEventListener(activityListener)
34
+ }
35
+
36
+ override fun getName(): String = NAME
37
+
38
+ // MARK: - scanDocument
39
+
40
+ override fun scanDocument(options: ReadableMap, promise: Promise) {
41
+ val activity = currentActivity
42
+ if (activity == null) {
43
+ promise.reject("CAMERA_UNAVAILABLE", "No activity available")
44
+ return
45
+ }
46
+
47
+ val maxPages = if (options.hasKey("maxPages")) options.getInt("maxPages") else 0
48
+
49
+ val scannerOptions = GmsDocumentScannerOptions.Builder()
50
+ .setGalleryImportAllowed(true)
51
+ .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
52
+ .setScannerMode(GmsDocumentScannerOptions.SCANNER_MODE_FULL)
53
+ .apply {
54
+ if (maxPages > 0) setPageLimit(maxPages)
55
+ }
56
+ .build()
57
+
58
+ val scanner = GmsDocumentScanning.getClient(scannerOptions)
59
+
60
+ scanPromise = promise
61
+
62
+ scanner.getStartScanIntent(activity)
63
+ .addOnSuccessListener { intentSender: IntentSender ->
64
+ try {
65
+ activity.startIntentSenderForResult(
66
+ intentSender,
67
+ SCAN_REQUEST_CODE,
68
+ null, 0, 0, 0
69
+ )
70
+ } catch (e: Exception) {
71
+ scanPromise?.reject("SCANNER_FAILED", "Failed to start scanner: ${e.message}", e)
72
+ scanPromise = null
73
+ }
74
+ }
75
+ .addOnFailureListener { e: Exception ->
76
+ scanPromise?.reject("SCANNER_FAILED", "Failed to initialize scanner: ${e.message}", e)
77
+ scanPromise = null
78
+ }
79
+ }
80
+
81
+ // MARK: - processImage
82
+
83
+ override fun processImage(options: ReadableMap, promise: Promise) {
84
+ scope.launch {
85
+ try {
86
+ val imageUrl = options.getString("imageUrl")
87
+ ?: throw IllegalArgumentException("imageUrl is required")
88
+ val grayscale = if (options.hasKey("grayscale")) options.getBoolean("grayscale") else false
89
+ val contrast = if (options.hasKey("contrast")) options.getDouble("contrast") else 0.0
90
+ val brightness = if (options.hasKey("brightness")) options.getDouble("brightness") else 0.0
91
+ val sharpness = if (options.hasKey("sharpness")) options.getDouble("sharpness") else 0.0
92
+ val rotation = if (options.hasKey("rotation")) options.getDouble("rotation").toFloat() else 0f
93
+ val cropX = if (options.hasKey("cropX")) options.getDouble("cropX") else -1.0
94
+ val cropY = if (options.hasKey("cropY")) options.getDouble("cropY") else -1.0
95
+ val cropWidth = if (options.hasKey("cropWidth")) options.getDouble("cropWidth") else -1.0
96
+ val cropHeight = if (options.hasKey("cropHeight")) options.getDouble("cropHeight") else -1.0
97
+ val threshold = if (options.hasKey("threshold")) options.getDouble("threshold").toInt() else 0
98
+ val outputFormat = if (options.hasKey("outputFormat")) options.getString("outputFormat") ?: "jpeg" else "jpeg"
99
+ val quality = if (options.hasKey("quality")) options.getDouble("quality").toInt() else 90
100
+
101
+ // Load bitmap
102
+ val filePath = imageUrl.removePrefix("file://")
103
+ val originalBitmap = BitmapFactory.decodeFile(filePath)
104
+ ?: throw IllegalArgumentException("Failed to load image from $imageUrl")
105
+
106
+ var bitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true)
107
+ originalBitmap.recycle()
108
+
109
+ // --- Filter order: crop -> rotate -> brightness/contrast -> sharpen -> grayscale/threshold ---
110
+
111
+ // 1. CROP
112
+ if (cropX >= 0 && cropY >= 0 && cropWidth > 0 && cropHeight > 0) {
113
+ val x = (cropX * bitmap.width).toInt().coerceAtLeast(0)
114
+ val y = (cropY * bitmap.height).toInt().coerceAtLeast(0)
115
+ val w = (cropWidth * bitmap.width).toInt().coerceAtMost(bitmap.width - x)
116
+ val h = (cropHeight * bitmap.height).toInt().coerceAtMost(bitmap.height - y)
117
+ if (w > 0 && h > 0) {
118
+ val cropped = Bitmap.createBitmap(bitmap, x, y, w, h)
119
+ bitmap.recycle()
120
+ bitmap = cropped
121
+ }
122
+ }
123
+
124
+ // 2. ROTATION
125
+ val rotationInt = rotation.toInt() % 360
126
+ if (rotationInt != 0) {
127
+ val matrix = android.graphics.Matrix()
128
+ matrix.postRotate(rotationInt.toFloat())
129
+ val rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
130
+ bitmap.recycle()
131
+ bitmap = rotated
132
+ }
133
+
134
+ // 3. BRIGHTNESS & CONTRAST (ColorMatrix)
135
+ if (brightness != 0.0 || contrast != 0.0) {
136
+ val contrastFactor = (1.0 + contrast / 100.0).toFloat()
137
+ val brightnessOffset = (brightness / 100.0 * 255.0).toFloat()
138
+ val translate = (1.0f - contrastFactor) * 128f
139
+
140
+ val cm = android.graphics.ColorMatrix(floatArrayOf(
141
+ contrastFactor, 0f, 0f, 0f, translate + brightnessOffset,
142
+ 0f, contrastFactor, 0f, 0f, translate + brightnessOffset,
143
+ 0f, 0f, contrastFactor, 0f, translate + brightnessOffset,
144
+ 0f, 0f, 0f, 1f, 0f
145
+ ))
146
+
147
+ val paint = android.graphics.Paint()
148
+ paint.colorFilter = android.graphics.ColorMatrixColorFilter(cm)
149
+ val temp = bitmap.copy(Bitmap.Config.ARGB_8888, true)
150
+ val canvas = android.graphics.Canvas(temp)
151
+ canvas.drawBitmap(bitmap, 0f, 0f, paint)
152
+ bitmap.recycle()
153
+ bitmap = temp
154
+ }
155
+
156
+ // 4. SHARPNESS (3x3 convolution kernel)
157
+ if (sharpness > 0) {
158
+ val amount = (sharpness / 100.0).toFloat()
159
+ val center = 1.0f + 4.0f * amount
160
+ val side = -amount
161
+ val kernel = floatArrayOf(
162
+ 0f, side, 0f,
163
+ side, center, side,
164
+ 0f, side, 0f
165
+ )
166
+
167
+ val width = bitmap.width
168
+ val height = bitmap.height
169
+ val srcPixels = IntArray(width * height)
170
+ bitmap.getPixels(srcPixels, 0, width, 0, 0, width, height)
171
+ val dstPixels = IntArray(width * height)
172
+ // Copy edge pixels
173
+ System.arraycopy(srcPixels, 0, dstPixels, 0, srcPixels.size)
174
+
175
+ for (y in 1 until height - 1) {
176
+ for (x in 1 until width - 1) {
177
+ var r = 0f; var g = 0f; var b = 0f
178
+ for (ky in -1..1) {
179
+ for (kx in -1..1) {
180
+ val pixel = srcPixels[(y + ky) * width + (x + kx)]
181
+ val k = kernel[(ky + 1) * 3 + (kx + 1)]
182
+ r += ((pixel shr 16) and 0xFF) * k
183
+ g += ((pixel shr 8) and 0xFF) * k
184
+ b += (pixel and 0xFF) * k
185
+ }
186
+ }
187
+ val a = (srcPixels[y * width + x] shr 24) and 0xFF
188
+ dstPixels[y * width + x] = (a shl 24) or
189
+ (r.toInt().coerceIn(0, 255) shl 16) or
190
+ (g.toInt().coerceIn(0, 255) shl 8) or
191
+ b.toInt().coerceIn(0, 255)
192
+ }
193
+ }
194
+ bitmap.setPixels(dstPixels, 0, width, 0, 0, width, height)
195
+ }
196
+
197
+ // 5. GRAYSCALE or THRESHOLD
198
+ if (threshold > 0) {
199
+ val width = bitmap.width
200
+ val height = bitmap.height
201
+ val pixels = IntArray(width * height)
202
+ bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
203
+
204
+ for (i in pixels.indices) {
205
+ val pixel = pixels[i]
206
+ val a = (pixel shr 24) and 0xFF
207
+ val r = (pixel shr 16) and 0xFF
208
+ val g = (pixel shr 8) and 0xFF
209
+ val b = pixel and 0xFF
210
+ val gray = (0.299 * r + 0.587 * g + 0.114 * b).toInt()
211
+ val bw = if (gray >= threshold) 255 else 0
212
+ pixels[i] = (a shl 24) or (bw shl 16) or (bw shl 8) or bw
213
+ }
214
+ bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
215
+ } else if (grayscale) {
216
+ val cm = android.graphics.ColorMatrix()
217
+ cm.setSaturation(0f)
218
+ val paint = android.graphics.Paint()
219
+ paint.colorFilter = android.graphics.ColorMatrixColorFilter(cm)
220
+ val temp = bitmap.copy(Bitmap.Config.ARGB_8888, true)
221
+ val canvas = android.graphics.Canvas(temp)
222
+ canvas.drawBitmap(bitmap, 0f, 0f, paint)
223
+ bitmap.recycle()
224
+ bitmap = temp
225
+ }
226
+
227
+ // 6. Save result
228
+ val savedUrl = saveBitmap(bitmap, outputFormat, quality)
229
+ bitmap.recycle()
230
+
231
+ if (savedUrl != null) {
232
+ val resultMap = Arguments.createMap().apply {
233
+ putString("imageUrl", savedUrl)
234
+ }
235
+ promise.resolve(resultMap)
236
+ } else {
237
+ promise.reject("PROCESS_FAILED", "Failed to save processed image")
238
+ }
239
+ } catch (e: Exception) {
240
+ promise.reject("PROCESS_FAILED", "Failed to process image: ${e.message}", e)
241
+ }
242
+ }
243
+ }
244
+
245
+ // MARK: - cleanupTempFiles
246
+
247
+ override fun cleanupTempFiles(promise: Promise) {
248
+ scope.launch {
249
+ try {
250
+ if (tempDir.exists()) {
251
+ tempDir.deleteRecursively()
252
+ }
253
+ promise.resolve(true)
254
+ } catch (e: Exception) {
255
+ promise.reject("CLEANUP_FAILED", "Failed to cleanup temp files: ${e.message}", e)
256
+ }
257
+ }
258
+ }
259
+
260
+ // MARK: - Event Emitter
261
+
262
+ override fun addListener(eventType: String) {}
263
+ override fun removeListeners(count: Double) {}
264
+
265
+ // MARK: - Helpers
266
+
267
+ private fun saveBitmap(bitmap: Bitmap, format: String = "jpeg", quality: Int = 90): String? {
268
+ return try {
269
+ val ext = if (format == "png") "png" else "jpg"
270
+ val file = File(tempDir, "${UUID.randomUUID()}.$ext")
271
+
272
+ FileOutputStream(file).use { out ->
273
+ val compressFormat = if (format == "png") Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG
274
+ bitmap.compress(compressFormat, quality, out)
275
+ }
276
+
277
+ "file://${file.absolutePath}"
278
+ } catch (e: Exception) {
279
+ null
280
+ }
281
+ }
282
+
283
+ // MARK: - Activity Result Handling
284
+
285
+ private inner class ScanActivityListener : BaseActivityEventListener() {
286
+ override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
287
+ if (requestCode != SCAN_REQUEST_CODE) return
288
+
289
+ val promise = scanPromise ?: return
290
+ scanPromise = null
291
+
292
+ if (resultCode == Activity.RESULT_CANCELED) {
293
+ promise.reject("SCANNER_CANCELLED", "Document scanner was cancelled")
294
+ return
295
+ }
296
+
297
+ val result = GmsDocumentScanningResult.fromActivityResultIntent(data)
298
+ if (result == null) {
299
+ promise.reject("SCANNER_FAILED", "Failed to get scanner result")
300
+ return
301
+ }
302
+
303
+ scope.launch {
304
+ try {
305
+ val urls = mutableListOf<String>()
306
+ val pages = result.getPages() ?: emptyList()
307
+
308
+ for (page in pages) {
309
+ val imageUri = page.getImageUri()
310
+ val inputStream = reactApplicationContext.contentResolver.openInputStream(imageUri)
311
+ val bitmap = BitmapFactory.decodeStream(inputStream)
312
+ inputStream?.close()
313
+
314
+ if (bitmap != null) {
315
+ val savedUrl = saveBitmap(bitmap)
316
+ if (savedUrl != null) {
317
+ urls.add(savedUrl)
318
+ }
319
+ bitmap.recycle()
320
+ }
321
+ }
322
+
323
+ val resultMap = Arguments.createMap().apply {
324
+ putArray("imageUrls", Arguments.createArray().apply {
325
+ urls.forEach { pushString(it) }
326
+ })
327
+ putInt("pageCount", urls.size)
328
+ }
329
+
330
+ promise.resolve(resultMap)
331
+ } catch (e: Exception) {
332
+ promise.reject("SCANNER_FAILED", "Failed to process scanned documents: ${e.message}", e)
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ companion object {
339
+ const val NAME = NativeNeuroscanSpec.NAME
340
+ private const val SCAN_REQUEST_CODE = 9001
341
+ }
342
+ }
@@ -0,0 +1,36 @@
1
+ package com.neuroscan
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
+ @Suppress("unused")
9
+ import com.facebook.react.uimanager.ViewManager
10
+
11
+ class NeuroscanPackage : BaseReactPackage() {
12
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
13
+ return if (name == NeuroscanModule.NAME) {
14
+ NeuroscanModule(reactContext)
15
+ } else {
16
+ null
17
+ }
18
+ }
19
+
20
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
21
+ mapOf(
22
+ NeuroscanModule.NAME to ReactModuleInfo(
23
+ name = NeuroscanModule.NAME,
24
+ className = NeuroscanModule.NAME,
25
+ canOverrideExistingModule = false,
26
+ needsEagerInit = false,
27
+ isCxxModule = false,
28
+ isTurboModule = true
29
+ )
30
+ )
31
+ }
32
+
33
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
34
+ return emptyList()
35
+ }
36
+ }