# HG changeset patch # User David Barts # Date 1613690347 28800 # Node ID 73beb7c973ae060317a238810ddf6ef13dd8092e # Parent e8059b166de1a53ad49bbdef62e79905f53e8e67# Parent b1605be35bcc53e7f7cc75c7aa683b3cd483eac3 Merge memo.oo in to trunk/default. diff -r e8059b166de1 -r 73beb7c973ae app/src/main/java/com/bartsent/simpleresizer/EditImage.kt --- 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 { diff -r e8059b166de1 -r 73beb7c973ae app/src/main/java/com/bartsent/simpleresizer/lib/LanczosKernel.kt --- 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() + 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 = diff -r e8059b166de1 -r 73beb7c973ae app/src/main/java/com/bartsent/simpleresizer/lib/Resizer.kt --- /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> { + val du = srcSize.toDouble() / dstSize.toDouble() + val scale = maxOf(1.0, du) + val ru = ceil(scale * filter.size) + val TEMPLATE = arrayOf() + val out = Array>(dstSize) { TEMPLATE } + val tmp = ArrayList((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, 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>, 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 diff -r e8059b166de1 -r 73beb7c973ae app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt --- 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> { - val du = srcSize.toDouble() / dstSize.toDouble() - val scale = maxOf(1.0, du) - val ru = ceil(scale * filter.size) - val TEMPLATE = arrayOf() - val out = Array>(dstSize) { TEMPLATE } - val tmp = ArrayList((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, 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 -} diff -r e8059b166de1 -r 73beb7c973ae app/src/main/res/layout/activity_edit_image.xml --- 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" /> + + \ No newline at end of file