This commit is contained in:
tateisu 2019-05-05 20:33:08 +09:00
parent afe8f39fe3
commit c8a22febf2
1 changed files with 69 additions and 81 deletions

View File

@ -7,58 +7,51 @@ import android.util.SparseIntArray
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
import java.lang.Math.pow import java.lang.Math.pow
import kotlin.math.abs
import kotlin.math.sign import kotlin.math.sign
class Blurhash(blurhash : String, punch : Float = 1f) { class Blurhash(blurhash : String, punch : Float = 1f) {
companion object { companion object {
private fun String.sub1(idx : Int) = substring(idx, idx + 1)
private fun String.sub2(idx : Int) = substring(idx, idx + 2)
private fun String.sub4(idx : Int) = substring(idx, idx + 4)
// map from base83 character to index // map from base83 character to index
private val base83Map = SparseIntArray().apply { private val base83Map = SparseIntArray().apply {
val base83Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" .forEachIndexed { index, c ->
for(i in 0 until base83Chars.length) { put(c.toInt(), index)
val c = base83Chars[i] }
put(c.toInt(), i)
}
} }
// convert from base83 chars(1..4) to integer // convert from base83 chars(1..4 length) to integer
private fun String.decodeBase83() : Int { private fun String.decodeBase83(start : Int, length : Int) : Int {
var v = 0 var v = 0
for(c in this) { for(i in start until start + length) {
val ci = c.toInt() val ci = this[i].toInt()
val i = base83Map.get(ci, - 1) val idx = base83Map.get(ci, - 1)
if(i == - 1) error("decodeBase83: incorrect char code $ci") if(idx == - 1) {
v = v * 83 + i error("decodeBase83: incorrect char code $ci")
}
v = v * 83 + idx
} }
return v return v
} }
// array to convert gamma curve from sRGB(0..255) to linear(0..1f) // array to convert gamma curve from sRGB(0..255) to linear(0..1f)
private val arraySRGB2Linear = FloatArray(256).apply { private val arraySRGB2Linear = FloatArray(256) { i ->
for(i in 0 until 256) { val v = i.toDouble() / 255.0
val v = i.toDouble() / 255.0 if(v <= 0.04045) {
this[i] = v / 12.92
if(v <= 0.04045) { } else {
v / 12.92 pow(((v + 0.055) / 1.055), 2.4)
} else {
pow(((v + 0.055) / 1.055), 2.4)
}
.toFloat()
} }
.toFloat()
} }
private fun sRGBToLinear(value : Int) = private fun clip(min:Int,max:Int,value:Int) = if(value < min) min else if(value > max) max else value
arraySRGB2Linear[if(value < 0) 0 else if(value > 255) 255 else value]
private fun sRGBToLinear(value : Int) = arraySRGB2Linear[clip(0,255,value)]
private fun linearTosRGB(value : Float) : Int { private fun linearTosRGB(value : Float) : Int {
// binary search in arraySRGB2Linear // binary search in arraySRGB2Linear to avoid using pow()
var start = 0 var start = 0
var end = 256 var end = 256
while(end - start > 1) { while(end - start > 1) {
@ -70,61 +63,54 @@ class Blurhash(blurhash : String, punch : Float = 1f) {
else -> return mid else -> return mid
} }
} }
return when { return clip(0,255,start)
start < 0 -> 0
start >= 256 -> 255
else -> start
}
} }
private fun signPow(v : Float, exp : Double) : Float = private fun signPow2(v : Float) : Float = sign(v) * (v * v)
sign(v) * pow(abs(v).toDouble(), exp).toFloat()
private fun decodeDC(value : Int) = floatArrayOf(
sRGBToLinear((value ushr 16) and 255),
sRGBToLinear((value ushr 8) and 255),
sRGBToLinear((value) and 255)
)
private fun decodeACSub(maximumValue : Float, v : Int) : Float = private fun decodeACSub(maximumValue : Float, v : Int) : Float =
signPow((v - 9).toFloat() / 9f, 2.0) * maximumValue signPow2((v - 9).toFloat() / 9f) * maximumValue
private fun decodeAC(value : Int, maximumValue : Float) = floatArrayOf( private fun decodeAC(value : Int, maximumValue : Float) = floatArrayOf(
decodeACSub(maximumValue, value / 361), // 19*19 decodeACSub(maximumValue, value / 361), // 19*19
decodeACSub(maximumValue, ((value / 19) % 19)), decodeACSub(maximumValue, ((value / 19) % 19)),
decodeACSub(maximumValue, value % 19) decodeACSub(maximumValue, value % 19)
) )
private fun decodeDC(value : Int) = floatArrayOf(
sRGBToLinear((value ushr 16) and 255),
sRGBToLinear((value ushr 8) and 255),
sRGBToLinear((value) and 255)
)
} }
private val numY : Int private val height : Int
private val numX : Int private val width : Int
private val colors : ArrayList<FloatArray> private val colors : Array<FloatArray>
init { init {
if(blurhash.length < 6) error("blurhash: too short $blurhash") if(blurhash.length < 6) {
error("blurhash: too short $blurhash")
val sizeFlag = blurhash.sub1(0).decodeBase83()
this.numY = (sizeFlag / 9) + 1
this.numX = (sizeFlag % 9) + 1
val quantisedMaximumValue : Int = blurhash.sub1(1).decodeBase83()
val maximumValue : Float = ((quantisedMaximumValue + 1).toFloat() / 166f) * punch
if(blurhash.length != 4 + 2 * numX * numY) {
error("'blurhash length mismatch. actual=${blurhash.length},expect=${4 + 2 * numX * numY}")
} }
this.colors = ArrayList() val sizeFlag = blurhash.decodeBase83(0, 1)
for(i in 0 until numX * numY) { this.height = (sizeFlag / 9) + 1
colors.add( this.width = (sizeFlag % 9) + 1
if(i == 0) {
decodeDC(blurhash.sub4(2).decodeBase83()) // 2..5 val lengthExpect = 4 + 2 * width * height
} else { if(blurhash.length != lengthExpect) {
// 6..8,... error("'blurhash length mismatch. expect=$lengthExpect,actual=${blurhash.length}")
decodeAC(blurhash.sub2(4 + i * 2).decodeBase83(), maximumValue)
}
)
} }
val quantisedMaximumValue = blurhash.decodeBase83(1, 1)
val maximumValue = ((quantisedMaximumValue + 1).toFloat() / 166f) * punch
this.colors = Array(width * height) { i ->
when(i) {
0 -> decodeDC(blurhash.decodeBase83(2, 4))
else -> decodeAC(blurhash.decodeBase83(4 + i * 2, 2), maximumValue)
}
}
} }
// render to IntArray that can be used to Bitmap.setPixels() // render to IntArray that can be used to Bitmap.setPixels()
@ -137,10 +123,10 @@ class Blurhash(blurhash : String, punch : Float = 1f) {
var r = 0f var r = 0f
var g = 0f var g = 0f
var b = 0f var b = 0f
for(j in 0 until numY) { for(j in 0 until height) {
for(i in 0 until numX) { for(i in 0 until width) {
val basis = (Math.cos(kx * i) * Math.cos(ky * j)).toFloat() val basis = (Math.cos(kx * i) * Math.cos(ky * j)).toFloat()
val color = colors[i + j * numX] val color = colors[i + j * width]
r += color[0] * basis r += color[0] * basis
g += color[1] * basis g += color[1] * basis
b += color[2] * basis b += color[2] * basis
@ -162,20 +148,20 @@ class BlurhashView : AppCompatTextView {
companion object { companion object {
val log = LogCategory("BlurhashView") val log = LogCategory("BlurhashView")
// blurhashビットマップのサイズ
const val bitmapWidth = 16 const val bitmapWidth = 16
const val bitmapHeight = 16 const val bitmapHeight = 16
} }
constructor(context : Context) : super(context) constructor(context : Context) :
constructor(context : Context, attrs : AttributeSet) : super(context, attrs) super(context)
constructor(context : Context, attrs : AttributeSet, defStyleAttr : Int) : super(
context,
attrs,
defStyleAttr
)
// bitmapとIntArrayはViewに保持して再利用する constructor(context : Context, attrs : AttributeSet) :
super(context, attrs)
constructor(context : Context, attrs : AttributeSet, defStyleAttr : Int) :
super(context, attrs, defStyleAttr)
// keep bitmap and IntArray to reuse it.
private val pixels = IntArray(bitmapWidth * bitmapHeight) private val pixels = IntArray(bitmapWidth * bitmapHeight)
private val blurhashBitmap = Bitmap.createBitmap( private val blurhashBitmap = Bitmap.createBitmap(
bitmapWidth, bitmapWidth,
@ -200,7 +186,6 @@ class BlurhashView : AppCompatTextView {
var blurhash : String? = null var blurhash : String? = null
set(v) { set(v) {
if(v == field) return if(v == field) return
field = v
blurhashDecodeOk = if(v?.isEmpty() != false) { blurhashDecodeOk = if(v?.isEmpty() != false) {
false false
@ -220,6 +205,9 @@ class BlurhashView : AppCompatTextView {
log.e(ex, "blurhash decode failed.") log.e(ex, "blurhash decode failed.")
false false
} }
field = v
invalidate()
} }
override fun onDraw(canvas : Canvas) { override fun onDraw(canvas : Canvas) {