From 5082f5b4a4dddfc79c3e915343102dc57922cea8 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:45:40 +0300 Subject: [PATCH] android: colored and clickable qr code with logo (#1884) * android: colored and clickable qr code with logo * save qr code as jpg for better quality * bigger logo * bigger logo (0.16f), low error correction (QR scans ok with up to 0.26f circle size) --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../chat/simplex/app/views/helpers/Util.kt | 19 ++++ .../chat/simplex/app/views/newchat/QRCode.kt | 88 ++++++++++++++++--- .../app/src/main/res/xml/file_paths.xml | 1 + 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 74a99e03bf..777f7d1100 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -1,5 +1,6 @@ package chat.simplex.app.views.helpers +import android.app.Application import android.content.Context import android.content.res.Resources import android.graphics.* @@ -374,6 +375,24 @@ fun saveAnimImage(context: Context, uri: Uri): String? { } } +fun saveTempImageUncompressed(image: Bitmap, asPng: Boolean): File? { + return try { + val ext = if (asPng) "png" else "jpg" + val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE) + return File(tmpDir.absolutePath + File.separator + generateNewFileName(SimplexApp.context, "IMG", ext)).apply { + outputStream().use { out -> + image.compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out) + out.flush() + } + deleteOnExit() + SimplexApp.context.chatModel.filesToDelete.add(this) + } + } catch (e: Exception) { + Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}") + null + } +} + fun saveFileFromUri(context: Context, uri: Uri): String? { return try { val inputStream = context.contentResolver.openInputStream(uri) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt index b0f5a0c718..cd39cddc60 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt @@ -1,34 +1,98 @@ package chat.simplex.app.views.newchat import android.graphics.Bitmap -import androidx.compose.foundation.Image -import androidx.compose.runtime.Composable +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.* +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import boofcv.alg.fiducial.qrcode.QrCodeEncoder -import boofcv.alg.fiducial.qrcode.QrCodeGeneratorImage +import androidx.core.graphics.applyCanvas +import boofcv.alg.fiducial.qrcode.* import boofcv.android.ConvertBitmap import chat.simplex.app.R import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.* +import kotlinx.coroutines.launch @Composable -fun QRCode(connReq: String, modifier: Modifier = Modifier) { - Image( - bitmap = qrCodeBitmap(connReq, 1024).asImageBitmap(), - contentDescription = stringResource(R.string.image_descr_qr_code), - modifier = modifier - ) +fun QRCode(connReq: String, modifier: Modifier = Modifier, withLogo: Boolean = true, tintColor: Color = Color(0xff062d56)) { + val view = LocalView.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + var rect by remember { mutableStateOf(null) } + BoxWithConstraints(Modifier + .onGloballyPositioned { + rect = it.boundsInRoot() + } + .clickable { + scope.launch { + val r = rect + if (r != null) { + val image = Bitmap.createBitmap(r.width.toInt(), r.height.toInt(), Bitmap.Config.ARGB_8888).applyCanvas { + translate(-r.left, -r.top) + view.draw(this) + } + val file = saveTempImageUncompressed(image, false) + if (file != null) { + shareFile(context, "", file.absolutePath) + } + } + } + }, + contentAlignment = Alignment.Center + ) { + Image( + bitmap = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb()).asImageBitmap(), + contentDescription = stringResource(R.string.image_descr_qr_code), + modifier = modifier + ) + if (withLogo) { + Box( + Modifier + .size(maxWidth * 0.16f) + .background(Color.White, RoundedCornerShape(100)) + ) + Image( + painterResource(R.mipmap.icon_foreground), + null, + Modifier + .size(maxWidth * 0.24f) + ) + } + } } fun qrCodeBitmap(content: String, size: Int): Bitmap { - val qrCode = QrCodeEncoder().addAutomatic(content).fixate() + val qrCode = QrCodeEncoder().addAutomatic(content).setError(QrCode.ErrorLevel.L).fixate() val renderer = QrCodeGeneratorImage(5) renderer.render(qrCode) return ConvertBitmap.grayToBitmap(renderer.gray, Bitmap.Config.RGB_565) } +fun Bitmap.replaceColor(from: Int, to: Int): Bitmap { + val pixels = IntArray(width * height) + getPixels(pixels, 0, width, 0, 0, width, height) + var i = 0 + while (i < pixels.size) { + if (pixels[i] == from) { + pixels[i] = to + } + i++ + } + setPixels(pixels, 0, width, 0, 0, width, height) + return this +} + @Preview @Composable fun PreviewQRCode() { diff --git a/apps/android/app/src/main/res/xml/file_paths.xml b/apps/android/app/src/main/res/xml/file_paths.xml index 5cb7c4876d..a990405f17 100644 --- a/apps/android/app/src/main/res/xml/file_paths.xml +++ b/apps/android/app/src/main/res/xml/file_paths.xml @@ -1,3 +1,4 @@ +