Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ed049a62d | ||
|
|
58ff00300c | ||
|
|
e6dd85b4ea | ||
|
|
75117f1287 | ||
|
|
4323cb608d | ||
|
|
448cf1a9e0 | ||
|
|
a8535da0d3 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -85,6 +85,7 @@ example/.yarn/*
|
||||
ios/build/
|
||||
ios/DerivedData/
|
||||
dist/
|
||||
.codegen/
|
||||
### Xcode ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
.codegen/
|
||||
old-example/
|
||||
example/
|
||||
example-js-code/
|
||||
|
||||
@ -180,7 +180,7 @@ The library is published to npm as `react-native-camera-kit` with the `files` ar
|
||||
|
||||
**Last synchronized upstream commit**: 8e5149a6e6d3902ae87dad50da0d06ec2c61d2b8
|
||||
**Upstream version**: 17.0.1
|
||||
**Fork version**: 17.0.1
|
||||
**Fork version**: 17.0.3
|
||||
**Last sync date**: 2026-01-17
|
||||
**Sync status**: success
|
||||
|
||||
@ -202,7 +202,7 @@ The library is published to npm as `react-native-camera-kit` with the `files` ar
|
||||
### Fork-Specific Code Preserved
|
||||
|
||||
All QR-only Android architecture preserved:
|
||||
- `android/build.gradle` - Uses `implementation 'com.github.limpbrains:qr:v0.0.2'`
|
||||
- `android/build.gradle` - Uses `implementation 'com.github.limpbrains:qr:v0.0.3'`
|
||||
- `android/src/main/java/com/rncamerakit/QRCodeAnalyzer.kt` - Uses `QRDecoder.decode()` from limpbrains/qr
|
||||
- `android/src/main/java/com/rncamerakit/CodeFormat.kt` - Simplified enum (no ML Kit conversions)
|
||||
- `android/src/main/java/com/rncamerakit/CKCamera.kt` - String callback `onQRCodeDetected(String)`
|
||||
|
||||
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_
|
||||
|
||||
@ -66,7 +66,7 @@ dependencies {
|
||||
// If you want to additionally use the CameraX Extensions library
|
||||
// implementation "androidx.camera:camera-extensions:${camerax_version}"
|
||||
|
||||
implementation 'com.github.limpbrains:qr:v0.0.2'
|
||||
implementation 'com.github.limpbrains:qr:v0.0.3'
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
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.1):
|
||||
- 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,7 +2584,7 @@ SPEC CHECKSUMS:
|
||||
ReactAppDependencyProvider: c91900fa724baee992f01c05eeb4c9e01a807f78
|
||||
ReactCodegen: a55799cae416c387aeaae3aabc1bc0289ac19cee
|
||||
ReactCommon: 116d6ee71679243698620d8cd9a9042541e44aa6
|
||||
ReactNativeCameraKit: ee0b7f2b655e3416881772468fd4f8c70ec73c9d
|
||||
ReactNativeCameraKit: 21c717ad3fc6f040b4293a07c4300aae3f1007d0
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
Yoga: 00013dd9cde63a2d98e8002fcc4f5ddb66c10782
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,14 +483,6 @@ public class CameraView: UIView {
|
||||
return allowedTypes.compactMap { CodeFormat(rawValue: $0) }
|
||||
}
|
||||
|
||||
private func convertAllowedBarcodeTypes() -> [CodeFormat] {
|
||||
guard let allowedTypes = allowedBarcodeTypes as? [String], !allowedTypes.isEmpty else {
|
||||
return CodeFormat.allCases
|
||||
}
|
||||
|
||||
return allowedTypes.compactMap { CodeFormat(rawValue: $0) }
|
||||
}
|
||||
|
||||
// MARK: - Gesture selectors
|
||||
|
||||
@objc func handlePinchToZoomRecognizer(_ pinchRecognizer: UIPinchGestureRecognizer) {
|
||||
|
||||
@ -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:
|
||||
|
||||
16
package.json
16
package.json
@ -7,12 +7,13 @@
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"version": "17.0.1",
|
||||
"version": "17.0.4",
|
||||
"description": "A high performance, fully featured, rock solid camera library for React Native applications",
|
||||
"nativePackage": true,
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"clean": "rm -rf dist/",
|
||||
"clean": "rm -rf dist/ build/ .codegen/ android/app/build/",
|
||||
"prepare": "tsc --project tsconfig.json",
|
||||
"test": "jest",
|
||||
"lint": "yarn eslint -c .eslintrc.js",
|
||||
"check-ios": "scripts/check-ios.sh",
|
||||
@ -20,7 +21,7 @@
|
||||
"release:beta": "yarn clean && yarn build && yarn publish --tag beta --verbose",
|
||||
"release:local": "yarn clean && yarn build && tmp=$(mktemp) && yarn pack --filename $tmp.tar.gz && open -R $tmp.tar.gz",
|
||||
"start": "watchman watch-del-all && node node_modules/react-native/local-cli/cli.js start",
|
||||
"codegen": "react-native codegen --verbose --path . --platform ios --source library && react-native codegen --verbose --path . --platform android --source library",
|
||||
"codegen": "rm -rf .codegen/ && react-native codegen --verbose --path . --platform ios --outputPath .codegen --source library && react-native codegen --verbose --path . --platform android --outputPath .codegen --source library",
|
||||
"bootstrap": "cd example/ && bundle install && yarn && cd ios/ && bundle exec pod install",
|
||||
"bootstrap-linux": "cd example/ && yarn"
|
||||
},
|
||||
@ -30,8 +31,11 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"react-native": "src/index",
|
||||
"files": [
|
||||
"android",
|
||||
"build",
|
||||
"android/build.gradle",
|
||||
"android/gradle",
|
||||
"android/gradlew",
|
||||
"android/gradlew.bat",
|
||||
"android/src",
|
||||
"dist",
|
||||
"ios",
|
||||
"src",
|
||||
@ -50,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