Use iOS (Swift)
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.
Method A: Swift Package Manager (Recommended)
In Xcode
- Open your project in Xcode.
- Navigate to File > Add Package Dependencies...
- Enter the repository URL:
https://github.com/byrizki/rusto-rs.git - Select
RustOand 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
}
}