From 35f6c0439e53c04f21de46fdd954f12aba26dd7c Mon Sep 17 00:00:00 2001 From: Serhii Chaban <> Date: Wed, 25 Mar 2026 15:43:16 +0100 Subject: [PATCH 1/7] add document enhancer classic example module --- classic-components-example/build.gradle | 2 +- .../document-enhancer/build.gradle | 47 ++++ .../src/main/AndroidManifest.xml | 31 +++ .../scanbot/example/DocumentCameraActivity.kt | 256 ++++++++++++++++++ .../io/scanbot/example/ExampleApplication.kt | 55 ++++ .../java/io/scanbot/example/MainActivity.kt | 119 ++++++++ .../src/main/res/layout/activity_camera.xml | 59 ++++ .../src/main/res/layout/activity_main.xml | 45 +++ .../src/main/res/menu/menu_main.xml | 6 + .../src/main/res/mipmap-hdpi/icon.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/icon.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/icon.png | Bin 0 -> 4842 bytes .../src/main/res/mipmap-xxhdpi/icon.png | Bin 0 -> 7718 bytes .../src/main/res/values-w820dp/dimens.xml | 6 + .../src/main/res/values/dimens.xml | 5 + .../src/main/res/values/strings.xml | 6 + .../src/main/res/values/styles.xml | 8 + .../settings.gradle.kts | 1 + 18 files changed, 645 insertions(+), 1 deletion(-) create mode 100644 classic-components-example/document-enhancer/build.gradle create mode 100755 classic-components-example/document-enhancer/src/main/AndroidManifest.xml create mode 100755 classic-components-example/document-enhancer/src/main/java/io/scanbot/example/DocumentCameraActivity.kt create mode 100755 classic-components-example/document-enhancer/src/main/java/io/scanbot/example/ExampleApplication.kt create mode 100644 classic-components-example/document-enhancer/src/main/java/io/scanbot/example/MainActivity.kt create mode 100755 classic-components-example/document-enhancer/src/main/res/layout/activity_camera.xml create mode 100755 classic-components-example/document-enhancer/src/main/res/layout/activity_main.xml create mode 100755 classic-components-example/document-enhancer/src/main/res/menu/menu_main.xml create mode 100755 classic-components-example/document-enhancer/src/main/res/mipmap-hdpi/icon.png create mode 100755 classic-components-example/document-enhancer/src/main/res/mipmap-mdpi/icon.png create mode 100755 classic-components-example/document-enhancer/src/main/res/mipmap-xhdpi/icon.png create mode 100755 classic-components-example/document-enhancer/src/main/res/mipmap-xxhdpi/icon.png create mode 100755 classic-components-example/document-enhancer/src/main/res/values-w820dp/dimens.xml create mode 100755 classic-components-example/document-enhancer/src/main/res/values/dimens.xml create mode 100755 classic-components-example/document-enhancer/src/main/res/values/strings.xml create mode 100755 classic-components-example/document-enhancer/src/main/res/values/styles.xml diff --git a/classic-components-example/build.gradle b/classic-components-example/build.gradle index c3b0b6fa..d953f471 100644 --- a/classic-components-example/build.gradle +++ b/classic-components-example/build.gradle @@ -15,7 +15,7 @@ allprojects { jvmToolchainVersion = 17 - scanbotSdkVersion = "8.1.0" + scanbotSdkVersion = "9.0.0.99-STAGING-SNAPSHOT" androidCoreKtxVersion = "1.6.0" constraintLayoutVersion = "2.0.4" diff --git a/classic-components-example/document-enhancer/build.gradle b/classic-components-example/document-enhancer/build.gradle new file mode 100644 index 00000000..c3414bb5 --- /dev/null +++ b/classic-components-example/document-enhancer/build.gradle @@ -0,0 +1,47 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = project.ext.submodulesNamespace + compileSdk = project.ext.compileSdkVersion + + defaultConfig { + applicationId = project.ext.exampleAppId + minSdk = project.ext.minSdkVersion + targetSdk = project.ext.targetSdkVersion + versionCode = 1 + versionName = "1.0" + } + + buildTypes { + named("debug") { + // set this to `false` to allow debugging and run a "non-release" build + minifyEnabled = false + debuggable = true + } + } + + kotlin { + jvmToolchain(project.ext.jvmToolchainVersion) + } + + packagingOptions { + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/LICENSE' + exclude 'META-INF/NOTICE.txt' + exclude 'META-INF/NOTICE' + exclude 'META-INF/DEPENDENCIES' + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(project(":common")) + implementation("io.scanbot:sdk-package-1:${project.ext.scanbotSdkVersion}") + implementation("androidx.appcompat:appcompat:${project.ext.androidxAppcompatVersion}") +} diff --git a/classic-components-example/document-enhancer/src/main/AndroidManifest.xml b/classic-components-example/document-enhancer/src/main/AndroidManifest.xml new file mode 100755 index 00000000..4d1192e4 --- /dev/null +++ b/classic-components-example/document-enhancer/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/classic-components-example/document-enhancer/src/main/java/io/scanbot/example/DocumentCameraActivity.kt b/classic-components-example/document-enhancer/src/main/java/io/scanbot/example/DocumentCameraActivity.kt new file mode 100755 index 00000000..04db5f27 --- /dev/null +++ b/classic-components-example/document-enhancer/src/main/java/io/scanbot/example/DocumentCameraActivity.kt @@ -0,0 +1,256 @@ +package io.scanbot.example + +import android.Manifest +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Color +import android.graphics.Matrix +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.view.WindowCompat +import io.scanbot.common.onSuccess + + +import io.scanbot.example.common.applyEdgeToEdge +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.camera.CaptureInfo +import io.scanbot.sdk.document.DocumentScannerFrameHandler +import io.scanbot.sdk.document.ui.DocumentScannerView +import io.scanbot.sdk.document.ui.IDocumentScannerViewCallback +import io.scanbot.sdk.documentscanner.DocumentDetectionStatus +import io.scanbot.sdk.documentscanner.DocumentEnhancer +import io.scanbot.sdk.documentscanner.DocumentScanner +import io.scanbot.sdk.documentscanner.DocumentStraighteningMode +import io.scanbot.sdk.documentscanner.DocumentStraighteningParameters +import io.scanbot.sdk.geometry.AspectRatio +import io.scanbot.sdk.image.ImageRef +import io.scanbot.sdk.process.ImageProcessor +import io.scanbot.sdk.ui.camera.ShutterButton +import io.scanbot.sdk.ui.view.base.configuration.CameraOrientationMode + +class DocumentCameraActivity : AppCompatActivity() { + + private var lastUserGuidanceHintTs = 0L + private var flashEnabled = false + private var autoSnappingEnabled = true + private val ignoreOrientationMistmatch = true + + private lateinit var documentScannerView: DocumentScannerView + + private lateinit var resultView: ImageView + private lateinit var userGuidanceHint: TextView + private lateinit var autoSnappingToggleButton: Button + private lateinit var shutterButton: ShutterButton + + + override fun onCreate(savedInstanceState: Bundle?) { + supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY) + + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_camera) + askPermission() + supportActionBar!!.hide() + applyEdgeToEdge(findViewById(R.id.root_view)) + + val scanbotSdk = ScanbotSDK(this) + + documentScannerView = findViewById(R.id.document_scanner_view) + + resultView = findViewById(R.id.result) as ImageView + val documentEnhancer = scanbotSdk.createDocumentEnhancer() + scanbotSdk.createDocumentScanner().onSuccess { documentScanner -> + + documentScannerView.apply { + initCamera() + initScanningBehavior( + documentScanner, + { result, frame -> + // Here you are continuously notified about document scanning results. + // For example, you can show a user guidance text depending on the current scanning status. + result.onSuccess { data -> + userGuidanceHint.post { + showUserGuidance(data.status) + } + } + false // typically you need to return false + }, + object : IDocumentScannerViewCallback { + override fun onCameraOpen() { + // In this example we demonstrate how to lock the orientation of the UI (Activity) + // as well as the orientation of the taken picture to portrait. + documentScannerView.cameraConfiguration.setCameraOrientationMode( + CameraOrientationMode.PORTRAIT + ) + + documentScannerView.viewController.useFlash(flashEnabled) + } + + override fun onPictureTaken(image: ImageRef, captureInfo: CaptureInfo) { + documentEnhancer.onSuccess { documentEnhancer -> + processPictureTaken(image, documentEnhancer) + } + + + // continue scanning + documentScannerView.postDelayed({ + documentScannerView.viewController.startPreview() + }, 1000) + } + } + ) + + // See https://docs.scanbot.io/document-scanner-sdk/android/features/document-scanner/using-scanbot-camera-view/#preview-mode + // cameraConfiguration.setCameraPreviewMode(io.scanbot.sdk.camera.CameraPreviewMode.FIT_IN) + } + } + + + + documentScannerView.polygonConfiguration.apply { + setPolygonFillColor(POLYGON_FILL_COLOR) + setPolygonFillColorOK(POLYGON_FILL_COLOR_OK) + } + + + + documentScannerView.viewController.apply { + setAcceptedAngleScore(60.0) + setAcceptedSizeScore(75.0) + setIgnoreOrientationMismatch(ignoreOrientationMistmatch) + + // Please note: https://docs.scanbot.io/document-scanner-sdk/android/features/document-scanner/autosnapping/#sensitivity + setAutoSnappingSensitivity(0.85f) + } + + userGuidanceHint = findViewById(R.id.userGuidanceHint) + + shutterButton = findViewById(R.id.shutterButton) + shutterButton.setOnClickListener { documentScannerView.viewController.takePicture(false) } + shutterButton.visibility = View.VISIBLE + + findViewById(R.id.flashToggle).setOnClickListener { + flashEnabled = !flashEnabled + documentScannerView.viewController.useFlash(flashEnabled) + } + + autoSnappingToggleButton = findViewById(R.id.autoSnappingToggle) + autoSnappingToggleButton.setOnClickListener { + autoSnappingEnabled = !autoSnappingEnabled + setAutoSnapEnabled(autoSnappingEnabled) + } + autoSnappingToggleButton.post { setAutoSnapEnabled(autoSnappingEnabled) } + } + + private fun askPermission() { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.CAMERA + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 999) + } + } + + override fun onResume() { + super.onResume() + documentScannerView.viewController.onResume() + } + + override fun onPause() { + super.onPause() + documentScannerView.viewController.onPause() + } + + private fun showUserGuidance(result: DocumentDetectionStatus) { + if (!autoSnappingEnabled) { + return + } + if (System.currentTimeMillis() - lastUserGuidanceHintTs < 400) { + return + } + + when (result) { + DocumentDetectionStatus.OK -> { + userGuidanceHint.text = "Don't move" + userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.OK_BUT_TOO_SMALL -> { + userGuidanceHint.text = "Move closer" + userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.OK_BUT_BAD_ANGLES -> { + userGuidanceHint.text = "Perspective" + userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.ERROR_NOTHING_DETECTED -> { + userGuidanceHint.text = "No Document" + userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.ERROR_TOO_NOISY -> { + userGuidanceHint.text = "Background too noisy" + userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.OK_BUT_BAD_ASPECT_RATIO -> { + if (ignoreOrientationMistmatch) { + userGuidanceHint.text = "Don't move" + } else { + userGuidanceHint.text = "Wrong aspect ratio.\nRotate your device." + } + userGuidanceHint.visibility = View.VISIBLE + } + + DocumentDetectionStatus.ERROR_TOO_DARK -> { + userGuidanceHint.text = "Poor light" + userGuidanceHint.visibility = View.VISIBLE + } + + else -> userGuidanceHint.visibility = View.GONE + } + lastUserGuidanceHintTs = System.currentTimeMillis() + } + + private fun processPictureTaken(image: ImageRef, documentEnhancer: DocumentEnhancer) { + // STRAIGHTEN SCANNED IMAGE ASSUMING DOCUMENT IS BENT + // Run document enhancer unwarping on original image: + val result = documentEnhancer.straighten(image, DocumentStraighteningParameters().apply { + straighteningMode = DocumentStraighteningMode.STRAIGHTEN + // uncomment if you want wo set specific aspect ratios for documents + // aspectRatios = listOf(AspectRatio(29.0, 21.0)) + }).getOrNull() + + resultView.post { resultView.setImageBitmap(result?.straightenedImage?.toBitmap()?.getOrNull()) } + } + + private fun setAutoSnapEnabled(enabled: Boolean) { + documentScannerView.viewController.apply { + autoSnappingEnabled = enabled + isFrameProcessingEnabled = enabled + } + documentScannerView.polygonConfiguration.setPolygonViewVisible(enabled) + + autoSnappingToggleButton.text = "Automatic ${if (enabled) "ON" else "OFF"}" + if (enabled) { + shutterButton.showAutoButton() + } else { + shutterButton.showManualButton() + userGuidanceHint.visibility = View.GONE + } + } + + companion object { + private val POLYGON_FILL_COLOR = Color.parseColor("#55ff0000") + private val POLYGON_FILL_COLOR_OK = Color.parseColor("#4400ff00") + } +} diff --git a/classic-components-example/document-enhancer/src/main/java/io/scanbot/example/ExampleApplication.kt b/classic-components-example/document-enhancer/src/main/java/io/scanbot/example/ExampleApplication.kt new file mode 100755 index 00000000..d366bec5 --- /dev/null +++ b/classic-components-example/document-enhancer/src/main/java/io/scanbot/example/ExampleApplication.kt @@ -0,0 +1,55 @@ +package io.scanbot.example + +import android.app.Application +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.ScanbotSDKInitializer +import io.scanbot.sdk.util.log.LoggerProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +class ExampleApplication : Application(), CoroutineScope { + + private var job: Job = Job() + override val coroutineContext: CoroutineContext + get() = Dispatchers.IO + job + + /* + * TODO 1/2: Add the Scanbot SDK license key here. + * Please note: The Scanbot SDK will run without a license key for one minute per session! + * After the trial period is over all Scanbot SDK functions as well as the UI components will stop working. + * You can get an unrestricted "no-strings-attached" 30 day trial license key for free. + * Please submit the trial license form (https://scanbot.io/trial/) on our website by using + * the app identifier "io.scanbot.example.sdk.android" of this example app. + */ + val licenseKey = "" + + override fun onCreate() { + super.onCreate() + + ScanbotSDKInitializer() + .withLogging(true) + // TODO 2/2: Enable the Scanbot SDK license key + //.license(this, licenseKey) + .licenseErrorHandler { status, feature, statusMessage -> + LoggerProvider.logger.d("ExampleApplication", "+++> License status: ${status.name}. Status message: $statusMessage") + LoggerProvider.logger.d("ExampleApplication", "+++> Feature not available: ${feature.name}") + } + //.sdkFilesDirectory(this, getExternalFilesDir(null)!!) + .initialize(this) + + LoggerProvider.logger.d("ExampleApplication", "Scanbot SDK was initialized") + + val licenseInfo = ScanbotSDK(this).licenseInfo + LoggerProvider.logger.d("ExampleApplication", "License status: ${licenseInfo.status}") + LoggerProvider.logger.d("ExampleApplication", "License isValid: ${licenseInfo.isValid}") + LoggerProvider.logger.d("ExampleApplication", "License expirationDate: ${licenseInfo.expirationDateString}") + + launch { + // Clear all previously created documents in storage + ScanbotSDK(this@ExampleApplication).documentApi.deleteAllDocuments() + } + } +} diff --git a/classic-components-example/document-enhancer/src/main/java/io/scanbot/example/MainActivity.kt b/classic-components-example/document-enhancer/src/main/java/io/scanbot/example/MainActivity.kt new file mode 100644 index 00000000..ddb5fe21 --- /dev/null +++ b/classic-components-example/document-enhancer/src/main/java/io/scanbot/example/MainActivity.kt @@ -0,0 +1,119 @@ +package io.scanbot.example + +import android.Manifest +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import io.scanbot.common.onSuccess + + +import io.scanbot.example.common.Const +import io.scanbot.example.common.applyEdgeToEdge +import io.scanbot.example.common.showToast +import io.scanbot.example.databinding.ActivityMainBinding +import io.scanbot.sdk.ScanbotSDK +import io.scanbot.sdk.documentscanner.DocumentStraighteningMode +import io.scanbot.sdk.documentscanner.DocumentStraighteningParameters +import io.scanbot.sdk.image.ImageRef +import io.scanbot.sdk.util.PolygonHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** +Ths example uses new sdk APIs presented in Scanbot SDK v.8.x.x +Please, check the official documentation for more details: +Result API https://docs.scanbot.io/android/document-scanner-sdk/detailed-setup-guide/result-api/ +ImageRef API https://docs.scanbot.io/android/document-scanner-sdk/detailed-setup-guide/image-ref-api/ + */ + +class MainActivity : AppCompatActivity() { + + private val scanbotSdk: ScanbotSDK by lazy { ScanbotSDK(this) } + + private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } + + private val requestCameraLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + startActivity(Intent(this, DocumentCameraActivity::class.java)) + } else { + this@MainActivity.showToast("Camera permission is required to run this example!") + } + } + + private val selectGalleryImageResultLauncher = + registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + if (!scanbotSdk.licenseInfo.isValid) { + this@MainActivity.showToast("1-minute trial license has expired!") + Log.e(Const.LOG_TAG, "1-minute trial license has expired!") + return@registerForActivityResult + } + + if (uri == null) { + showToast("Error obtaining selected image!") + Log.e(Const.LOG_TAG, "Error obtaining selected image!") + return@registerForActivityResult + } + + lifecycleScope.launch { processImage(uri) } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + supportActionBar?.hide() + applyEdgeToEdge(findViewById(R.id.root_view)) + + binding.showDocScannerBtn.setOnClickListener { + requestCameraLauncher.launch(Manifest.permission.CAMERA) + } + + binding.importImage.setOnClickListener { + selectGalleryImageResultLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } + } + + /** Imports a selected image as original image and performs auto document scanning on it. */ + private suspend fun processImage(uri: Uri) { + withContext(Dispatchers.Main) { + binding.progressBar.visibility = View.VISIBLE + this@MainActivity.showToast("Importing page...") + } + + val documentImage = withContext(Dispatchers.Default) { + // load the selected image + val image = contentResolver.openInputStream(uri)?.use { inputStream -> + ImageRef.fromInputStream(inputStream) + } ?: throw IllegalStateException("Cannot open input stream from URI: $uri") + + // create a new Page object with given image as original image: + val document = scanbotSdk.documentApi.createDocument() + .getOrNull() //can be handled with .getOrThrow() if needed + val page = + document?.addPage(image)?.getOrNull() //can be handled with .getOrThrow() if needed + + // run document scanning on the page image: + page?.apply(newStraighteningParameters = DocumentStraighteningParameters().apply { + straighteningMode = DocumentStraighteningMode.STRAIGHTEN + // uncomment if you want wo set specific aspect ratios for documents + // aspectRatios = listOf(AspectRatio(29.0, 21.0)) + }) + page?.documentImage + } + + withContext(Dispatchers.Main) { + binding.progressBar.visibility = View.GONE + + // present cropped page image: + binding.importResultImage.setImageBitmap(documentImage) + binding.importResultImage.visibility = View.VISIBLE + } + } +} diff --git a/classic-components-example/document-enhancer/src/main/res/layout/activity_camera.xml b/classic-components-example/document-enhancer/src/main/res/layout/activity_camera.xml new file mode 100755 index 00000000..ea4ca5ab --- /dev/null +++ b/classic-components-example/document-enhancer/src/main/res/layout/activity_camera.xml @@ -0,0 +1,59 @@ + + + + +