Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ed049a62d | ||
|
|
58ff00300c |
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` |
|
||||
| `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
|
||||
|
||||
_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.uimanager.UIManagerHelper
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import com.rncamerakit.NativeCameraKitModuleSpec
|
||||
|
||||
@ -38,6 +39,8 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Nat
|
||||
const val LANDSCAPE_RIGHT = 3 // ➡️
|
||||
|
||||
const val REACT_CLASS = "RNCameraKitModule"
|
||||
|
||||
private val qrDecodeExecutor = Executors.newCachedThreadPool()
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
@ -62,6 +65,19 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Nat
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@ -2,6 +2,7 @@ package com.rncamerakit
|
||||
|
||||
import com.facebook.react.bridge.*
|
||||
import com.facebook.react.uimanager.UIManagerHelper
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* 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 REACT_CLASS = "RNCameraKitModule"
|
||||
|
||||
private val qrDecodeExecutor = Executors.newCachedThreadPool()
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
@ -60,6 +63,19 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Rea
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
<string>3B52.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
|
||||
@ -1719,6 +1719,34 @@ PODS:
|
||||
- React-RCTFBReactNativeSpec
|
||||
- ReactCommon/turbomodule/core
|
||||
- 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):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@ -2223,7 +2251,7 @@ PODS:
|
||||
- React-perflogger (= 0.81.0)
|
||||
- React-utils (= 0.81.0)
|
||||
- SocketRocket
|
||||
- ReactNativeCameraKit (17.0.3):
|
||||
- ReactNativeCameraKit (17.0.4):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@ -2296,6 +2324,7 @@ DEPENDENCIES:
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
||||
- 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-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||
@ -2416,6 +2445,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
React-microtasksnativemodule:
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||
react-native-image-picker:
|
||||
:path: "../node_modules/react-native-image-picker"
|
||||
React-NativeModulesApple:
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
||||
React-oscompat:
|
||||
@ -2522,6 +2553,7 @@ SPEC CHECKSUMS:
|
||||
React-logger: 04ce9229cb57db2c2a8164eaec1105f89da7fb22
|
||||
React-Mapbuffer: e402e7a0535b2213c50727553621480fe8cd8ade
|
||||
React-microtasksnativemodule: a63ce5595016996a9bac1f10c70a7a7fe6506649
|
||||
react-native-image-picker: b16541b587b275e81a12f9b82f215c5e9b0ccbf3
|
||||
React-NativeModulesApple: b3766e1f87b08064ebc459b9e1538da2447ca874
|
||||
React-oscompat: 34f3d3c06cadcbc470bc4509c717fb9b919eaa8b
|
||||
React-perflogger: a1edb025fd5d44f61bf09307e248f7608d7b2dcf
|
||||
@ -2552,10 +2584,10 @@ SPEC CHECKSUMS:
|
||||
ReactAppDependencyProvider: c91900fa724baee992f01c05eeb4c9e01a807f78
|
||||
ReactCodegen: a55799cae416c387aeaae3aabc1bc0289ac19cee
|
||||
ReactCommon: 116d6ee71679243698620d8cd9a9042541e44aa6
|
||||
ReactNativeCameraKit: f6dff03bb7b545e868f35762e5e7d38076a1f424
|
||||
ReactNativeCameraKit: 21c717ad3fc6f040b4293a07c4300aae3f1007d0
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
Yoga: 00013dd9cde63a2d98e8002fcc4f5ddb66c10782
|
||||
|
||||
PODFILE CHECKSUM: 831b9773c4c6aed2643524d13cb247994d19e1e9
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
"dependencies": {
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.0",
|
||||
"react-native-camera-kit": "link:../"
|
||||
"react-native-camera-kit": "link:../",
|
||||
"react-native-image-picker": "^7.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
|
||||
@ -3,6 +3,7 @@ import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Button, Alert, Te
|
||||
|
||||
import BarcodeScreenExample from './BarcodeScreenExample';
|
||||
import CameraExample from './CameraExample';
|
||||
import DetectQRExample from './DetectQRExample';
|
||||
|
||||
const App = () => {
|
||||
const [example, setExample] = useState<any>(undefined);
|
||||
@ -23,6 +24,7 @@ const App = () => {
|
||||
<Text style={styles.headerText}>React Native Camera Kit</Text>
|
||||
<Button title="Camera" onPress={() => setExample(<CameraExample 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>
|
||||
<Text style={[styles.stressHeader, { marginTop: 12 }]}>Mount Stress Test</Text>
|
||||
<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"
|
||||
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:
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.81.0.tgz#ebb645f3fb2fc2ffb222d2f294ca4e81e6568f15"
|
||||
|
||||
@ -4,8 +4,10 @@
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import CoreImage
|
||||
import Foundation
|
||||
import React
|
||||
import Vision
|
||||
|
||||
/*
|
||||
* Class managing the communication between React Native and the native implementation
|
||||
@ -60,4 +62,48 @@ import React
|
||||
AVCaptureDevice.requestAccess(for: .video, completionHandler: { resolve($0) })
|
||||
#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];
|
||||
}
|
||||
|
||||
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.
|
||||
#ifdef RCT_NEW_ARCH_ENABLED
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"publishConfig": {
|
||||
"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",
|
||||
"nativePackage": true,
|
||||
"scripts": {
|
||||
@ -54,9 +54,11 @@
|
||||
"@react-native/eslint-config": "0.79.0",
|
||||
"@react-native/metro-config": "0.79.0",
|
||||
"@react-native/typescript-config": "0.79.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-test-renderer": "^19.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
"jest": "^30.3.0",
|
||||
"prettier": "2.8.8",
|
||||
"react": "19.0.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 Camera from './Camera';
|
||||
import NativeCameraKitModule from './specs/NativeCameraKitModule';
|
||||
import {
|
||||
CameraType,
|
||||
type CameraApi,
|
||||
@ -15,6 +16,10 @@ import {
|
||||
|
||||
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
|
||||
export const Orientation = {
|
||||
PORTRAIT: 0, // ⬆️
|
||||
|
||||
@ -18,6 +18,7 @@ export interface Spec extends TurboModule {
|
||||
capture(options?: UnsafeObject, tag?: Double): Promise<CaptureData>;
|
||||
requestDeviceCameraAuthorization: () => Promise<boolean>;
|
||||
checkDeviceCameraAuthorizationStatus: () => Promise<boolean>;
|
||||
detectQRCodeInImage(base64: string): Promise<string | null>;
|
||||
}
|
||||
|
||||
export default TurboModuleRegistry.getEnforcing<Spec>('RNCameraKitModule');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user