# HG changeset patch # User David Barts # Date 1613525392 28800 # Node ID e8059b166de1a53ad49bbdef62e79905f53e8e67 # Parent 247e03baf77c9dacc2b5f78d015de4a97eef2cc5 Lanczos works, but is painfully slow. diff -r 247e03baf77c -r e8059b166de1 app/src/main/java/com/bartsent/simpleresizer/EditImage.kt --- a/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt Wed Feb 10 16:31:01 2021 -0800 +++ b/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt Tue Feb 16 17:29:52 2021 -0800 @@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import com.bartsent.simpleresizer.databinding.ActivityEditImageBinding +import com.bartsent.simpleresizer.lib.getScaledInstance import java.io.File import java.io.IOException import kotlin.math.roundToInt @@ -135,17 +136,15 @@ fun doScale(newMax: Int): Unit { val oldBitmap = State.bitmap!! - val factor = newMax.toFloat() / maxOf(oldBitmap.width, oldBitmap.height).toFloat() + val factor = newMax.toDouble() / maxOf(oldBitmap.width, oldBitmap.height).toDouble() 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) - val scaler = Matrix().apply { setScale(factor, factor) } - Canvas(newBitmap).run { - drawBitmap(oldBitmap, scaler, null) - } - setImage(newBitmap) + setImage(oldBitmap.getScaledInstance( + (oldBitmap.width.toDouble() * factor + 0.5).toInt(), + (oldBitmap.height.toDouble() * factor + 0.5).toInt())) } // is there any way to remember the last scale value? diff -r 247e03baf77c -r e8059b166de1 app/src/main/java/com/bartsent/simpleresizer/lib/LanczosKernel.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/src/main/java/com/bartsent/simpleresizer/lib/LanczosKernel.kt Tue Feb 16 17:29:52 2021 -0800 @@ -0,0 +1,19 @@ +package com.bartsent.simpleresizer.lib + +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.sin + +object LanczosKernel: ScalingKernel { + override val size = 3.0 + + private fun sinc(x: Double): Double { + if (x == 0.0) + return 1.0 + val pix = PI * x + return sin(pix) / pix + } + + override fun weight(x: Double): Double = + if (abs(x) < size) sinc(x) * sinc(x/size) else 0.0 +} \ No newline at end of file diff -r 247e03baf77c -r e8059b166de1 app/src/main/java/com/bartsent/simpleresizer/lib/ScalingKernel.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/src/main/java/com/bartsent/simpleresizer/lib/ScalingKernel.kt Tue Feb 16 17:29:52 2021 -0800 @@ -0,0 +1,8 @@ +package com.bartsent.simpleresizer.lib + +import android.graphics.Bitmap + +interface ScalingKernel { + val size: Double + fun weight(x: Double): Double +} diff -r 247e03baf77c -r e8059b166de1 app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt Tue Feb 16 17:29:52 2021 -0800 @@ -0,0 +1,113 @@ +package com.bartsent.simpleresizer.lib + +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) + +fun Bitmap.getScaledInstance(newWidth: Int, newHeight: Int, kernel: ScalingKernel = LanczosKernel): 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) + } + } + 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 +} + +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 +}