From 96b253c3e70b4e51fac8328232d113ae0b35267f Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 25 Aug 2023 19:28:39 +0300 Subject: [PATCH] dekstop: image compression (#2979) * dekstop: image compression * refactor --- .../simplex/common/platform/Images.android.kt | 4 +- .../chat/simplex/common/platform/Images.kt | 1 + .../simplex/common/views/helpers/Utils.kt | 4 +- .../simplex/common/platform/Images.desktop.kt | 41 ++++++++++++++++--- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt index 4140cd19a9..832f0d9cbb 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt @@ -62,7 +62,7 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap { } actual fun compressImageStr(bitmap: ImageBitmap): String { - val usePng = bitmap.hasAlpha + val usePng = bitmap.hasAlpha() val ext = if (usePng) "png" else "jpg" return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP) } @@ -89,6 +89,8 @@ actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSiz actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this, Bitmap.Config.RGB_565).asImageBitmap() +actual fun ImageBitmap.hasAlpha(): Boolean = hasAlpha + actual fun ImageBitmap.addLogo(): ImageBitmap = asAndroidBitmap().applyCanvas { val radius = (width * 0.16f) / 2 val paint = android.graphics.Paint() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt index e73786b2e1..fca69d5398 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt @@ -15,6 +15,7 @@ expect fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOut expect fun GrayU8.toImageBitmap(): ImageBitmap +expect fun ImageBitmap.hasAlpha(): Boolean expect fun ImageBitmap.addLogo(): ImageBitmap expect fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index a6746da694..e4670fef15 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -102,7 +102,7 @@ fun saveImage(uri: URI): String? { fun saveImage(image: ImageBitmap): String? { return try { - val ext = if (image.hasAlpha) "png" else "jpg" + val ext = if (image.hasAlpha()) "png" else "jpg" val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE) val fileToSave = generateNewFileName("IMG", ext) val file = File(getAppFilePath(fileToSave)) @@ -112,7 +112,7 @@ fun saveImage(image: ImageBitmap): String? { output.close() fileToSave } catch (e: Exception) { - Log.e(TAG, "Util.kt saveImage error: ${e.message}") + Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}") null } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index 81699a6cc1..b7341cb4a8 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -10,7 +10,8 @@ import java.awt.image.BufferedImage import java.io.* import java.net.URI import java.util.* -import javax.imageio.ImageIO +import javax.imageio.* +import javax.imageio.stream.MemoryCacheImageOutputStream import kotlin.math.sqrt private fun errorBitmap(): ImageBitmap = @@ -69,7 +70,7 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap { } actual fun compressImageStr(bitmap: ImageBitmap): String { - val usePng = bitmap.hasAlpha + val usePng = bitmap.hasAlpha() val ext = if (usePng) "png" else "jpg" return try { val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray()) @@ -81,15 +82,45 @@ actual fun compressImageStr(bitmap: ImageBitmap): String { } actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream { + val writer = ImageIO.getImageWritersByFormatName(if (usePng) "png" else "jpg").next() + val writeParam = writer.defaultWriteParam + writeParam.compressionMode = ImageWriteParam.MODE_EXPLICIT + writeParam.compressionQuality = 0.85f val stream = ByteArrayOutputStream() - stream.use { s -> ImageIO.write(bitmap.toAwtImage(), if (usePng) "png" else "jpg", s) } - // MAKE REAL COMPRESSION - //bitmap.compress(if (!usePng) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, 85, stream) + writer.output = MemoryCacheImageOutputStream(stream) + val outputImage = IIOImage(if (usePng) bitmap.toAwtImage() else removeAlphaChannel(bitmap.toAwtImage()), null, null) + writer.write(null, outputImage, writeParam) + writer.dispose() + stream.flush() return stream } +private fun removeAlphaChannel(img: BufferedImage): BufferedImage { + if (!img.colorModel.hasAlpha()) return img + val target = BufferedImage(img.width, img.height, BufferedImage.TYPE_INT_RGB) + val g = target.createGraphics() + g.fillRect(0, 0, img.width, img.height) + g.drawImage(img, 0, 0, null) + g.dispose() + return target +} + actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBufferedImage.extractBuffered(this).toComposeImageBitmap() +actual fun ImageBitmap.hasAlpha(): Boolean { + val map = toPixelMap() + var y = 0 + while (y < height) { + var x = 0 + while (x < width) { + if (map[x, y].alpha < 1f) return true + x++ + } + y++ + } + return false +} + actual fun ImageBitmap.addLogo(): ImageBitmap { val radius = (width * 0.16f).toInt() val logoSize = (width * 0.24).toInt()