Menggunakan React Native
Paket @rustors/react-native menyediakan bridge native dan pengetikan TypeScript untuk aplikasi React Native iOS dan Android. Ditulis secara native menggunakan Objective-C++ di iOS dan C++/JNI di Android, pustaka ini menyuguhkan kompatibilitas optimal di seluruh versi React Native tanpa overhead memori.
Instalasi
npm install @rustors/react-native
# atau yarn add @rustors/react-native / pnpm add @rustors/react-native
Pembahasan Mendalam Konfigurasi iOS
1. Implementasi Native Bebas Hambatan Toolchain
Modul native iOS ditulis sepenuhnya dalam Objective-C++ (.mm). Hal ini meniadakan kendala linker yang biasanya muncul pada modul berbasis Swift murni (seperti penanaman runtime library Swift atau keharusan membuat berkas dummy .swift pada aplikasi host). Modul ini dapat ditautkan ke proyek Objective-C maupun Swift tanpa konfigurasi tambahan.
2. Dukungan Ganda: New Architecture & Paper
react-native-rusto secara otomatis menyesuaikan dengan arsitektur proyek Anda:
- New Architecture (TurboModule): Ketika
RCT_NEW_ARCH_ENABLED=1aktif, modul dikompilasi menggunakan antarmuka C++ TurboModule React Native untuk performa pemanggilan sinkron JSI terbaik. - Old Architecture (Paper): Menggunakan loop event asinkron standar
RCTBridgeModuletanpa peringatan usang (deprecation).
3. Konfigurasi CocoaPods (ios/Podfile)
Tambahkan dependensi modul dan tentukan bundel model OCR pilihan Anda:
target 'YourApp' do
# Kompatibel dengan framework dinamis maupun statis:
use_frameworks! :linkage => :static
# Pod bundel model (pilih satu atau lebih):
pod 'RustO-Models-PPOCRv6-Tiny', '~> 0.3.2' # ~6 MB (Direkomendasikan)
# pod 'RustO-Models-PPOCRv6-Small', '~> 0.3.2' # ~30 MB
# pod 'RustO-Models-PPOCRv5-Mobile', '~> 0.3.2' # ~28 MB
end
Kemudian jalankan instalasi CocoaPods:
cd ios && pod install
4. Konfigurasi Izin Kamera & Galeri (ios/YourApp/Info.plist)
Jika menggunakan kamera atau pemilih galeri (misalnya react-native-image-picker), deklarasikan izin yang diperlukan:
<key>NSCameraUsageDescription</key>
<string>Aplikasi ini memerlukan akses kamera untuk memindai dokumen fisik dengan RustO! OCR.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Aplikasi ini memerlukan akses galeri foto untuk memilih gambar dokumen yang akan dipindai.</string>
5. Konfigurasi Expo (Bare Workflow & Config Plugins)
Karena RustO! bergantung pada biner terkompilasi native, aplikasi tidak dapat berjalan pada Expo Go standar. Gunakan Expo Prebuild atau development build:
npx expo run:ios
Konfigurasi pada app.json:
{
"expo": {
"plugins": [
[
"expo-image-picker",
{
"photosPermission": "Aplikasi membutuhkan izin galeri untuk memindai dokumen struk."
}
]
]
}
}
Konfigurasi Android
Pada android/settings.gradle (atau android/build.gradle), pastikan repositori JitPack telah didaftarkan:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
Pada android/app/build.gradle, tambahkan dependensi paket model yang sesuai:
dependencies {
implementation 'com.github.byrizki.rusto-rs:rusto-models-ppocrv6-tiny:0.3.2'
}
1. Inisialisasi Mesin
Panggil initialize() satu kali saat aplikasi pertama kali dimuat:
import { initialize } from '@rustors/react-native';
// 1. Inisialisasi dengan model bawaan PP-OCRv6 Tiny
await initialize({ preset: 'ppv6' });
// 2. Atau inisialisasi dengan berkas model kustom:
await initialize({
preset: 'ppv6',
models: {
detection: 'custom_det.rten',
recognition: 'custom_rec.rten',
dictionary: 'custom_dict.txt',
},
});
2. Sumber Masukan Gambar di iOS & Android
detectText mendukung beragam representasi sumber gambar:
import { detectText } from '@rustors/react-native';
// 1. File URI (file://, atau Android content://)
const r1 = await detectText({ uri: 'file:///var/mobile/Containers/Data/.../receipt.jpg' });
// 2. Data Gambar Terenkode Base64
const r2 = await detectText({ base64: base64DataString });
// 3. Buffer Byte Mentah (Uint8Array)
const r3 = await detectText({ bytes: rawUint8Array });
!TIP Di iOS, meneruskan URI
file://lokal yang diperoleh langsung dari image picker atau cache kamera menghindari proses penyalinan string base64 berukuran besar melintasi jembatan JavaScript, sehingga menghemat alokasi memori secara signifikan.
3. Mode Keluaran & Opsi Deteksi
import { detectText, TextResult } from '@rustors/react-native';
// Mode 1: Deteksi Baris Teks (Default) -> mengembalikan TextResult[]
const lines: TextResult[] = await detectText(
{ uri: imageUri },
{ output: 'lines', textScore: 0.5 }
);
// Mode 2: Deteksi Tingkat Kata -> mengembalikan TextResult[]
const words: TextResult[] = await detectText(
{ uri: imageUri },
{ output: 'words', wordXThreshold: 0.4 }
);
// Mode 3: Rekonstruksi Tata Letak Spasial -> mengembalikan string
const spatialText: string = await detectText(
{ uri: imageUri },
{ output: 'spatial', lineYThreshold: 0.5, wordXThreshold: 0.4 }
);
4. Contoh Komponen Pemindai Dokumen di React Native
Berikut adalah contoh komponen siap pakai dengan pemilihan foto, tampilan daftar baris terdeteksi, dan rekonstruksi spasial dokumen:
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('Inisialisasi RustO Gagal:', 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('Kesalahan OCR:', error);
} finally {
setLoading(false);
}
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<TouchableOpacity style={styles.btn} onPress={handlePickImage}>
<Text style={styles.btnText}>Pilih Dokumen</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 || 'Belum ada teks spasial.'}</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 },
});