diff --git a/build.gradle.kts b/build.gradle.kts index dd1b9a32..9193624a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,7 @@ buildscript { } dependencies { classpath(libs.gradle) + classpath(libs.kotlin.gradle.plugin) // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0eca8dcd..8ae6f34d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,12 @@ test = "1.6.1" tesseract4android = "4.7.0" coreKtx = "1.13.1" kotlin = "2.0.0" +kotlinGradlePlugin = "1.9.0" +lifecycleRuntimeKtx = "2.8.3" +activityCompose = "1.9.0" +composeBom = "2024.06.00" +window = "1.3.0" +adaptiveAndroid = "1.0.0-beta04" [libraries] androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } @@ -25,8 +31,10 @@ androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", vers androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidJUnit" } androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata", version.ref = "lifecycleLivedata" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleLivedata" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" } androidx-rules = { module = "androidx.test:rules", version.ref = "test" } androidx-runner = { module = "androidx.test:runner", version.ref = "test" } +androidx-window = { module = "androidx.window:window", version.ref = "window" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } junit = { module = "junit:junit", version.ref = "junit" } material = { module = "com.google.android.material:material", version.ref = "material" } @@ -38,6 +46,18 @@ tesseract4android-jitpack-openmp = { group = "cz.adaptech.tesseract4android", na tesseract4android-local = { group = "cz.adaptech", name = "tesseract4android", version.ref = "tesseract4android" } tesseract4android-local-openmp = { group = "cz.adaptech", name = "tesseract4android-openmp", version.ref = "tesseract4android" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlinGradlePlugin" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-adaptive-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-android", version.ref = "adaptiveAndroid" } [plugins] -jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } \ No newline at end of file +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android" } \ No newline at end of file diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index fe00f18a..73f83658 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -15,11 +15,13 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } } buildTypes { release { - isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -32,10 +34,19 @@ android { } buildFeatures { viewBinding = true + compose = true } kotlinOptions { jvmTarget = "17" } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } } // In case you are using dependency on local library (the project(":tesseract4android") below), @@ -64,13 +75,26 @@ dependencies { // Which flavor to use is determined by missingDimensionStrategy parameter above. //implementation(project(":tesseract4android")) - implementation(libs.androidx.appcompat) implementation(libs.material) - implementation(libs.androidx.constraintlayout) - implementation(libs.androidx.lifecycle.livedata) - implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.window) + + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.adaptive.android) + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) } \ No newline at end of file diff --git a/sample/src/androidTest/java/cz/adaptech/tesseract4android/sample/ExampleInstrumentedTest.java b/sample/src/androidTest/java/cz/adaptech/tesseract4android/sample/ExampleInstrumentedTest.java deleted file mode 100644 index 8c29efb0..00000000 --- a/sample/src/androidTest/java/cz/adaptech/tesseract4android/sample/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package cz.adaptech.tesseract4android.sample; - -import android.content.Context; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("cz.adaptech.tesseract4android.sample", appContext.getPackageName()); - } -} \ No newline at end of file diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.kt index 12f8fc6f..ae003bca 100644 --- a/sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.kt +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.kt @@ -1,17 +1,23 @@ package cz.adaptech.tesseract4android.sample import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity -import cz.adaptech.tesseract4android.sample.ui.main.MainFragment +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.Modifier +import cz.adaptech.tesseract4android.sample.ui.main.MainView +import cz.adaptech.tesseract4android.sample.ui.theme.Tesseract4AndroidTheme -class MainActivity : AppCompatActivity() { +class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - if (savedInstanceState == null) { - supportFragmentManager.beginTransaction() - .replace(R.id.container, MainFragment.newInstance()) - .commitNow() + enableEdgeToEdge() + setContent { + MainView() } } } \ No newline at end of file diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/OCRState.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/OCRState.kt new file mode 100644 index 00000000..864f3db7 --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/OCRState.kt @@ -0,0 +1,52 @@ +package cz.adaptech.tesseract4android.sample + + +/** + * Represents the various states that the OCR can be in. + * + * @since 2024/07/22 + * @author Clocks + */ +sealed interface OCRState { + /** + * OCR is loading up. + */ + data object Loading : OCRState + + /** + * OCR is prepared. + * + * @param version Version of tesseract + * @param flavour Build flavour of tesseract + */ + data class StartUp(val version: String, val flavour: String) : OCRState + + /** + * OCR has been stopped. + */ + data object Stopped : OCRState + + /** + * OCR is being stopped. + */ + data object Stopping : OCRState + + /** + * OCR is starting up. + */ + data object Processing : OCRState + + /** + * OCR is currently in process. + * + * @param progress 0-100 progress indication. + */ + data class Progress(val progress: Int) : OCRState + + /** + * OCR has completed its task. + * + * @param time How many seconds it took to process the image. + */ + data class Finished(val time: Float) : OCRState +} \ No newline at end of file diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.kt deleted file mode 100644 index f5d17e07..00000000 --- a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.kt +++ /dev/null @@ -1,74 +0,0 @@ -package cz.adaptech.tesseract4android.sample.ui.main - -import android.os.Bundle -import android.text.method.ScrollingMovementMethod -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import cz.adaptech.tesseract4android.sample.Assets.extractAssets -import cz.adaptech.tesseract4android.sample.Assets.getImageBitmap -import cz.adaptech.tesseract4android.sample.Assets.getImageFile -import cz.adaptech.tesseract4android.sample.Assets.getTessDataPath -import cz.adaptech.tesseract4android.sample.Config -import cz.adaptech.tesseract4android.sample.databinding.FragmentMainBinding -import cz.adaptech.tesseract4android.sample.ui.main.MainViewModel - -class MainFragment : Fragment() { - private var binding: FragmentMainBinding? = null - - private var viewModel: MainViewModel? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel = ViewModelProvider(this).get(MainViewModel::class.java) - - // Copy sample image and language data to storage - extractAssets(requireContext()) - - if (!viewModel!!.isInitialized) { - val dataPath = getTessDataPath(requireContext()) - viewModel!!.initTesseract(dataPath, Config.TESS_LANG, Config.TESS_ENGINE) - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentMainBinding.inflate(inflater, container, false) - return binding!!.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding!!.image.setImageBitmap(getImageBitmap(requireContext())) - binding!!.start.setOnClickListener { v: View? -> - val imageFile = getImageFile(requireContext()) - viewModel!!.recognizeImage(imageFile) - } - binding!!.stop.setOnClickListener { v: View? -> - viewModel!!.stop() - } - binding!!.text.movementMethod = ScrollingMovementMethod() - - viewModel!!.getProcessing().observe(viewLifecycleOwner) { processing: Boolean? -> - binding!!.start.isEnabled = !processing!! - binding!!.stop.isEnabled = processing - } - viewModel!!.getProgress().observe(viewLifecycleOwner) { progress: String? -> - binding!!.status.text = progress - } - viewModel!!.getResult().observe(viewLifecycleOwner) { result: String? -> - binding!!.text.text = result - } - } - - companion object { - fun newInstance(): MainFragment { - return MainFragment() - } - } -} \ No newline at end of file diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainView.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainView.kt new file mode 100644 index 00000000..ddf9c5a7 --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainView.kt @@ -0,0 +1,169 @@ +package cz.adaptech.tesseract4android.sample.ui.main + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowWidthSizeClass +import cz.adaptech.tesseract4android.sample.OCRState +import cz.adaptech.tesseract4android.sample.ui.theme.Tesseract4AndroidTheme +import java.util.Locale + + +/** + * @since 2024/07/22 + */ +@Composable +fun MainView() { + val viewModel = viewModel() + val image by viewModel.image.collectAsState() + val status by viewModel.status.collectAsState() + val result by viewModel.result.collectAsState() + + val isStartEnabled by viewModel.isStartEnabled.collectAsState() + val isStopEnabled by viewModel.isStopEnabled.collectAsState() + + val sizeClass = currentWindowAdaptiveInfo().windowSizeClass + val landscape = sizeClass.windowWidthSizeClass != WindowWidthSizeClass.COMPACT + + Tesseract4AndroidTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + MainContent( + innerPadding, + image, + status, + result, + viewModel::start, + viewModel::stop, + isStartEnabled, + isStopEnabled, + landscape + ) + } + } +} + +@Composable +fun MainContent( + innerPadding: PaddingValues, + bitmap: ImageBitmap?, + status: OCRState, + result: String, + onStart: () -> Unit, + onStop: () -> Unit, + isStartEnabled: Boolean, + isStopEnabled: Boolean, + landscape: Boolean +) { + if (landscape) { + Row( + Modifier + .padding(innerPadding) + .fillMaxSize(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Image(bitmap) + + Column( + Modifier + .verticalScroll(rememberScrollState()) + .weight(1f), // let it fill up space + horizontalAlignment = Alignment.CenterHorizontally + ) { + Status(status) + + Controls(onStart, onStop, isStartEnabled, isStopEnabled) + + Result(result) + } + } + } else { + Column( + Modifier + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image(bitmap) + + Status(status) + + Controls(onStart, onStop, isStartEnabled, isStopEnabled) + + Result(result) + } + } +} + +@Composable +fun Result(result: String) { + Text(text = result, Modifier.padding(16.dp)) +} + +@Composable +fun Image(bitmap: ImageBitmap?) { + AnimatedVisibility(visible = bitmap != null) { + Image(bitmap = bitmap!!, contentDescription = "Sample") + } +} + +@Composable +fun Controls( + onStart: () -> Unit, + onStop: () -> Unit, + isStartEnabled: Boolean, + isStopEnabled: Boolean +) { + Row { + Button(onClick = onStart, enabled = isStartEnabled) { + Text(text = "START") + } + Button(onClick = onStop, enabled = isStopEnabled) { + Text(text = "STOP") + } + } +} + +@Composable +fun Status(status: OCRState) { + Row { + Text(text = "Status: ") + Text( + text = when (status) { + is OCRState.Finished -> + "Completed in %.3fs.".format(Locale.getDefault(), status.time) + + OCRState.Processing -> "Processing..." + is OCRState.Progress -> "Processing ${status.progress}%" + is OCRState.StartUp -> + "Tesseract %s (%s)" + .format(Locale.getDefault(), status.version, status.flavour) + + OCRState.Stopped -> "Stopped." + OCRState.Stopping -> "Stopping..." + OCRState.Loading -> "Loading..." + } + ) + } +} \ No newline at end of file diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.kt index 64edf88f..1129abca 100644 --- a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.kt +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.kt @@ -1,66 +1,124 @@ package cz.adaptech.tesseract4android.sample.ui.main import android.app.Application +import android.graphics.Bitmap import android.os.SystemClock import android.util.Log +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import com.googlecode.tesseract.android.TessBaseAPI -import java.io.File -import java.util.Locale -import kotlin.concurrent.Volatile - +import cz.adaptech.tesseract4android.sample.Assets +import cz.adaptech.tesseract4android.sample.Assets.extractAssets +import cz.adaptech.tesseract4android.sample.Assets.getTessDataPath +import cz.adaptech.tesseract4android.sample.Config +import cz.adaptech.tesseract4android.sample.OCRState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** + * View Model for Main View. + */ class MainViewModel(application: Application) : AndroidViewModel(application) { + /** + * Tesseract API + */ private val tessApi: TessBaseAPI - private val processing = MutableLiveData(false) - - private val progress = MutableLiveData() - - private val result = MutableLiveData() + /** + * Is the OCR in progress? + */ + private val processing = MutableStateFlow(false) + + /** + * The current state of the OCR + */ + private val _progress = MutableStateFlow(OCRState.Loading) + + /** + * The resulting text from the OCR. + */ + private val _result = MutableStateFlow("") + + /** + * Has the tesseract API been initialized? + */ + private var isInitialized = false + + /** + * If the OCR has been stopped by the user or not. + */ + private var stopped: Boolean = false + + /** + * Holds the bitmap of the sample image. + */ + private val _image = MutableStateFlow(null) + + /** + * Immutable version for view access. + */ + val status: StateFlow = _progress + + /** + * Immutable version for view access. + */ + val result: StateFlow = _result + + /** + * Is the start button enabled or not. + */ + val isStartEnabled: StateFlow = processing.map { !it } + .stateIn(viewModelScope, SharingStarted.Lazily, false) + + /** + * Is the stop button enabled or not. + */ + val isStopEnabled: StateFlow = processing + + /** + * Converts the sample image into an ImageBitmap for UI + */ + val image: StateFlow = _image.map { + it?.asImageBitmap() + }.stateIn(viewModelScope, SharingStarted.Eagerly, null) - var isInitialized: Boolean = false - private set - - @Volatile - private var stopped = false - - @Volatile - private var tessProcessing = false + init { + // Instantiate the API + tessApi = TessBaseAPI { progressValues: TessBaseAPI.ProgressValues -> + _progress.tryEmit(OCRState.Progress(progressValues.percent)) + } - @Volatile - private var recycleAfterProcessing = false + // IO Tasks + viewModelScope.launch(Dispatchers.IO) { + // Copy sample image and language data to storage + extractAssets(application) - private val recycleLock = Any() + // Load the image + _image.emit(Assets.getImageBitmap(application)) - init { - tessApi = TessBaseAPI { progressValues: TessBaseAPI.ProgressValues -> - progress.postValue("Progress: " + progressValues.percent + " %") + // Initialize tesseract + initTesseract(getTessDataPath(application), Config.TESS_LANG, Config.TESS_ENGINE) } // Show Tesseract version and library flavor at startup - progress.value = String.format( - Locale.ENGLISH, "Tesseract %s (%s)", - tessApi.version, tessApi.libraryFlavor - ) + _progress.value = OCRState.StartUp(tessApi.version, tessApi.libraryFlavor) } override fun onCleared() { - synchronized(recycleLock) { - if (tessProcessing) { - // Processing is active, set flag to recycle tessApi after processing is completed - recycleAfterProcessing = true - // Stop the processing as we don't care about the result anymore - tessApi.stop() - } else { - // No ongoing processing, we must recycle it here - tessApi.recycle() - } - } + tessApi.stop() + tessApi.recycle() } - fun initTesseract(dataPath: String, language: String, engineMode: Int) { + private fun initTesseract(dataPath: String, language: String, engineMode: Int) { Log.i( TAG, "Initializing Tesseract with: dataPath = [" + dataPath + "], " + "language = [" + language + "], engineMode = [" + engineMode + "]" @@ -73,27 +131,25 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } } - fun recognizeImage(imagePath: File) { + private fun recognizeImage() { if (!this.isInitialized) { Log.e(TAG, "recognizeImage: Tesseract is not initialized") return } - if (tessProcessing) { + if (processing.value) { Log.e(TAG, "recognizeImage: Processing is in progress") return } - tessProcessing = true - - result.value = "" + _result.value = "" processing.value = true - progress.value = "Processing..." + _progress.value = OCRState.Processing stopped = false // Start process in another thread - Thread { - tessApi.setImage(imagePath) - // Or set it as Bitmap, Pix,... - // tessApi.setImage(imageBitmap); + viewModelScope.launch(Dispatchers.IO) { + tessApi.setImage(_image.value!!) + // Or set it via a File. + // tessApi.setImage(imageFile); val startTime = SystemClock.uptimeMillis() // Use getHOCRText(0) method to trigger recognition with progress notifications and @@ -113,48 +169,34 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { tessApi.clear() // Publish the results - result.postValue(text) - processing.postValue(false) + _result.emit(text) + processing.emit(false) if (stopped) { - progress.postValue("Stopped.") + _progress.emit(OCRState.Stopped) } else { val duration = SystemClock.uptimeMillis() - startTime - progress.postValue( - String.format( - Locale.ENGLISH, - "Completed in %.3fs.", (duration / 1000f) - ) - ) - } - synchronized(recycleLock) { - tessProcessing = false - // Recycle the instance here if the view model is already destroyed - if (recycleAfterProcessing) { - tessApi.recycle() - } + _progress.emit(OCRState.Finished(duration / 1000f)) } - }.start() + } } + /** + * Stops the OCR. + */ fun stop() { - if (!tessProcessing) { + if (!processing.value) { return } - progress.value = "Stopping..." + _progress.value = OCRState.Stopping stopped = true tessApi.stop() } - fun getProcessing(): LiveData { - return processing - } - - fun getProgress(): LiveData { - return progress - } - - fun getResult(): LiveData { - return result + /** + * Start the OCR + */ + fun start() { + recognizeImage() } companion object { diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Color.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Color.kt new file mode 100644 index 00000000..0a9b48ae --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package cz.adaptech.tesseract4android.sample.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Theme.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Theme.kt new file mode 100644 index 00000000..57a326eb --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Theme.kt @@ -0,0 +1,57 @@ +package cz.adaptech.tesseract4android.sample.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun Tesseract4AndroidTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Type.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Type.kt new file mode 100644 index 00000000..813ec6e4 --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package cz.adaptech.tesseract4android.sample.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/sample/src/main/res/layout-land/fragment_main.xml b/sample/src/main/res/layout-land/fragment_main.xml deleted file mode 100644 index 9f2755b8..00000000 --- a/sample/src/main/res/layout-land/fragment_main.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - -