react-native-text-reader 1.4.0 → 2.0.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/README.md +71 -48
- package/android/build.gradle +9 -2
- package/android/gradle.properties +5 -5
- package/android/src/main/java/com/textreader/TextReaderModule.kt +195 -109
- package/app.plugin.js +15 -0
- package/ios/TextReader.mm +6 -0
- package/ios/TextReader.swift +195 -35
- package/lib/commonjs/NativeTextReader.js +9 -0
- package/lib/commonjs/NativeTextReader.js.map +1 -0
- package/lib/commonjs/index.js +35 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/package.json +1 -0
- package/lib/module/NativeTextReader.js +5 -0
- package/lib/module/NativeTextReader.js.map +1 -0
- package/lib/module/index.js +32 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/package.json +1 -0
- package/lib/typescript/commonjs/src/NativeTextReader.d.ts +34 -0
- package/lib/typescript/commonjs/src/NativeTextReader.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +27 -2
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/NativeTextReader.d.ts +34 -0
- package/lib/typescript/module/src/NativeTextReader.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +27 -2
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/package.json +70 -38
- package/react-native.config.json +1 -0
- package/src/NativeTextReader.ts +41 -0
- package/src/index.tsx +72 -18
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# `react-native-text-reader`
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
3
|
+
A React Native library for extracting text from images using native iOS (Vision Framework) and Android (ML Kit) capabilities.
|
|
5
4
|
|
|
6
5
|
## Table of Contents
|
|
7
6
|
|
|
@@ -9,92 +8,120 @@ A simple React Native library for extracting text from images using native iOS (
|
|
|
9
8
|
- [Platform Setup](#platform-setup)
|
|
10
9
|
- [iOS](#ios)
|
|
11
10
|
- [Android](#android)
|
|
11
|
+
- [Expo](#expo)
|
|
12
12
|
- [Usage](#usage)
|
|
13
13
|
- [Basic Example](#basic-example)
|
|
14
|
+
- [Detailed Result](#detailed-result)
|
|
14
15
|
- [Options](#options)
|
|
15
16
|
- [ScriptOptions Enum](#scriptoptions-enum)
|
|
16
17
|
- [Contributing](#contributing)
|
|
17
18
|
- [License](#license)
|
|
18
19
|
|
|
19
|
-
### NOTE:
|
|
20
|
-
I currently do not work extensively with React Native, so this project may not receive frequent updates. However, if anyone is interested in contributing, please feel free to reach out!
|
|
21
|
-
|
|
22
20
|
## Installation
|
|
23
21
|
|
|
24
|
-
Install the package
|
|
22
|
+
Install the package with your preferred package manager:
|
|
25
23
|
|
|
26
24
|
```bash
|
|
27
25
|
npm install react-native-text-reader
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add react-native-text-reader
|
|
30
|
+
```
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
yarn add react-native-text-reader
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
The published package is package-manager agnostic. Use whichever tool your app already uses.
|
|
37
37
|
|
|
38
38
|
### iOS
|
|
39
39
|
|
|
40
|
-
If you're using iOS, run the following command to install the necessary native modules:
|
|
41
|
-
|
|
42
40
|
```bash
|
|
43
|
-
cd ios
|
|
41
|
+
cd ios && pod install
|
|
44
42
|
```
|
|
45
43
|
|
|
46
44
|
### Android
|
|
47
45
|
|
|
48
|
-
No additional steps are required
|
|
46
|
+
No additional manual steps are required. Autolinking handles native setup.
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
## Expo
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
This library uses custom native code (Vision + ML Kit), so it does **not** work in **Expo Go**.
|
|
51
|
+
|
|
52
|
+
It **does** work with Expo development builds and EAS Build (tested with **Expo SDK 56**):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx expo install react-native-text-reader expo-dev-client
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Add the config plugin to your `app.json`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"expo": {
|
|
63
|
+
"plugins": ["react-native-text-reader"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then create native projects and run a dev build:
|
|
53
69
|
|
|
54
|
-
|
|
70
|
+
```bash
|
|
71
|
+
npx expo prebuild
|
|
72
|
+
npx expo run:ios
|
|
73
|
+
# or
|
|
74
|
+
npx expo run:android
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
55
78
|
|
|
56
79
|
### Basic Example
|
|
57
80
|
|
|
58
|
-
```
|
|
81
|
+
```typescript
|
|
59
82
|
import TextReader, { ScriptOptions } from 'react-native-text-reader';
|
|
60
83
|
|
|
61
|
-
const imagePath = 'path/to/your/image.jpg';
|
|
62
|
-
const options = {
|
|
63
|
-
visionIgnoreThreshold: 0.5, // iOS only
|
|
64
|
-
script: ScriptOptions.LATIN, // Android only
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const readTextFromImage = async () => {
|
|
68
|
-
try {
|
|
69
|
-
const text = await TextReader.read(imagePath, options);
|
|
70
|
-
console.log('Extracted text:', text);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error('Error reading text:', error);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
84
|
+
const imagePath = 'file:///path/to/your/image.jpg';
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
const lines = await TextReader.read(imagePath, {
|
|
87
|
+
visionIgnoreThreshold: 0.5, // iOS
|
|
88
|
+
confidenceThreshold: 0.5, // Android
|
|
89
|
+
script: ScriptOptions.LATIN, // Android
|
|
90
|
+
});
|
|
77
91
|
|
|
92
|
+
console.log('Extracted lines:', lines);
|
|
78
93
|
```
|
|
79
94
|
|
|
80
|
-
|
|
95
|
+
`read()` returns `Promise<string[]>` — one entry per detected line.
|
|
81
96
|
|
|
82
|
-
|
|
97
|
+
### Detailed Result
|
|
83
98
|
|
|
84
|
-
|
|
99
|
+
```typescript
|
|
100
|
+
const result = await TextReader.readDetailed(imagePath, {
|
|
101
|
+
script: ScriptOptions.LATIN,
|
|
102
|
+
});
|
|
85
103
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
console.log(result.fullText);
|
|
105
|
+
console.log(result.lines);
|
|
106
|
+
console.log(result.details); // confidence, frame, languages
|
|
107
|
+
```
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
## Options
|
|
92
110
|
|
|
93
|
-
|
|
111
|
+
| Property | Type | Platform | Description |
|
|
112
|
+
|----------|------|----------|-------------|
|
|
113
|
+
| `visionIgnoreThreshold` | `number` | iOS | Confidence threshold (default: `0`) |
|
|
114
|
+
| `confidenceThreshold` | `number` | Android | Confidence threshold (default: `0`) |
|
|
115
|
+
| `script` | `ScriptOptions` | Android | Script model (default: `LATIN`) |
|
|
116
|
+
| `recognitionLevel` | `'fast' \| 'accurate'` | iOS | Speed vs accuracy (default: `accurate`) |
|
|
117
|
+
| `recognitionLanguages` | `string[]` | iOS | Language hints, e.g. `['en-US']` |
|
|
118
|
+
| `customWords` | `string[]` | iOS | Domain vocabulary hints |
|
|
119
|
+
| `useLanguageCorrection` | `boolean` | iOS | Enable language correction |
|
|
120
|
+
| `minimumTextHeight` | `number` | iOS | Ignore text smaller than this fraction |
|
|
94
121
|
|
|
95
|
-
|
|
122
|
+
## ScriptOptions
|
|
96
123
|
|
|
97
|
-
```
|
|
124
|
+
```typescript
|
|
98
125
|
export enum ScriptOptions {
|
|
99
126
|
LATIN = 'Latin',
|
|
100
127
|
CHINESE = 'Chinese',
|
|
@@ -104,14 +131,10 @@ export enum ScriptOptions {
|
|
|
104
131
|
}
|
|
105
132
|
```
|
|
106
133
|
|
|
107
|
-
---
|
|
108
|
-
|
|
109
134
|
## Contributing
|
|
110
135
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
---
|
|
136
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Development in this repository uses **pnpm**.
|
|
114
137
|
|
|
115
138
|
## License
|
|
116
139
|
|
|
117
|
-
|
|
140
|
+
MIT — see [LICENSE](LICENSE).
|
package/android/build.gradle
CHANGED
|
@@ -8,7 +8,7 @@ buildscript {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
dependencies {
|
|
11
|
-
classpath "com.android.tools.build:gradle:
|
|
11
|
+
classpath "com.android.tools.build:gradle:8.5.1"
|
|
12
12
|
// noinspection DifferentKotlinGradleVersion
|
|
13
13
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
14
14
|
}
|
|
@@ -71,6 +71,9 @@ android {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Optimized for 16KB page size - AGP 8.5.1 handles alignment automatically
|
|
75
|
+
// Removed useLegacyPackaging to use modern packaging with better performance
|
|
76
|
+
|
|
74
77
|
lintOptions {
|
|
75
78
|
disable "GradleCompatible"
|
|
76
79
|
abortOnError false
|
|
@@ -96,6 +99,7 @@ dependencies {
|
|
|
96
99
|
implementation "com.facebook.react:react-native:+"
|
|
97
100
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
98
101
|
|
|
102
|
+
// ML Kit Text Recognition - Latest versions (2024)
|
|
99
103
|
// To recognize Latin script
|
|
100
104
|
implementation 'com.google.mlkit:text-recognition:16.0.1'
|
|
101
105
|
// To recognize Chinese script
|
|
@@ -105,6 +109,9 @@ dependencies {
|
|
|
105
109
|
// To recognize Japanese script
|
|
106
110
|
implementation 'com.google.mlkit:text-recognition-japanese:16.0.1'
|
|
107
111
|
// To recognize Korean script
|
|
108
|
-
implementation 'com.google.mlkit:text-recognition-korean:16.0.1'
|
|
112
|
+
implementation 'com.google.mlkit:text-recognition-korean:16.0.1'
|
|
113
|
+
|
|
114
|
+
// ML Kit Common library for better performance
|
|
115
|
+
implementation 'com.google.mlkit:common:18.11.0'
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
TextReader_kotlinVersion=
|
|
2
|
-
TextReader_minSdkVersion=
|
|
3
|
-
TextReader_targetSdkVersion=
|
|
4
|
-
TextReader_compileSdkVersion=
|
|
5
|
-
TextReader_ndkversion=
|
|
1
|
+
TextReader_kotlinVersion=2.0.21
|
|
2
|
+
TextReader_minSdkVersion=23
|
|
3
|
+
TextReader_targetSdkVersion=35
|
|
4
|
+
TextReader_compileSdkVersion=35
|
|
5
|
+
TextReader_ndkversion=25.1.8937393
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
package com.textreader
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import com.facebook.react.bridge.ReactMethod
|
|
6
|
-
import com.facebook.react.bridge.Promise
|
|
7
|
-
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
8
5
|
import android.graphics.Point
|
|
9
6
|
import android.graphics.Rect
|
|
10
7
|
import android.net.Uri
|
|
11
|
-
import android.graphics.BitmapFactory
|
|
12
|
-
import android.graphics.Bitmap
|
|
13
8
|
import androidx.annotation.NonNull
|
|
14
9
|
import androidx.annotation.Nullable
|
|
15
10
|
import com.facebook.react.bridge.*
|
|
16
|
-
import com.google.android.gms.tasks.OnFailureListener
|
|
17
|
-
import com.google.android.gms.tasks.OnSuccessListener
|
|
18
11
|
import com.google.mlkit.vision.common.InputImage
|
|
19
12
|
import com.google.mlkit.vision.text.Text
|
|
20
13
|
import com.google.mlkit.vision.text.TextRecognition
|
|
@@ -25,127 +18,220 @@ import com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions
|
|
|
25
18
|
import com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions
|
|
26
19
|
import com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions
|
|
27
20
|
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
|
|
21
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
22
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
23
|
+
import com.facebook.react.bridge.ReactMethod
|
|
24
|
+
import com.facebook.react.bridge.Promise
|
|
28
25
|
import java.io.IOException
|
|
26
|
+
import java.net.HttpURLConnection
|
|
29
27
|
import java.net.URL
|
|
30
28
|
|
|
31
29
|
class TextReaderModule(reactContext: ReactApplicationContext) :
|
|
32
30
|
ReactContextBaseJavaModule(reactContext) {
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
|
|
32
|
+
companion object {
|
|
33
|
+
private const val HTTP_CONNECT_TIMEOUT_MS = 10_000
|
|
34
|
+
private const val HTTP_READ_TIMEOUT_MS = 10_000
|
|
35
|
+
private const val MAX_IMAGE_BYTES = 20 * 1024 * 1024
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun getName(): String = "TextReader"
|
|
39
|
+
|
|
40
|
+
@Throws(IOException::class)
|
|
41
|
+
private fun getInputImage(reactContext: ReactApplicationContext, url: String): InputImage {
|
|
42
|
+
return if (url.contains("http://") || url.contains("https://")) {
|
|
43
|
+
val connection = URL(url).openConnection() as HttpURLConnection
|
|
44
|
+
connection.connectTimeout = HTTP_CONNECT_TIMEOUT_MS
|
|
45
|
+
connection.readTimeout = HTTP_READ_TIMEOUT_MS
|
|
46
|
+
connection.connect()
|
|
47
|
+
|
|
48
|
+
val contentLength = connection.contentLength
|
|
49
|
+
if (contentLength > MAX_IMAGE_BYTES) {
|
|
50
|
+
connection.disconnect()
|
|
51
|
+
throw IOException("Remote image exceeds maximum allowed size.")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
val image: Bitmap? = connection.inputStream.use { stream ->
|
|
55
|
+
BitmapFactory.decodeStream(stream)
|
|
56
|
+
}
|
|
57
|
+
connection.disconnect()
|
|
58
|
+
|
|
59
|
+
if (image == null) {
|
|
60
|
+
throw IOException("Failed to decode remote image.")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
InputImage.fromBitmap(image, 0)
|
|
64
|
+
} else {
|
|
65
|
+
val uri = Uri.parse(url)
|
|
66
|
+
InputImage.fromFilePath(reactContext, uri)
|
|
35
67
|
}
|
|
68
|
+
}
|
|
36
69
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} else {
|
|
44
|
-
val uri = Uri.parse(url)
|
|
45
|
-
InputImage.fromFilePath(reactContext, uri)
|
|
46
|
-
}
|
|
70
|
+
private fun rectToMap(rect: Rect): WritableMap {
|
|
71
|
+
return Arguments.createMap().apply {
|
|
72
|
+
putInt("width", rect.width())
|
|
73
|
+
putInt("height", rect.height())
|
|
74
|
+
putInt("top", rect.top)
|
|
75
|
+
putInt("left", rect.left)
|
|
47
76
|
}
|
|
77
|
+
}
|
|
48
78
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
79
|
+
private fun cornerPointsToMap(points: Array<Point>): WritableArray {
|
|
80
|
+
return Arguments.createArray().apply {
|
|
81
|
+
points.forEach { point ->
|
|
82
|
+
pushMap(Arguments.createMap().apply {
|
|
83
|
+
putInt("x", point.x)
|
|
84
|
+
putInt("y", point.y)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
56
87
|
}
|
|
88
|
+
}
|
|
57
89
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
putInt("y", point.y)
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
}
|
|
90
|
+
private fun langToMap(lang: String): WritableArray {
|
|
91
|
+
return Arguments.createArray().apply {
|
|
92
|
+
pushMap(Arguments.createMap().apply {
|
|
93
|
+
putString("languageCode", lang)
|
|
94
|
+
})
|
|
67
95
|
}
|
|
96
|
+
}
|
|
68
97
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
98
|
+
private fun lineConfidence(line: Text.Line): Float {
|
|
99
|
+
val elements = line.elements
|
|
100
|
+
if (elements.isEmpty()) return 1.0f
|
|
101
|
+
return elements.map { element ->
|
|
102
|
+
try {
|
|
103
|
+
val method = element.javaClass.getMethod("getConfidence")
|
|
104
|
+
(method.invoke(element) as? Float) ?: 1.0f
|
|
105
|
+
} catch (_: Exception) {
|
|
106
|
+
1.0f
|
|
107
|
+
}
|
|
108
|
+
}.average().toFloat()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private fun lineToMap(line: Text.Line): WritableMap {
|
|
112
|
+
return Arguments.createMap().apply {
|
|
113
|
+
putString("text", line.text)
|
|
114
|
+
putDouble("confidence", lineConfidence(line).toDouble())
|
|
115
|
+
line.boundingBox?.let { putMap("frame", rectToMap(it)) }
|
|
116
|
+
line.cornerPoints?.let { putArray("cornerPoints", cornerPointsToMap(it)) }
|
|
117
|
+
putArray("recognizedLanguages", langToMap(line.recognizedLanguage))
|
|
118
|
+
|
|
119
|
+
val elementsArray = Arguments.createArray()
|
|
120
|
+
line.elements.forEach { element ->
|
|
121
|
+
elementsArray.pushMap(Arguments.createMap().apply {
|
|
122
|
+
putString("text", element.text)
|
|
123
|
+
element.boundingBox?.let { putMap("frame", rectToMap(it)) }
|
|
124
|
+
element.cornerPoints?.let { putArray("cornerPoints", cornerPointsToMap(it)) }
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
putArray("elements", elementsArray)
|
|
75
128
|
}
|
|
129
|
+
}
|
|
76
130
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
131
|
+
private fun blockToMap(block: Text.TextBlock): WritableMap {
|
|
132
|
+
return Arguments.createMap().apply {
|
|
133
|
+
putString("text", block.text)
|
|
134
|
+
block.boundingBox?.let { putMap("frame", rectToMap(it)) }
|
|
135
|
+
block.cornerPoints?.let { putArray("cornerPoints", cornerPointsToMap(it)) }
|
|
136
|
+
|
|
137
|
+
val linesArray = Arguments.createArray()
|
|
138
|
+
block.lines.forEach { line ->
|
|
139
|
+
linesArray.pushMap(lineToMap(line))
|
|
140
|
+
}
|
|
141
|
+
putArray("lines", linesArray)
|
|
142
|
+
putArray("recognizedLanguages", langToMap(block.recognizedLanguage))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@NonNull
|
|
147
|
+
private fun getScriptTextRecognizerOptions(@Nullable script: String?): TextRecognizerOptionsInterface {
|
|
148
|
+
return when (script) {
|
|
149
|
+
"Chinese" -> ChineseTextRecognizerOptions.Builder().build()
|
|
150
|
+
"Devanagari" -> DevanagariTextRecognizerOptions.Builder().build()
|
|
151
|
+
"Japanese" -> JapaneseTextRecognizerOptions.Builder().build()
|
|
152
|
+
"Korean" -> KoreanTextRecognizerOptions.Builder().build()
|
|
153
|
+
else -> TextRecognizerOptions.DEFAULT_OPTIONS
|
|
94
154
|
}
|
|
155
|
+
}
|
|
95
156
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
157
|
+
private fun sortedLines(visionText: Text, confidenceThreshold: Float): List<Text.Line> {
|
|
158
|
+
return visionText.textBlocks
|
|
159
|
+
.flatMap { block -> block.lines }
|
|
160
|
+
.filter { line -> lineConfidence(line) >= confidenceThreshold }
|
|
161
|
+
.sortedWith(compareBy({ it.boundingBox?.top ?: 0 }, { it.boundingBox?.left ?: 0 }))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private fun processImage(
|
|
165
|
+
url: String,
|
|
166
|
+
options: ReadableMap?,
|
|
167
|
+
detailed: Boolean,
|
|
168
|
+
promise: Promise
|
|
169
|
+
) {
|
|
170
|
+
if (url.isEmpty()) {
|
|
171
|
+
promise.reject("ERR_EMPTY_PATH", "Image path cannot be empty.")
|
|
172
|
+
return
|
|
173
|
+
}
|
|
101
174
|
|
|
175
|
+
val script = options?.getString("script")
|
|
176
|
+
val confidenceThreshold = if (options != null && options.hasKey("confidenceThreshold")) {
|
|
177
|
+
options.getDouble("confidenceThreshold").toFloat()
|
|
178
|
+
} else {
|
|
179
|
+
0.0f
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
val recognizer: TextRecognizer = TextRecognition.getClient(getScriptTextRecognizerOptions(script))
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
val image = getInputImage(reactApplicationContext, url)
|
|
186
|
+
recognizer.process(image)
|
|
187
|
+
.addOnSuccessListener { visionText ->
|
|
188
|
+
val lines = sortedLines(visionText, confidenceThreshold)
|
|
189
|
+
if (detailed) {
|
|
190
|
+
promise.resolve(buildDetailedResult(visionText, lines))
|
|
191
|
+
} else {
|
|
102
192
|
val linesArray = Arguments.createArray()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
193
|
+
lines.forEach { line -> linesArray.pushString(line.text) }
|
|
194
|
+
promise.resolve(linesArray)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
.addOnFailureListener { e ->
|
|
198
|
+
promise.reject("ERR_OCR", "Text recognition failed: ${e.message}", e)
|
|
199
|
+
}
|
|
200
|
+
.addOnCompleteListener {
|
|
201
|
+
recognizer.close()
|
|
108
202
|
}
|
|
203
|
+
} catch (e: IOException) {
|
|
204
|
+
recognizer.close()
|
|
205
|
+
promise.reject("ERR_IMAGE_LOADING", "Failed to load image: ${e.message}", e)
|
|
206
|
+
} catch (e: Exception) {
|
|
207
|
+
recognizer.close()
|
|
208
|
+
promise.reject("ERR_IMAGE_PROCESSING", "Failed to process image: ${e.message}", e)
|
|
109
209
|
}
|
|
210
|
+
}
|
|
110
211
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"Japanese" -> JapaneseTextRecognizerOptions.Builder().build()
|
|
117
|
-
"Korean" -> KoreanTextRecognizerOptions.Builder().build()
|
|
118
|
-
else -> TextRecognizerOptions.DEFAULT_OPTIONS
|
|
119
|
-
}
|
|
212
|
+
private fun buildDetailedResult(visionText: Text, lines: List<Text.Line>): WritableMap {
|
|
213
|
+
val lineTexts = lines.map { it.text }
|
|
214
|
+
val detailsArray = Arguments.createArray()
|
|
215
|
+
lines.forEach { line ->
|
|
216
|
+
detailsArray.pushMap(lineToMap(line))
|
|
120
217
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
val options = getScriptTextRecognizerOptions(script)
|
|
129
|
-
|
|
130
|
-
val recognizer: TextRecognizer = TextRecognition.getClient(options)
|
|
131
|
-
|
|
132
|
-
recognizer.process(image)
|
|
133
|
-
.addOnSuccessListener { visionText ->
|
|
134
|
-
val linesArray = Arguments.createArray()
|
|
135
|
-
visionText.textBlocks.forEach { block ->
|
|
136
|
-
block.lines.forEach { line ->
|
|
137
|
-
linesArray.pushString(line.text)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
promise.resolve(linesArray)
|
|
141
|
-
}
|
|
142
|
-
.addOnFailureListener { e ->
|
|
143
|
-
e.printStackTrace()
|
|
144
|
-
promise.reject("Text recognition failed", e)
|
|
145
|
-
}
|
|
146
|
-
} catch (e: IOException) {
|
|
147
|
-
e.printStackTrace()
|
|
148
|
-
promise.reject("Text recognition failed", e)
|
|
149
|
-
}
|
|
218
|
+
|
|
219
|
+
return Arguments.createMap().apply {
|
|
220
|
+
putString("fullText", visionText.text)
|
|
221
|
+
putArray("lines", Arguments.createArray().apply {
|
|
222
|
+
lineTexts.forEach { pushString(it) }
|
|
223
|
+
})
|
|
224
|
+
putArray("details", detailsArray)
|
|
150
225
|
}
|
|
151
|
-
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@ReactMethod
|
|
229
|
+
fun read(url: String, options: ReadableMap?, promise: Promise) {
|
|
230
|
+
processImage(url, options, detailed = false, promise = promise)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@ReactMethod
|
|
234
|
+
fun readDetailed(url: String, options: ReadableMap?, promise: Promise) {
|
|
235
|
+
processImage(url, options, detailed = true, promise = promise)
|
|
236
|
+
}
|
|
237
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { createRunOncePlugin } = require('expo/config-plugins');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures react-native-text-reader is configured for Expo development builds.
|
|
5
|
+
* Requires expo-dev-client — does not work in Expo Go.
|
|
6
|
+
*/
|
|
7
|
+
function withReactNativeTextReader(config) {
|
|
8
|
+
return config;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = createRunOncePlugin(
|
|
12
|
+
withReactNativeTextReader,
|
|
13
|
+
'react-native-text-reader',
|
|
14
|
+
'2.0.0'
|
|
15
|
+
);
|
package/ios/TextReader.mm
CHANGED
|
@@ -8,6 +8,12 @@ RCT_EXTERN_METHOD(read
|
|
|
8
8
|
: (RCTPromiseResolveBlock)resolve withRejecter
|
|
9
9
|
: (RCTPromiseRejectBlock)reject)
|
|
10
10
|
|
|
11
|
+
RCT_EXTERN_METHOD(readDetailed
|
|
12
|
+
: (NSString *)imgPath withOptions
|
|
13
|
+
: (NSDictionary *)options withResolver
|
|
14
|
+
: (RCTPromiseResolveBlock)resolve withRejecter
|
|
15
|
+
: (RCTPromiseRejectBlock)reject)
|
|
16
|
+
|
|
11
17
|
+ (BOOL)requiresMainQueueSetup
|
|
12
18
|
{
|
|
13
19
|
return NO;
|