How To
Menggunakan Android (Kotlin)
Penggunaan RustO! pada aplikasi Android Kotlin dan Jetpack Compose via JitPack.
SDK Android didistribusikan via JitPack sebagai AAR yang menyertakan pustaka biner native JNI untuk arm64-v8a, armeabi-v7a, x86, dan x86_64.
Instalasi
Tambahkan JitPack ke dalam settings.gradle root proyek Anda:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
Pada build.gradle modul aplikasi:
dependencies {
// Runtime OCR inti dengan biner native JNI
implementation 'com.github.byrizki.rusto-rs:rusto-android:0.3.2'
// Paket model bawaan PP-OCRv6 Tiny (~6 MB)
implementation 'com.github.byrizki.rusto-rs:rusto-models-ppocrv6-tiny:0.3.2'
}
1. Siklus Hidup Mesin & Resolusi Aset
Menyertakan Context Android ke RustO.initialize() akan mengekstrak dan menyimpan cache model serta kamus secara otomatis saat aplikasi pertama kali dijalankan:
package com.example.ocr
import android.content.Context
import com.byrizki.rusto.*
class OcrManager(private val context: Context) : AutoCloseable {
// 1. Inisialisasi dengan model bawaan default PP-OCRv6
private val ocr: RustO = RustO.initialize(context)
// 2. Atau inisialisasi dengan konfigurasi kustom:
fun initCustom(): RustO {
val config = InitializeConfig(
template = "ppv6",
detection = DetectionConfig(modelPath = "custom_det.rten"),
recognition = RecognitionConfig(
modelPath = "custom_rec.rten",
dictPath = "custom_dict.txt"
)
)
return RustO.initialize(context, config)
}
override fun close() {
ocr.close()
}
}
2. Sumber Masukan Gambar yang Didukung
ImageSource mampu mengurai URI content:// dari Photo Picker atau Kamera, path berkas file://, serta larik byte dalam memori ByteArray secara native:
// 1. Content URI dari Photo Picker / Intent Kamera (resolusi stream otomatis)
val uriSource = ImageSource.Uri("content://media/external/images/media/1002")
// 2. Path berkas filesystem lokal
val pathSource = ImageSource.Uri("/data/user/0/com.example/cache/doc.jpg")
// 3. Larik byte mentah dalam memori
val rawBytes: ByteArray = file.readBytes()
val byteSource = ImageSource.Bytes(rawBytes)
3. Opsi Permintaan Lokal (OcrRunOptions)
Atur konfigurasi pemindaian secara dinamis:
val options = OcrRunOptions(
output = OutputGranularity.LINES, // LINES, WORDS, atau SPATIAL
lineYThreshold = 0.5f, // Toleransi dinamis pengelompokan baris
wordXThreshold = 0.4f, // Toleransi pemisahan kata
textScore = 0.55f, // Batas skor kepercayaan pengenalan
maxSideLen = 1600f, // Batas panjang sisi maksimum gambar
detection = DetectionRunOptions(
limitSideLen = 960,
limitType = "max"
),
postprocess = PostprocessRunOptions(
useDilation = true // Dilatasi morfologi untuk struk redup
)
)
4. Contoh Lengkap Pemindai Dokumen Jetpack Compose
Berikut implementasi lengkap menggunakan Kotlin Coroutines dan Photo Picker modern di Jetpack Compose:
package com.example.ocr
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import com.byrizki.rusto.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun DocumentScannerScreen() {
val context = LocalContext.current
var selectedUri by remember { mutableStateOf<Uri?>(null) }
var results by remember { mutableStateOf<List<TextResult>>(emptyList()) }
var spatialText by remember { mutableStateOf("") }
var isSpatial by remember { mutableStateOf(false) }
var isLoading by remember { mutableStateOf(false) }
val photoPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia()
) { uri ->
if (uri != null) {
selectedUri = uri
}
}
LaunchedEffect(selectedUri, isSpatial) {
val uri = selectedUri ?: return@LaunchedEffect
isLoading = true
withContext(Dispatchers.Default) {
RustO.initialize(context).use { ocr ->
val source = ImageSource.Uri(uri.toString())
if (isSpatial) {
val spatialRes = ocr.detectText(
source,
OcrRunOptions(output = OutputGranularity.SPATIAL, lineYThreshold = 0.5f)
)
if (spatialRes is DetectTextResult.Spatial) {
spatialText = spatialRes.text
results = emptyList()
}
} else {
val structRes = ocr.detectText(
source,
OcrRunOptions(output = OutputGranularity.LINES, textScore = 0.5f)
)
if (structRes is DetectTextResult.Structured) {
results = structRes.items
spatialText = ""
}
}
}
}
isLoading = false
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = { photoPicker.launch(androidx.activity.result.PickVisualMediaRequest()) },
modifier = Modifier.weight(1f)
) {
Text("Pilih Dokumen")
}
FilterChip(
selected = isSpatial,
onClick = { isSpatial = !isSpatial },
label = { Text(if (isSpatial) "Tata Letak Spasial" else "Baris Terstruktur") }
)
}
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.padding(vertical = 16.dp))
}
Spacer(modifier = Modifier.height(16.dp))
if (isSpatial && spatialText.isNotEmpty()) {
Text(
text = spatialText,
fontFamily = FontFamily.Monospace,
style = MaterialTheme.typography.bodySmall
)
} else {
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(results) { item ->
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = item.text, modifier = Modifier.weight(1f))
Text(
text = "${(item.score * 100).toInt()}%",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
}
}