QrCodeManager.kt
/*
* This file is part of GNU Taler
* (C) 2020 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3, or (at your option) any later version.
*
* GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
package net.taler.lib.android
import android.graphics.Bitmap
import android.graphics.Bitmap.Config.ARGB_8888
import android.graphics.Bitmap.Config.RGB_565
import android.graphics.Canvas
import android.graphics.Color.BLACK
import android.graphics.Color.WHITE
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.core.graphics.createBitmap
import androidx.core.graphics.set
import com.google.zxing.BarcodeFormat.QR_CODE
import com.google.zxing.EncodeHintType.ERROR_CORRECTION
import com.google.zxing.EncodeHintType.MARGIN
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
enum class QrLogoSize(val size: Float) {
SMALL(0.15f),
MEDIUM(0.20f),
BIG(0.25f),
}
object QrCodeManager {
suspend fun makeQrCode(
text: String,
size: Int = 256,
margin: Int = 2,
errorCorrection: ErrorCorrectionLevel = ErrorCorrectionLevel.M,
centerLogo: Drawable? = null,
centerLogoSize: QrLogoSize? = QrLogoSize.MEDIUM,
drawBackground: Boolean? = false,
darkColor: Int = BLACK,
lightColor: Int = WHITE,
trimQuietZone: Boolean = false,
): Bitmap = withContext(Dispatchers.IO) {
val qrCodeWriter = QRCodeWriter()
val hints = mapOf(
MARGIN to margin.coerceAtLeast(0),
ERROR_CORRECTION to errorCorrection,
)
val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size, hints)
val height = bitMatrix.height
val width = bitMatrix.width
val bmp = createBitmap(width, height, RGB_565)
for (x in 0 until width) {
for (y in 0 until height) {
bmp[x, y] = if (bitMatrix.get(x, y)) darkColor else lightColor
}
}
val qrBitmap = if (trimQuietZone) trimQrQuietZone(bmp, lightColor) else bmp
return@withContext if (centerLogo != null && centerLogoSize != null && drawBackground != null) {
addCenteredLogo(qrBitmap, centerLogo, centerLogoSize, drawBackground, lightColor)
} else {
qrBitmap
}
}
private fun trimQrQuietZone(bitmap: Bitmap, lightColor: Int): Bitmap {
val width = bitmap.width
val height = bitmap.height
var minX = width
var minY = height
var maxX = -1
var maxY = -1
for (y in 0 until height) {
for (x in 0 until width) {
if (bitmap.getPixel(x, y) != lightColor) {
if (x < minX) minX = x
if (x > maxX) maxX = x
if (y < minY) minY = y
if (y > maxY) maxY = y
}
}
}
if (maxX < minX || maxY < minY) return bitmap
val croppedWidth = maxX - minX + 1
val croppedHeight = maxY - minY + 1
if (croppedWidth == width && croppedHeight == height) return bitmap
return Bitmap.createBitmap(bitmap, minX, minY, croppedWidth, croppedHeight)
}
private fun addCenteredLogo(
qrBitmap: Bitmap,
logoDrawable: Drawable,
logoSize: QrLogoSize = QrLogoSize.MEDIUM,
drawBackground: Boolean = false,
logoBackgroundColor: Int = WHITE,
): Bitmap {
val result = qrBitmap.copy(ARGB_8888, true)
val canvas = Canvas(result)
val logoBitmap = drawableToBitmap(logoDrawable)
var logoMaxWidth = (result.width * logoSize.size).toInt()
val logoAspectRatio = logoBitmap.width.toFloat() / logoBitmap.height.toFloat()
var logoWidth = logoMaxWidth
var logoHeight = (logoWidth / logoAspectRatio).toInt().coerceAtLeast(1)
var horizontalPadding = (logoHeight * 0.12f).toInt()
var verticalPadding = (logoHeight * 0.09f).toInt()
val maxOcclusionRatio = 0.11f
val currentOcclusionRatio =
((logoWidth + horizontalPadding * 2f) * (logoHeight + verticalPadding * 2f)) /
(result.width.toFloat() * result.height.toFloat())
if (currentOcclusionRatio > maxOcclusionRatio) {
val scale = kotlin.math.sqrt(maxOcclusionRatio / currentOcclusionRatio)
logoMaxWidth = (logoMaxWidth * scale).toInt().coerceAtLeast(1)
logoWidth = logoMaxWidth
logoHeight = (logoWidth / logoAspectRatio).toInt().coerceAtLeast(1)
horizontalPadding = (horizontalPadding * scale).toInt()
verticalPadding = (verticalPadding * scale).toInt()
}
val centerX = result.width / 2
val centerY = result.height / 2
if (drawBackground) {
val halfBackgroundWidth = (logoWidth / 2f) + horizontalPadding
val halfBackgroundHeight = (logoHeight / 2f) + verticalPadding
val backgroundRect = RectF(
centerX - halfBackgroundWidth,
centerY - halfBackgroundHeight,
centerX + halfBackgroundWidth,
centerY + halfBackgroundHeight,
)
val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = logoBackgroundColor
}
val cornerRadius =
halfBackgroundHeight // * 0.8f taler has circle in logo, so it can be fine
canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint)
}
val destinationRect = Rect(
centerX - logoWidth / 2,
centerY - logoHeight / 2,
centerX + logoWidth / 2,
centerY + logoHeight / 2,
)
canvas.drawBitmap(logoBitmap, null, destinationRect, Paint(Paint.ANTI_ALIAS_FLAG))
return result
}
private fun drawableToBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable && drawable.bitmap != null) {
return drawable.bitmap
}
val width = drawable.intrinsicWidth.coerceAtLeast(1)
val height = drawable.intrinsicHeight.coerceAtLeast(1)
val bitmap = createBitmap(width, height)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
}