From 30c893da07d976743d11deaf8c114844637752cc Mon Sep 17 00:00:00 2001 From: Taras Smakula <tarassmakula@gmail.com> Date: Tue, 5 Sep 2023 15:06:01 +0300 Subject: [PATCH] Add image thumbnails --- .../matrix/android/sdk/api/util/MimeTypes.kt | 1 + .../session/content/ThumbnailExtractor.kt | 57 +++++++++++++++++-- .../session/content/UploadContentWorker.kt | 15 +++-- .../room/send/LocalEchoEventFactory.kt | 10 ++++ 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt index 5ec0deda..af8ab71a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -30,6 +30,7 @@ object MimeTypes { const val BadJpg = "image/jpg" const val Jpeg = "image/jpeg" const val Gif = "image/gif" + const val Webp = "image/webp" const val Ogg = "audio/ogg" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt index 55db64f3..de805f59 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt @@ -18,7 +18,12 @@ package org.matrix.android.sdk.internal.session.content import android.content.Context import android.graphics.Bitmap +import android.graphics.ImageDecoder import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.util.Size import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.util.MimeTypes import timber.log.Timber @@ -38,10 +43,11 @@ internal class ThumbnailExtractor @Inject constructor( ) fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? { - return if (attachment.type == ContentAttachmentData.Type.VIDEO) { - extractVideoThumbnail(attachment) - } else { - null + if (attachment.mimeType == MimeTypes.Gif || attachment.mimeType == MimeTypes.Webp) return null + return when (attachment.type) { + ContentAttachmentData.Type.VIDEO -> extractVideoThumbnail(attachment) + ContentAttachmentData.Type.IMAGE -> extractImageThumbnail(attachment) + else -> null } } @@ -50,7 +56,8 @@ internal class ThumbnailExtractor @Inject constructor( val mediaMetadataRetriever = MediaMetadataRetriever() try { mediaMetadataRetriever.setDataSource(context, attachment.queryUri) - mediaMetadataRetriever.frameAtTime?.let { thumbnail -> + val scaledBitmap = mediaMetadataRetriever.frameAtTime?.let { createScaledThumbnailBitmap(it) } + scaledBitmap?.let { thumbnail -> val outputStream = ByteArrayOutputStream() thumbnail.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) val thumbnailWidth = thumbnail.width @@ -75,4 +82,44 @@ internal class ThumbnailExtractor @Inject constructor( } return thumbnailData } + + private fun extractImageThumbnail(attachment: ContentAttachmentData): ThumbnailData? { + var thumbnailData: ThumbnailData? = null + try { + val thumbnail = createScaledThumbnailBitmap(getBitmapFromUri(attachment.queryUri)) + val outputStream = ByteArrayOutputStream() + thumbnail.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) + val thumbnailWidth = thumbnail.width + val thumbnailHeight = thumbnail.height + val thumbnailSize = outputStream.size() + thumbnailData = ThumbnailData( + width = thumbnailWidth, + height = thumbnailHeight, + size = thumbnailSize.toLong(), + bytes = outputStream.toByteArray(), + mimeType = MimeTypes.Jpeg + ) + thumbnail.recycle() + outputStream.reset() + } catch (e: Exception) { + Timber.e(e, "Cannot extract image thumbnail") + } + return thumbnailData + } + + private fun getBitmapFromUri(uri: Uri) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri)) + } else { + MediaStore.Images.Media.getBitmap(context.contentResolver, uri) + } + + private fun createScaledThumbnailBitmap(originalBitmap: Bitmap): Bitmap { + val maxThumbnailSize = 800 + val originalWidth = originalBitmap.width + val originalHeight = originalBitmap.height + val aspectRatio = originalWidth.toFloat() / originalHeight.toFloat() + val size = if (originalHeight > originalWidth) Size((maxThumbnailSize * aspectRatio).toInt(), maxThumbnailSize) + else Size(maxThumbnailSize, (maxThumbnailSize / aspectRatio).toInt()) + return Bitmap.createScaledBitmap(originalBitmap, size.width, size.height, true) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 3dd44073..6aa1fb52 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -185,6 +185,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } else if (attachment.type == ContentAttachmentData.Type.VIDEO && // Do not compress gif attachment.mimeType != MimeTypes.Gif && + attachment.mimeType != MimeTypes.Webp && params.compressBeforeSending) { fileToUpload = videoCompressor.compress(workingFile, object : ProgressListener { override fun onProgress(progress: Int, total: Int) { @@ -193,7 +194,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter }) .let { videoCompressionResult -> when (videoCompressionResult) { - is VideoCompressionResult.Success -> { + is VideoCompressionResult.Success -> { val compressedFile = videoCompressionResult.compressedFile var compressedWidth: Int? = null var compressedHeight: Int? = null @@ -217,10 +218,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter compressedFile .also { filesToDelete.add(it) } } + VideoCompressionResult.CompressionNotNeeded, VideoCompressionResult.CompressionCancelled -> { workingFile } + is VideoCompressionResult.CompressionFailed -> { Timber.e(videoCompressionResult.failure, "Video compression failed") workingFile @@ -413,11 +416,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter // Retrieve potential additional content from the original event val additionalContent = content.orEmpty() - messageContent?.toContent().orEmpty().keys val updatedContent = when (messageContent) { - is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes) + is MessageImageContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newAttachmentAttributes) is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newAttachmentAttributes) - is MessageFileContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) + is MessageFileContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) is MessageAudioContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) - else -> messageContent + else -> messageContent } event.content = ContentMapper.map(updatedContent.toContent().plus(additionalContent)) } @@ -430,12 +433,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter private fun MessageImageContent.update( url: String, encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String?, + thumbnailEncryptedFileInfo: EncryptedFileInfo?, newAttachmentAttributes: NewAttachmentAttributes? ): MessageImageContent { return copy( url = if (encryptedFileInfo == null) url else null, encryptedFileInfo = encryptedFileInfo?.copy(url = url), info = info?.copy( + thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null, + thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl), width = newAttachmentAttributes?.newWidth ?: info.width, height = newAttachmentAttributes?.newHeight ?: info.height, size = newAttachmentAttributes?.newFileSize ?: info.size diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index cf0d54d0..05350991 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -427,6 +427,14 @@ internal class LocalEchoEventFactory @Inject constructor( } } + val thumbnailInfo = thumbnailExtractor.extractThumbnail(attachment)?.let { + ThumbnailInfo( + width = it.width, + height = it.height, + size = it.size, + mimeType = it.mimeType + ) + } val content = MessageImageContent( msgType = MessageType.MSGTYPE_IMAGE, body = attachment.name ?: "image", @@ -435,6 +443,8 @@ internal class LocalEchoEventFactory @Inject constructor( width = width?.toInt() ?: 0, height = height?.toInt() ?: 0, size = attachment.size, + thumbnailUrl = attachment.queryUri.toString(), + thumbnailInfo = thumbnailInfo, thumbHash = attachment.thumbHash ), url = attachment.queryUri.toString(), -- GitLab