mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-10 19:26:49 +01:00
added blurhash to blossom upload
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 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.service.uploads
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.net.Uri
|
||||||
|
import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
|
||||||
|
import com.vitorpamplona.amethyst.service.images.BlurhashWrapper
|
||||||
|
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||||
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
|
|
||||||
|
object BlurhashMetadataCalculator {
|
||||||
|
fun shouldAttempt(mimeType: String?): Boolean =
|
||||||
|
mimeType?.let {
|
||||||
|
it.startsWith("image/", ignoreCase = true) || it.startsWith("video/", ignoreCase = true)
|
||||||
|
} ?: false
|
||||||
|
|
||||||
|
fun computeFromBytes(
|
||||||
|
data: ByteArray,
|
||||||
|
mimeType: String?,
|
||||||
|
dimPrecomputed: DimensionTag?,
|
||||||
|
): Pair<BlurhashWrapper?, DimensionTag?> =
|
||||||
|
when {
|
||||||
|
mimeType?.startsWith("image/", ignoreCase = true) == true -> {
|
||||||
|
val options = BitmapFactory.Options()
|
||||||
|
options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, options)
|
||||||
|
val (blur, dim) = processBitmap(bitmap)
|
||||||
|
blur to (dim ?: dimPrecomputed)
|
||||||
|
}
|
||||||
|
mimeType?.startsWith("video/", ignoreCase = true) == true -> {
|
||||||
|
val retriever = MediaMetadataRetriever()
|
||||||
|
try {
|
||||||
|
retriever.setDataSource(ByteArrayMediaDataSource(data))
|
||||||
|
processRetriever(retriever, dimPrecomputed)
|
||||||
|
} finally {
|
||||||
|
retriever.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null to dimPrecomputed
|
||||||
|
}
|
||||||
|
|
||||||
|
fun computeFromUri(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String?,
|
||||||
|
dimPrecomputed: DimensionTag? = null,
|
||||||
|
): Pair<BlurhashWrapper?, DimensionTag?>? {
|
||||||
|
if (!shouldAttempt(mimeType)) return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
when {
|
||||||
|
mimeType?.startsWith("image/", ignoreCase = true) == true ->
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||||
|
val options = BitmapFactory.Options()
|
||||||
|
options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||||
|
val bitmap = BitmapFactory.decodeStream(stream, null, options)
|
||||||
|
val (blur, dim) = processBitmap(bitmap)
|
||||||
|
blur to (dim ?: dimPrecomputed)
|
||||||
|
} ?: (null to dimPrecomputed)
|
||||||
|
|
||||||
|
mimeType?.startsWith("video/", ignoreCase = true) == true -> {
|
||||||
|
val retriever = MediaMetadataRetriever()
|
||||||
|
try {
|
||||||
|
retriever.setDataSource(context, uri)
|
||||||
|
processRetriever(retriever, dimPrecomputed)
|
||||||
|
} finally {
|
||||||
|
retriever.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("BlurhashMetadataCalc", "Failed to compute metadata from uri", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processBitmap(bitmap: Bitmap?): Pair<BlurhashWrapper?, DimensionTag?> =
|
||||||
|
if (bitmap != null) {
|
||||||
|
try {
|
||||||
|
val blurhash = BlurhashWrapper(bitmap.toBlurhash())
|
||||||
|
blurhash to DimensionTag(bitmap.width, bitmap.height)
|
||||||
|
} finally {
|
||||||
|
bitmap.recycle()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null to null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processRetriever(
|
||||||
|
retriever: MediaMetadataRetriever,
|
||||||
|
dimPrecomputed: DimensionTag?,
|
||||||
|
): Pair<BlurhashWrapper?, DimensionTag?> {
|
||||||
|
val dim = retriever.prepareDimFromVideo() ?: dimPrecomputed
|
||||||
|
val blurhash = retriever.getThumbnail()?.toBlurhash()?.let { BlurhashWrapper(it) }
|
||||||
|
return if (dim?.hasSize() == true) {
|
||||||
|
blurhash to dim
|
||||||
|
} else {
|
||||||
|
blurhash to null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,11 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.uploads
|
package com.vitorpamplona.amethyst.service.uploads
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.media.MediaDataSource
|
import android.media.MediaDataSource
|
||||||
import android.media.MediaMetadataRetriever
|
|
||||||
import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
|
|
||||||
import com.vitorpamplona.amethyst.service.images.BlurhashWrapper
|
import com.vitorpamplona.amethyst.service.images.BlurhashWrapper
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||||
@@ -75,27 +71,7 @@ class FileHeader(
|
|||||||
val hash = sha256(data).toHexKey()
|
val hash = sha256(data).toHexKey()
|
||||||
val size = data.size
|
val size = data.size
|
||||||
|
|
||||||
val (blurHash, dim) =
|
val (blurHash, dim) = BlurhashMetadataCalculator.computeFromBytes(data, mimeType, dimPrecomputed)
|
||||||
if (mimeType?.startsWith("image/") == true) {
|
|
||||||
val opt = BitmapFactory.Options()
|
|
||||||
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
|
|
||||||
val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt)
|
|
||||||
Pair(BlurhashWrapper(mBitmap.toBlurhash()), DimensionTag(mBitmap.width, mBitmap.height))
|
|
||||||
} else if (mimeType?.startsWith("video/") == true) {
|
|
||||||
val mediaMetadataRetriever = MediaMetadataRetriever()
|
|
||||||
mediaMetadataRetriever.setDataSource(ByteArrayMediaDataSource(data))
|
|
||||||
|
|
||||||
val newDim = mediaMetadataRetriever.prepareDimFromVideo() ?: dimPrecomputed
|
|
||||||
val blurhash = mediaMetadataRetriever.getThumbnail()?.toBlurhash()?.let { BlurhashWrapper(it) }
|
|
||||||
|
|
||||||
if (newDim?.hasSize() == true) {
|
|
||||||
Pair(blurhash, newDim)
|
|
||||||
} else {
|
|
||||||
Pair(blurhash, null)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Pair(null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
Result.success(FileHeader(mimeType, hash, size, dim, blurHash))
|
Result.success(FileHeader(mimeType, hash, size, dim, blurHash))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.uploads
|
package com.vitorpamplona.amethyst.service.uploads
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.service.images.BlurhashWrapper
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||||
|
|
||||||
@@ -42,4 +43,6 @@ data class MediaUploadResult(
|
|||||||
val infohash: String? = null,
|
val infohash: String? = null,
|
||||||
// ipfs link
|
// ipfs link
|
||||||
val ipfs: String? = null,
|
val ipfs: String? = null,
|
||||||
|
// blurhash value for previews
|
||||||
|
val blurHash: BlurhashWrapper? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ class UploadOrchestrator {
|
|||||||
hash = verification.hash,
|
hash = verification.hash,
|
||||||
size = verification.size.toInt(),
|
size = verification.size.toInt(),
|
||||||
dim = uploadResult.dimension,
|
dim = uploadResult.dimension,
|
||||||
blurHash = null, // Skip blurhash generation for verification
|
blurHash = uploadResult.blurHash,
|
||||||
)
|
)
|
||||||
|
|
||||||
return finish(
|
return finish(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import android.webkit.MimeTypeMap
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.BlurhashMetadataCalculator
|
||||||
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
|
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
@@ -104,25 +105,35 @@ class BlossomUploader {
|
|||||||
calculateHashAndSize(it)
|
calculateHashAndSize(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val localMetadata = BlurhashMetadataCalculator.computeFromUri(context, uri, myContentType)
|
||||||
|
|
||||||
val imageInputStream = contentResolver.openInputStream(uri)
|
val imageInputStream = contentResolver.openInputStream(uri)
|
||||||
|
|
||||||
checkNotNull(imageInputStream) { "Can't open the image input stream" }
|
checkNotNull(imageInputStream) { "Can't open the image input stream" }
|
||||||
|
|
||||||
return imageInputStream.use { stream ->
|
val serverResult =
|
||||||
upload(
|
imageInputStream.use { stream ->
|
||||||
stream,
|
upload(
|
||||||
streamInfo.hash,
|
stream,
|
||||||
streamInfo.size,
|
streamInfo.hash,
|
||||||
fileName,
|
streamInfo.size,
|
||||||
myContentType,
|
fileName,
|
||||||
alt,
|
myContentType,
|
||||||
sensitiveContent,
|
alt,
|
||||||
serverBaseUrl,
|
sensitiveContent,
|
||||||
okHttpClient,
|
serverBaseUrl,
|
||||||
httpAuth,
|
okHttpClient,
|
||||||
context,
|
httpAuth,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return localMetadata?.let { (blur, dim) ->
|
||||||
|
serverResult.copy(
|
||||||
|
dimension = dim ?: serverResult.dimension,
|
||||||
|
blurHash = blur ?: serverResult.blurHash,
|
||||||
)
|
)
|
||||||
}
|
} ?: serverResult
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encodeAuth(event: BlossomAuthorizationEvent): String {
|
fun encodeAuth(event: BlossomAuthorizationEvent): String {
|
||||||
|
|||||||
Reference in New Issue
Block a user