- Adds support for multiple media uploads at the same time.
- Adds support to display PictureEvents with multiple images at the same time - Improves Uploading feedback for the NewPost screen - 10x better performance on Blurhash generation - Fixes cosine caching on Blurhash - Removes troublesome dependency on blurhash encoder liberary - Restructures contentScale for Images and Video dialogs - Refactors Media Uploaders to improve code reuse - Refactors iMeta usage on Quartz to move away from NIP-94
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.log
|
||||
|
||||
object Base83 {
|
||||
fun encode(value: Long): String =
|
||||
if (value > 82) {
|
||||
encode(value, ceil(log((value + 1).toDouble(), 83.0)).toInt())
|
||||
} else {
|
||||
encode(value, 1)
|
||||
}
|
||||
|
||||
fun encode(
|
||||
value: Long,
|
||||
length: Int,
|
||||
): String {
|
||||
val buffer = CharArray(length)
|
||||
encode(value, length, buffer, 0)
|
||||
return String(buffer)
|
||||
}
|
||||
|
||||
fun encode(
|
||||
value: Long,
|
||||
length: Int,
|
||||
buffer: CharArray,
|
||||
offset: Int,
|
||||
) {
|
||||
var exp = 1L
|
||||
for (i in 1..length) {
|
||||
val digit = (value / exp % 83).toInt()
|
||||
buffer[offset + length - i] = ALPHABET[digit]
|
||||
exp *= 83
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeAt(
|
||||
str: String,
|
||||
at: Int = 0,
|
||||
): Int = charMap[str[at].code]
|
||||
|
||||
fun decodeFixed2(
|
||||
str: String,
|
||||
from: Int = 0,
|
||||
): Int = charMap[str[from].code] * 83 + charMap[str[from + 1].code]
|
||||
|
||||
fun decode(
|
||||
str: String,
|
||||
from: Int = 0,
|
||||
to: Int = str.length,
|
||||
): Int {
|
||||
var result = 0
|
||||
for (i in from until to) {
|
||||
result = result * 83 + charMap[str[i].code]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
val ALPHABET: CharArray = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".toCharArray()
|
||||
|
||||
private val charMap =
|
||||
ALPHABET
|
||||
.mapIndexed { i, c -> c.code to i }
|
||||
.toMap()
|
||||
.let { charMap ->
|
||||
Array(255) {
|
||||
charMap[it] ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Bitmap.toBlurhash(): String {
|
||||
val aspectRatio = this.width.toFloat() / this.height.toFloat()
|
||||
|
||||
if (this.width > 100 && this.height > 100) {
|
||||
return Bitmap.createScaledBitmap(this, 100, (100 / aspectRatio).toInt(), false).toBlurhash()
|
||||
}
|
||||
|
||||
val intArray = IntArray(width * height)
|
||||
this.getPixels(intArray, 0, width, 0, 0, width, height)
|
||||
|
||||
val numX =
|
||||
if (aspectRatio > 1) {
|
||||
9
|
||||
} else if (aspectRatio < 1) {
|
||||
(9 * aspectRatio).roundToInt()
|
||||
} else {
|
||||
4
|
||||
}
|
||||
|
||||
val numY =
|
||||
if (aspectRatio > 1) {
|
||||
(9 * (1 / aspectRatio)).roundToInt()
|
||||
} else if (aspectRatio < 1) {
|
||||
9
|
||||
} else {
|
||||
4
|
||||
}
|
||||
|
||||
return BlurHashEncoder().encode(
|
||||
intArray,
|
||||
width,
|
||||
height,
|
||||
numX,
|
||||
numY,
|
||||
)
|
||||
}
|
||||
+12
-200
@@ -18,38 +18,22 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.preview
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import kotlin.math.cos
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.SRGB.Companion.linearToSrgb
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.SRGB.Companion.srgbToLinear
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.withSign
|
||||
|
||||
object BlurHashDecoder {
|
||||
// cache Math.cos() calculations to improve performance.
|
||||
// The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY *
|
||||
// 2 * nBitmaps
|
||||
// the cache is enabled by default, it is recommended to disable it only when just a few images
|
||||
// are displayed
|
||||
private val cacheCosinesX = HashMap<Int, DoubleArray>()
|
||||
private val cacheCosinesY = HashMap<Int, DoubleArray>()
|
||||
|
||||
/**
|
||||
* Clear calculations stored in memory cache. The cache is not big, but will increase when many
|
||||
* image sizes are used, if the app needs memory it is recommended to clear it.
|
||||
*/
|
||||
fun clearCache() {
|
||||
cacheCosinesX.clear()
|
||||
cacheCosinesY.clear()
|
||||
}
|
||||
|
||||
/** Returns width/height */
|
||||
fun aspectRatio(blurHash: String?): Float? {
|
||||
if (blurHash == null || blurHash.length < 6) {
|
||||
return null
|
||||
}
|
||||
val numCompEnc = decode83At(blurHash, 0)
|
||||
val numCompEnc = Base83.decodeAt(blurHash, 0)
|
||||
val numCompX = (numCompEnc % 9) + 1
|
||||
val numCompY = (numCompEnc / 9) + 1
|
||||
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
|
||||
@@ -64,18 +48,18 @@ object BlurHashDecoder {
|
||||
numCompY: Int,
|
||||
blurHash: String,
|
||||
): Array<FloatArray> {
|
||||
val maxAc = (decode83At(blurHash, 1) + 1) / 166f
|
||||
val maxAc = (Base83.decodeAt(blurHash, 1) + 1) / 166f
|
||||
return Array(numCompX * numCompY) { i ->
|
||||
if (i == 0) {
|
||||
decodeDc(decode83(blurHash, 2, 6))
|
||||
decodeDc(Base83.decode(blurHash, 2, 6))
|
||||
} else {
|
||||
decodeAc(decode83Fixed2(blurHash, 4 + i * 2), maxAc)
|
||||
decodeAc(Base83.decodeFixed2(blurHash, 4 + i * 2), maxAc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun computeNumComponets(blurHash: String): Pair<Int, Int> {
|
||||
val numCompEnc = decode83At(blurHash, 0)
|
||||
val numCompEnc = Base83.decodeAt(blurHash, 0)
|
||||
val numCompX = (numCompEnc % 9) + 1
|
||||
val numCompY = (numCompEnc / 9) + 1
|
||||
return Pair(numCompX, numCompY)
|
||||
@@ -106,28 +90,6 @@ object BlurHashDecoder {
|
||||
return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
|
||||
private fun decode83At(
|
||||
str: String,
|
||||
at: Int = 0,
|
||||
): Int = charMap[str[at].code]
|
||||
|
||||
private fun decode83Fixed2(
|
||||
str: String,
|
||||
from: Int = 0,
|
||||
): Int = charMap[str[from].code] * 83 + charMap[str[from + 1].code]
|
||||
|
||||
private fun decode83(
|
||||
str: String,
|
||||
from: Int = 0,
|
||||
to: Int = str.length,
|
||||
): Int {
|
||||
var result = 0
|
||||
for (i in from until to) {
|
||||
result = result * 83 + charMap[str[i].code]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun decodeDc(colorEnc: Int): FloatArray {
|
||||
val r = colorEnc shr 16
|
||||
val g = (colorEnc shr 8) and 255
|
||||
@@ -135,15 +97,6 @@ object BlurHashDecoder {
|
||||
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
|
||||
}
|
||||
|
||||
private fun srgbToLinear(colorEnc: Int): Float {
|
||||
val v = colorEnc / 255f
|
||||
return if (v <= 0.04045f) {
|
||||
(v / 12.92f)
|
||||
} else {
|
||||
((v + 0.055f) / 1.055f).pow(2.4f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeAc(
|
||||
value: Int,
|
||||
maxAc: Float,
|
||||
@@ -170,10 +123,10 @@ object BlurHashDecoder {
|
||||
): IntArray {
|
||||
// use an array for better performance when writing pixel colors
|
||||
val imageArray = IntArray(width * height)
|
||||
val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX)
|
||||
val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX)
|
||||
val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY)
|
||||
val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY)
|
||||
val calculateCosX = !useCache || !CosineCache.hasX(width * numCompX)
|
||||
val cosinesX = CosineCache.getArrayForCosinesX(calculateCosX, width, numCompX)
|
||||
val calculateCosY = !useCache || !CosineCache.hasY(height * numCompY)
|
||||
val cosinesY = CosineCache.getArrayForCosinesY(calculateCosY, height, numCompY)
|
||||
|
||||
var r = 0.0f
|
||||
var g = 0.0f
|
||||
@@ -208,145 +161,4 @@ object BlurHashDecoder {
|
||||
green: Int,
|
||||
blue: Int,
|
||||
): Int = -0x1000000 or (red shl 16) or (green shl 8) or blue
|
||||
|
||||
private fun getArrayForCosinesY(
|
||||
calculate: Boolean,
|
||||
height: Int,
|
||||
numCompY: Int,
|
||||
) = when {
|
||||
calculate -> {
|
||||
DoubleArray(height * numCompY) {
|
||||
val y = it / numCompY
|
||||
val j = it % numCompY
|
||||
cos(Math.PI * y * j / height)
|
||||
}.also {
|
||||
cacheCosinesY[height * numCompY] = it
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
cacheCosinesY[height * numCompY]!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun getArrayForCosinesX(
|
||||
calculate: Boolean,
|
||||
width: Int,
|
||||
numCompX: Int,
|
||||
) = when {
|
||||
calculate -> {
|
||||
DoubleArray(width * numCompX) {
|
||||
val x = it / numCompX
|
||||
val i = it % numCompX
|
||||
cos(Math.PI * x * i / width)
|
||||
}.also { cacheCosinesX[width * numCompX] = it }
|
||||
}
|
||||
else -> cacheCosinesX[width * numCompX]!!
|
||||
}
|
||||
|
||||
private fun linearToSrgb(value: Float): Int {
|
||||
val v = value.coerceIn(0f, 1f)
|
||||
return if (v <= 0.0031308f) {
|
||||
(v * 12.92f * 255f + 0.5f).toInt()
|
||||
} else {
|
||||
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private val linToSrgbApproximation =
|
||||
Array(255) {
|
||||
linearToSrgb(it / 255f)
|
||||
}
|
||||
|
||||
private val charMap =
|
||||
listOf(
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'i',
|
||||
'j',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'o',
|
||||
'p',
|
||||
'q',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
'u',
|
||||
'v',
|
||||
'w',
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
'#',
|
||||
'$',
|
||||
'%',
|
||||
'*',
|
||||
'+',
|
||||
',',
|
||||
'-',
|
||||
'.',
|
||||
':',
|
||||
';',
|
||||
'=',
|
||||
'?',
|
||||
'@',
|
||||
'[',
|
||||
']',
|
||||
'^',
|
||||
'_',
|
||||
'{',
|
||||
'|',
|
||||
'}',
|
||||
'~',
|
||||
).mapIndexed { i, c -> c.code to i }
|
||||
.toMap()
|
||||
.let { charMap ->
|
||||
Array(255) {
|
||||
charMap[it] ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.preview
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.Base83.encode
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.SRGB.Companion.linearToSrgb
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.withSign
|
||||
|
||||
class BlurHashEncoder {
|
||||
fun signPow(
|
||||
value: Double,
|
||||
exp: Double,
|
||||
): Double = abs(value).pow(exp).withSign(value)
|
||||
|
||||
private fun encodeAC(
|
||||
value: DoubleArray,
|
||||
maximumValue: Double,
|
||||
): Long {
|
||||
val quantR = floor(max(0.0, min(18.0, floor(signPow(value[0] / maximumValue, 0.5) * 9 + 9.5))))
|
||||
val quantG = floor(max(0.0, min(18.0, floor(signPow(value[1] / maximumValue, 0.5) * 9 + 9.5))))
|
||||
val quantB = floor(max(0.0, min(18.0, floor(signPow(value[2] / maximumValue, 0.5) * 9 + 9.5))))
|
||||
return Math.round(quantR * 19 * 19 + quantG * 19 + quantB)
|
||||
}
|
||||
|
||||
private fun encodeDC(value: DoubleArray): Long {
|
||||
val r = linearToSrgb(value[0]).toLong()
|
||||
val g = linearToSrgb(value[1]).toLong()
|
||||
val b = linearToSrgb(value[2]).toLong()
|
||||
return (r shl 16) + (g shl 8) + b
|
||||
}
|
||||
|
||||
fun max(
|
||||
values: Array<DoubleArray>,
|
||||
from: Int,
|
||||
endExclusive: Int,
|
||||
): Double {
|
||||
var result = Double.NEGATIVE_INFINITY
|
||||
for (i in from until endExclusive) {
|
||||
for (j in values[i].indices) {
|
||||
val value = values[i][j]
|
||||
if (value > result) {
|
||||
result = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun encode(
|
||||
pixels: IntArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
componentX: Int,
|
||||
componentY: Int,
|
||||
useCache: Boolean = true,
|
||||
): String {
|
||||
require(!(componentX < 1 || componentX > 9 || componentY < 1 || componentY > 9)) { "Blur hash must have between 1 and 9 components" }
|
||||
require(width * height == pixels.size) { "Width and height must match the pixels array" }
|
||||
|
||||
val factors = Array(componentX * componentY) { DoubleArray(3) }
|
||||
|
||||
val calculateCosX = !useCache || !CosineCache.hasX(width * componentX)
|
||||
val cosinesX = CosineCache.getArrayForCosinesX(calculateCosX, width, componentX)
|
||||
val calculateCosY = !useCache || !CosineCache.hasY(height * componentY)
|
||||
val cosinesY = CosineCache.getArrayForCosinesY(calculateCosY, height, componentY)
|
||||
|
||||
val scale = 1.0 / (width * height)
|
||||
|
||||
var r = 0.0
|
||||
var g = 0.0
|
||||
var b = 0.0
|
||||
|
||||
for (j in 0 until componentY) {
|
||||
for (i in 0 until componentX) {
|
||||
val normalisation = (if (i == 0 && j == 0) 1 else 2).toDouble()
|
||||
|
||||
r = 0.0
|
||||
g = 0.0
|
||||
b = 0.0
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
val basis = normalisation * cosinesY[j + componentY * y] * cosinesX[i + componentX * x]
|
||||
val pixel = pixels[y * width + x]
|
||||
r += basis * SRGB.srgbToLinear((pixel shr 16) and 0xff)
|
||||
g += basis * SRGB.srgbToLinear((pixel shr 8) and 0xff)
|
||||
b += basis * SRGB.srgbToLinear(pixel and 0xff)
|
||||
}
|
||||
}
|
||||
|
||||
val colors = factors[j * componentX + i]
|
||||
colors[0] = r * scale
|
||||
colors[1] = g * scale
|
||||
colors[2] = b * scale
|
||||
}
|
||||
}
|
||||
|
||||
val hash = CharArray(1 + 1 + 4 + 2 * (factors.size - 1)) // size flag + max AC + DC + 2 * AC components
|
||||
val sizeFlag = (componentX - 1 + (componentY - 1) * 9).toLong()
|
||||
encode(sizeFlag, 1, hash, 0)
|
||||
|
||||
val maximumValue: Double
|
||||
if (factors.size > 1) {
|
||||
val actualMaximumValue = max(factors, 1, factors.size)
|
||||
val quantisedMaximumValue = floor(max(0.0, min(82.0, floor(actualMaximumValue * 166 - 0.5))))
|
||||
maximumValue = (quantisedMaximumValue + 1) / 166
|
||||
encode(Math.round(quantisedMaximumValue), 1, hash, 1)
|
||||
} else {
|
||||
maximumValue = 1.0
|
||||
encode(0, 1, hash, 1)
|
||||
}
|
||||
|
||||
val dc = factors[0]
|
||||
encode(encodeDC(dc), 4, hash, 2)
|
||||
|
||||
for (i in 1 until factors.size) {
|
||||
encode(encodeAC(factors[i], maximumValue), 2, hash, 6 + 2 * (i - 1))
|
||||
}
|
||||
return String(hash)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import kotlin.math.cos
|
||||
|
||||
object CosineCache {
|
||||
// cache Math.cos() calculations to improve performance.
|
||||
// The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY *
|
||||
// 2 * nBitmaps
|
||||
// the cache is enabled by default, it is recommended to disable it only when just a few images
|
||||
// are displayed
|
||||
private val cacheCosinesX = LruCache<Int, DoubleArray>(20)
|
||||
private val cacheCosinesY = LruCache<Int, DoubleArray>(20)
|
||||
|
||||
/**
|
||||
* Clear calculations stored in memory cache. The cache is not big, but will increase when many
|
||||
* image sizes are used, if the app needs memory it is recommended to clear it.
|
||||
*/
|
||||
fun clearCache() {
|
||||
cacheCosinesX.evictAll()
|
||||
cacheCosinesY.evictAll()
|
||||
}
|
||||
|
||||
fun hasX(idx: Int) = cacheCosinesX.get(idx) != null
|
||||
|
||||
fun hasY(idx: Int) = cacheCosinesY.get(idx) != null
|
||||
|
||||
fun getArrayForCosinesY(
|
||||
calculate: Boolean,
|
||||
height: Int,
|
||||
numCompY: Int,
|
||||
) = when {
|
||||
calculate -> {
|
||||
DoubleArray(height * numCompY) {
|
||||
val y = it / numCompY
|
||||
val j = it % numCompY
|
||||
cos(Math.PI * y * j / height)
|
||||
}.also {
|
||||
cacheCosinesY.put(height * numCompY, it)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
cacheCosinesY[height * numCompY]!!
|
||||
}
|
||||
}
|
||||
|
||||
fun getArrayForCosinesX(
|
||||
calculate: Boolean,
|
||||
width: Int,
|
||||
numCompX: Int,
|
||||
) = when {
|
||||
calculate -> {
|
||||
DoubleArray(width * numCompX) {
|
||||
val x = it / numCompX
|
||||
val i = it % numCompX
|
||||
cos(Math.PI * x * i / width)
|
||||
}.also { cacheCosinesX.put(width * numCompX, it) }
|
||||
}
|
||||
else -> cacheCosinesX[width * numCompX]!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import kotlin.math.pow
|
||||
|
||||
class SRGB {
|
||||
companion object {
|
||||
fun linearToSrgb(value: Float): Int {
|
||||
val v = value.coerceIn(0.0f, 1.0f)
|
||||
return if (v <= 0.0031308f) {
|
||||
(v * 12.92f * 255f + 0.5f).toInt()
|
||||
} else {
|
||||
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun linearToSrgb(value: Double): Int {
|
||||
val v = value.coerceIn(0.0, 1.0)
|
||||
return if (v <= 0.0031308f) {
|
||||
(v * 12.92f * 255f + 0.5f).toInt()
|
||||
} else {
|
||||
((1.055f * v.pow(1 / 2.4) - 0.055f) * 255 + 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun srgbToLinear(value: Int): Float {
|
||||
val valueCheck = value.coerceIn(0, 255)
|
||||
|
||||
val v = valueCheck / 255f
|
||||
return if (v <= 0.04045f) {
|
||||
v / 12.92f
|
||||
} else {
|
||||
((v + 0.055f) / 1.055f).pow(2.4f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.commons.richtext
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.events.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import java.io.File
|
||||
|
||||
@Immutable
|
||||
|
||||
@@ -24,10 +24,10 @@ import android.util.Log
|
||||
import android.util.Patterns
|
||||
import com.linkedin.urls.detection.UrlDetector
|
||||
import com.linkedin.urls.detection.UrlDetectorOptions
|
||||
import com.vitorpamplona.quartz.encoders.Dimension
|
||||
import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji
|
||||
import com.vitorpamplona.quartz.encoders.Nip54InlineMetadata
|
||||
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||
import com.vitorpamplona.quartz.events.Dimension
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -50,7 +50,7 @@ class RichTextParser {
|
||||
callbackUri: String? = null,
|
||||
): MediaUrlContent? {
|
||||
val frags = Nip54InlineMetadata().parse(fullUrl)
|
||||
val tags = Nip92MediaAttachments().parse(fullUrl, eventTags.lists)
|
||||
val tags = Nip92MediaAttachments.parse(fullUrl, eventTags.lists)
|
||||
|
||||
val contentType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user