Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cbe0f9d47 | ||
|
|
81c46cc7ef |
34
README.md
34
README.md
@ -237,6 +237,40 @@ Additionally, the Camera can be used for barcode scanning
|
|||||||
| `frameColor` | Color | Color of barcode scanner frame visualization. Default: `yellow` |
|
| `frameColor` | Color | Color of barcode scanner frame visualization. Default: `yellow` |
|
||||||
| `onReadCode` | Function | Callback when scanner successfully reads barcode. Returned event contains `codeStringValue`. Default: `null`. Ex: `onReadCode={(event) => console.log(event.nativeEvent.codeStringValue)}` |
|
| `onReadCode` | Function | Callback when scanner successfully reads barcode. Returned event contains `codeStringValue`. Default: `null`. Ex: `onReadCode={(event) => console.log(event.nativeEvent.codeStringValue)}` |
|
||||||
|
|
||||||
|
### detectQRCodeInImage(base64)
|
||||||
|
|
||||||
|
Detect and decode a QR code from a static image. Takes a base64-encoded image string. Returns a promise that resolves with the decoded string, or `null` if the image is valid but contains no QR code. Rejects only on actual errors (invalid base64, undecodable image data, detector failure).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { detectQRCodeInImage } from 'react-native-camera-kit-no-google';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = await detectQRCodeInImage(base64ImageData);
|
||||||
|
if (decoded === null) {
|
||||||
|
// Image was valid but no QR code was found
|
||||||
|
} else {
|
||||||
|
console.log('QR code:', decoded);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid image data or internal detection failure — e.message has details
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rejection error codes: `E_INVALID_IMAGE` (bad base64 or undecodable image) and `E_QR_DETECTION_FAILED` (Android: QR decoder error; iOS: Vision framework error or detector init failure).
|
||||||
|
|
||||||
|
This API is exclusive to this fork and is not available in the original react-native-camera-kit.
|
||||||
|
|
||||||
|
It does **not** require camera permissions — it works purely on image data. Useful for detecting QR codes from:
|
||||||
|
- Images picked from the photo library
|
||||||
|
- Clipboard images
|
||||||
|
- Files received via deep links or share extensions
|
||||||
|
|
||||||
|
| Platform | Implementation |
|
||||||
|
|----------|---------------|
|
||||||
|
| iOS (device) | Vision framework (`VNDetectBarcodesRequest`) |
|
||||||
|
| iOS (simulator) | CoreImage (`CIDetector`) |
|
||||||
|
| Android | [limpbrains/qr](https://github.com/limpbrains/qr) |
|
||||||
|
|
||||||
### Imperative API
|
### Imperative API
|
||||||
|
|
||||||
_Note: Must be called on a valid camera ref_
|
_Note: Must be called on a valid camera ref_
|
||||||
|
|||||||
99
android/src/main/java/com/rncamerakit/ImageQRCodeDecoder.kt
Normal file
99
android/src/main/java/com/rncamerakit/ImageQRCodeDecoder.kt
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package com.rncamerakit
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Base64
|
||||||
|
import qr.QRDecoder
|
||||||
|
|
||||||
|
object ImageQRCodeDecoder {
|
||||||
|
|
||||||
|
// Center-crop retry removes a 10% border on each side. Borders often contain
|
||||||
|
// background clutter (fingers, page edges, UI chrome) that confuses the
|
||||||
|
// detector without contributing QR data.
|
||||||
|
private const val CROP_FRACTION = 0.1
|
||||||
|
|
||||||
|
// The underlying QR decoder works best on images around ~600px on the longest
|
||||||
|
// side. Larger inputs waste CPU on pixel scanning; much smaller inputs lose
|
||||||
|
// the finder-pattern resolution. 600 is the empirical sweet spot.
|
||||||
|
private const val RETRY_MAX_DIM = 600
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a QR code from a base64-encoded image.
|
||||||
|
* Returns the decoded string, or null if no QR code could be found.
|
||||||
|
* Throws IllegalArgumentException if the input is not a valid image.
|
||||||
|
*/
|
||||||
|
fun decode(base64: String): String? {
|
||||||
|
val imageBytes = Base64.decode(base64, Base64.DEFAULT)
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||||
|
?: throw IllegalArgumentException("Could not decode base64 image data")
|
||||||
|
|
||||||
|
return try {
|
||||||
|
decodeFromBitmap(bitmap)
|
||||||
|
} finally {
|
||||||
|
bitmap.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeFromBitmap(bitmap: Bitmap): String? {
|
||||||
|
val rgba = bitmapToRgba(bitmap)
|
||||||
|
return try {
|
||||||
|
QRDecoder.decode(bitmap.width, bitmap.height, rgba)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Retry with cropped-and-scaled image for hard-to-read QR codes
|
||||||
|
decodeCroppedAndScaled(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeCroppedAndScaled(original: Bitmap): String? {
|
||||||
|
val x = (original.width * CROP_FRACTION).toInt()
|
||||||
|
val y = (original.height * CROP_FRACTION).toInt()
|
||||||
|
val cropW = original.width - 2 * x
|
||||||
|
val cropH = original.height - 2 * y
|
||||||
|
|
||||||
|
// Image is too small to retry with a center-crop — treat as "no QR found".
|
||||||
|
if (cropW < 1 || cropH < 1) return null
|
||||||
|
|
||||||
|
val cropped = Bitmap.createBitmap(original, x, y, cropW, cropH)
|
||||||
|
try {
|
||||||
|
val longest = maxOf(cropW, cropH)
|
||||||
|
val scale = if (longest > RETRY_MAX_DIM) RETRY_MAX_DIM.toFloat() / longest else 1f
|
||||||
|
|
||||||
|
val scaled = if (scale < 1f) {
|
||||||
|
Bitmap.createScaledBitmap(cropped, (cropW * scale).toInt(), (cropH * scale).toInt(), true)
|
||||||
|
} else {
|
||||||
|
cropped
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val rgba = bitmapToRgba(scaled)
|
||||||
|
return try {
|
||||||
|
QRDecoder.decode(scaled.width, scaled.height, rgba)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (scaled !== cropped) scaled.recycle()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cropped.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bitmapToRgba(bitmap: Bitmap): ByteArray {
|
||||||
|
val width = bitmap.width
|
||||||
|
val height = bitmap.height
|
||||||
|
val pixels = IntArray(width * height)
|
||||||
|
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||||
|
|
||||||
|
val rgba = ByteArray(width * height * 4)
|
||||||
|
for (i in pixels.indices) {
|
||||||
|
val pixel = pixels[i]
|
||||||
|
val offset = i * 4
|
||||||
|
rgba[offset] = ((pixel shr 16) and 0xFF).toByte() // R
|
||||||
|
rgba[offset + 1] = ((pixel shr 8) and 0xFF).toByte() // G
|
||||||
|
rgba[offset + 2] = (pixel and 0xFF).toByte() // B
|
||||||
|
rgba[offset + 3] = ((pixel shr 24) and 0xFF).toByte() // A
|
||||||
|
}
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package com.rncamerakit
|
|||||||
|
|
||||||
import com.facebook.react.bridge.*
|
import com.facebook.react.bridge.*
|
||||||
import com.facebook.react.uimanager.UIManagerHelper
|
import com.facebook.react.uimanager.UIManagerHelper
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
import com.rncamerakit.NativeCameraKitModuleSpec
|
import com.rncamerakit.NativeCameraKitModuleSpec
|
||||||
|
|
||||||
@ -38,6 +39,8 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Nat
|
|||||||
const val LANDSCAPE_RIGHT = 3 // ➡️
|
const val LANDSCAPE_RIGHT = 3 // ➡️
|
||||||
|
|
||||||
const val REACT_CLASS = "RNCameraKitModule"
|
const val REACT_CLASS = "RNCameraKitModule"
|
||||||
|
|
||||||
|
private val qrDecodeExecutor = Executors.newCachedThreadPool()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
@ -62,6 +65,19 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Nat
|
|||||||
|
|
||||||
override fun checkDeviceCameraAuthorizationStatus(promise: Promise?) = Unit
|
override fun checkDeviceCameraAuthorizationStatus(promise: Promise?) = Unit
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
override fun detectQRCodeInImage(base64: String, promise: Promise) {
|
||||||
|
qrDecodeExecutor.execute {
|
||||||
|
try {
|
||||||
|
promise.resolve(ImageQRCodeDecoder.decode(base64))
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
promise.reject("E_INVALID_IMAGE", e.message ?: "Invalid image data", e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
promise.reject("E_QR_DETECTION_FAILED", e.message ?: "QR detection failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Captures a photo using the camera.
|
* Captures a photo using the camera.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.rncamerakit
|
|||||||
|
|
||||||
import com.facebook.react.bridge.*
|
import com.facebook.react.bridge.*
|
||||||
import com.facebook.react.uimanager.UIManagerHelper
|
import com.facebook.react.uimanager.UIManagerHelper
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Native module for interacting with the camera in React Native applications.
|
* Native module for interacting with the camera in React Native applications.
|
||||||
@ -36,6 +37,8 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Rea
|
|||||||
const val LANDSCAPE_RIGHT = 3 // ➡️
|
const val LANDSCAPE_RIGHT = 3 // ➡️
|
||||||
|
|
||||||
const val REACT_CLASS = "RNCameraKitModule"
|
const val REACT_CLASS = "RNCameraKitModule"
|
||||||
|
|
||||||
|
private val qrDecodeExecutor = Executors.newCachedThreadPool()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
@ -60,6 +63,19 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Rea
|
|||||||
|
|
||||||
fun checkDeviceCameraAuthorizationStatus(promise: Promise?) = Unit
|
fun checkDeviceCameraAuthorizationStatus(promise: Promise?) = Unit
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun detectQRCodeInImage(base64: String, promise: Promise) {
|
||||||
|
qrDecodeExecutor.execute {
|
||||||
|
try {
|
||||||
|
promise.resolve(ImageQRCodeDecoder.decode(base64))
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
promise.reject("E_INVALID_IMAGE", e.message ?: "Invalid image data", e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
promise.reject("E_QR_DETECTION_FAILED", e.message ?: "QR detection failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Captures a photo using the camera.
|
* Captures a photo using the camera.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>C617.1</string>
|
<string>C617.1</string>
|
||||||
|
<string>3B52.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@ -1719,6 +1719,34 @@ PODS:
|
|||||||
- React-RCTFBReactNativeSpec
|
- React-RCTFBReactNativeSpec
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
|
- react-native-image-picker (7.2.3):
|
||||||
|
- boost
|
||||||
|
- DoubleConversion
|
||||||
|
- fast_float
|
||||||
|
- fmt
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly
|
||||||
|
- RCT-Folly/Fabric
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-ImageManager
|
||||||
|
- React-jsi
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-renderercss
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- SocketRocket
|
||||||
|
- Yoga
|
||||||
- React-NativeModulesApple (0.81.0):
|
- React-NativeModulesApple (0.81.0):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@ -2223,7 +2251,7 @@ PODS:
|
|||||||
- React-perflogger (= 0.81.0)
|
- React-perflogger (= 0.81.0)
|
||||||
- React-utils (= 0.81.0)
|
- React-utils (= 0.81.0)
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- ReactNativeCameraKit (17.0.3):
|
- ReactNativeCameraKit (17.0.4):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@ -2296,6 +2324,7 @@ DEPENDENCIES:
|
|||||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||||
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
||||||
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||||
|
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
|
||||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||||
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
||||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||||
@ -2416,6 +2445,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../node_modules/react-native/ReactCommon"
|
||||||
React-microtasksnativemodule:
|
React-microtasksnativemodule:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||||
|
react-native-image-picker:
|
||||||
|
:path: "../node_modules/react-native-image-picker"
|
||||||
React-NativeModulesApple:
|
React-NativeModulesApple:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
||||||
React-oscompat:
|
React-oscompat:
|
||||||
@ -2522,6 +2553,7 @@ SPEC CHECKSUMS:
|
|||||||
React-logger: 04ce9229cb57db2c2a8164eaec1105f89da7fb22
|
React-logger: 04ce9229cb57db2c2a8164eaec1105f89da7fb22
|
||||||
React-Mapbuffer: e402e7a0535b2213c50727553621480fe8cd8ade
|
React-Mapbuffer: e402e7a0535b2213c50727553621480fe8cd8ade
|
||||||
React-microtasksnativemodule: a63ce5595016996a9bac1f10c70a7a7fe6506649
|
React-microtasksnativemodule: a63ce5595016996a9bac1f10c70a7a7fe6506649
|
||||||
|
react-native-image-picker: b16541b587b275e81a12f9b82f215c5e9b0ccbf3
|
||||||
React-NativeModulesApple: b3766e1f87b08064ebc459b9e1538da2447ca874
|
React-NativeModulesApple: b3766e1f87b08064ebc459b9e1538da2447ca874
|
||||||
React-oscompat: 34f3d3c06cadcbc470bc4509c717fb9b919eaa8b
|
React-oscompat: 34f3d3c06cadcbc470bc4509c717fb9b919eaa8b
|
||||||
React-perflogger: a1edb025fd5d44f61bf09307e248f7608d7b2dcf
|
React-perflogger: a1edb025fd5d44f61bf09307e248f7608d7b2dcf
|
||||||
@ -2552,10 +2584,10 @@ SPEC CHECKSUMS:
|
|||||||
ReactAppDependencyProvider: c91900fa724baee992f01c05eeb4c9e01a807f78
|
ReactAppDependencyProvider: c91900fa724baee992f01c05eeb4c9e01a807f78
|
||||||
ReactCodegen: a55799cae416c387aeaae3aabc1bc0289ac19cee
|
ReactCodegen: a55799cae416c387aeaae3aabc1bc0289ac19cee
|
||||||
ReactCommon: 116d6ee71679243698620d8cd9a9042541e44aa6
|
ReactCommon: 116d6ee71679243698620d8cd9a9042541e44aa6
|
||||||
ReactNativeCameraKit: f6dff03bb7b545e868f35762e5e7d38076a1f424
|
ReactNativeCameraKit: 21c717ad3fc6f040b4293a07c4300aae3f1007d0
|
||||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||||
Yoga: 00013dd9cde63a2d98e8002fcc4f5ddb66c10782
|
Yoga: 00013dd9cde63a2d98e8002fcc4f5ddb66c10782
|
||||||
|
|
||||||
PODFILE CHECKSUM: 831b9773c4c6aed2643524d13cb247994d19e1e9
|
PODFILE CHECKSUM: 831b9773c4c6aed2643524d13cb247994d19e1e9
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.15.2
|
||||||
|
|||||||
@ -15,7 +15,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.81.0",
|
"react-native": "0.81.0",
|
||||||
"react-native-camera-kit": "link:../"
|
"react-native-camera-kit": "link:../",
|
||||||
|
"react-native-image-picker": "^7.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Button, Alert, Te
|
|||||||
|
|
||||||
import BarcodeScreenExample from './BarcodeScreenExample';
|
import BarcodeScreenExample from './BarcodeScreenExample';
|
||||||
import CameraExample from './CameraExample';
|
import CameraExample from './CameraExample';
|
||||||
|
import DetectQRExample from './DetectQRExample';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [example, setExample] = useState<any>(undefined);
|
const [example, setExample] = useState<any>(undefined);
|
||||||
@ -23,6 +24,7 @@ const App = () => {
|
|||||||
<Text style={styles.headerText}>React Native Camera Kit</Text>
|
<Text style={styles.headerText}>React Native Camera Kit</Text>
|
||||||
<Button title="Camera" onPress={() => setExample(<CameraExample onBack={onBack} />)}></Button>
|
<Button title="Camera" onPress={() => setExample(<CameraExample onBack={onBack} />)}></Button>
|
||||||
<Button title="Barcode Scanner" onPress={() => setExample(<BarcodeScreenExample onBack={onBack} />)}></Button>
|
<Button title="Barcode Scanner" onPress={() => setExample(<BarcodeScreenExample onBack={onBack} />)}></Button>
|
||||||
|
<Button title="Detect QR from Image" onPress={() => setExample(<DetectQRExample onBack={onBack} />)}></Button>
|
||||||
<View>
|
<View>
|
||||||
<Text style={[styles.stressHeader, { marginTop: 12 }]}>Mount Stress Test</Text>
|
<Text style={[styles.stressHeader, { marginTop: 12 }]}>Mount Stress Test</Text>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
|
|||||||
152
example/src/DetectQRExample.tsx
Normal file
152
example/src/DetectQRExample.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { StyleSheet, Text, View, TouchableOpacity, Image, ScrollView, ActivityIndicator } from 'react-native';
|
||||||
|
import { launchImageLibrary } from 'react-native-image-picker';
|
||||||
|
import { detectQRCodeInImage } from '../../src';
|
||||||
|
import SafeAreaView from './SafeAreaView';
|
||||||
|
|
||||||
|
const DetectQRExample = ({ onBack }: { onBack: () => void }) => {
|
||||||
|
const [imageUri, setImageUri] = useState<string | undefined>();
|
||||||
|
const [result, setResult] = useState<string | undefined>();
|
||||||
|
const [error, setError] = useState<string | undefined>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const onPickImage = async () => {
|
||||||
|
setResult(undefined);
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
|
const response = await launchImageLibrary({
|
||||||
|
mediaType: 'photo',
|
||||||
|
selectionLimit: 1,
|
||||||
|
includeBase64: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.didCancel || !response.assets?.length) return;
|
||||||
|
|
||||||
|
const asset = response.assets[0];
|
||||||
|
setImageUri(asset.uri);
|
||||||
|
|
||||||
|
const base64 = asset.base64;
|
||||||
|
if (!base64) {
|
||||||
|
setError('No base64 data returned from image picker');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const decoded = await detectQRCodeInImage(base64);
|
||||||
|
setResult(decoded === null ? '(no QR code found)' : decoded);
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message ?? 'Detection failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.screen}>
|
||||||
|
<SafeAreaView style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={onBack}>
|
||||||
|
<Text style={styles.backText}>Back</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.title}>Detect QR from Image</Text>
|
||||||
|
<View style={{ width: 50 }} />
|
||||||
|
</SafeAreaView>
|
||||||
|
|
||||||
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
|
<TouchableOpacity style={styles.pickButton} onPress={onPickImage}>
|
||||||
|
<Text style={styles.pickButtonText}>Pick Image</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{imageUri && <Image source={{ uri: imageUri }} style={styles.preview} resizeMode="contain" />}
|
||||||
|
|
||||||
|
{loading && <ActivityIndicator size="large" color="#ffffff" style={{ marginTop: 20 }} />}
|
||||||
|
|
||||||
|
{result !== undefined && (
|
||||||
|
<View style={styles.resultBox}>
|
||||||
|
<Text style={styles.resultLabel}>Decoded:</Text>
|
||||||
|
<Text style={styles.resultValue} selectable>
|
||||||
|
{result}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error !== undefined && (
|
||||||
|
<View style={[styles.resultBox, styles.errorBox]}>
|
||||||
|
<Text style={styles.resultLabel}>Error:</Text>
|
||||||
|
<Text style={styles.errorValue}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetectQRExample;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
screen: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#000',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
backText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 24,
|
||||||
|
},
|
||||||
|
pickButton: {
|
||||||
|
backgroundColor: '#2196F3',
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
pickButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
width: 280,
|
||||||
|
height: 280,
|
||||||
|
marginTop: 24,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#111',
|
||||||
|
},
|
||||||
|
resultBox: {
|
||||||
|
marginTop: 20,
|
||||||
|
backgroundColor: '#1a3a1a',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
errorBox: {
|
||||||
|
backgroundColor: '#3a1a1a',
|
||||||
|
},
|
||||||
|
resultLabel: {
|
||||||
|
color: '#aaa',
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
resultValue: {
|
||||||
|
color: '#4caf50',
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
errorValue: {
|
||||||
|
color: '#f44336',
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -4817,6 +4817,11 @@ react-is@^19.1.0:
|
|||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
||||||
|
react-native-image-picker@^7.1.2:
|
||||||
|
version "7.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-7.2.3.tgz#9c402591462af256cdd9aed796c28083a48f90cd"
|
||||||
|
integrity sha512-zKIZUlQNU3EtqizsXSH92zPeve4vpUrsqHu2kkpCxWE9TZhJFZBb+irDsBOY8J21k0+Edgt06TMQGJ+iPUIXyA==
|
||||||
|
|
||||||
react-native@0.81.0:
|
react-native@0.81.0:
|
||||||
version "0.81.0"
|
version "0.81.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.81.0.tgz#ebb645f3fb2fc2ffb222d2f294ca4e81e6568f15"
|
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.81.0.tgz#ebb645f3fb2fc2ffb222d2f294ca4e81e6568f15"
|
||||||
|
|||||||
@ -4,8 +4,10 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import CoreImage
|
||||||
import Foundation
|
import Foundation
|
||||||
import React
|
import React
|
||||||
|
import Vision
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Class managing the communication between React Native and the native implementation
|
* Class managing the communication between React Native and the native implementation
|
||||||
@ -60,4 +62,48 @@ import React
|
|||||||
AVCaptureDevice.requestAccess(for: .video, completionHandler: { resolve($0) })
|
AVCaptureDevice.requestAccess(for: .video, completionHandler: { resolve($0) })
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc public static func detectQRCodeInImage(_ base64: String,
|
||||||
|
resolve: @escaping RCTPromiseResolveBlock,
|
||||||
|
reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
guard let data = Data(base64Encoded: base64, options: .ignoreUnknownCharacters) else {
|
||||||
|
reject("E_INVALID_IMAGE", "Could not decode base64 image data", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
guard let ciImage = CIImage(data: data) else {
|
||||||
|
reject("E_INVALID_IMAGE", "Could not decode base64 image data", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let detector = CIDetector(ofType: CIDetectorTypeQRCode,
|
||||||
|
context: nil,
|
||||||
|
options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) else {
|
||||||
|
reject("E_QR_DETECTION_FAILED", "Could not initialize QR detector", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let features = detector.features(in: ciImage) as? [CIQRCodeFeature]
|
||||||
|
let value = features?.first?.messageString
|
||||||
|
resolve(value?.isEmpty == false ? value : nil)
|
||||||
|
#else
|
||||||
|
guard let uiImage = UIImage(data: data),
|
||||||
|
let cgImage = uiImage.cgImage else {
|
||||||
|
reject("E_INVALID_IMAGE", "Could not decode base64 image data", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let request = VNDetectBarcodesRequest()
|
||||||
|
request.symbologies = [.qr]
|
||||||
|
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
||||||
|
do {
|
||||||
|
try handler.perform([request])
|
||||||
|
} catch {
|
||||||
|
reject("E_QR_DETECTION_FAILED", "Vision request failed: \(error.localizedDescription)", error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let value = request.results?.first?.payloadStringValue
|
||||||
|
resolve(value?.isEmpty == false ? value : nil)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,10 @@ RCT_EXPORT_METHOD(capture:(NSDictionary *)options tag:(nonnull NSNumber *)tag re
|
|||||||
[CKCameraManager requestDeviceCameraAuthorization:resolve reject:reject];
|
[CKCameraManager requestDeviceCameraAuthorization:resolve reject:reject];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(detectQRCodeInImage:(NSString *)base64 resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
||||||
|
[CKCameraManager detectQRCodeInImage:base64 resolve:resolve reject:reject];
|
||||||
|
}
|
||||||
|
|
||||||
// Thanks to this guard, we won't compile this code when we build for the old architecture.
|
// Thanks to this guard, we won't compile this code when we build for the old architecture.
|
||||||
#ifdef RCT_NEW_ARCH_ENABLED
|
#ifdef RCT_NEW_ARCH_ENABLED
|
||||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"registry": "https://registry.npmjs.org/"
|
"registry": "https://registry.npmjs.org/"
|
||||||
},
|
},
|
||||||
"version": "17.0.3",
|
"version": "17.0.4",
|
||||||
"description": "A high performance, fully featured, rock solid camera library for React Native applications",
|
"description": "A high performance, fully featured, rock solid camera library for React Native applications",
|
||||||
"nativePackage": true,
|
"nativePackage": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -54,9 +54,11 @@
|
|||||||
"@react-native/eslint-config": "0.79.0",
|
"@react-native/eslint-config": "0.79.0",
|
||||||
"@react-native/metro-config": "0.79.0",
|
"@react-native/metro-config": "0.79.0",
|
||||||
"@react-native/typescript-config": "0.79.0",
|
"@react-native/typescript-config": "0.79.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-test-renderer": "^19.0.0",
|
"@types/react-test-renderer": "^19.0.0",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
|
"jest": "^30.3.0",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-native": "0.79.0",
|
"react-native": "0.79.0",
|
||||||
|
|||||||
51
src/__tests__/detectQRCodeInImage.test.tsx
Normal file
51
src/__tests__/detectQRCodeInImage.test.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
jest.mock('react-native', () => ({
|
||||||
|
NativeModules: { CameraKit: {} },
|
||||||
|
Platform: { OS: 'ios' },
|
||||||
|
TurboModuleRegistry: { getEnforcing: jest.fn(() => ({})) },
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../specs/NativeCameraKitModule', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
detectQRCodeInImage: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import NativeCameraKitModule from '../specs/NativeCameraKitModule';
|
||||||
|
import { detectQRCodeInImage } from '../index';
|
||||||
|
|
||||||
|
const mockedDetect = NativeCameraKitModule.detectQRCodeInImage as jest.Mock;
|
||||||
|
|
||||||
|
describe('detectQRCodeInImage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedDetect.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards the base64 argument to the native module', async () => {
|
||||||
|
mockedDetect.mockResolvedValueOnce('decoded-value');
|
||||||
|
|
||||||
|
await detectQRCodeInImage('base64-data');
|
||||||
|
|
||||||
|
expect(mockedDetect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockedDetect).toHaveBeenCalledWith('base64-data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves with the decoded string from the native module', async () => {
|
||||||
|
mockedDetect.mockResolvedValueOnce('https://example.com');
|
||||||
|
|
||||||
|
await expect(detectQRCodeInImage('xyz')).resolves.toBe('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves with null when no QR code was found', async () => {
|
||||||
|
mockedDetect.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
await expect(detectQRCodeInImage('xyz')).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('propagates native rejections (e.g. invalid image)', async () => {
|
||||||
|
const err = new Error('Could not decode base64 image data');
|
||||||
|
mockedDetect.mockRejectedValueOnce(err);
|
||||||
|
|
||||||
|
await expect(detectQRCodeInImage('xyz')).rejects.toBe(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { NativeModules } from 'react-native';
|
import { NativeModules } from 'react-native';
|
||||||
|
|
||||||
import Camera from './Camera';
|
import Camera from './Camera';
|
||||||
|
import NativeCameraKitModule from './specs/NativeCameraKitModule';
|
||||||
import {
|
import {
|
||||||
CameraType,
|
CameraType,
|
||||||
type CameraApi,
|
type CameraApi,
|
||||||
@ -15,6 +16,10 @@ import {
|
|||||||
|
|
||||||
const { CameraKit } = NativeModules;
|
const { CameraKit } = NativeModules;
|
||||||
|
|
||||||
|
export const detectQRCodeInImage = (base64: string): Promise<string | null> => {
|
||||||
|
return NativeCameraKitModule.detectQRCodeInImage(base64);
|
||||||
|
};
|
||||||
|
|
||||||
// Start with portrait/pointing up, increment while moving counter-clockwise
|
// Start with portrait/pointing up, increment while moving counter-clockwise
|
||||||
export const Orientation = {
|
export const Orientation = {
|
||||||
PORTRAIT: 0, // ⬆️
|
PORTRAIT: 0, // ⬆️
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export interface Spec extends TurboModule {
|
|||||||
capture(options?: UnsafeObject, tag?: Double): Promise<CaptureData>;
|
capture(options?: UnsafeObject, tag?: Double): Promise<CaptureData>;
|
||||||
requestDeviceCameraAuthorization: () => Promise<boolean>;
|
requestDeviceCameraAuthorization: () => Promise<boolean>;
|
||||||
checkDeviceCameraAuthorizationStatus: () => Promise<boolean>;
|
checkDeviceCameraAuthorizationStatus: () => Promise<boolean>;
|
||||||
|
detectQRCodeInImage(base64: string): Promise<string | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TurboModuleRegistry.getEnforcing<Spec>('RNCameraKitModule');
|
export default TurboModuleRegistry.getEnforcing<Spec>('RNCameraKitModule');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user