How To

Menggunakan iOS (Swift)

Integrasi RustO! pada aplikasi iOS Swift dan SwiftUI via Swift Package Manager (SPM) atau CocoaPods.

RustO! menyediakan dukungan kelas satu untuk iOS dan macOS melalui XCFramework universal dan pembungkus Swift yang ramah pengembang. Mendukung arsitektur Apple Silicon (arm64), Intel (x86_64), dan Simulator iOS tanpa memerlukan rantai alat kompilasi C++.


Instalasi

Anda dapat mengintegrasikan RustO! ke dalam proyek iOS Anda menggunakan Swift Package Manager (SPM) atau CocoaPods.

Metode A: Swift Package Manager (Direkomendasikan)

Melalui Xcode

  1. Buka proyek Anda di Xcode.
  2. Buka menu File > Add Package Dependencies...
  3. Masukkan URL repositori:
    https://github.com/byrizki/rusto-rs.git
    
  4. Pilih produk pustaka RustO dan tentukan aturan versi paket yang diinginkan (misalnya 0.3.2).

Melalui Package.swift

Jika Anda membuat paket modular atau pustaka Swift:

// 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")
            ]
        )
    ]
)

Paket ini secara otomatis menyediakan modul C FFI level rendah (RustOFFI) dan API Swift level tinggi (RustO).


Metode B: CocoaPods

Pada file Podfile proyek Anda:

target 'YourApp' do
  use_frameworks!

  # XCFramework runtime mesin OCR inti
  pod 'RustO', '~> 0.3.2'

  # Paket bundel model bawaan (~6 MB total)
  pod 'RustO-Models-PPOCRv6-Tiny', '~> 0.3.2'
  # Atau varian tingkatan model yang lebih besar:
  # pod 'RustO-Models-PPOCRv6-Small'
  # pod 'RustO-Models-PPOCRv5-Mobile'
end

Kemudian jalankan:

pod install

1. Konfigurasi Izin (Info.plist)

Jika mengambil gambar dari kamera atau galeri foto, tambahkan deklarasi deskripsi izin pada file Info.plist aplikasi Anda:

<key>NSCameraUsageDescription</key>
<string>Izinkan akses kamera untuk memindai dokumen dan mengekstrak teks.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Izinkan akses galeri foto untuk memilih gambar dokumen untuk dipindai.</string>

2. Inisialisasi Mesin & Resolusi Model

Opsi 1: Resolusi Model Bundel Otomatis

Jika menggunakan pod model resmi (seperti RustO-Models-PPOCRv6-Tiny), RustO! secara otomatis menemukan berkas model di dalam bundel framework:

import RustO

do {
    // Secara otomatis menemukan berkas det.rten, rec.rten, dan dict.txt bawaan PP-OCRv6
    let ocr = try RustO.initialize()
} catch {
    print("Inisialisasi OCR gagal: \(error)")
}

Opsi 2: Menggunakan Bundel Kustom atau Jalur File Sistem

Jika Anda ingin menyertakan model kustom di dalam bundel utama aplikasi atau mengunduhnya ke folder penyimpanan lokal:

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: "Berkas model tidak ditemukan di bundel"])
    }

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

    return try RustO.initialize(config: config)
}

3. Sumber Masukan Gambar

ImageSource mendukung jalur file lokal, URL, maupun buffer byte gambar di memori:

import UIKit
import RustO

// 1. Jalur berkas lokal atau file URL
let fileSource = ImageSource.uri("/path/to/invoice.png")

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

let memorySource = ImageSource.bytes(imageData)

4. Opsi Eksekusi (OcrRunOptions)

Sesuaikan parameter deteksi per permintaan pemindaian:

let options = OcrRunOptions(
    output: .lines,            // .lines, .words, atau .spatial
    lineYThreshold: 0.5,       // Ambang pengelompokan baris (0.0 - 1.0)
    wordXThreshold: 0.4,       // Ambang pemisah kolom kata (0.0 - 1.0)
    textScore: 0.50,           // Batas minimum skor keyakinan
    maxSideLen: 1600,          // Batas resolusi sisi gambar terpanjang
    detection: DetectionRunOptions(
        limitSideLen: 960,
        limitType: "max"
    ),
    postprocess: PostprocessRunOptions(
        useDilation: true,     // Dilasi morfologis untuk struk dengan cetakan tipis
        unclipRatio: 1.8       // Rasio ekspansi poligon batas teks
    )
)

5. Deteksi Terstruktur vs Rekonstruksi Spasial

Deteksi Baris Terstruktur

Mengembalikan koordinat kotak pembatas, titik poligon, dan skor keyakinan:

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("  Titik Poligon: \(line.boxPoints)")
    }
}

Rekonstruksi Tata Letak Spasial

Menyusun ulang geometri teks multi-kolom dokumen menjadi teks rapi terformat:

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

if case .spatial(let layoutText) = result {
    print("--- Tata Letak Spasial ---\n\(layoutText)")
}

6. Contoh Lengkap Pemindai Dokumen di SwiftUI

Selalu jalankan inferensi OCR pada antrean latar belakang (background thread) atau Swift Concurrency (Task.detached) agar tidak mengunci antarmuka utama pengguna:

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("Pilih Dokumen", 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("Mengekstrak teks dengan RustO!...")
                        .padding()
                }

                ScrollView {
                    Text(recognizedText.isEmpty ? "Belum ada teks yang dipindai." : recognizedText)
                        .font(.system(.body, design: .monospaced))
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding()
                        .background(Color(UIColor.secondarySystemBackground))
                        .cornerRadius(8)
                }
            }
            .padding()
            .navigationTitle("Pemindai Dokumen RustO!")
            .task {
                ocrEngine = try? RustO.initialize()
            }
        }
    }

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

        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 "Tidak ada teks yang terdeteksi."
            } catch {
                return "Kesalahan OCR: \(error.localizedDescription)"
            }
        }.value

        recognizedText = resultText
        isProcessing = false
    }
}