diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 9af4cf2..0000000 --- a/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - repositories { - google() - mavenCentral() - - } - dependencies { - classpath 'com.android.tools.build:gradle:8.2.2' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - mavenLocal() - google() - mavenCentral() - maven { url 'https://jitpack.io' } - } -} - -ext { - tesseract4AndroidVersion = '4.7.0' -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..9193624 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + mavenCentral() + + } + 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 + } +} + +allprojects { + repositories { + mavenLocal() + google() + mavenCentral() + maven("https://jitpack.io") + } +} + +tasks.register("clean") { + delete(rootProject.buildDir) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..8ae6f34 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,63 @@ +[versions] +# Intentionally use old version of annotation library which doesn"t depend on kotlin-stdlib +# to not unnecessarily complicate client projects due to potential duplicate class build errors +# caused by https://kotlinlang.org/docs/whatsnew18.html#updated-jvm-compilation-target +#noinspection GradleDependency +annotation = "1.3.0" +appcompat = "1.6.1" +espressoCore = "3.5.1" +gradle = "8.5.0" +junit = "4.13.2" +androidJUnit = "1.1.5" +lifecycleLivedata = "2.7.0" +material = "1.11.0" +constraintlayout = "2.1.4" +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" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } +androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } +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" } + +# Note that since we have 2 artifacts, we must use cz.adaptech.tesseract4android groupId, +# instead of just cz.adaptech groupId we use when using local maven repository. +tesseract4android-jitpack = { group = "cz.adaptech.tesseract4android", name = "tesseract4android", version.ref = "tesseract4android" } +tesseract4android-jitpack-openmp = { group = "cz.adaptech.tesseract4android", name = "tesseract4android-openmp", version.ref = "tesseract4android" } +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" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c0b79d0..cd88db1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip diff --git a/sample/build.gradle b/sample/build.gradle deleted file mode 100644 index 4e5552e..0000000 --- a/sample/build.gradle +++ /dev/null @@ -1,70 +0,0 @@ -plugins { - id 'com.android.application' -} - -android { - namespace 'cz.adaptech.tesseract4android.sample' - compileSdk 34 - - defaultConfig { - applicationId "cz.adaptech.tesseract4android.sample" - minSdk 21 - targetSdk 34 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - buildFeatures { - viewBinding true - } -} - -// In case you are using dependency on local library (the project(':tesseract4android') below), -// uncomment this to specify which flavor you want to build. -// Or you can specify *same* flavors also for the app - then they will be matched automatically. -// See more: https://developer.android.com/studio/build/build-variants#variant_aware -/*android { - defaultConfig { - // Choose 'standard' or 'openmp' flavor of the library - missingDimensionStrategy 'parallelization', 'standard' - } - flavorDimensions = ['parallelization'] -}*/ - -dependencies { - // To use library from JitPack - // Note that since we have 2 artifacts, we must use cz.adaptech.tesseract4android groupId, - // instead of just cz.adaptech groupId we use when using local maven repository. - implementation "cz.adaptech.tesseract4android:tesseract4android:$tesseract4AndroidVersion" // standard flavor -// implementation "cz.adaptech.tesseract4android:tesseract4android-openmp:$tesseract4AndroidVersion" // openmp flavor - - // To use library from local maven repository - // Don't forget to specify mavenLocal() in repositories block in project's build.gradle file -// implementation "cz.adaptech:tesseract4android:$tesseract4AndroidVersion" // standard flavor -// implementation "cz.adaptech:tesseract4android-openmp:$tesseract4AndroidVersion" // openmp flavor - - // To use library compiled locally - // Which flavor to use is determined by missingDimensionStrategy parameter above. -// implementation project(':tesseract4android') - - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.11.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.lifecycle:lifecycle-livedata:2.7.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel:2.7.0' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} \ No newline at end of file diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts new file mode 100644 index 0000000..73f8365 --- /dev/null +++ b/sample/build.gradle.kts @@ -0,0 +1,100 @@ +plugins { + id("com.android.application") + alias(libs.plugins.jetbrains.kotlin.android) +} + +android { + namespace = "cz.adaptech.tesseract4android.sample" + compileSdk = 34 + + defaultConfig { + applicationId = "cz.adaptech.tesseract4android.sample" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + 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), +// uncomment this to specify which flavor you want to build. +// Or you can specify *same* flavors also for the app - then they will be matched automatically. +// See more: https://developer.android.com/studio/build/build-variants#variant_aware +/*android { + defaultConfig { + // Choose "standard" or "openmp" flavor of the library + missingDimensionStrategy "parallelization", "standard" + } + flavorDimensions = ["parallelization"] +}*/ + +dependencies { + // To use library from JitPack + implementation(libs.tesseract4android.jitpack) // standard flavor + //implementation(libs.tesseract4android.jitpack.openmp) // openmp flavor + + // To use library from local maven repository + // Don't forget to specify mavenLocal() in repositories block in project's build.gradle file + //implementation(libs.tesseract4android.local) // standard flavor + //implementation(libs.tesseract4android.local.openmp) // openmp flavor + + // To use library compiled locally + // Which flavor to use is determined by missingDimensionStrategy parameter above. + //implementation(project(":tesseract4android")) + + implementation(libs.material) + 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 8c29efb..0000000 --- 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/Assets.java b/sample/src/main/java/cz/adaptech/tesseract4android/sample/Assets.java deleted file mode 100644 index 25f2385..0000000 --- a/sample/src/main/java/cz/adaptech/tesseract4android/sample/Assets.java +++ /dev/null @@ -1,93 +0,0 @@ -package cz.adaptech.tesseract4android.sample; - -import android.content.Context; -import android.content.res.AssetManager; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class Assets { - - /** - * Returns locally accessible directory where our assets are extracted. - */ - @NonNull - public static File getLocalDir(@NonNull Context context) { - return context.getFilesDir(); - } - - /** - * Returns locally accessible directory path which contains the "tessdata" subdirectory - * with *.traineddata files. - */ - @NonNull - public static String getTessDataPath(@NonNull Context context) { - return getLocalDir(context).getAbsolutePath(); - } - - @NonNull - public static File getImageFile(@NonNull Context context) { - return new File(getLocalDir(context), Config.IMAGE_NAME); - } - - @Nullable - public static Bitmap getImageBitmap(@NonNull Context context) { - return BitmapFactory.decodeFile(getImageFile(context).getAbsolutePath()); - } - - public static void extractAssets(@NonNull Context context) { - AssetManager am = context.getAssets(); - - File localDir = getLocalDir(context); - if (!localDir.exists() && !localDir.mkdir()) { - throw new RuntimeException("Can't create directory " + localDir); - } - - File tessDir = new File(getTessDataPath(context), "tessdata"); - if (!tessDir.exists() && !tessDir.mkdir()) { - throw new RuntimeException("Can't create directory " + tessDir); - } - - // Extract all assets to our local directory. - // All *.traineddata into "tessdata" subdirectory, other files into root. - try { - for (String assetName : am.list("")) { - final File targetFile; - if (assetName.endsWith(".traineddata")) { - targetFile = new File(tessDir, assetName); - } else { - targetFile = new File(localDir, assetName); - } - if (!targetFile.exists()) { - copyFile(am, assetName, targetFile); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void copyFile(@NonNull AssetManager am, @NonNull String assetName, - @NonNull File outFile) { - try ( - InputStream in = am.open(assetName); - OutputStream out = new FileOutputStream(outFile) - ) { - byte[] buffer = new byte[1024]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - } catch (IOException e) { - e.printStackTrace(); - } - } -} diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/Assets.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/Assets.kt new file mode 100644 index 0000000..d5d0306 --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/Assets.kt @@ -0,0 +1,88 @@ +package cz.adaptech.tesseract4android.sample + +import android.content.Context +import android.content.res.AssetManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +object Assets { + /** + * Returns locally accessible directory where our assets are extracted. + */ + fun getLocalDir(context: Context): File { + return context.filesDir + } + + /** + * Returns locally accessible directory path which contains the "tessdata" subdirectory + * with *.traineddata files. + */ + @JvmStatic + fun getTessDataPath(context: Context): String { + return getLocalDir(context).absolutePath + } + + @JvmStatic + fun getImageFile(context: Context): File { + return File(getLocalDir(context), Config.IMAGE_NAME) + } + + @JvmStatic + fun getImageBitmap(context: Context): Bitmap? { + return BitmapFactory.decodeFile(getImageFile(context).absolutePath) + } + + @JvmStatic + fun extractAssets(context: Context) { + val am = context.assets + + val localDir = getLocalDir(context) + if (!localDir.exists() && !localDir.mkdir()) { + throw RuntimeException("Can't create directory $localDir") + } + + val tessDir = File(getTessDataPath(context), "tessdata") + if (!tessDir.exists() && !tessDir.mkdir()) { + throw RuntimeException("Can't create directory $tessDir") + } + + // Extract all assets to our local directory. + // All *.traineddata into "tessdata" subdirectory, other files into root. + try { + for (assetName in am.list("")!!) { + val targetFile = if (assetName.endsWith(".traineddata")) { + File(tessDir, assetName) + } else { + File(localDir, assetName) + } + if (!targetFile.exists()) { + copyFile(am, assetName, targetFile) + } + } + } catch (e: IOException) { + e.printStackTrace() + } + } + + private fun copyFile( + am: AssetManager, assetName: String, + outFile: File + ) { + try { + am.open(assetName).use { `in` -> + FileOutputStream(outFile).use { out -> + val buffer = ByteArray(1024) + var read: Int + while ((`in`.read(buffer).also { read = it }) != -1) { + out.write(buffer, 0, read) + } + } + } + } catch (e: IOException) { + e.printStackTrace() + } + } +} diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/Config.java b/sample/src/main/java/cz/adaptech/tesseract4android/sample/Config.java deleted file mode 100644 index d0f81a2..0000000 --- a/sample/src/main/java/cz/adaptech/tesseract4android/sample/Config.java +++ /dev/null @@ -1,12 +0,0 @@ -package cz.adaptech.tesseract4android.sample; - -import com.googlecode.tesseract.android.TessBaseAPI; - -public class Config { - - public static final int TESS_ENGINE = TessBaseAPI.OEM_LSTM_ONLY; - - public static final String TESS_LANG = "eng"; - - public static final String IMAGE_NAME = "sample.jpg"; -} diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/Config.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/Config.kt new file mode 100644 index 0000000..0113bfa --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/Config.kt @@ -0,0 +1,11 @@ +package cz.adaptech.tesseract4android.sample + +import com.googlecode.tesseract.android.TessBaseAPI + +object Config { + const val TESS_ENGINE: Int = TessBaseAPI.OEM_LSTM_ONLY + + const val TESS_LANG: String = "eng" + + const val IMAGE_NAME: String = "sample.jpg" +} diff --git a/sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.java b/sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.java deleted file mode 100644 index f3df1ea..0000000 --- a/sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.java +++ /dev/null @@ -1,21 +0,0 @@ -package cz.adaptech.tesseract4android.sample; - -import androidx.appcompat.app.AppCompatActivity; - -import android.os.Bundle; - -import cz.adaptech.tesseract4android.sample.ui.main.MainFragment; - -public class MainActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - if (savedInstanceState == null) { - getSupportFragmentManager().beginTransaction() - .replace(R.id.container, MainFragment.newInstance()) - .commitNow(); - } - } -} \ 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 new file mode 100644 index 0000000..ae003bc --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.kt @@ -0,0 +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 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 : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + 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 0000000..864f3db --- /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.java b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.java deleted file mode 100644 index 5c9cb30..0000000 --- a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.java +++ /dev/null @@ -1,77 +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.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; - -import java.io.File; - -import cz.adaptech.tesseract4android.sample.Assets; -import cz.adaptech.tesseract4android.sample.Config; -import cz.adaptech.tesseract4android.sample.databinding.FragmentMainBinding; - -public class MainFragment extends Fragment { - - private FragmentMainBinding binding; - - private MainViewModel viewModel; - - public static MainFragment newInstance() { - return new MainFragment(); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this).get(MainViewModel.class); - - // Copy sample image and language data to storage - Assets.extractAssets(requireContext()); - - if (!viewModel.isInitialized()) { - String dataPath = Assets.getTessDataPath(requireContext()); - viewModel.initTesseract(dataPath, Config.TESS_LANG, Config.TESS_ENGINE); - } - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - binding = FragmentMainBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - binding.image.setImageBitmap(Assets.getImageBitmap(requireContext())); - binding.start.setOnClickListener(v -> { - File imageFile = Assets.getImageFile(requireContext()); - viewModel.recognizeImage(imageFile); - }); - binding.stop.setOnClickListener(v -> { - viewModel.stop(); - }); - binding.text.setMovementMethod(new ScrollingMovementMethod()); - - viewModel.getProcessing().observe(getViewLifecycleOwner(), processing -> { - binding.start.setEnabled(!processing); - binding.stop.setEnabled(processing); - }); - viewModel.getProgress().observe(getViewLifecycleOwner(), progress -> { - binding.status.setText(progress); - }); - viewModel.getResult().observe(getViewLifecycleOwner(), result -> { - binding.text.setText(result); - }); - } -} \ 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 0000000..ddf9c5a --- /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.java b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.java deleted file mode 100644 index 26d9826..0000000 --- a/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.java +++ /dev/null @@ -1,166 +0,0 @@ -package cz.adaptech.tesseract4android.sample.ui.main; - -import android.app.Application; -import android.os.SystemClock; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; - -import com.googlecode.tesseract.android.TessBaseAPI; - -import java.io.File; -import java.util.Locale; - -public class MainViewModel extends AndroidViewModel { - - private static final String TAG = "MainViewModel"; - - private final TessBaseAPI tessApi; - - private final MutableLiveData processing = new MutableLiveData<>(false); - - private final MutableLiveData progress = new MutableLiveData<>(); - - private final MutableLiveData result = new MutableLiveData<>(); - - private boolean tessInit; - - private volatile boolean stopped; - - private volatile boolean tessProcessing; - - private volatile boolean recycleAfterProcessing; - - private final Object recycleLock = new Object(); - - public MainViewModel(@NonNull Application application) { - super(application); - - tessApi = new TessBaseAPI(progressValues -> { - progress.postValue("Progress: " + progressValues.getPercent() + " %"); - }); - - // Show Tesseract version and library flavor at startup - progress.setValue(String.format(Locale.ENGLISH, "Tesseract %s (%s)", - tessApi.getVersion(), tessApi.getLibraryFlavor())); - } - - @Override - protected void 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(); - } - } - } - - public void initTesseract(@NonNull String dataPath, @NonNull String language, int engineMode) { - Log.i(TAG, "Initializing Tesseract with: dataPath = [" + dataPath + "], " + - "language = [" + language + "], engineMode = [" + engineMode + "]"); - try { - tessInit = tessApi.init(dataPath, language, engineMode); - } catch (IllegalArgumentException e) { - tessInit = false; - Log.e(TAG, "Cannot initialize Tesseract:", e); - } - } - - public void recognizeImage(@NonNull File imagePath) { - if (!tessInit) { - Log.e(TAG, "recognizeImage: Tesseract is not initialized"); - return; - } - if (tessProcessing) { - Log.e(TAG, "recognizeImage: Processing is in progress"); - return; - } - tessProcessing = true; - - result.setValue(""); - processing.setValue(true); - progress.setValue("Processing..."); - stopped = false; - - // Start process in another thread - new Thread(() -> { - tessApi.setImage(imagePath); - // Or set it as Bitmap, Pix,... - // tessApi.setImage(imageBitmap); - - long startTime = SystemClock.uptimeMillis(); - - // Use getHOCRText(0) method to trigger recognition with progress notifications and - // ability to cancel ongoing processing. - tessApi.getHOCRText(0); - - // At this point the recognition has completed (or was interrupted by calling stop()) - // and we can get the results we want. In this case just normal UTF8 text. - // - // Note that calling only this method (without the getHOCRText() above) would also - // trigger the recognition and return the same result, but we would received no progress - // notifications and we wouldn't be able to stop() the ongoing recognition. - String text = tessApi.getUTF8Text(); - - // We can free up the recognition results and any stored image data in the tessApi - // if we don't need them anymore. - tessApi.clear(); - - // Publish the results - result.postValue(text); - processing.postValue(false); - if (stopped) { - progress.postValue("Stopped."); - } else { - long 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(); - } - } - }).start(); - } - - public void stop() { - if (!tessProcessing) { - return; - } - progress.setValue("Stopping..."); - stopped = true; - tessApi.stop(); - } - - public boolean isInitialized() { - return tessInit; - } - - @NonNull - public LiveData getProcessing() { - return processing; - } - - @NonNull - public LiveData getProgress() { - return progress; - } - - @NonNull - public LiveData getResult() { - return result; - } -} \ 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 new file mode 100644 index 0000000..1129abc --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.kt @@ -0,0 +1,205 @@ +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.viewModelScope +import com.googlecode.tesseract.android.TessBaseAPI +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 + + /** + * 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) + + init { + // Instantiate the API + tessApi = TessBaseAPI { progressValues: TessBaseAPI.ProgressValues -> + _progress.tryEmit(OCRState.Progress(progressValues.percent)) + } + + // IO Tasks + viewModelScope.launch(Dispatchers.IO) { + // Copy sample image and language data to storage + extractAssets(application) + + // Load the image + _image.emit(Assets.getImageBitmap(application)) + + // Initialize tesseract + initTesseract(getTessDataPath(application), Config.TESS_LANG, Config.TESS_ENGINE) + } + + // Show Tesseract version and library flavor at startup + _progress.value = OCRState.StartUp(tessApi.version, tessApi.libraryFlavor) + } + + override fun onCleared() { + tessApi.stop() + tessApi.recycle() + } + + private fun initTesseract(dataPath: String, language: String, engineMode: Int) { + Log.i( + TAG, "Initializing Tesseract with: dataPath = [" + dataPath + "], " + + "language = [" + language + "], engineMode = [" + engineMode + "]" + ) + try { + this.isInitialized = tessApi.init(dataPath, language, engineMode) + } catch (e: IllegalArgumentException) { + this.isInitialized = false + Log.e(TAG, "Cannot initialize Tesseract:", e) + } + } + + private fun recognizeImage() { + if (!this.isInitialized) { + Log.e(TAG, "recognizeImage: Tesseract is not initialized") + return + } + if (processing.value) { + Log.e(TAG, "recognizeImage: Processing is in progress") + return + } + _result.value = "" + processing.value = true + _progress.value = OCRState.Processing + stopped = false + + // Start process in another thread + 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 + // ability to cancel ongoing processing. + tessApi.getHOCRText(0) + + // At this point the recognition has completed (or was interrupted by calling stop()) + // and we can get the results we want. In this case just normal UTF8 text. + // + // Note that calling only this method (without the getHOCRText() above) would also + // trigger the recognition and return the same result, but we would received no progress + // notifications and we wouldn't be able to stop() the ongoing recognition. + val text = tessApi.utF8Text + + // We can free up the recognition results and any stored image data in the tessApi + // if we don't need them anymore. + tessApi.clear() + + // Publish the results + _result.emit(text) + processing.emit(false) + if (stopped) { + _progress.emit(OCRState.Stopped) + } else { + val duration = SystemClock.uptimeMillis() - startTime + _progress.emit(OCRState.Finished(duration / 1000f)) + } + } + } + + /** + * Stops the OCR. + */ + fun stop() { + if (!processing.value) { + return + } + _progress.value = OCRState.Stopping + stopped = true + tessApi.stop() + } + + /** + * Start the OCR + */ + fun start() { + recognizeImage() + } + + companion object { + private const val TAG = "MainViewModel" + } +} \ No newline at end of file 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 0000000..0a9b48a --- /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 0000000..57a326e --- /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 0000000..813ec6e --- /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 9f2755b..0000000 --- a/sample/src/main/res/layout-land/fragment_main.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - -