changeset 9:884092efe31a concur

Gets gratuitously killed. WTF?
author David Barts <n5jrn@me.com>
date Wed, 17 Feb 2021 13:20:25 -0800
parents 9374d044a132
children 62780d38a879
files app/src/main/java/com/bartsent/simpleresizer/EditImage.kt app/src/main/java/com/bartsent/simpleresizer/lib/Channel.kt app/src/main/java/com/bartsent/simpleresizer/lib/Resizer.kt app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt app/src/main/res/layout/activity_edit_image.xml
diffstat 5 files changed, 185 insertions(+), 59 deletions(-) [+]
line wrap: on
line diff
--- a/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Wed Feb 17 07:24:26 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Wed Feb 17 13:20:25 2021 -0800
@@ -12,7 +12,9 @@
 import android.provider.OpenableColumns
 import android.view.MenuItem
 import android.view.View
+import android.view.Window
 import android.widget.EditText
+import android.widget.ProgressBar
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.widget.PopupMenu
@@ -20,6 +22,7 @@
 import com.bartsent.simpleresizer.lib.getScaledInstance
 import java.io.File
 import java.io.IOException
+import kotlin.concurrent.thread
 import kotlin.math.roundToInt
 
 class EditImage : AppCompatActivity() {
@@ -140,11 +143,16 @@
         if (factor >= 1.0) {
             throw IllegalArgumentException("can only scale down")
         }
-        val newBitmap = Bitmap.createBitmap((oldBitmap.width * factor).roundToInt(), (oldBitmap.height * factor).roundToInt(), oldBitmap.config)
-        copyColorSpace(oldBitmap, newBitmap)
-        setImage(oldBitmap.getScaledInstance(
-                (oldBitmap.width.toDouble() * factor + 0.5).toInt(),
-                (oldBitmap.height.toDouble() * factor + 0.5).toInt()))
+        binding.progressBar.visibility = ProgressBar.VISIBLE
+        thread {
+            val newBitmap = oldBitmap.getScaledInstance(
+                    (oldBitmap.width.toDouble() * factor + 0.5).toInt(),
+                    (oldBitmap.height.toDouble() * factor + 0.5).toInt())
+            runOnUiThread {
+                binding.progressBar.visibility = ProgressBar.INVISIBLE
+                setImage(newBitmap)
+            }
+        }
     }
 
     // is there any way to remember the last scale value?
@@ -215,10 +223,16 @@
             setRotate(deg.toFloat(), oldBitmap.width.toFloat()/2.0f, oldBitmap.height.toFloat()/2.0f)
             postTranslate((w - oldBitmap.width).toFloat()/2.0f, (h - oldBitmap.height).toFloat()/2.0f)
         }
-        Canvas(newBitmap).run {
-            drawBitmap(oldBitmap, rotater, null)
+        binding.progressBar.visibility = ProgressBar.VISIBLE
+        thread {
+            Canvas(newBitmap).run {
+                drawBitmap(oldBitmap, rotater, null)
+            }
+            runOnUiThread {
+                binding.progressBar.visibility = ProgressBar.INVISIBLE
+                setImage(newBitmap)
+            }
         }
-        setImage(newBitmap)
     }
 
      fun cancelClicked(view: View): Unit {
--- a/app/src/main/java/com/bartsent/simpleresizer/lib/Channel.kt	Wed Feb 17 07:24:26 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/Channel.kt	Wed Feb 17 13:20:25 2021 -0800
@@ -1,5 +1,6 @@
 package com.bartsent.simpleresizer.lib
 
+import android.util.Log
 import java.util.concurrent.Semaphore
 
 /**
@@ -17,11 +18,13 @@
      * @param item      Item to write
      */
     fun write(item: T): Unit {
+        Log.d("Channel<${hashCode()}>", "write…")
         wSem.acquire()
         synchronized(this) {
             buffer[(start + rSem.availablePermits()) % buffer.size] = item
             rSem.release()
         }
+        Log.d("Channel<${hashCode()}>", "write done")
     }
 
     /**
@@ -29,12 +32,14 @@
      * @return          The item read
      */
     fun read(): T {
+        Log.d("Channel<${hashCode()}>", "read…")
         rSem.acquire()
         synchronized(this) {
             val ret = buffer[start]!! as T
             buffer[start] = null // unref
             start = (start + 1) % buffer.size
             wSem.release()
+            Log.d("Channel<${hashCode()}>", "read done")
             return ret
         }
     }
--- a/app/src/main/java/com/bartsent/simpleresizer/lib/Resizer.kt	Wed Feb 17 07:24:26 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/Resizer.kt	Wed Feb 17 13:20:25 2021 -0800
@@ -2,6 +2,7 @@
 
 import android.graphics.Bitmap
 import android.graphics.Color
+import android.util.Log
 import java.io.Closeable
 import kotlin.math.ceil
 import kotlin.math.floor
@@ -10,21 +11,28 @@
     private val NWORKERS = maxOf(1, Runtime.getRuntime().availableProcessors() - 2)
     private val workers = Channel<Worker>(NWORKERS)
     init {
+        Log.d("Resizer", "workers channel is ${workers.hashCode()}")
+        Log.d("Resizer", "writing workers…")
         for (i in 0 until NWORKERS)
-            workers.write(Worker(workers))
+            workers.write(Worker(workers).apply { start() })
+        Log.d("Resizer", "writing done")
     }
 
     override fun close() {
+        Log.d("Resizer", "closing, stack trace follows…", Exception("dummy exception"))
         for (i in 0 until NWORKERS)
             workers.read().run {
                 interrupt()
                 join()
             }
+        Log.d("Resizer", "closing done")
     }
 
     private fun drain(): Unit {
+        Log.d("Resizer", "draining workers…")
         val saved = Array<Worker>(NWORKERS) { workers.read() }
         saved.forEach { workers.write(it) }
+        Log.d("Resizer", "draining done")
     }
 
     private data class IndexWeight(var index: Int, var weight: Double)
@@ -61,80 +69,88 @@
         return out
     }
 
-    private data class WorkUnit(val target: Bitmap, val x: Int, val y: Int,
-                                val weights: Array<IndexWeight>, val scanLine: IntArray)
-
     private class Worker(private val workers: Channel<Worker>): Thread() {
-        private val _input = Channel<WorkUnit>(1)
+        private val _input = Channel<() -> Unit>(1)
 
         override fun run() {
             while (true) {
-                val data = _input.read()
-                if (isInterrupted)
+                val (block: () -> Unit, interrupted: Boolean) = try {
+                    Pair(_input.read(), isInterrupted)
+                } catch (e: InterruptedException) {
+                    Pair({}, true)
+                }
+                if (interrupted)
                     return
-                resample(data.target, data.x, data.y, data.weights, data.scanLine)
+                block()
                 workers.write(this)
             }
         }
 
-        fun send(data: WorkUnit) {
-            _input.write(data)
-        }
-
-        private fun clamp(v: Double): Int = minOf(255, maxOf(0, (v + 0.5).toInt()))
-
-        private fun resample(target: Bitmap, x: Int, y: Int, weights: Array<IndexWeight>, scanLine: IntArray): Unit {
-            var r = 0.0; var g = 0.0; var b = 0.0; var a = 0.0
-            weights.forEach {
-                val c = scanLine[it.index]
-                val aw = Color.alpha(c).toDouble() * it.weight
-                r += Color.red(c).toDouble() * aw
-                g += Color.green(c).toDouble() * aw
-                b += Color.blue(c).toDouble() * aw
-                a += aw
-            }
-            if (a == 0.0)
-                return
-            val argb = Color.argb(clamp(a), clamp(r/a), clamp(g/a), clamp(b/a))
-            synchronized(target) {
-                target.setPixel(x, y, argb)
-            }
+        fun send(block: () -> Unit) {
+            _input.write(block)
         }
     }
 
+    private fun clamp(v: Double): Int = minOf(255, maxOf(0, (v + 0.5).toInt()))
+
+    fun parallel(block: () -> Unit) = workers.read().send(block)
+
     private fun resample(target: Bitmap, x: Int, y: Int, weights: Array<IndexWeight>, scanLine: IntArray): Unit {
-        workers.read().send(WorkUnit(target, x, y, weights, scanLine))
+        // Log.d("Resizer", "resample: x=$x, y=$y")
+        var r = 0.0; var g = 0.0; var b = 0.0; var a = 0.0
+        weights.forEach {
+            val c = scanLine[it.index]
+            val aw = Color.alpha(c).toDouble() * it.weight
+            r += Color.red(c).toDouble() * aw
+            g += Color.green(c).toDouble() * aw
+            b += Color.blue(c).toDouble() * aw
+            a += aw
+        }
+        if (a == 0.0) {
+            Log.d("Resizer", "resample: a=0.0, done")
+            return
+        }
+        val argb = Color.argb(clamp(a), clamp(r/a), clamp(g/a), clamp(b/a))
+        // Log.d("Resizer", "setting pixel…")
+        synchronized(target) {
+            target.setPixel(x, y, argb)
+        }
+        // Log.d("Resizer", "resample: a=$a, done")
     }
 
     fun horizontal(image: Bitmap, newWidth: Int, kernel: ScalingKernel): Bitmap {
+        Log.d("Resizer", "horizontal…")
         val dst = Bitmap.createBitmap(newWidth, image.height, Bitmap.Config.ARGB_8888)
         val weights = precomputeWeights(newWidth, image.width, kernel)
-        val scanLine = IntArray(image.width)
         for (y in 0 until image.height) {
-            for (x in 0 until image.width) {
-                scanLine[x] = image.getPixel(x, y)
-            }
-            for (x in weights.indices) {
-                resample(dst, x, y, weights[x], scanLine)
+            val scanLine = IntArray(image.width) { image.getPixel(it, y) }
+            parallel {
+                Log.d("Resizer", "horizontal: y=$y")
+                for (x in weights.indices) {
+                    resample(dst, x, y, weights[x], scanLine)
+                }
             }
         }
         drain()
+        Log.d("Resizer", "horizontal done")
         return dst
     }
 
     fun vertical(image: Bitmap, newHeight: Int, kernel: ScalingKernel): Bitmap {
+        Log.d("Resizer", "vertical…")
         val dst = Bitmap.createBitmap(image.width, newHeight, Bitmap.Config.ARGB_8888)
         val weights = precomputeWeights(newHeight, image.height, kernel)
-        val scanLine = IntArray(image.height)
         for (x in 0 until image.width) {
-            for (y in 0 until image.height) {
-                scanLine[y] = image.getPixel(x, y)
-            }
-            for (y in weights.indices) {
-                resample(dst, x, y, weights[y], scanLine)
+            val scanLine = IntArray(image.height) { image.getPixel(x, it) }
+            parallel {
+                Log.d("Resizer", "vertical: x=$x")
+                for (y in weights.indices) {
+                    resample(dst, x, y, weights[y], scanLine)
+                }
             }
         }
         drain()
+        Log.d("Resizer", "vertical done")
         return dst
     }
 }
\ No newline at end of file
--- a/app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt	Wed Feb 17 07:24:26 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt	Wed Feb 17 13:20:25 2021 -0800
@@ -2,7 +2,10 @@
 
 import android.graphics.Bitmap
 import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Matrix
+import kotlin.math.ceil
+import kotlin.math.floor
 
 private data class IndexWeight(var index: Int, var weight: Double)
 
@@ -20,14 +23,91 @@
             Canvas(it).drawBitmap(this, Matrix(), null)
         }
     }
-    Resizer().use { resize ->
-        if (width != newWidth) {
-            if (height != newHeight)
-                return resize.vertical(resize.horizontal(input, newWidth, kernel), newHeight, kernel)
-            else
-                return resize.horizontal(input, newWidth, kernel)
-        } else {
-            return resize.vertical(input, newHeight, kernel)
+    if (width != newWidth) {
+        if (height != newHeight)
+            return resizeVertical(resizeHorizontal(input, newWidth, kernel), newHeight, kernel)
+        else
+            return resizeHorizontal(input, newWidth, kernel)
+    } else {
+        return resizeVertical(input, newHeight, kernel)
+    }
+}
+
+private fun precomputeWeights(dstSize: Int, srcSize: Int, filter: ScalingKernel): Array<Array<IndexWeight>> {
+    val du = srcSize.toDouble() / dstSize.toDouble()
+    val scale = maxOf(1.0, du)
+    val ru = ceil(scale * filter.size)
+    val TEMPLATE = arrayOf<IndexWeight>()
+    val out = Array<Array<IndexWeight>>(dstSize) { TEMPLATE }
+    val tmp = ArrayList<IndexWeight>((ru.toInt()+2)*2)
+    val emax = srcSize - 1
+
+    for (v in 0 until dstSize) {
+        val fu = (v.toDouble()+0.5)*du - 0.5
+        val begin = maxOf(0, ceil(fu - ru).toInt())
+        val end = minOf(emax, floor(fu + ru).toInt())
+        var sum: Double = 0.0
+        for (u in begin..end) {
+            val w = filter.weight((u.toDouble() - fu) / scale)
+            if (w != 0.0) {
+                sum += w
+                tmp.add(IndexWeight(u, w))
+            }
+        }
+        if (sum != 0.0) {
+            tmp.forEach {
+                it.weight /= sum
+            }
+        }
+        out[v] = tmp.toArray(TEMPLATE)
+        tmp.clear()
+    }
+    return out
+}
+
+private fun clamp(v: Double): Int = minOf(255, maxOf(0, (v + 0.5).toInt()))
+
+private fun resample(target: Bitmap, x: Int, y: Int, weights: Array<IndexWeight>, scanLine: IntArray): Unit {
+    var r = 0.0; var g = 0.0; var b = 0.0; var a = 0.0
+    weights.forEach {
+        val c = scanLine[it.index]
+        val aw = Color.alpha(c).toDouble() * it.weight
+        r += Color.red(c).toDouble() * aw
+        g += Color.green(c).toDouble() * aw
+        b += Color.blue(c).toDouble() * aw
+        a += aw
+    }
+    if (a == 0.0)
+        return
+    target.setPixel(x, y, Color.argb(clamp(a), clamp(r/a), clamp(g/a), clamp(b/a)))
+}
+
+private fun resizeHorizontal(image: Bitmap, newWidth: Int, kernel: ScalingKernel): Bitmap {
+    val dst = Bitmap.createBitmap(newWidth, image.height, Bitmap.Config.ARGB_8888)
+    val weights = precomputeWeights(newWidth, image.width, kernel)
+    val scanLine = IntArray(image.width)
+    for (y in 0 until image.height) {
+        for (x in 0 until image.width) {
+            scanLine[x] = image.getPixel(x, y)
+        }
+        for (x in weights.indices) {
+            resample(dst, x, y, weights[x], scanLine)
         }
     }
+    return dst
 }
+
+private fun resizeVertical(image: Bitmap, newHeight: Int, kernel: ScalingKernel): Bitmap {
+    val dst = Bitmap.createBitmap(image.width, newHeight, Bitmap.Config.ARGB_8888)
+    val weights = precomputeWeights(newHeight, image.height, kernel)
+    val scanLine = IntArray(image.height)
+    for (x in 0 until image.width) {
+        for (y in 0 until image.height) {
+            scanLine[y] = image.getPixel(x, y)
+        }
+        for (y in weights.indices) {
+            resample(dst, x, y, weights[y], scanLine)
+        }
+    }
+    return dst
+}
--- a/app/src/main/res/layout/activity_edit_image.xml	Wed Feb 17 07:24:26 2021 -0800
+++ b/app/src/main/res/layout/activity_edit_image.xml	Wed Feb 17 13:20:25 2021 -0800
@@ -72,4 +72,15 @@
         app:layout_constraintStart_toEndOf="@id/cancel_button"
         app:layout_constraintTop_toBottomOf="@id/rotate_button" />
 
+    <ProgressBar
+        android:id="@+id/progress_bar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:indeterminate="true"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file