changeset 20:21be543e9127

Concurrency works, massively improves speed.
author David Barts <n5jrn@me.com>
date Mon, 22 Feb 2021 17:20:40 -0800
parents 86740f593b6c (current diff) 3aaf65a7fb9c (diff)
children 7e7e71724770
files
diffstat 4 files changed, 90 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Sun Feb 21 22:14:07 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Mon Feb 22 17:20:40 2021 -0800
@@ -28,6 +28,7 @@
 import java.io.IOException
 import kotlin.concurrent.thread
 import androidx.preference.PreferenceManager
+import com.bartsent.simpleresizer.lib.ThreadPools
 
 class EditImage : AppCompatActivity() {
     private object State {
@@ -108,21 +109,27 @@
         // Being stateful stops data loss when the phone gets rotated.
         if (imageUri != State.uri) {
             State.uri = imageUri
-            State.bitmap = contentResolver.openInputStream(imageUri).use {
-                BitmapFactory.decodeStream(it)
+            binding.progressBar.visibility = ProgressBar.VISIBLE
+            ThreadPools.WORKERS.execute {
+                State.bitmap = contentResolver.openInputStream(imageUri).use {
+                    BitmapFactory.decodeStream(it)
+                }
+                runOnUiThread {
+                    binding.progressBar.visibility = ProgressBar.INVISIBLE
+                    if (State.bitmap == null)
+                        showFatalError(getString(R.string.error_bad_image))
+                    else
+                        setImage(State.bitmap!!)
+                }
             }
         }
-        if (State.bitmap == null) {
-            showFatalError(getString(R.string.error_bad_image))
-            return
-        }
-        setImage(State.bitmap!!)
     }
 
-    fun setImage(image: Bitmap): Unit {
+    private fun setImage(image: Bitmap): Unit {
         binding.imageSize.text = getString(R.string.image_size_text, image.width, image.height)
         binding.image.setImageBitmap(image)
         State.bitmap = image
+        binding.root.invalidate()
     }
 
     private val CUSTOM = 999998
@@ -147,7 +154,7 @@
         }
     }
 
-    fun scaleMenuItemClicked(item: MenuItem) : Boolean =
+    private fun scaleMenuItemClicked(item: MenuItem) : Boolean =
         when (item.itemId) {
             CUSTOM -> { showCustomScaleDialog(); true }
             CANCEL -> true
@@ -163,7 +170,7 @@
         }
     }
 
-    fun doScale(newMax: Int): Unit {
+    private fun doScale(newMax: Int): Unit {
         val oldBitmap = State.bitmap!!
         val factor = newMax.toDouble() / maxOf(oldBitmap.width, oldBitmap.height).toDouble()
         if (factor >= 1.0) {
@@ -173,7 +180,7 @@
             "scale_type", "speed" )
         Log.d("EditImage", "scaling, scale_type = $scaleType")
         binding.progressBar.visibility = ProgressBar.VISIBLE
-        thread {
+        ThreadPools.WORKERS.execute {
             val newWidth = (oldBitmap.width.toDouble() * factor + 0.5).toInt()
             val newHeight = (oldBitmap.height.toDouble() * factor + 0.5).toInt()
             val newBitmap = if (scaleType == "quality")
@@ -187,7 +194,7 @@
         }
     }
 
-    fun showCustomScaleDialog(): Unit {
+    private fun showCustomScaleDialog(): Unit {
         val image = State.bitmap!!
         val curMaxDim = maxOf(image.width, image.height)
         val dialogView = layoutInflater.inflate(R.layout.dialog_custom_scale, null)
@@ -234,7 +241,7 @@
         }
     }
 
-    fun doRotate(deg: Int): Unit {
+    private fun doRotate(deg: Int): Unit {
         val oldBitmap = State.bitmap!!
         if (deg % 90 != 0) {
             throw IllegalArgumentException("$deg not a multiple of 90")
@@ -245,7 +252,7 @@
             postTranslate((w - oldBitmap.width).toFloat()/2.0f, (h - oldBitmap.height).toFloat()/2.0f)
         }
         binding.progressBar.visibility = ProgressBar.VISIBLE
-        thread {
+        ThreadPools.WORKERS.execute {
             val newBitmap = Bitmap.createBitmap(w, h, oldBitmap.config)
             copyColorSpace(oldBitmap, newBitmap)
             Canvas(newBitmap).run {
@@ -287,27 +294,37 @@
                     File(Environment.DIRECTORY_PICTURES, getString(R.string.app_name)).getPath())
             }
         }
-        try {
-            val myUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
-            if (myUri == null) {
-                throw IOException(getString(R.string.error_create_mediastore))
-            }
-            val stream = contentResolver.openOutputStream(myUri)
-            if (stream == null) {
-                throw IOException(getString(R.string.error_get_output))
+        binding.progressBar.visibility = ProgressBar.VISIBLE
+        ThreadPools.WORKERS.execute {
+            var errorMessage: String? = null
+            try {
+                val myUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+                if (myUri == null) {
+                    throw IOException(getString(R.string.error_create_mediastore))
+                }
+                val stream = contentResolver.openOutputStream(myUri)
+                if (stream == null) {
+                    throw IOException(getString(R.string.error_get_output))
+                }
+                val quality = maxOf(0, minOf(100,
+                        PreferenceManager.getDefaultSharedPreferences(applicationContext).getInt(
+                                "jpeg_quality", 85)))
+                Log.d("EditImage", "saving, jpeg_quality = $quality")
+                stream.use {
+                    if (!State.bitmap!!.compress(Bitmap.CompressFormat.JPEG, quality, it)) {
+                        throw IOException(getString(R.string.error_save_bitmap))
+                    }
+                }
+            } catch (ioe: IOException) {
+                errorMessage = ioe.message ?: getString(R.string.error_io)
             }
-            val quality = maxOf(0, minOf(100,
-                PreferenceManager.getDefaultSharedPreferences(applicationContext).getInt(
-                    "jpeg_quality", 85)))
-            Log.d("EditImage", "saving, jpeg_quality = $quality")
-            stream.use {
-                if (!State.bitmap!!.compress(Bitmap.CompressFormat.JPEG, quality, it)) {
-                    throw IOException(getString(R.string.error_save_bitmap))
-                }
+            runOnUiThread {
+                binding.progressBar.visibility = ProgressBar.INVISIBLE
+                if (errorMessage == null)
+                    finish()
+                else
+                    Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_LONG).show()
             }
-        } catch (ioe: IOException) {
-            Toast.makeText(applicationContext, ioe.message ?: getString(R.string.error_io), Toast.LENGTH_LONG).show()
         }
-        finish()
     }
 }
--- a/app/src/main/java/com/bartsent/simpleresizer/lib/Resizer.kt	Sun Feb 21 22:14:07 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/Resizer.kt	Mon Feb 22 17:20:40 2021 -0800
@@ -2,6 +2,8 @@
 
 import android.graphics.Bitmap
 import android.graphics.Color
+import android.util.Log
+import java.util.concurrent.Callable
 import kotlin.math.ceil
 import kotlin.math.floor
 
@@ -111,9 +113,24 @@
         target[i] = Color.argb(clamp(a), clamp(r/a), clamp(g/a), clamp(b/a))
     }
 
-    private fun resampleBand(target: Band, weights: Array<Array<IndexWeight>>, source: Band) {
-        for (i in 0 until target.size) {
-            resample(target, i, weights[i], source)
+    private fun makeResampler(target: Band, weights: Array<Array<IndexWeight>>, source: Band): Callable<Exception?> {
+        return Callable<Exception?> {
+            try {
+                for (i in 0 until target.size) {
+                    resample(target, i, weights[i], source)
+                }
+                null
+            } catch (e: Exception) {
+                e
+            }
+        }
+    }
+
+    private fun runAndReap(jobs: ArrayList<Callable<Exception?>>) {
+        ThreadPools.WORKERS.invokeAll(jobs).forEach {
+            val exc = it.get()
+            if (exc != null)
+                Log.e("Resizer", "worker thread threw exception", exc)
         }
     }
 
@@ -126,9 +143,11 @@
     fun horizontal(newWidth: Int, kernel: ScalingKernel): Resizer {
         val dst = Resizer(newWidth, height)
         val weights = precomputeWeights(newWidth, width, kernel)
+        val jobs = ArrayList<Callable<Exception?>>(height)
         for (y in 0 until height) {
-            resampleBand(Row(dst, y), weights, Row(this, y))
+            jobs.add(makeResampler(Row(dst, y), weights, Row(this, y)))
         }
+        runAndReap(jobs)
         return dst
     }
 
@@ -141,9 +160,11 @@
     fun vertical(newHeight: Int, kernel: ScalingKernel): Resizer {
         val dst = Resizer(width, newHeight)
         val weights = precomputeWeights(newHeight, height, kernel)
+        val jobs = ArrayList<Callable<Exception?>>(width)
         for (x in 0 until width) {
-            resampleBand(Column(dst, x), weights, Column(this, x))
+            jobs.add(makeResampler(Column(dst, x), weights, Column(this, x)))
         }
+        runAndReap(jobs)
         return dst
     }
 }
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/ThreadPools.kt	Mon Feb 22 17:20:40 2021 -0800
@@ -0,0 +1,13 @@
+package com.bartsent.simpleresizer.lib
+
+import java.util.concurrent.Executors
+
+/**
+ * Thread pools. We use just one, and it is sized to the maximum responsible thread use,
+ * which is one more than the number of cores. This is because when scaling, we have one
+ * worker thread sitting around and waiting for the remaining busy ones to finish.
+ */
+object ThreadPools {
+    val NWORKERS = Runtime.getRuntime().availableProcessors() + 1
+    val WORKERS = Executors.newFixedThreadPool(NWORKERS)
+}
\ No newline at end of file
--- a/app/src/main/res/layout/activity_edit_image.xml	Sun Feb 21 22:14:07 2021 -0800
+++ b/app/src/main/res/layout/activity_edit_image.xml	Mon Feb 22 17:20:40 2021 -0800
@@ -11,6 +11,7 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginBottom="5dp"
+        android:adjustViewBounds="true"
         android:contentDescription="Image being processed."
         android:scaleType="centerInside"
         app:layout_constraintBottom_toTopOf="@id/scale_button"