How To

Use iOS (Swift)

Integrating RustO! into iOS Swift and SwiftUI applications via Swift Package Manager (SPM) or CocoaPods.

RustO! provides first-class iOS and macOS support via a universal XCFramework and idiomatic Swift wrapper. It supports Apple Silicon (arm64), Intel (x86_64), and iOS Simulators with zero C++ toolchain requirements.


Installation

You can integrate RustO! into your iOS project using either Swift Package Manager (SPM) or CocoaPods.

In Xcode

  1. Open your project in Xcode.
  2. Navigate to File > Add Package Dependencies...
  3. Enter the repository URL:
    https://github.com/byrizki/rusto-rs.git
    
  4. Select RustO and set the dependency rule to your preferred version (e.g., 0.3.2).

In Package.swift

If authoring a Swift package or modular application:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyScannerApp",
    platforms: [
        .iOS(.v13),
        .macOS(.v11)
    ],
    dependencies: [
        .package(url: "https://github.com/byrizki/rusto-rs.git", from: "0.3.2")
    ],
    targets: [
        .target(
            name: "MyScannerApp",
            dependencies: [
                .product(name: "RustO", package: "rusto-rs")
            ]
        )
    ]
)

The package automatically exposes both the low-level C FFI module (RustOFFI) and high-level Swift API (RustO).


Method B: CocoaPods

In your project's Podfile:

target 'YourApp' do
  use_frameworks!

  # Core OCR engine runtime XCFramework
  pod 'RustO', '~> 0.3.2'

  # Decoupled pre-packaged model bundle (~6 MB total)
  pod 'RustO-Models-PPOCRv6-Tiny', '~> 0.3.2'
  # Or larger model tiers:
  # pod 'RustO-Models-PPOCRv6-Small'
  # pod 'RustO-Models-PPOCRv5-Mobile'
end

Then install the dependencies:

pod install

1. Permissions Configuration (Info.plist)

If capturing images from the camera or photo library, declare the appropriate usage descriptions in your app's Info.plist:

<key>NSCameraUsageDescription</key>
<string>Allow access to the camera to scan documents and extract text.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow access to photo library to select document images for OCR.</string>

2. Engine Initialization & Model Resolution

Option 1: Automatic Bundled Model Resolution

When using official model pods (e.g. RustO-Models-PPOCRv6-Tiny), RustO! locates model weights inside the bundle automatically:

import RustO

do {
    // Automatically locates bundled PP-OCRv6 det.rten, rec.rten, and dict.txt
    let ocr = try RustO.initialize()
} catch {
    print("OCR initialization failed: \(error)")
}

Option 2: Custom Model Bundle or Filesystem Path

To supply custom models bundled in your app's main bundle or downloaded into Application Support:

import Foundation
import RustO

func createCustomEngine() throws -> RustO {
    guard let detPath = Bundle.main.path(forResource: "custom_det", ofType: "rten"),
          let recPath = Bundle.main.path(forResource: "custom_rec", ofType: "rten"),
          let dictPath = Bundle.main.path(forResource: "custom_dict", ofType: "txt") else {
        throw NSError(domain: "RustO", code: -1, userInfo: [NSLocalizedDescriptionKey: "Model files missing from bundle"])
    }

    let config = InitializeConfig(
        template: "ppv6",
        detection: DetectionConfig(modelPath: detPath),
        recognition: RecognitionConfig(
            modelPath: recPath,
            dictPath: dictPath
        )
    )

    return try RustO.initialize(config: config)
}

3. Input Sources

ImageSource supports local files, URLs, or in-memory image byte buffers:

import UIKit
import RustO

// 1. Local filesystem path or file URL
let fileSource = ImageSource.uri("/path/to/invoice.png")

// 2. UIImage converted to JPEG or PNG data
guard let image = UIImage(named: "receipt"),
      let imageData = image.jpegData(compressionQuality: 0.95) else { return }

let memorySource = ImageSource.bytes(imageData)

4. Request-Local Options (OcrRunOptions)

Fine-tune recognition parameters per request:

let options = OcrRunOptions(
    output: .lines,            // .lines, .words, or .spatial
    lineYThreshold: 0.5,       // Line clustering threshold (0.0 - 1.0)
    wordXThreshold: 0.4,       // Word separation threshold (0.0 - 1.0)
    textScore: 0.50,           // Recognition confidence cutoff
    maxSideLen: 1600,          // Downscale bound for ultra-high-res images
    detection: DetectionRunOptions(
        limitSideLen: 960,
        limitType: "max"
    ),
    postprocess: PostprocessRunOptions(
        useDilation: true,     // Morphological dilation for faint text receipts
        unclipRatio: 1.8       // Bounding box expansion ratio
    )
)

5. Structured vs Spatial Recognition

Structured Line Detection

Returns coordinates, polygon vertices, and recognition score for each line:

let result = try ocr.detectText(memorySource, options: OcrRunOptions(output: .lines))

if case .structured(let items) = result {
    for line in items {
        print("[\(String(format: "%.2f", line.score))] \(line.text)")
        print("  Frame: X=\(line.frame.left), Y=\(line.frame.top), W=\(line.frame.width), H=\(line.frame.height)")
        print("  Polygon Quads: \(line.boxPoints)")
    }
}

Spatial Document Reconstruction

Reconstructs original multi-column document geometry into aligned plain text:

let result = try ocr.detectText(memorySource, options: OcrRunOptions(output: .spatial))

if case .spatial(let layoutText) = result {
    print("--- Spatial Layout ---\n\(layoutText)")
}

6. Complete SwiftUI Scanner Implementation

Always offload OCR execution to background threads or Swift Concurrency (Task.detached) to avoid blocking the main UI thread:

import SwiftUI
import PhotosUI
import RustO

struct DocumentScannerView: View {
    @State private var selectedItem: PhotosPickerItem?
    @State private var previewImage: UIImage?
    @State private var recognizedText: String = ""
    @State private var isProcessing: Bool = false
    @State private var ocrEngine: RustO?

    var body: some View {
        NavigationStack {
            VStack(spacing: 16) {
                if let previewImage {
                    Image(uiImage: previewImage)
                        .resizable()
                        .scaledToFit()
                        .frame(maxHeight: 240)
                        .cornerRadius(12)
                }

                PhotosPicker(selection: $selectedItem, matching: .images) {
                    Label("Choose Document", systemImage: "doc.text.viewfinder")
                        .font(.headline)
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.orange)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .onChange(of: selectedItem) { _, newItem in
                    Task {
                        guard let data = try? await newItem?.loadTransferable(type: Data.self),
                              let uiImage = UIImage(data: data) else { return }
                        previewImage = uiImage
                        await runOcr(data: data)
                    }
                }

                if isProcessing {
                    ProgressView("Extracting text with RustO!...")
                        .padding()
                }

                ScrollView {
                    Text(recognizedText.isEmpty ? "No text recognized yet." : recognizedText)
                        .font(.system(.body, design: .monospaced))
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding()
                        .background(Color(UIColor.secondarySystemBackground))
                        .cornerRadius(8)
                }
            }
            .padding()
            .navigationTitle("RustO! iOS Scanner")
            .task {
                // Initialize engine in background
                ocrEngine = try? RustO.initialize()
            }
        }
    }

    private func runOcr(data: Data) async {
        guard let engine = ocrEngine else { return }
        isProcessing = true
        recognizedText = ""

        // Run inference detached from MainActor
        let resultText: String = await Task.detached(priority: .userInitiated) {
            do {
                let options = OcrRunOptions(output: .spatial, lineYThreshold: 0.5, wordXThreshold: 0.4)
                let result = try engine.detectText(.bytes(data), options: options)
                if case .spatial(let text) = result {
                    return text
                }
                return "No text extracted."
            } catch {
                return "OCR Error: \(error.localizedDescription)"
            }
        }.value

        recognizedText = resultText
        isProcessing = false
    }
}