Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compose-custom-ui-example/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version")

def scanbotSdkVersion = "9.0.1"
def scanbotSdkVersion = "10.0.0.123-STAGING-SNAPSHOT"

implementation("io.scanbot:sdk-package-4:$scanbotSdkVersion")
implementation("io.scanbot:rtu-ui-v2-bundle:$scanbotSdkVersion")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
@file:kotlin.OptIn(ExperimentalPermissionsApi::class)

package io.scanbot.example.compose

import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import io.scanbot.sdk.image.ImageRef
import io.scanbot.sdk.imageprocessing.DocumentCleanupConfiguration
import io.scanbot.sdk.ui_v2.document.DocumentCleanupCustomUI
import io.scanbot.sdk.ui_v2.document.screen.documentcleanup.DocumentCleanupActionController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
* Interactive demo screen for [DocumentCleanupCustomUI]. The user picks an image from the
* gallery, paints a mask on top of it and the masked regions are cleaned up by the SDK.
*
* For a code-only snippet (no image picker, no navigation), see
* [io.scanbot.example.compose.doc_code_snippet.document.DocumentCleanupCustomUISnippet].
*/
@Composable
fun DocumentCleanupScreen(navController: NavHostController) {
val context = LocalContext.current
val scope = rememberCoroutineScope()

var inputImage by remember { mutableStateOf<ImageRef?>(null) }
val controller = remember { mutableStateOf<DocumentCleanupActionController?>(null) }
val brushSize = remember { mutableFloatStateOf(40f) }
var inProgress by remember { mutableStateOf(false) }

val pickImageLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri: Uri? ->
if (uri == null) return@rememberLauncherForActivityResult
scope.launch {
val newImage = withContext(Dispatchers.IO) {
runCatching {
context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream)
}
}.getOrNull()?.let { ImageRef.fromBitmap(it) }
}
if (newImage != null) {
inputImage?.close()
inputImage = newImage
} else {
Log.e("DocumentCleanupScreen", "Failed to load image from $uri")
}
}
}

Column(modifier = Modifier.fillMaxSize().systemBarsPadding()) {
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
val image = inputImage
if (image != null) {
DocumentCleanupCustomUI(
image = image,
modifier = Modifier.fillMaxSize(),
documentCleanupConfiguration = DocumentCleanupConfiguration(
keepText = true,
maxUndoRedoStackSize = 10,
),
brushSize = brushSize.floatValue.dp,
brushColor = Color.Red.copy(alpha = 0.5f),
onActionControllerCreated = { controller.value = it },
onResultImageChanged = {
Log.d("DocumentCleanupScreen", "New result image received")
},
onProgressChanged = { inProgress = it },
onError = { error ->
Log.e("DocumentCleanupScreen", "Cleanup error", error)
},
)
if (inProgress) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter),
)
}
} else {
Box(modifier = Modifier.fillMaxSize()) {
Text(
text = "Pick an image to start cleanup",
modifier = Modifier.padding(16.dp),
)
}
}
}

Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Button(
modifier = Modifier.weight(1f),
onClick = {
pickImageLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
},
) { Text("Pick image") }
}

Spacer(modifier = Modifier.height(8.dp))

val activeController = controller.value
val canUndo = activeController?.canUndoFlow?.collectAsState()?.value ?: false
val canRedo = activeController?.canRedoFlow?.collectAsState()?.value ?: false
val progress = activeController?.progressFlow?.collectAsState()?.value ?: false

Row(modifier = Modifier.fillMaxWidth()) {
Button(
modifier = Modifier.weight(1f),
enabled = canUndo && !progress,
onClick = { activeController?.undo() },
) { Text("Undo") }
Button(
modifier = Modifier.weight(1f),
enabled = canRedo && !progress,
onClick = { activeController?.redo() },
) { Text("Redo") }
Button(
modifier = Modifier.weight(1f),
enabled = canUndo && !progress,
onClick = { activeController?.reset() },
) { Text("Reset") }
}

Text(
modifier = Modifier.padding(top = 8.dp),
text = "Brush size: ${brushSize.floatValue.toInt()} dp",
)
Slider(
value = brushSize.floatValue,
onValueChange = { brushSize.floatValue = it },
valueRange = 8f..120f,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ sealed class Screen(val route: String) {
object BarcodeFindAndPick : Screen("BarcodeFindAndPick")

object DocumentScanner1 : Screen("DocumentScanner1")
object DocumentCleanup1 : Screen("DocumentCleanup1")
object MrzScanner1 : Screen("MrzScanner1")
data class BarcodeDetail(val data: String, val format: String) :
Screen("barcodeDetail/{data}/{format}") {
Expand All @@ -90,6 +91,7 @@ fun AppNavHost(navController: NavHostController) {
NavHost(navController = navController, startDestination = Screen.Menu.route) {
composable(Screen.Menu.route) { MenuScreen(navController) }
composable(Screen.DocumentScanner1.route) { DocumentScannerScreen(navController) }
composable(Screen.DocumentCleanup1.route) { DocumentCleanupScreen(navController) }
composable(Screen.MrzScanner1.route) { MrzScannerScreen(navController) }
composable(
route = "barcodeDetail/{data}/{format}",
Expand Down Expand Up @@ -122,6 +124,11 @@ fun MenuScreen(navController: NavHostController) {
Screen.DocumentScanner1.route,
""
),
Triple(
"Document Cleanup",
Screen.DocumentCleanup1.route,
""
),
Triple(
"MRZ Scanner",
Screen.MrzScanner1.route,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package io.scanbot.example.compose.doc_code_snippet.document

import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.scanbot.sdk.image.ImageRef
import io.scanbot.sdk.imageprocessing.DocumentCleanupConfiguration
import io.scanbot.sdk.ui_v2.document.DocumentCleanupCustomUI
import io.scanbot.sdk.ui_v2.document.screen.documentcleanup.DocumentCleanupActionController

// @Tag("Document Cleanup Custom UI Composable")
/**
* Renders the `DocumentCleanupCustomUI` composable on top of the supplied [image].
*
* The composable wraps the `DocumentCleanup` classic API and gives the host application
* full control over the surrounding chrome (top bar, bottom bar, undo / redo / reset
* controls, brush-size slider, etc.).
*
* Use the [DocumentCleanupActionController] exposed via `onActionControllerCreated` to
* trigger undo / redo / reset programmatically and to observe the current operation
* progress and undo / redo availability.
*/
@Composable
fun DocumentCleanupCustomUISnippet(image: ImageRef) {
val controller = remember { mutableStateOf<DocumentCleanupActionController?>(null) }
val brushSize = remember { mutableFloatStateOf(40f) }
var inProgress by remember { mutableStateOf(false) }

Column(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
DocumentCleanupCustomUI(
image = image,
modifier = Modifier.fillMaxSize(),
// SDK-level cleanup configuration: keepText, undo/redo stack size, etc.
documentCleanupConfiguration = DocumentCleanupConfiguration(
keepText = true,
maxUndoRedoStackSize = 10,
),
// Background color behind the image canvas
backgroundColor = Color.Black,
// Diameter of the drawing brush
brushSize = brushSize.floatValue.dp,
// Brush stroke preview color
brushColor = Color.Red.copy(alpha = 0.5f),
// Maximum zoom scale supported by pinch-to-zoom
maxZoomScale = 10f,
// Capture the action controller to trigger undo / redo / reset and observe state.
onActionControllerCreated = { controller.value = it },
// Triggered after every successful cleanup, undo, redo or reset.
// The provided ImageRef is owned by the composable - DO NOT close it.
onResultImageChanged = { newImage ->
Log.d("DocumentCleanupCustomUI", "New result image: $newImage")
},
// Invoked when a heavy cleanup operation starts (true) or finishes (false).
onProgressChanged = { inProgress = it },
// Invoked on SDK setup or cleanup failures.
onError = { error ->
Log.e("DocumentCleanupCustomUI", "Cleanup error: ${error.message}")
},
)

if (inProgress) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter),
)
}
}

Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
val activeController = controller.value
val canUndo = activeController?.canUndoFlow?.collectAsState()?.value ?: false
val canRedo = activeController?.canRedoFlow?.collectAsState()?.value ?: false
val progress = activeController?.progressFlow?.collectAsState()?.value ?: false

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Button(
modifier = Modifier.weight(1f),
enabled = canUndo && !progress,
onClick = { activeController?.undo() },
) { Text("Undo") }
Button(
modifier = Modifier.weight(1f),
enabled = canRedo && !progress,
onClick = { activeController?.redo() },
) { Text("Redo") }
Button(
modifier = Modifier.weight(1f),
enabled = canUndo && !progress,
onClick = { activeController?.reset() },
) { Text("Reset") }
}

Spacer(modifier = Modifier.height(8.dp))

Text(text = "Brush size: ${brushSize.floatValue.toInt()} dp")
Slider(
value = brushSize.floatValue,
onValueChange = { brushSize.floatValue = it },
valueRange = 8f..120f,
)
}
}
}
// @EndTag("Document Cleanup Custom UI Composable")
2 changes: 1 addition & 1 deletion data-capture-ready-to-use-ui-example/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version")

def scanbotSdkVersion = "9.0.1"
def scanbotSdkVersion = "10.0.0.123-STAGING-SNAPSHOT"

implementation("io.scanbot:sdk-package-4:$scanbotSdkVersion")
implementation("io.scanbot:rtu-ui-v2-bundle:$scanbotSdkVersion")
Expand Down
2 changes: 1 addition & 1 deletion document-scanner-ready-to-use-ui-example/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ android {
}
}

def scanbotSdkVersion = "9.0.1"
def scanbotSdkVersion = "10.0.0.124-STAGING-SNAPSHOT"

dependencies {

Expand Down
Loading