From 8ea19c89ce49a57ba5010b04a6e3f4ecb6053a68 Mon Sep 17 00:00:00 2001 From: Slava Date: Tue, 30 Apr 2024 19:03:23 +0600 Subject: [PATCH 1/4] Transcode images to png format [+] Added fun to transcode images [+] Added util func to cats to inputStream --- .../mattermost/MattermostRepositoryImpl.kt | 147 ++++++++++++------ .../band/effective/utils/toInputStream.kt | 8 + 2 files changed, 107 insertions(+), 48 deletions(-) create mode 100644 mattermost-bot/src/main/java/band/effective/utils/toInputStream.kt diff --git a/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt b/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt index 82238932..f1ffd650 100644 --- a/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt +++ b/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt @@ -3,20 +3,32 @@ package band.effective.mattermost import band.effective.MattermostSettings import band.effective.core.Either import band.effective.core.ErrorReason -import band.effective.mattermost.models.FileInfo -import band.effective.mattermost.models.response.ResponseGetPostsForChannel import band.effective.core.mattermostApi +import band.effective.mattermost.models.FileInfo import band.effective.mattermost.models.UserInfo -import band.effective.mattermost.models.response.models.* +import band.effective.mattermost.models.response.models.EmojiInfo +import band.effective.mattermost.models.response.models.EmojiInfoForApi +import band.effective.mattermost.models.response.models.Post +import band.effective.mattermost.models.response.models.toDataModel import band.effective.mattermost.models.response.toUserInfo import band.effective.utils.getEnv -import kotlinx.coroutines.* -import okhttp3.RequestBody -import org.json.JSONObject -import java.util.Calendar -import java.util.Collections +import band.effective.utils.toInputStream +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.MediaType +import okhttp3.ResponseBody +import retrofit2.Response +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.* +import javax.imageio.ImageIO +import javax.imageio.stream.ImageInputStream -class MattermostRepositoryImpl(private val token: String, private val coroutineScope: CoroutineScope) : MattermostRepository { +class MattermostRepositoryImpl(private val token: String, private val coroutineScope: CoroutineScope) : + MattermostRepository { private var userId: String? = null @@ -26,10 +38,11 @@ class MattermostRepositoryImpl(private val token: String, private val coroutineS private fun initUser() { coroutineScope.launch { - when(val userInfo = getUserIdFromToken()) { + when (val userInfo = getUserIdFromToken()) { is Either.Success -> { userId = userInfo.data.userId } + is Either.Failure -> { throw Error("Token is not correct or user not found. \n Token should be like this: Bearer ") } @@ -38,11 +51,19 @@ class MattermostRepositoryImpl(private val token: String, private val coroutineS } override suspend fun downloadFile(fileId: String): ByteArray? { - val response = mattermostApi.downloadFile(token = token, fileId = fileId) - val buffer = response.body()?.byteStream()?.readBytes() + val response: Response = mattermostApi.downloadFile(token = token, fileId = fileId) + + val mediaType: MediaType? = response.body()?.contentType() + val type: String? = mediaType?.type() + + val buffer: ByteArray? = response.body()?.byteStream()?.readBytes() println("read byte from mattermost ${buffer?.size}") + + val transcodeToPNG: ByteArray? = transcodeToPNG(imageData = buffer, type = type) + println("transcoded image bytes ${transcodeToPNG?.size}") + response.body()?.byteStream()?.close() - return buffer + return transcodeToPNG } private suspend fun getAllPostsFromChannels(): Either> { @@ -52,9 +73,9 @@ class MattermostRepositoryImpl(private val token: String, private val coroutineS channels.data.forEach { channel -> withContext(Dispatchers.IO) { val posts = mattermostApi.getPostsFromChannel( - token = token, - channelId = channel.id, - sinceTime = System.currentTimeMillis() - MILLISECOND_IN_DAY + token = token, + channelId = channel.id, + sinceTime = System.currentTimeMillis() - MILLISECOND_IN_DAY ) when (posts) { is Either.Failure -> { @@ -69,6 +90,7 @@ class MattermostRepositoryImpl(private val token: String, private val coroutineS } return Either.Success(postsCache.toList()) } + is Either.Failure -> { return channels } @@ -79,48 +101,77 @@ class MattermostRepositoryImpl(private val token: String, private val coroutineS This method return fileIds from posts, what has "star" reaction without "save" * */ override suspend fun getFilesIdsFromPosts(): Either> = - when (val posts = getAllPostsFromChannels()) { - is Either.Success -> { - val postsWithReaction = posts.data.filter { post -> - if (post.metadata.reactions != null) { - val countToRequestSaveReaction = - post.metadata.reactions.count { reaction -> reaction.emoji_name == getEnv(MattermostSettings.emojiToRequestSave) } - val countSaveReaction = - post.metadata.reactions.count { reaction -> reaction.emoji_name == getEnv(MattermostSettings.emojiToSaveSuccess) } - countToRequestSaveReaction > 0 && countSaveReaction == 0 - } else false - } - val filesInPostsWithReaction = postsWithReaction.map { post -> post.metadata.files } - - val files: List = filesInPostsWithReaction.flatMap { listIsFile -> - listIsFile?.filter { file -> - file.mime_type.contains("image") - }?.map { file -> - FileInfo(file.id, file.name, file.mime_type, file.post_id) - } ?: emptyList() - } - Either.Success(files) + when (val posts = getAllPostsFromChannels()) { + is Either.Success -> { + val postsWithReaction = posts.data.filter { post -> + if (post.metadata.reactions != null) { + val countToRequestSaveReaction = + post.metadata.reactions.count { reaction -> reaction.emoji_name == getEnv(MattermostSettings.emojiToRequestSave) } + val countSaveReaction = + post.metadata.reactions.count { reaction -> reaction.emoji_name == getEnv(MattermostSettings.emojiToSaveSuccess) } + countToRequestSaveReaction > 0 && countSaveReaction == 0 + } else false } - is Either.Failure -> { - posts + val filesInPostsWithReaction = postsWithReaction.map { post -> post.metadata.files } + + val files: List = filesInPostsWithReaction.flatMap { listIsFile -> + listIsFile?.filter { file -> + file.mime_type.contains("image") + }?.map { file -> + FileInfo(file.id, file.name, file.mime_type, file.post_id) + } ?: emptyList() } + Either.Success(files) + } + + is Either.Failure -> { + posts } + } override suspend fun makeReaction(emojiInfo: EmojiInfo): Either = - mattermostApi.makeReaction( - token = token, - emojiInfo = emojiInfo.toDataModel(userId = userId.orEmpty()) - ) + mattermostApi.makeReaction( + token = token, + emojiInfo = emojiInfo.toDataModel(userId = userId.orEmpty()) + ) private suspend fun getUserIdFromToken(): Either = - when(val userInfo = mattermostApi.getUserInfoFromToken(token)) { - is Either.Success -> { - Either.Success(userInfo.data.toUserInfo()) - } - is Either.Failure -> { + when (val userInfo = mattermostApi.getUserInfoFromToken(token)) { + is Either.Success -> { + Either.Success(userInfo.data.toUserInfo()) + } + + is Either.Failure -> { Either.Failure(userInfo.error) } } + + private fun transcodeToPNG(imageData: ByteArray?, type: String?): ByteArray? { + if (imageData == null) { + return imageData + } + if (type?.startsWith("image/") == true) { + val fileExtension = "png" //Transcode to this photo extension + try { + val stream: ImageInputStream = ImageIO.createImageInputStream(imageData.toInputStream()) + val inputImage: BufferedImage = ImageIO.read(stream) + val outputStream = ByteArrayOutputStream() + val result: Boolean = ImageIO.write(inputImage, fileExtension, outputStream) + + //close all streams + stream.close() + inputImage.flush() + + return if (result) outputStream.toByteArray() else imageData + } catch (e: IOException) { + e.printStackTrace() + println("Error in transcoding image: ${e.message}") + return imageData + } + } else { + return imageData + } + } } private const val MILLISECOND_IN_DAY = 86400000 \ No newline at end of file diff --git a/mattermost-bot/src/main/java/band/effective/utils/toInputStream.kt b/mattermost-bot/src/main/java/band/effective/utils/toInputStream.kt new file mode 100644 index 00000000..04113f9a --- /dev/null +++ b/mattermost-bot/src/main/java/band/effective/utils/toInputStream.kt @@ -0,0 +1,8 @@ +package band.effective.utils + +import java.io.ByteArrayInputStream +import java.io.InputStream + +fun ByteArray.toInputStream(): InputStream { + return ByteArrayInputStream(this) +} \ No newline at end of file -- GitLab From 8cc2dcf8aec7ae00b1727e7fc60e5712090cc03d Mon Sep 17 00:00:00 2001 From: Slava Date: Wed, 1 May 2024 13:51:50 +0600 Subject: [PATCH 2/4] Transcode images to png format [~] Tested in test project. So it worked. It transcoded `test.heic` image from 9134 byte size to 7879 byte size --- .../java/band/effective/mattermost/MattermostRepositoryImpl.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt b/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt index f1ffd650..73ade724 100644 --- a/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt +++ b/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt @@ -158,8 +158,6 @@ class MattermostRepositoryImpl(private val token: String, private val coroutineS val outputStream = ByteArrayOutputStream() val result: Boolean = ImageIO.write(inputImage, fileExtension, outputStream) - //close all streams - stream.close() inputImage.flush() return if (result) outputStream.toByteArray() else imageData -- GitLab From c88e730f492b012180d2d5d0fee0c7e89e7fda0e Mon Sep 17 00:00:00 2001 From: Slava Date: Thu, 2 May 2024 19:39:06 +0600 Subject: [PATCH 3/4] Transcode images to png format [~] Code changes according to review --- .../java/band/effective/mattermost/MattermostRepositoryImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt b/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt index 73ade724..c040f019 100644 --- a/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt +++ b/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt @@ -150,7 +150,7 @@ class MattermostRepositoryImpl(private val token: String, private val coroutineS if (imageData == null) { return imageData } - if (type?.startsWith("image/") == true) { + if (type?.startsWith("image/heic") == true) { val fileExtension = "png" //Transcode to this photo extension try { val stream: ImageInputStream = ImageIO.createImageInputStream(imageData.toInputStream()) -- GitLab From 9148604521d60d0c32b21ab34014331cd948b8c7 Mon Sep 17 00:00:00 2001 From: Slava Date: Fri, 3 May 2024 12:18:01 +0600 Subject: [PATCH 4/4] Custom Interceptor [+] created new Interceptor [+] added dependency in pom.xml --- mattermost-bot/pom.xml | 6 +++ .../effective/core/HEICToPNGInterceptor.kt | 38 +++++++++++++++++++ .../java/band/effective/core/RetrofitInit.kt | 1 + .../mattermost/MattermostRepositoryImpl.kt | 9 +---- 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 mattermost-bot/src/main/java/band/effective/core/HEICToPNGInterceptor.kt diff --git a/mattermost-bot/pom.xml b/mattermost-bot/pom.xml index 22c0c986..a6fbc7be 100644 --- a/mattermost-bot/pom.xml +++ b/mattermost-bot/pom.xml @@ -83,6 +83,12 @@ logging-interceptor 3.14.9 + + + com.google.android + android + 4.1.1.4 + diff --git a/mattermost-bot/src/main/java/band/effective/core/HEICToPNGInterceptor.kt b/mattermost-bot/src/main/java/band/effective/core/HEICToPNGInterceptor.kt new file mode 100644 index 00000000..073b525a --- /dev/null +++ b/mattermost-bot/src/main/java/band/effective/core/HEICToPNGInterceptor.kt @@ -0,0 +1,38 @@ +package band.effective.core + +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.ResponseBody +import android.graphics.BitmapFactory +import android.graphics.Bitmap +import okhttp3.MediaType +import java.io.ByteArrayOutputStream + +class HEICToPNGInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val originalResponse = chain.proceed(chain.request()) + val contentType = originalResponse.body()?.contentType() + val bodyBytes = originalResponse.body()?.bytes() + + if (contentType?.type() == "image/heic" && bodyBytes != null) { + val bitmap = decodeHEIC(bodyBytes) + val outputStream = ByteArrayOutputStream() + bitmap?.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val pngBytes = outputStream.toByteArray() + + return originalResponse.newBuilder() + .body(ResponseBody.create(MediaType.get("image/png"), pngBytes)) + .build() + } + return originalResponse + } + + private fun decodeHEIC(bytes: ByteArray?): Bitmap? { + return try { + val inputStream = bytes?.inputStream() + BitmapFactory.decodeStream(inputStream) + } catch (e: Exception) { + null + } + } +} \ No newline at end of file diff --git a/mattermost-bot/src/main/java/band/effective/core/RetrofitInit.kt b/mattermost-bot/src/main/java/band/effective/core/RetrofitInit.kt index 269aa02d..b9b2b4c2 100644 --- a/mattermost-bot/src/main/java/band/effective/core/RetrofitInit.kt +++ b/mattermost-bot/src/main/java/band/effective/core/RetrofitInit.kt @@ -24,6 +24,7 @@ val moshi: Moshi = Moshi.Builder() val moshiConverterFactory = MoshiConverterFactory.create(moshi).asLenient() val okHttpClient = OkHttpClient.Builder() + .addInterceptor(HEICToPNGInterceptor()) .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }).build() diff --git a/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt b/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt index c040f019..7c9d05d8 100644 --- a/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt +++ b/mattermost-bot/src/main/java/band/effective/mattermost/MattermostRepositoryImpl.kt @@ -17,7 +17,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import okhttp3.MediaType import okhttp3.ResponseBody import retrofit2.Response import java.awt.image.BufferedImage @@ -53,17 +52,11 @@ class MattermostRepositoryImpl(private val token: String, private val coroutineS override suspend fun downloadFile(fileId: String): ByteArray? { val response: Response = mattermostApi.downloadFile(token = token, fileId = fileId) - val mediaType: MediaType? = response.body()?.contentType() - val type: String? = mediaType?.type() - val buffer: ByteArray? = response.body()?.byteStream()?.readBytes() println("read byte from mattermost ${buffer?.size}") - val transcodeToPNG: ByteArray? = transcodeToPNG(imageData = buffer, type = type) - println("transcoded image bytes ${transcodeToPNG?.size}") - response.body()?.byteStream()?.close() - return transcodeToPNG + return buffer } private suspend fun getAllPostsFromChannels(): Either> { -- GitLab