How To

Use React Native

How to use @rustors/react-native in React Native applications for iOS and Android, covering New Architecture (TurboModules), CocoaPods, and Expo.

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=1 is set, the module compiles against React Native's C++ TurboModule interface with synchronous method bindings and JSI performance.
  • Old Architecture (Paper): Uses standard asynchronous RCTBridgeModule event 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 },
});