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>
This commit is contained in:
Stanislav Dmitrenko
2023-02-06 11:45:40 +03:00
committed by GitHub
parent c8fae0ec43
commit 5082f5b4a4
3 changed files with 96 additions and 12 deletions
@@ -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)
@@ -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<Rect?>(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() {
@@ -1,3 +1,4 @@
<paths>
<files-path name="my_files" path="/"/>
<files-path name="app_temp" path="../app_temp"/>
</paths>