Merge 550a818d7187001101e464f05e8b0dc3828327ff into 71597673649efe75a56a052fb885f4dda77c5ff0

This commit is contained in:
Clocks 2024-07-24 01:13:29 -04:00 committed by GitHub
commit b47a1990b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 971 additions and 816 deletions

View File

@ -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
}

29
build.gradle.kts Normal file
View File

@ -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<Delete>("clean") {
delete(rootProject.buildDir)
}

63
gradle/libs.versions.toml Normal file
View File

@ -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" }

View File

@ -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

View File

@ -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'
}

100
sample/build.gradle.kts Normal file
View File

@ -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)
}

View File

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

View File

@ -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();
}
}
}

View File

@ -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()
}
}
}

View File

@ -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";
}

View File

@ -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"
}

View File

@ -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();
}
}
}

View File

@ -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()
}
}
}

View File

@ -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
}

View File

@ -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);
});
}
}

View File

@ -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<MainViewModel>()
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..."
}
)
}
}

View File

@ -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<Boolean> processing = new MutableLiveData<>(false);
private final MutableLiveData<String> progress = new MutableLiveData<>();
private final MutableLiveData<String> 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<Boolean> getProcessing() {
return processing;
}
@NonNull
public LiveData<String> getProgress() {
return progress;
}
@NonNull
public LiveData<String> getResult() {
return result;
}
}

View File

@ -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>(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<Bitmap?>(null)
/**
* Immutable version for view access.
*/
val status: StateFlow<OCRState> = _progress
/**
* Immutable version for view access.
*/
val result: StateFlow<String> = _result
/**
* Is the start button enabled or not.
*/
val isStartEnabled: StateFlow<Boolean> = processing.map { !it }
.stateIn(viewModelScope, SharingStarted.Lazily, false)
/**
* Is the stop button enabled or not.
*/
val isStopEnabled: StateFlow<Boolean> = processing
/**
* Converts the sample image into an ImageBitmap for UI
*/
val image: StateFlow<ImageBitmap?> = _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"
}
}

View File

@ -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)

View File

@ -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
)
}

View File

@ -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
)
*/
)

View File

@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".ui.main.MainFragment">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAlignment="center"
android:textStyle="bold"
tools:text="Status" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="Start" />
<Button
android:id="@+id/stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="Stop" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="16dp"
android:scrollbars="vertical"
android:text="" />
</LinearLayout>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" />

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.main.MainFragment">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAlignment="center"
android:textStyle="bold"
tools:text="Status" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="Start" />
<Button
android:id="@+id/stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="Stop" />
</LinearLayout>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="16dp"
android:scrollbars="vertical"
android:text="" />
</LinearLayout>

View File

@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Tesseract4Android" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -1,25 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Tesseract4Android" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<style name="Theme.Tesseract4Android" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
<style name="Theme.Tesseract4Android.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.Tesseract4Android.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.Tesseract4Android.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

View File

@ -1,17 +0,0 @@
package cz.adaptech.tesseract4android.sample;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View File

@ -1,4 +0,0 @@
include ':tesseract4android'
if (!System.env.JITPACK) {
include ':sample'
}

4
settings.gradle.kts Normal file
View File

@ -0,0 +1,4 @@
include(":tesseract4android")
if (System.getenv("JITPACK") == null) {
include(":sample")
}

View File

@ -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
}
}
}
}

View File

@ -0,0 +1,122 @@
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 {
implementation(libs.androidx.annotation)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.runner)
androidTestImplementation(libs.androidx.rules)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
afterEvaluate {
publishing {
publications {
create<MavenPublication>("standard") {
from(components.findByName("standardRelease"))
groupId = "cz.adaptech"
artifactId = "tesseract4android"
version = libs.versions.tesseract4android.get()
}
create<MavenPublication>("openmp") {
from(components.findByName("openmpRelease"))
groupId = "cz.adaptech"
artifactId = "tesseract4android-openmp"
version = libs.versions.tesseract4android.get()
}
}
}
}