Skip to content

Add OCR quality toggle with ML Kit and Tesseract options#1

Merged
Godzilla675 merged 6 commits into
mainfrom
copilot/add-ocr-model-toggle
Dec 29, 2025
Merged

Add OCR quality toggle with ML Kit and Tesseract options#1
Godzilla675 merged 6 commits into
mainfrom
copilot/add-ocr-model-toggle

Conversation

Copilot AI commented Dec 29, 2025

Copy link
Copy Markdown
  • Explore repository structure and understand current OCR implementation
  • Research better OCR models for Android (local/offline)
  • Add Google ML Kit Text Recognition as high-quality OCR option
    • Add ML Kit Text Recognition dependency to libs.versions.toml
    • Add dependency to app/build.gradle.kts
  • Create OcrQuality enum for toggling between Fast (Tesseract) and Best (ML Kit)
  • Create MlKitOcrHelper for ML Kit text recognition
  • Create OcrEngine to unify both OCR backends
  • Update SettingsScreen to add OCR quality toggle
  • Update MainActivity to persist OCR quality preference
  • Update Navigation to pass OCR quality setting and use OcrEngine
  • Build and test the implementation (BUILD SUCCESSFUL)
  • Run code review and address feedback
    • Fix mutex consistency in MlKitOcrHelper (ML Kit handles concurrency internally)
    • Extract "ara" language code as constant in OcrEngine
    • Fix LaunchedEffect dependencies to not reprocess on quality changes
  • Run CodeQL security scan (no issues found)
  • Address Gemini code review feedback
    • Fix thread-safety in MlKitOcrHelper by wrapping recognizeFromInputImage with mutex.withLock
    • Fix memory leak by using rememberCoroutineScope() instead of CoroutineScope(Dispatchers.IO)
  • Simplify confidence calculation with flatMap instead of mapNotNull+flatten
Original prompt

add a better ocr model since the current one is EXETREEMLY BAD add a toggle for the high quality and low qualit ocr models which ar eboth local and make sure EVERYTHING WORKS and research everything before starting


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits December 29, 2025 02:33
Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com>
Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com>
Copilot AI changed the title [WIP] Add toggle for high and low quality OCR models Add OCR quality toggle with ML Kit and Tesseract options Dec 29, 2025
Copilot AI requested a review from Godzilla675 December 29, 2025 02:37
@Godzilla675

Copy link
Copy Markdown
Owner

/gemini code review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively adds Google's ML Kit as a high-quality OCR alternative to Tesseract, introducing a user-configurable quality setting. The implementation of the OcrEngine as a facade to abstract the two OCR backends is a solid design choice. It's also a good decision to continue using Tesseract for PDF processing to retain its broader language support.

My review includes a few important suggestions:

  • A critical fix for a thread-safety issue in the new MlKitOcrHelper.
  • A high-severity fix to prevent potential memory leaks in the PDF processing logic by using a lifecycle-aware coroutine scope.
  • A medium-severity refactoring suggestion to improve maintainability on the settings screen by removing hardcoded strings.

Overall, this is a great feature addition, and with these changes, it will be more robust and maintainable.

Comment on lines +96 to +136
private suspend fun recognizeFromInputImage(inputImage: InputImage): OcrResult? {
return suspendCancellableCoroutine { continuation ->
val startTime = System.currentTimeMillis()

recognizer?.process(inputImage)
?.addOnSuccessListener { visionText ->
val processingTime = System.currentTimeMillis() - startTime

val text = visionText.text

// Calculate confidence from block-level confidence
val confidences = visionText.textBlocks.mapNotNull { block ->
block.lines.flatMap { line ->
line.elements.mapNotNull { it.confidence }
}
}.flatten()

val avgConfidence = if (confidences.isNotEmpty()) {
(confidences.average() * 100).toInt()
} else {
0
}

Log.d(TAG, "OCR completed in ${processingTime}ms, confidence: $avgConfidence%")

continuation.resume(
OcrResult(
text = text,
confidence = avgConfidence,
languages = listOf("eng"), // ML Kit Latin
processingTimeMs = processingTime
)
)
}
?.addOnFailureListener { e ->
Log.e(TAG, "ML Kit text recognition failed", e)
continuation.resume(null)
}
?: continuation.resume(null)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

According to the official ML Kit documentation, TextRecognizer instances are not thread-safe and should not be accessed from multiple threads concurrently. Since the recognizeText methods can be called from multiple coroutines on Dispatchers.IO, there is a risk of a race condition when calling recognizer.process().

To ensure thread safety, you should wrap the logic in recognizeFromInputImage within a mutex.withLock block. This will serialize access to the recognizer, preventing crashes and unpredictable behavior.

    return mutex.withLock {
        suspendCancellableCoroutine { continuation ->
            val startTime = System.currentTimeMillis()
            
            recognizer?.process(inputImage)
                ?.addOnSuccessListener { visionText ->
                    val processingTime = System.currentTimeMillis() - startTime
                    
                    val text = visionText.text
                    
                    // Calculate confidence from block-level confidence
                    val confidences = visionText.textBlocks.mapNotNull { block ->
                        block.lines.flatMap { line ->
                            line.elements.mapNotNull { it.confidence }
                        }
                    }.flatten()
                    
                    val avgConfidence = if (confidences.isNotEmpty()) {
                        (confidences.average() * 100).toInt()
                    } else {
                        0
                    }
                    
                    Log.d(TAG, "OCR completed in ${processingTime}ms, confidence: $avgConfidence%")
                    
                    continuation.resume(
                        OcrResult(
                            text = text,
                            confidence = avgConfidence,
                            languages = listOf("eng"), // ML Kit Latin
                            processingTimeMs = processingTime
                        )
                    )
                }
                ?.addOnFailureListener { e ->
                    Log.e(TAG, "ML Kit text recognition failed", e)
                    continuation.resume(null)
                }
                ?: continuation.resume(null)
        }
    }

@@ -87,10 +91,13 @@ fun ScanelyNavigation(

kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using CoroutineScope(Dispatchers.IO) creates a new coroutine scope that is not tied to the lifecycle of this composable. If the user navigates away while the PDF is being processed, this coroutine will not be cancelled, leading to potential memory leaks and wasted resources. It's better to use a scope that is aware of the composable's lifecycle.

You should use rememberCoroutineScope() to get a lifecycle-aware scope and launch your coroutine from it. You'll need to add val scope = rememberCoroutineScope() at the top of the ScanelyNavigation composable (e.g., around line 54).

Suggested change
kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch {
scope.launch(kotlinx.coroutines.Dispatchers.IO) {

Comment thread app/src/main/java/com/skeler/scanely/ui/screens/SettingsScreen.kt Outdated
@Godzilla675 Godzilla675 marked this pull request as ready for review December 29, 2025 02:40
Copilot AI review requested due to automatic review settings December 29, 2025 02:40
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@Godzilla675

Copy link
Copy Markdown
Owner

@copilot look at the gemini code review comment and resolve the issues

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a user-configurable OCR quality toggle to address poor accuracy issues with the current Tesseract-only implementation. It introduces Google ML Kit Text Recognition as a higher-quality alternative while maintaining Tesseract for multi-language support. The implementation uses a facade pattern (OcrEngine) to abstract the two OCR backends, with user preferences persisted via SharedPreferences.

Key Changes:

  • New OCR infrastructure with OcrQuality enum, MlKitOcrHelper wrapper, and unified OcrEngine facade
  • Settings UI additions for OCR quality selection with radio toggle between Fast (Tesseract) and Best (ML Kit) modes
  • State management and persistence of OCR quality preference alongside existing theme and language settings

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
gradle/libs.versions.toml Adds ML Kit Text Recognition dependency version 16.0.1
app/build.gradle.kts Implements ML Kit Text Recognition library
app/src/main/java/com/skeler/scanely/ocr/OcrQuality.kt Defines FAST and BEST quality enum with labels and descriptions
app/src/main/java/com/skeler/scanely/ocr/MlKitOcrHelper.kt Wraps ML Kit Text Recognition API with coroutine-based interface matching OcrHelper
app/src/main/java/com/skeler/scanely/ocr/OcrEngine.kt Facade that delegates to either Tesseract or ML Kit based on quality setting
app/src/main/java/com/skeler/scanely/ui/screens/SettingsScreen.kt Adds OCR Quality section with radio options and descriptions
app/src/main/java/com/skeler/scanely/navigation/Navigation.kt Updates to use OcrEngine facade, maintains Tesseract for PDF processing
app/src/main/java/com/skeler/scanely/MainActivity.kt Loads and persists OCR quality preference from SharedPreferences

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +94 to +95
// Get Tesseract helper for PDF processing (always use Tesseract for multi-language PDFs)
val tesseractHelper = ocrEngine.getTesseractHelper()

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment explains that PDF processing uses Tesseract for multi-language support, but this creates an inconsistency in user experience. If a user selects "Best (ML Kit)" quality and then processes a Latin-script PDF, they would expect higher quality OCR, but they'll get Tesseract instead. This should either be documented in the UI (e.g., "Note: PDFs always use Tesseract engine") or the logic should respect the user's quality preference for single-language PDFs.

Copilot uses AI. Check for mistakes.
Comment on lines +334 to +359
quality: OcrQuality,
currentQuality: OcrQuality,
onSelect: (OcrQuality) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onSelect(quality) }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (quality == currentQuality),
onClick = { onSelect(quality) }
)
Column(
modifier = Modifier.padding(start = 8.dp)
) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OcrQualityOptionRow composable is missing semantic information for accessibility. The Row should have role and state information so screen readers can properly announce that these are radio button options. Consider using Modifier.semantics or Modifier.selectable with the appropriate role to improve accessibility for users with disabilities.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +140
OcrQuality.entries.forEach { quality ->
val label = when (quality) {
OcrQuality.FAST -> "Fast (Tesseract)"
OcrQuality.BEST -> "Best (ML Kit)"
}
val description = when (quality) {
OcrQuality.FAST -> "Faster, supports more languages including Arabic"
OcrQuality.BEST -> "Higher accuracy for Latin text"
}

OcrQualityOptionRow(
label = label,
description = description,
quality = quality,

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI hardcodes OCR quality labels and descriptions instead of using the label and description properties defined in the OcrQuality enum. This creates duplication and inconsistency - the enum properties are never used, and changes to the labels would need to be made in multiple places. Consider either using the enum properties directly or removing them from the enum if they're not needed.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +54
suspend fun initialize(quality: OcrQuality, languages: List<String>): Boolean {
Log.d(TAG, "Initializing OCR engine with quality=$quality, languages=$languages")

currentQuality = quality
currentLanguages = languages

val success = when (quality) {
OcrQuality.FAST -> tesseractHelper.initialize(languages)
OcrQuality.BEST -> mlKitHelper.initialize(languages)
}

isInitialized = success
return success
}

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initialize method doesn't clean up the previously initialized helper when switching quality modes. If a user initializes with FAST mode (Tesseract), then reinitializes with BEST mode (ML Kit), the Tesseract resources remain allocated. Consider releasing the non-active helper's resources during initialization to prevent unnecessary memory usage.

Copilot uses AI. Check for mistakes.
Comment on lines +164 to 165
// Note: ocrQuality is intentionally NOT in dependencies - quality changes only affect new scans
LaunchedEffect(selectedImageUri, isPdfProcessing, shouldAutoScan) {

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "ocrQuality is intentionally NOT in dependencies" but this could lead to unexpected behavior. When a user changes OCR quality in settings and returns to the main screen with a previously selected image, the quality change won't take effect until they select a new image. This behavior should be clearly documented in the UI or the dependency should be added to reinitialize when quality changes.

Suggested change
// Note: ocrQuality is intentionally NOT in dependencies - quality changes only affect new scans
LaunchedEffect(selectedImageUri, isPdfProcessing, shouldAutoScan) {
LaunchedEffect(selectedImageUri, isPdfProcessing, shouldAutoScan, ocrQuality) {

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +134

recognizer?.process(inputImage)
?.addOnSuccessListener { visionText ->
val processingTime = System.currentTimeMillis() - startTime

val text = visionText.text

// Calculate confidence from block-level confidence
val confidences = visionText.textBlocks.mapNotNull { block ->
block.lines.flatMap { line ->
line.elements.mapNotNull { it.confidence }
}
}.flatten()

val avgConfidence = if (confidences.isNotEmpty()) {
(confidences.average() * 100).toInt()
} else {
0
}

Log.d(TAG, "OCR completed in ${processingTime}ms, confidence: $avgConfidence%")

continuation.resume(
OcrResult(
text = text,
confidence = avgConfidence,
languages = listOf("eng"), // ML Kit Latin
processingTimeMs = processingTime
)
)
}
?.addOnFailureListener { e ->
Log.e(TAG, "ML Kit text recognition failed", e)
continuation.resume(null)
}
?: continuation.resume(null)

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suspendCancellableCoroutine is not properly handling cancellation. When the coroutine is cancelled, the ML Kit process should be cancelled as well. Consider adding continuation.invokeOnCancellation to properly clean up the ML Kit task when the coroutine is cancelled, preventing resource leaks and unnecessary processing.

Suggested change
recognizer?.process(inputImage)
?.addOnSuccessListener { visionText ->
val processingTime = System.currentTimeMillis() - startTime
val text = visionText.text
// Calculate confidence from block-level confidence
val confidences = visionText.textBlocks.mapNotNull { block ->
block.lines.flatMap { line ->
line.elements.mapNotNull { it.confidence }
}
}.flatten()
val avgConfidence = if (confidences.isNotEmpty()) {
(confidences.average() * 100).toInt()
} else {
0
}
Log.d(TAG, "OCR completed in ${processingTime}ms, confidence: $avgConfidence%")
continuation.resume(
OcrResult(
text = text,
confidence = avgConfidence,
languages = listOf("eng"), // ML Kit Latin
processingTimeMs = processingTime
)
)
}
?.addOnFailureListener { e ->
Log.e(TAG, "ML Kit text recognition failed", e)
continuation.resume(null)
}
?: continuation.resume(null)
val task = recognizer?.process(inputImage)
if (task == null) {
continuation.resume(null)
return@suspendCancellableCoroutine
}
// Ensure ML Kit processing is cancelled if the coroutine is cancelled
continuation.invokeOnCancellation {
task.cancel()
}
task
.addOnSuccessListener { visionText ->
val processingTime = System.currentTimeMillis() - startTime
val text = visionText.text
// Calculate confidence from block-level confidence
val confidences = visionText.textBlocks.mapNotNull { block ->
block.lines.flatMap { line ->
line.elements.mapNotNull { it.confidence }
}
}.flatten()
val avgConfidence = if (confidences.isNotEmpty()) {
(confidences.average() * 100).toInt()
} else {
0
}
Log.d(TAG, "OCR completed in ${processingTime}ms, confidence: $avgConfidence%")
if (continuation.isActive) {
continuation.resume(
OcrResult(
text = text,
confidence = avgConfidence,
languages = listOf("eng"), // ML Kit Latin
processingTimeMs = processingTime
)
)
}
}
.addOnFailureListener { e ->
Log.e(TAG, "ML Kit text recognition failed", e)
if (continuation.isActive) {
continuation.resume(null)
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +24
/**
* ML Kit Text Recognition OCR helper.
* Provides high-quality text recognition for Latin script.
*
* Uses Google's ML Kit which runs on-device for privacy.

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that both OCR engines run locally/offline and maintain privacy-first design, but there's no mention that ML Kit may download models on first use. While ML Kit does support on-device processing, it typically needs to download the text recognition model initially, which requires an internet connection. This should be documented to set proper user expectations about the "offline" capability.

Copilot uses AI. Check for mistakes.
var shouldAutoScan by remember { mutableStateOf(true) }

val ocrHelper = remember { OcrHelper(context) }
val ocrEngine = remember { OcrEngine(context) }

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OcrEngine is created with remember but never released. Unlike the old single OcrHelper, the OcrEngine now manages two OCR helpers (Tesseract and ML Kit) that both allocate native resources. These resources should be released when the composable leaves composition using DisposableEffect to prevent memory leaks.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +111
val confidences = visionText.textBlocks.mapNotNull { block ->
block.lines.flatMap { line ->
line.elements.mapNotNull { it.confidence }
}
}.flatten()

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The confidence calculation is inefficient. Using mapNotNull followed by flatten() creates intermediate lists. This should use flatMap directly to map and flatten in one operation.

Suggested change
val confidences = visionText.textBlocks.mapNotNull { block ->
block.lines.flatMap { line ->
line.elements.mapNotNull { it.confidence }
}
}.flatten()
val confidences = visionText.textBlocks.flatMap { block ->
block.lines.flatMap { line ->
line.elements.mapNotNull { it.confidence }
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +126
tesseractHelper.release()
mlKitHelper.release()

Copilot AI Dec 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The release() method unconditionally releases both Tesseract and ML Kit helpers, even though only one is actively initialized based on the current quality mode. While this isn't strictly a bug since both helpers handle release when not initialized, it's inefficient and could cause unnecessary cleanup operations. Consider only releasing the currently active helper.

Suggested change
tesseractHelper.release()
mlKitHelper.release()
when (currentQuality) {
OcrQuality.FAST -> tesseractHelper.release()
OcrQuality.BEST -> mlKitHelper.release()
else -> {
// Fallback: release both if quality is unknown
tesseractHelper.release()
mlKitHelper.release()
}
}

Copilot uses AI. Check for mistakes.
Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com>

Copilot AI commented Dec 29, 2025

Copy link
Copy Markdown
Author

@copilot look at the gemini code review comment and resolve the issues

Fixed the Gemini code review issues in commit 15fb8bc:

  1. Critical fix: Added mutex.withLock to recognizeFromInputImage in MlKitOcrHelper.kt to ensure thread-safety for the TextRecognizer

  2. High priority fix: Replaced CoroutineScope(Dispatchers.IO) with rememberCoroutineScope() in Navigation.kt to prevent memory leaks by using a lifecycle-aware coroutine scope

The third issue (hardcoded labels in SettingsScreen) was already marked as resolved/outdated.

Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com>
@Godzilla675

Copy link
Copy Markdown
Owner

/gemini code review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request successfully adds a high-quality OCR option using Google's ML Kit and provides a toggle to switch between it and the existing Tesseract engine. The implementation is well-structured, introducing an OcrEngine to abstract the two backends and correctly updating the UI and data persistence logic. The refactoring to use rememberCoroutineScope is also a great improvement for lifecycle management.

I've provided a few suggestions to further enhance maintainability and performance. These include refactoring the settings screen to use the OcrQuality enum as a single source of truth for UI strings, optimizing the OCR initialization logic to avoid redundant calls, and clarifying a comment regarding thread safety in the new ML Kit helper. Overall, this is a solid contribution that significantly improves the app's core functionality.

Comment on lines +39 to +40
val name = prefs.getString("ocr_quality", OcrQuality.FAST.name) ?: OcrQuality.FAST.name
val quality = try { OcrQuality.valueOf(name) } catch (e: Exception) { OcrQuality.FAST }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic to load the OCR quality preference from SharedPreferences can be simplified. The getString method with a default value will never return null, making the elvis operator (?:) on line 39 redundant. You can make this logic more concise and idiomatic using runCatching.

Suggested change
val name = prefs.getString("ocr_quality", OcrQuality.FAST.name) ?: OcrQuality.FAST.name
val quality = try { OcrQuality.valueOf(name) } catch (e: Exception) { OcrQuality.FAST }
val name = prefs.getString("ocr_quality", OcrQuality.FAST.name)!!
val quality = runCatching { OcrQuality.valueOf(name) }.getOrDefault(OcrQuality.FAST)


/**
* Recognize text from a bitmap.
* Note: ML Kit handles thread safety internally, so no mutex is needed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This comment is misleading. According to the official ML Kit documentation, TextRecognizer instances are not thread-safe. Your implementation correctly uses a Mutex in recognizeFromInputImage to ensure thread safety, which contradicts this comment. To avoid confusion for future maintenance, this comment should be removed.

Comment on lines +127 to +144
OcrQuality.entries.forEach { quality ->
val label = when (quality) {
OcrQuality.FAST -> "Fast (Tesseract)"
OcrQuality.BEST -> "Best (ML Kit)"
}
val description = when (quality) {
OcrQuality.FAST -> "Faster, supports more languages including Arabic"
OcrQuality.BEST -> "Higher accuracy for Latin text"
}

OcrQualityOptionRow(
label = label,
description = description,
quality = quality,
currentQuality = ocrQuality,
onSelect = onOcrQualityChanged
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The labels and descriptions for the OCR quality options are hardcoded here, which duplicates information that could be held in the OcrQuality enum. This can lead to inconsistencies if one is updated and the other is not.

To improve maintainability and adhere to the single source of truth principle, I recommend moving these UI-specific strings into the OcrQuality enum and using them directly from there.

First, you can update OcrQuality.kt to hold the desired UI strings:

// in app/src/main/java/com/skeler/scanely/ocr/OcrQuality.kt
enum class OcrQuality(val label: String, val description: String) {
    FAST("Fast (Tesseract)", "Faster, supports more languages including Arabic"),
    BEST("Best (ML Kit)", "Higher accuracy for Latin text")
}

Then, you can simplify this forEach loop significantly.

            OcrQuality.entries.forEach { quality ->
                OcrQualityOptionRow(
                    label = quality.label,
                    description = quality.description,
                    quality = quality,
                    currentQuality = ocrQuality,
                    onSelect = onOcrQualityChanged
                )
            }

@Godzilla675 Godzilla675 merged commit dcf3e93 into main Dec 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants