Dirty Kotlin Migration

This commit is contained in:
clocks 2024-07-22 14:38:42 -04:00
parent c2c8de65ee
commit 8c61e0f7a1
No known key found for this signature in database
GPG Key ID: DF550F49A97F8A4B
12 changed files with 363 additions and 370 deletions

View File

@ -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]
[plugins]
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

View File

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

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

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

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,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<String>()
private val result = MutableLiveData<String>()
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<Boolean> {
return processing
}
fun getProgress(): LiveData<String> {
return progress
}
fun getResult(): LiveData<String> {
return result
}
companion object {
private const val TAG = "MainViewModel"
}
}