react-native-text-reader 1.5.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 CHANGED
@@ -1,7 +1,6 @@
1
1
  # `react-native-text-reader`
2
2
 
3
- A simple React Native library for extracting text from images using native iOS (Vision Framework) and Android (ML Kit) capabilities.
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 using npm or yarn:
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
- or
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
- After installing the package, make sure to link the native dependencies:
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/ && pod install
41
+ cd ios && pod install
44
42
  ```
45
43
 
46
44
  ### Android
47
45
 
48
- No additional steps are required for Android.
46
+ No additional manual steps are required. Autolinking handles native setup.
49
47
 
50
- ---
48
+ ## Expo
51
49
 
52
- ## Usage
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
- To use the text reader, import it and call the `read` method with the image path and options.
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
- ```javascript
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
- readTextFromImage();
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
- ## Options
97
+ ### Detailed Result
83
98
 
84
- ### `Options`
99
+ ```typescript
100
+ const result = await TextReader.readDetailed(imagePath, {
101
+ script: ScriptOptions.LATIN,
102
+ });
85
103
 
86
- | Property | Type | Description |
87
- |--------------------------|-------------------|------------------------------------------------------|
88
- | `visionIgnoreThreshold` | `number` | The confidence threshold for iOS (default: 0.0) |
89
- | `script` | `ScriptOptions` | The language script for Android (default: `LATIN`) |
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
- ## ScriptOptions
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
- The `ScriptOptions` enum allows you to specify different language scripts for Android:
122
+ ## ScriptOptions
96
123
 
97
- ```javascript
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
- Contributions are welcome! Please submit a pull request or open an issue for any enhancements or bugs.
112
-
113
- ---
136
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Development in this repository uses **pnpm**.
114
137
 
115
138
  ## License
116
139
 
117
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
140
+ MIT see [LICENSE](LICENSE).
@@ -1,5 +1,5 @@
1
- TextReader_kotlinVersion=1.9.10
2
- TextReader_minSdkVersion=21
3
- TextReader_targetSdkVersion=34
4
- TextReader_compileSdkVersion=34
1
+ TextReader_kotlinVersion=2.0.21
2
+ TextReader_minSdkVersion=23
3
+ TextReader_targetSdkVersion=35
4
+ TextReader_compileSdkVersion=35
5
5
  TextReader_ndkversion=25.1.8937393
@@ -1,20 +1,13 @@
1
1
  package com.textreader
2
2
 
3
- import com.facebook.react.bridge.ReactApplicationContext
4
- import com.facebook.react.bridge.ReactContextBaseJavaModule
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
- override fun getName(): String {
34
- return "TextReader"
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
- @Throws(IOException::class)
38
- private fun getInputImage(reactContext: ReactApplicationContext, url: String): InputImage {
39
- return if (url.contains("http://") || url.contains("https://")) {
40
- val urlInput = URL(url)
41
- val image: Bitmap = BitmapFactory.decodeStream(urlInput.openConnection().getInputStream())
42
- InputImage.fromBitmap(image, 0)
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
- private fun rectToMap(rect: Rect): WritableMap {
50
- return Arguments.createMap().apply {
51
- putInt("width", rect.width())
52
- putInt("height", rect.height())
53
- putInt("top", rect.top)
54
- putInt("left", rect.left)
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
- private fun cornerPointsToMap(points: Array<Point>): WritableArray {
59
- return Arguments.createArray().apply {
60
- points.forEach { point ->
61
- pushMap(Arguments.createMap().apply {
62
- putInt("x", point.x)
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
- private fun langToMap(lang: String): WritableArray {
70
- return Arguments.createArray().apply {
71
- pushMap(Arguments.createMap().apply {
72
- putString("languageCode", lang)
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
- private fun lineToMap(line: Text.Line): WritableMap {
78
- return Arguments.createMap().apply {
79
- putString("text", line.text)
80
- line.boundingBox?.let { putMap("frame", rectToMap(it)) }
81
- line.cornerPoints?.let { putArray("cornerPoints", cornerPointsToMap(it)) }
82
- putArray("recognizedLanguages", langToMap(line.recognizedLanguage))
83
-
84
- val elementsArray = Arguments.createArray()
85
- line.elements.forEach { element ->
86
- elementsArray.pushMap(Arguments.createMap().apply {
87
- putString("text", element.text)
88
- element.boundingBox?.let { putMap("frame", rectToMap(it)) }
89
- element.cornerPoints?.let { putArray("cornerPoints", cornerPointsToMap(it)) }
90
- })
91
- }
92
- putArray("elements", elementsArray)
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
- private fun blockToMap(block: Text.TextBlock): WritableMap {
97
- return Arguments.createMap().apply {
98
- putString("text", block.text)
99
- block.boundingBox?.let { putMap("frame", rectToMap(it)) }
100
- block.cornerPoints?.let { putArray("cornerPoints", cornerPointsToMap(it)) }
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
- block.lines.forEach { line ->
104
- linesArray.pushMap(lineToMap(line))
105
- }
106
- putArray("lines", linesArray)
107
- putArray("recognizedLanguages", langToMap(block.recognizedLanguage))
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
- @NonNull
112
- private fun getScriptTextRecognizerOptions(@Nullable script: String?): TextRecognizerOptionsInterface {
113
- return when (script) {
114
- "Chinese" -> ChineseTextRecognizerOptions.Builder().build()
115
- "Devanagari" -> DevanagariTextRecognizerOptions.Builder().build()
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
- @ReactMethod
123
- fun read(url: String, options: ReadableMap?, promise: Promise) {
124
- try {
125
- val script = options?.getString("script")
126
-
127
- val image = getInputImage(reactApplicationContext, url)
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;