From ad0e0f4434737e9f4cd02ab7fa8ca70567d5e4ce Mon Sep 17 00:00:00 2001 From: clocks Date: Mon, 22 Jul 2024 13:54:00 -0400 Subject: [PATCH 1/5] Convert gradle to kotlin script Its more idiomatic & JVM like compared Groovy. Also is the new standard. --- build.gradle | 32 -------- build.gradle.kts | 30 +++++++ sample/build.gradle | 70 ---------------- sample/build.gradle.kts | 76 +++++++++++++++++ settings.gradle | 4 - settings.gradle.kts | 4 + tesseract4android/build.gradle | 123 --------------------------- tesseract4android/build.gradle.kts | 128 +++++++++++++++++++++++++++++ 8 files changed, 238 insertions(+), 229 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 sample/build.gradle create mode 100644 sample/build.gradle.kts delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts delete mode 100644 tesseract4android/build.gradle create mode 100644 tesseract4android/build.gradle.kts diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 9af4cf21..00000000 --- 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 00000000..3cdd5d42 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,30 @@ +// 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("https://jitpack.io") + } +} + +val tesseract4AndroidVersion by ext("4.7.0") + +tasks.register("clean") { + delete(rootProject.buildDir) +} diff --git a/sample/build.gradle b/sample/build.gradle deleted file mode 100644 index 4e5552e4..00000000 --- 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 00000000..bd52d062 --- /dev/null +++ b/sample/build.gradle.kts @@ -0,0 +1,76 @@ +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 { + isMinifyEnabled = 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"] +}*/ + +val tesseract4AndroidVersion: String by rootProject.extra + +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/settings.gradle b/settings.gradle deleted file mode 100644 index 94444275..00000000 --- a/settings.gradle +++ /dev/null @@ -1,4 +0,0 @@ -include ':tesseract4android' -if (!System.env.JITPACK) { - include ':sample' -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..6c94d671 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,4 @@ +include(":tesseract4android") +if (System.getenv("JITPACK") == null) { + include(":sample") +} diff --git a/tesseract4android/build.gradle b/tesseract4android/build.gradle deleted file mode 100644 index 496f8dd3..00000000 --- a/tesseract4android/build.gradle +++ /dev/null @@ -1,123 +0,0 @@ -plugins { - id 'com.android.library' - id 'maven-publish' -} - -android { - namespace 'cz.adaptech.tesseract4android' - compileSdk 33 - ndkVersion "25.1.8937393" - - defaultConfig { - minSdk 16 - targetSdk 33 - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - externalNativeBuild { - cmake { - // Specifies which native libraries or executables to build and package. - // TODO: Include eyes-two in some build flavor of the library? - //targets "jpeg", "pngx", "leptonica", "tesseract" - } - } - ndk { - // Specify the ABI configurations that Gradle should build and package. - // By default it compiles all available ABIs. - //abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' - } - } - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - version '3.22.1' - } - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - debug { - externalNativeBuild { - cmake { - // Force building release version of native libraries even in debug variant. - // This is for projects that has direct dependency on this library, - // but doesn't really want its debug version, which is very slow. - // Note that this only affects native code. - arguments "-DCMAKE_BUILD_TYPE=Release" - } - } - } - } - flavorDimensions = ["parallelization"] - productFlavors { - standard { - } - openmp { - externalNativeBuild { - cmake { - // NOTE: We must add -static-openmp argument to build it statically, - // because shared library is not being included in the resulting APK. - // See: https://github.com/android/ndk/issues/1028 - // Use of that argument shows warnings during build: - // > C/C++: clang: warning: argument unused during compilation: '-static-openmp' [-Wunused-command-line-argument] - // But it has no effect on the result. - cFlags "-fopenmp -static-openmp -Wno-unused-command-line-argument" - cppFlags "-fopenmp -static-openmp -Wno-unused-command-line-argument" - } - } - } - } - publishing { - singleVariant("standardRelease") { - withSourcesJar() - withJavadocJar() - } - singleVariant("openmpRelease") { - withSourcesJar() - withJavadocJar() - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - buildFeatures { - buildConfig true - } -} - -dependencies { - // 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 - implementation 'androidx.annotation:annotation:1.3.0' - - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test:rules:1.5.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} - -afterEvaluate { - publishing { - publications { - standard(MavenPublication) { - from components.findByName('standardRelease') - - groupId 'cz.adaptech' - artifactId 'tesseract4android' - version rootProject.ext.tesseract4AndroidVersion - } - openmp(MavenPublication) { - from components.findByName('openmpRelease') - - groupId 'cz.adaptech' - artifactId 'tesseract4android-openmp' - version rootProject.ext.tesseract4AndroidVersion - } - } - } -} diff --git a/tesseract4android/build.gradle.kts b/tesseract4android/build.gradle.kts new file mode 100644 index 00000000..82a3fc86 --- /dev/null +++ b/tesseract4android/build.gradle.kts @@ -0,0 +1,128 @@ +plugins { + id("com.android.library") + id("maven-publish") +} + +android { + namespace = "cz.adaptech.tesseract4android" + compileSdk = 33 + ndkVersion = "25.1.8937393" + + defaultConfig { + minSdk = 16 + lint.targetSdk = 33 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + // Specifies which native libraries or executables to build and package. + // TODO: Include eyes-two in some build flavor of the library? + //targets "jpeg", "pngx", "leptonica", "tesseract" + } + } + ndk { + // Specify the ABI configurations that Gradle should build and package. + // By default it compiles all available ABIs. + //abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a" + } + } + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + debug { + externalNativeBuild { + cmake { + // Force building release version of native libraries even in debug variant. + // This is for projects that has direct dependency on this library, + // but doesn"t really want its debug version, which is very slow. + // Note that this only affects native code. + arguments("-DCMAKE_BUILD_TYPE=Release") + } + } + } + } + flavorDimensions += listOf("parallelization") + productFlavors { + create("standard") { + } + create("openmp") { + externalNativeBuild { + cmake { + // NOTE: We must add -static-openmp argument to build it statically, + // because shared library is not being included in the resulting APK. + // See: https://github.com/android/ndk/issues/1028 + // Use of that argument shows warnings during build: + // > C/C++: clang: warning: argument unused during compilation: "-static-openmp" [-Wunused-command-line-argument] + // But it has no effect on the result. + cFlags("-fopenmp -static-openmp -Wno-unused-command-line-argument") + cppFlags("-fopenmp -static-openmp -Wno-unused-command-line-argument") + } + } + } + } + publishing { + singleVariant("standardRelease") { + withSourcesJar() + withJavadocJar() + } + singleVariant("openmpRelease") { + withSourcesJar() + withJavadocJar() + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + buildFeatures { + buildConfig = true + } +} + +dependencies { + // 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 + implementation("androidx.annotation:annotation:1.3.0") + + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} + +val tesseract4AndroidVersion: String by rootProject.extra + +afterEvaluate { + publishing { + publications { + create("standard") { + from(components.findByName("standardRelease")) + + groupId = "cz.adaptech" + artifactId = "tesseract4android" + version = tesseract4AndroidVersion + } + create("openmp") { + from(components.findByName("openmpRelease")) + + groupId = "cz.adaptech" + artifactId = "tesseract4android-openmp" + version = tesseract4AndroidVersion + } + } + } +} From 7ee345e6de343077db7743264ab8f388cf02b480 Mon Sep 17 00:00:00 2001 From: clocks Date: Mon, 22 Jul 2024 14:01:23 -0400 Subject: [PATCH 2/5] Upgrade to AGP 8.5.0 Upgrades Gradle to 8.9 --- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3cdd5d42..63f5c038 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.2.2") + classpath("com.android.tools.build:gradle:8.5.0") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c0b79d0c..cd88db17 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 From c2c8de65ee7ea7178e612136214712458dd3ab29 Mon Sep 17 00:00:00 2001 From: clocks Date: Mon, 22 Jul 2024 14:14:39 -0400 Subject: [PATCH 3/5] Migrate to version catalogs --- build.gradle.kts | 4 +-- gradle/libs.versions.toml | 39 ++++++++++++++++++++++++++++++ sample/build.gradle.kts | 31 ++++++++++-------------- tesseract4android/build.gradle.kts | 22 ++++++----------- 4 files changed, 61 insertions(+), 35 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/build.gradle.kts b/build.gradle.kts index 63f5c038..dd1b9a32 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.5.0") + classpath(libs.gradle) // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -23,8 +23,6 @@ allprojects { } } -val tesseract4AndroidVersion by ext("4.7.0") - tasks.register("clean") { delete(rootProject.buildDir) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..92bc4735 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,39 @@ +[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" + +[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-rules = { module = "androidx.test:rules", version.ref = "test" } +androidx-runner = { module = "androidx.test:runner", version.ref = "test" } +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" } + +[plugins] \ No newline at end of file diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index bd52d062..28d59cab 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -46,31 +46,26 @@ android { flavorDimensions = ["parallelization"] }*/ -val tesseract4AndroidVersion: String by rootProject.extra - 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 + 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 "cz.adaptech:tesseract4android:$tesseract4AndroidVersion" // standard flavor -// implementation "cz.adaptech:tesseract4android-openmp:$tesseract4AndroidVersion" // openmp flavor + //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(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") + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.lifecycle.livedata) + implementation(libs.androidx.lifecycle.viewmodel) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) } \ No newline at end of file diff --git a/tesseract4android/build.gradle.kts b/tesseract4android/build.gradle.kts index 82a3fc86..0a0b3767 100644 --- a/tesseract4android/build.gradle.kts +++ b/tesseract4android/build.gradle.kts @@ -91,21 +91,15 @@ android { } dependencies { - // 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 - implementation("androidx.annotation:annotation:1.3.0") + implementation(libs.androidx.annotation) - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test:runner:1.5.2") - androidTestImplementation("androidx.test:rules:1.5.0") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.runner) + androidTestImplementation(libs.androidx.rules) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) } -val tesseract4AndroidVersion: String by rootProject.extra - afterEvaluate { publishing { publications { @@ -114,14 +108,14 @@ afterEvaluate { groupId = "cz.adaptech" artifactId = "tesseract4android" - version = tesseract4AndroidVersion + version = libs.versions.tesseract4android.get() } create("openmp") { from(components.findByName("openmpRelease")) groupId = "cz.adaptech" artifactId = "tesseract4android-openmp" - version = tesseract4AndroidVersion + version = libs.versions.tesseract4android.get() } } } From 8c61e0f7a182c493465c012d32e12268d47da1f3 Mon Sep 17 00:00:00 2001 From: clocks Date: Mon, 22 Jul 2024 14:38:42 -0400 Subject: [PATCH 4/5] Dirty Kotlin Migration --- gradle/libs.versions.toml | 6 +- sample/build.gradle.kts | 5 + .../tesseract4android/sample/Assets.java | 93 ---------- .../tesseract4android/sample/Assets.kt | 88 ++++++++++ .../tesseract4android/sample/Config.java | 12 -- .../tesseract4android/sample/Config.kt | 11 ++ .../sample/MainActivity.java | 21 --- .../tesseract4android/sample/MainActivity.kt | 17 ++ .../sample/ui/main/MainFragment.java | 77 -------- .../sample/ui/main/MainFragment.kt | 74 ++++++++ .../sample/ui/main/MainViewModel.java | 166 ------------------ .../sample/ui/main/MainViewModel.kt | 163 +++++++++++++++++ 12 files changed, 363 insertions(+), 370 deletions(-) delete mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/Assets.java create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/Assets.kt delete mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/Config.java create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/Config.kt delete mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.java create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.kt delete mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.java create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.kt delete mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.java create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92bc4735..0eca8dcd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,8 @@ material = "1.11.0" constraintlayout = "2.1.4" test = "1.6.1" tesseract4android = "4.7.0" +coreKtx = "1.13.1" +kotlin = "2.0.0" [libraries] androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } @@ -35,5 +37,7 @@ tesseract4android-jitpack = { group = "cz.adaptech.tesseract4android", name = "t 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" } -[plugins] \ No newline at end of file +[plugins] +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } \ No newline at end of file diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 28d59cab..fe00f18a 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("com.android.application") + alias(libs.plugins.jetbrains.kotlin.android) } android { @@ -32,6 +33,9 @@ android { buildFeatures { viewBinding = true } + kotlinOptions { + jvmTarget = "17" + } } // In case you are using dependency on local library (the project(":tesseract4android") below), @@ -65,6 +69,7 @@ dependencies { implementation(libs.androidx.constraintlayout) implementation(libs.androidx.lifecycle.livedata) implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.core.ktx) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) 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 25f23854..00000000 --- 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 00000000..d5d03066 --- /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 d0f81a2d..00000000 --- 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 00000000..0113bfa5 --- /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 f3df1ea9..00000000 --- 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 00000000..12f8fc6f --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/MainActivity.kt @@ -0,0 +1,17 @@ +package cz.adaptech.tesseract4android.sample + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import cz.adaptech.tesseract4android.sample.ui.main.MainFragment + +class MainActivity : AppCompatActivity() { + 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() + } + } +} \ 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 5c9cb30d..00000000 --- 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/MainFragment.kt b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.kt new file mode 100644 index 00000000..f5d17e07 --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.kt @@ -0,0 +1,74 @@ +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/MainViewModel.java b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.java deleted file mode 100644 index 26d98262..00000000 --- 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 00000000..64edf88f --- /dev/null +++ b/sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainViewModel.kt @@ -0,0 +1,163 @@ +package cz.adaptech.tesseract4android.sample.ui.main + +import android.app.Application +import android.os.SystemClock +import android.util.Log +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 +import kotlin.concurrent.Volatile + +class MainViewModel(application: Application) : AndroidViewModel(application) { + private val tessApi: TessBaseAPI + + private val processing = MutableLiveData(false) + + private val progress = MutableLiveData() + + private val result = MutableLiveData() + + var isInitialized: Boolean = false + private set + + @Volatile + private var stopped = false + + @Volatile + private var tessProcessing = false + + @Volatile + private var recycleAfterProcessing = false + + private val recycleLock = Any() + + init { + tessApi = TessBaseAPI { progressValues: TessBaseAPI.ProgressValues -> + progress.postValue("Progress: " + progressValues.percent + " %") + } + + // Show Tesseract version and library flavor at startup + progress.value = String.format( + Locale.ENGLISH, "Tesseract %s (%s)", + 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() + } + } + } + + 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) + } + } + + fun recognizeImage(imagePath: File) { + if (!this.isInitialized) { + Log.e(TAG, "recognizeImage: Tesseract is not initialized") + return + } + if (tessProcessing) { + Log.e(TAG, "recognizeImage: Processing is in progress") + return + } + tessProcessing = true + + result.value = "" + processing.value = true + progress.value = "Processing..." + stopped = false + + // Start process in another thread + Thread { + tessApi.setImage(imagePath) + // Or set it as Bitmap, Pix,... + // tessApi.setImage(imageBitmap); + 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.postValue(text) + processing.postValue(false) + if (stopped) { + progress.postValue("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() + } + } + }.start() + } + + fun stop() { + if (!tessProcessing) { + return + } + progress.value = "Stopping..." + stopped = true + tessApi.stop() + } + + fun getProcessing(): LiveData { + return processing + } + + fun getProgress(): LiveData { + return progress + } + + fun getResult(): LiveData { + return result + } + + companion object { + private const val TAG = "MainViewModel" + } +} \ No newline at end of file From 550a818d7187001101e464f05e8b0dc3828327ff Mon Sep 17 00:00:00 2001 From: clocks Date: Mon, 22 Jul 2024 16:04:09 -0400 Subject: [PATCH 5/5] Convert sample to compose Various improvements. 1. Compose to be up to date with android standards. 2. Kotlin since Java is no longer the programming language for Android. 3. Coroutines instead of threads for better multithreaded management. 4. Flows instead of LiveData 5. Document the code somewhat. --- build.gradle.kts | 1 + gradle/libs.versions.toml | 22 +- sample/build.gradle.kts | 34 ++- .../sample/ExampleInstrumentedTest.java | 26 --- .../tesseract4android/sample/MainActivity.kt | 20 +- .../tesseract4android/sample/OCRState.kt | 52 +++++ .../sample/ui/main/MainFragment.kt | 74 ------- .../sample/ui/main/MainView.kt | 169 +++++++++++++++ .../sample/ui/main/MainViewModel.kt | 202 +++++++++++------- .../sample/ui/theme/Color.kt | 11 + .../sample/ui/theme/Theme.kt | 57 +++++ .../tesseract4android/sample/ui/theme/Type.kt | 34 +++ .../main/res/layout-land/fragment_main.xml | 64 ------ sample/src/main/res/layout/activity_main.xml | 7 - sample/src/main/res/layout/fragment_main.xml | 56 ----- sample/src/main/res/values-night/themes.xml | 16 -- sample/src/main/res/values/colors.xml | 10 - sample/src/main/res/values/themes.xml | 23 +- .../sample/ExampleUnitTest.java | 17 -- 19 files changed, 511 insertions(+), 384 deletions(-) delete mode 100644 sample/src/androidTest/java/cz/adaptech/tesseract4android/sample/ExampleInstrumentedTest.java create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/OCRState.kt delete mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainFragment.kt create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/main/MainView.kt create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Color.kt create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Theme.kt create mode 100644 sample/src/main/java/cz/adaptech/tesseract4android/sample/ui/theme/Type.kt delete mode 100644 sample/src/main/res/layout-land/fragment_main.xml delete mode 100644 sample/src/main/res/layout/activity_main.xml delete mode 100644 sample/src/main/res/layout/fragment_main.xml delete mode 100644 sample/src/main/res/values-night/themes.xml delete mode 100644 sample/src/main/res/values/colors.xml delete mode 100644 sample/src/test/java/cz/adaptech/tesseract4android/sample/ExampleUnitTest.java 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 @@ - - - - - - - - - - - -