changeset 14:73beb7c973ae

Merge memo.oo in to trunk/default.
author David Barts <n5jrn@me.com>
date Thu, 18 Feb 2021 15:19:07 -0800
parents e8059b166de1 (current diff) b1605be35bcc (diff)
children 20da616dcda0
files
diffstat 5 files changed, 202 insertions(+), 107 deletions(-) [+]
line wrap: on
line diff
--- a/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Tue Feb 16 17:29:52 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Thu Feb 18 15:19:07 2021 -0800
@@ -13,6 +13,7 @@
 import android.view.MenuItem
 import android.view.View
 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 +21,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 +142,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?
@@ -209,16 +216,22 @@
             throw IllegalArgumentException("$deg not a multiple of 90")
         }
         val (w, h) = if (deg % 180 == 0) Pair(oldBitmap.width, oldBitmap.height) else Pair(oldBitmap.height, oldBitmap.width)
-        val newBitmap = Bitmap.createBitmap(w, h, oldBitmap.config)
-        copyColorSpace(oldBitmap, newBitmap)
         val rotater = Matrix().apply {
             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 {
+            val newBitmap = Bitmap.createBitmap(w, h, oldBitmap.config)
+            copyColorSpace(oldBitmap, newBitmap)
+            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/LanczosKernel.kt	Tue Feb 16 17:29:52 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/LanczosKernel.kt	Thu Feb 18 15:19:07 2021 -0800
@@ -4,14 +4,21 @@
 import kotlin.math.abs
 import kotlin.math.sin
 
-object LanczosKernel: ScalingKernel {
+class LanczosKernel: ScalingKernel {
     override val size = 3.0
+    private val memory = HashMap<Double, Double>()
+    init {
+        memory.put(0.0, 1.0)
+    }
 
     private fun sinc(x: Double): Double {
-        if (x == 0.0)
-            return 1.0
+        val remembered = memory[x]
+        if (remembered != null)
+            return remembered
         val pix = PI * x
-        return sin(pix) / pix
+        val calculated = sin(pix) / pix
+        memory[x] = calculated
+        return calculated
     }
 
     override fun weight(x: Double): Double =
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/Resizer.kt	Thu Feb 18 15:19:07 2021 -0800
@@ -0,0 +1,151 @@
+package com.bartsent.simpleresizer.lib
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import kotlin.math.ceil
+import kotlin.math.floor
+
+/**
+ * Bitmapped images designed for efficient resampling and per-pixel access via
+ * subscripting.
+ */
+class Resizer(private val bits: IntArray, val width: Int, val height: Int) {
+    constructor(width: Int, height: Int) : this(IntArray(width * height), width, height)
+
+    companion object {
+        /**
+         *  Make a Resizer from a standard Bitmap.
+         *  @param      image       Bitmap object.
+         *  @return                 A new Resizer.
+         */
+        fun fromBitmap(image: Bitmap): Resizer {
+            val basedOn = IntArray(image.width * image.height)
+            image.getPixels(basedOn, 0, image.width, 0, 0, image.width, image.height)
+            return Resizer(basedOn, image.width, image.height)
+        }
+    }
+
+    private interface Band {
+        operator fun get(i: Int): Int
+        operator fun set(i: Int, v: Int): Unit
+        val index: Int
+        val size: Int
+    }
+
+    private class Row(val basedOn: Resizer, override val index: Int): Band {
+        override operator fun get(i: Int) = basedOn[i, index]
+        override operator fun set(i: Int, v: Int): Unit {
+            basedOn[i, index] = v
+        }
+        override val size; get() = basedOn.width
+    }
+
+    private class Column(val basedOn: Resizer, override val index: Int): Band {
+        override operator fun get(i: Int) = basedOn[index, i]
+        override operator fun set(i: Int, v: Int): Unit {
+            basedOn[index, i] = v
+        }
+        override val size; get() = basedOn.height
+    }
+
+    private data class IndexWeight(var index: Int, var weight: Double)
+
+    operator fun get(x: Int, y: Int): Int = bits[x + y*width]
+
+    operator fun set(x: Int, y: Int, v: Int): Unit {
+        bits[x + y*width] = v
+    }
+
+    /**
+     * Return a standard Bitmap based on this Resizer's image.
+     * @return                 A new Bitmap, which may share this Resizer's image storage.
+     */
+    fun toBitmap(): Bitmap = Bitmap.createBitmap(bits, width, height, Bitmap.Config.ARGB_8888)
+
+    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: Band, i: Int, weights: Array<IndexWeight>, source: Band): Unit {
+        var r = 0.0; var g = 0.0; var b = 0.0; var a = 0.0
+        weights.forEach {
+            val c = source[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[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 val DEFAULT_KERNEL = LanczosKernel()
+
+    /**
+     * Resize the horizontal aspect.
+     * @param       newWidth    Width of new image
+     * @param       kernel      Scaling kernel to use (default: LanczosKernel)
+     * @return                  A new Resizer object, resampled per specifications
+     */
+    fun horizontal(newWidth: Int, kernel: ScalingKernel = DEFAULT_KERNEL): Resizer {
+        val dst = Resizer(newWidth, height)
+        val weights = precomputeWeights(newWidth, width, kernel)
+        for (y in 0 until height) {
+            resampleBand(Row(dst, y), weights, Row(this, y))
+        }
+        return dst
+    }
+
+    /**
+     * Resize the vertical aspect.
+     * @param       newHeight   Height of new image
+     * @param       kernel      Scaling kernel to use (default: LanczosKernel)
+     * @return                  A new Resizer object, resampled per specifications
+     */
+    fun vertical(newHeight: Int, kernel: ScalingKernel = DEFAULT_KERNEL): Resizer {
+        val dst = Resizer(width, newHeight)
+        val weights = precomputeWeights(newHeight, height, kernel)
+        for (x in 0 until width) {
+            resampleBand(Column(dst, x), weights, Column(this, x))
+        }
+        return dst
+    }
+}
\ No newline at end of file
--- a/app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt	Tue Feb 16 17:29:52 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt	Thu Feb 18 15:19:07 2021 -0800
@@ -9,105 +9,18 @@
 
 private data class IndexWeight(var index: Int, var weight: Double)
 
-fun Bitmap.getScaledInstance(newWidth: Int, newHeight: Int, kernel: ScalingKernel = LanczosKernel): Bitmap {
+fun Bitmap.getScaledInstance(newWidth: Int, newHeight: Int): Bitmap {
     if (newWidth <= 0)
         throw IllegalArgumentException("invalid width: $newWidth")
     if (newHeight <= 0)
         throw IllegalArgumentException("invalid height: $newHeight")
     if (width == newWidth && height == newHeight)
         return Bitmap.createBitmap(this)
-    val input = if (config == Bitmap.Config.ARGB_8888)
-        this
-    else {
-        Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also {
-            Canvas(it).drawBitmap(this, Matrix(), null)
+    return if (width != newWidth) {
+        Resizer.fromBitmap(this).horizontal(newWidth).let {
+            if (height == newHeight) it else it.vertical(newHeight)
         }
-    }
-    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
+        Resizer.fromBitmap(this).vertical(newHeight)
+    } .toBitmap()
 }
-
-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	Tue Feb 16 17:29:52 2021 -0800
+++ b/app/src/main/res/layout/activity_edit_image.xml	Thu Feb 18 15:19:07 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