Mercurial > cgi-bin > hgweb.cgi > SimpleResizer
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"