Use React Native
The @rustors/react-native package provides native bridges and TypeScript typings for React Native iOS and Android applications. Written natively in Objective-C++ on iOS and C++/JNI on Android, it delivers zero-overhead memory bridging and native compatibility across all React Native versions.
Installation
npm install @rustors/react-native
# or yarn add @rustors/react-native / pnpm add @rustors/react-native
iOS Configuration Deep Dive
1. Zero-Friction Native Implementation
The iOS native module is authored in Objective-C++ (.mm). This eliminates toolchain-dependent Swift linker issues (such as Swift runtime library embedding or requiring host applications to provide dummy .swift bridging stubs). It links natively into both Objective-C and Swift applications without extra flags.
2. Dual Architecture Support (New Architecture & Paper)
react-native-rusto automatically adapts to your project's architecture:
- New Architecture (TurboModule): When
RCT_NEW_ARCH_ENABLED=1is set, the module compiles against React Native's C++ TurboModule interface with synchronous method bindings and JSI performance. - Old Architecture (Paper): Uses standard asynchronous
RCTBridgeModuleevent loops with zero deprecation warnings.
3. CocoaPods Setup (ios/Podfile)
Add the core dependency and choose your desired decoupled OCR model bundle:
target 'YourApp' do
# Compatible with both dynamic frameworks and static linkage:
use_frameworks! :linkage => :static
# Model bundle pod (choose one or more):
pod 'RustO-Models-PPOCRv6-Tiny', '~> 0.3.2' # ~6 MB (Recommended default)
# pod 'RustO-Models-PPOCRv6-Small', '~> 0.3.2' # ~30 MB
# pod 'RustO-Models-PPOCRv5-Mobile', '~> 0.3.2' # ~28 MB
end
Then install the CocoaPods dependencies:
cd ios && pod install
4. Camera & Photo Permissions (ios/YourApp/Info.plist)
When integrating camera feeds or gallery image pickers (e.g. react-native-image-picker or react-native-vision-camera), declare the required permissions:
<key>NSCameraUsageDescription</key>
<string>This app requires camera access to scan physical documents with RustO! OCR.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires photo library access to import images for text recognition.</string>
5. Expo Configuration (Bare Workflow & Config Plugins)
Because RustO! relies on native compiled binaries, standard Expo Go cannot run it. Use Expo Prebuild or development builds:
npx expo run:ios
In app.json:
{
"expo": {
"plugins": [
[
"expo-image-picker",
{
"photosPermission": "The app accesses your photos to scan receipts with RustO!."
}
]
]
}
}
Android Configuration
In your root android/settings.gradle (or android/build.gradle), ensure the JitPack Maven repository is enabled:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
In android/app/build.gradle, add the model bundle matching your chosen tier:
dependencies {
implementation 'com.github.byrizki.rusto-rs:rusto-models-ppocrv6-tiny:0.3.2'
}
1. Engine Initialization
Call initialize() once during application boot (e.g., in your root App.tsx or a dedicated provider):
import { initialize } from '@rustors/react-native';
// 1. Initialize with default bundled PP-OCRv6 Tiny models
await initialize({ preset: 'ppv6' });
// 2. Or initialize with custom model file paths:
await initialize({
preset: 'ppv6',
models: {
detection: 'custom_det.rten',
recognition: 'custom_rec.rten',
dictionary: 'custom_dict.txt',
},
});
2. Input Sources on iOS & Android
detectText supports multiple input source representations:
import { detectText } from '@rustors/react-native';
// 1. File URI (file://, or Android content://)
const r1 = await detectText({ uri: 'file:///var/mobile/Containers/Data/.../receipt.jpg' });
// 2. Base64 Encoded Image Data
const r2 = await detectText({ base64: base64DataString });
// 3. Raw Bytes (Uint8Array)
const r3 = await detectText({ bytes: rawUint8Array });
!TIP On iOS, passing local
file://URIs obtained directly from image pickers or camera caches avoids copying large base64 strings across the JavaScript bridge, maximizing frame rates and minimizing memory allocations.
3. Output Modes & Options
import { detectText, TextResult } from '@rustors/react-native';
// Mode 1: Line detection (Default) -> returns TextResult[]
const lines: TextResult[] = await detectText(
{ uri: imageUri },
{ output: 'lines', textScore: 0.5 }
);
// Mode 2: Word-level detection -> returns TextResult[]
const words: TextResult[] = await detectText(
{ uri: imageUri },
{ output: 'words', wordXThreshold: 0.4 }
);
// Mode 3: Spatial layout reconstruction -> returns string
const spatialText: string = await detectText(
{ uri: imageUri },
{ output: 'spatial', lineYThreshold: 0.5, wordXThreshold: 0.4 }
);
4. Complete React Native Scanner Component
Here is a full production-ready component featuring image picking, structured line rendering, and spatial invoice reconstruction:
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
Image,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { launchImageLibrary } from 'react-native-image-picker';
import { detectText, initialize, TextResult } from '@rustors/react-native';
export default function DocumentScannerScreen() {
const [imageUri, setImageUri] = useState<string | null>(null);
const [lines, setLines] = useState<TextResult[]>([]);
const [spatialText, setSpatialText] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [mode, setMode] = useState<'lines' | 'spatial'>('lines');
useEffect(() => {
initialize({ preset: 'ppv6' }).catch((err) =>
console.error('RustO Initialization Failed:', err)
);
}, []);
const handlePickImage = async () => {
const result = await launchImageLibrary({ mediaType: 'photo', quality: 0.9 });
if (result.assets && result.assets[0]?.uri) {
const uri = result.assets[0].uri;
setImageUri(uri);
processOcr(uri);
}
};
const processOcr = async (uri: string) => {
setLoading(true);
try {
if (mode === 'lines') {
const results = await detectText(
{ uri },
{
output: 'lines',
maxSideLen: 1600,
postprocess: { useDilation: true },
}
);
setLines(results as TextResult[]);
setSpatialText('');
} else {
const formatted = await detectText(
{ uri },
{
output: 'spatial',
lineYThreshold: 0.5,
wordXThreshold: 0.4,
}
);
setSpatialText(formatted as string);
setLines([]);
}
} catch (error) {
console.error('OCR Error:', error);
} finally {
setLoading(false);
}
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<TouchableOpacity style={styles.btn} onPress={handlePickImage}>
<Text style={styles.btnText}>Pick Document</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, styles.toggleBtn]}
onPress={() => {
const nextMode = mode === 'lines' ? 'spatial' : 'lines';
setMode(nextMode);
if (imageUri) processOcr(imageUri);
}}
>
<Text style={styles.btnText}>Mode: {mode.toUpperCase()}</Text>
</TouchableOpacity>
</View>
{loading && <ActivityIndicator size="large" color="#f97316" style={{ marginVertical: 12 }} />}
<ScrollView style={styles.content}>
{imageUri && (
<View style={styles.imageWrapper}>
<Image source={{ uri: imageUri }} style={styles.previewImage} resizeMode="contain" />
</View>
)}
<View style={styles.resultContainer}>
{mode === 'spatial' ? (
<Text style={styles.spatialCode}>{spatialText || 'No spatial text reconstructed.'}</Text>
) : (
lines.map((item, idx) => (
<View key={idx} style={styles.lineItem}>
<Text style={styles.lineText}>{item.text}</Text>
<Text style={styles.lineScore}>({(item.score * 100).toFixed(0)}%)</Text>
</View>
))
)}
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#09090b' },
header: { flexDirection: 'row', padding: 12, gap: 8 },
btn: { flex: 1, backgroundColor: '#f97316', padding: 12, borderRadius: 8, alignItems: 'center' },
toggleBtn: { backgroundColor: '#27272a' },
btnText: { color: '#ffffff', fontWeight: 'bold' },
content: { flex: 1, padding: 12 },
imageWrapper: {
width: '100%',
height: 240,
backgroundColor: '#18181b',
borderRadius: 8,
marginBottom: 12,
},
previewImage: { width: '100%', height: '100%' },
resultContainer: { padding: 12, backgroundColor: '#18181b', borderRadius: 8 },
lineItem: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 4,
borderBottomWidth: 1,
borderBottomColor: '#27272a',
},
lineText: { color: '#fafafa', fontSize: 14, flex: 1 },
lineScore: { color: '#a1a1aa', fontSize: 12, marginLeft: 8 },
spatialCode: { color: '#38bdf8', fontFamily: 'monospace', fontSize: 13, lineHeight: 18 },
});