QrCodeManager.kt (7148B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2020 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.lib.android 18 19 import android.graphics.Bitmap 20 import android.graphics.Bitmap.Config.ARGB_8888 21 import android.graphics.Bitmap.Config.RGB_565 22 import android.graphics.Canvas 23 import android.graphics.Color.BLACK 24 import android.graphics.Color.WHITE 25 import android.graphics.Paint 26 import android.graphics.Rect 27 import android.graphics.RectF 28 import android.graphics.drawable.BitmapDrawable 29 import android.graphics.drawable.Drawable 30 import androidx.core.graphics.createBitmap 31 import androidx.core.graphics.set 32 import com.google.zxing.BarcodeFormat.QR_CODE 33 import com.google.zxing.EncodeHintType.ERROR_CORRECTION 34 import com.google.zxing.EncodeHintType.MARGIN 35 import com.google.zxing.qrcode.QRCodeWriter 36 import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel 37 import kotlinx.coroutines.Dispatchers 38 import kotlinx.coroutines.withContext 39 40 enum class QrLogoSize(val size: Float) { 41 SMALL(0.15f), 42 MEDIUM(0.20f), 43 BIG(0.25f), 44 } 45 46 object QrCodeManager { 47 48 suspend fun makeQrCode( 49 text: String, 50 size: Int = 256, 51 margin: Int = 2, 52 errorCorrection: ErrorCorrectionLevel = ErrorCorrectionLevel.M, 53 centerLogo: Drawable? = null, 54 centerLogoSize: QrLogoSize? = QrLogoSize.MEDIUM, 55 drawBackground: Boolean? = false, 56 darkColor: Int = BLACK, 57 lightColor: Int = WHITE, 58 trimQuietZone: Boolean = false, 59 ): Bitmap = withContext(Dispatchers.IO) { 60 val qrCodeWriter = QRCodeWriter() 61 val hints = mapOf( 62 MARGIN to margin.coerceAtLeast(0), 63 ERROR_CORRECTION to errorCorrection, 64 ) 65 val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size, hints) 66 val height = bitMatrix.height 67 val width = bitMatrix.width 68 val bmp = createBitmap(width, height, RGB_565) 69 for (x in 0 until width) { 70 for (y in 0 until height) { 71 bmp[x, y] = if (bitMatrix.get(x, y)) darkColor else lightColor 72 } 73 } 74 75 val qrBitmap = if (trimQuietZone) trimQrQuietZone(bmp, lightColor) else bmp 76 77 return@withContext if (centerLogo != null && centerLogoSize != null && drawBackground != null) { 78 addCenteredLogo(qrBitmap, centerLogo, centerLogoSize, drawBackground, lightColor) 79 } else { 80 qrBitmap 81 } 82 } 83 84 private fun trimQrQuietZone(bitmap: Bitmap, lightColor: Int): Bitmap { 85 val width = bitmap.width 86 val height = bitmap.height 87 var minX = width 88 var minY = height 89 var maxX = -1 90 var maxY = -1 91 92 for (y in 0 until height) { 93 for (x in 0 until width) { 94 if (bitmap.getPixel(x, y) != lightColor) { 95 if (x < minX) minX = x 96 if (x > maxX) maxX = x 97 if (y < minY) minY = y 98 if (y > maxY) maxY = y 99 } 100 } 101 } 102 103 if (maxX < minX || maxY < minY) return bitmap 104 val croppedWidth = maxX - minX + 1 105 val croppedHeight = maxY - minY + 1 106 if (croppedWidth == width && croppedHeight == height) return bitmap 107 return Bitmap.createBitmap(bitmap, minX, minY, croppedWidth, croppedHeight) 108 } 109 110 private fun addCenteredLogo( 111 qrBitmap: Bitmap, 112 logoDrawable: Drawable, 113 logoSize: QrLogoSize = QrLogoSize.MEDIUM, 114 drawBackground: Boolean = false, 115 logoBackgroundColor: Int = WHITE, 116 ): Bitmap { 117 val result = qrBitmap.copy(ARGB_8888, true) 118 val canvas = Canvas(result) 119 val logoBitmap = drawableToBitmap(logoDrawable) 120 121 var logoMaxWidth = (result.width * logoSize.size).toInt() 122 val logoAspectRatio = logoBitmap.width.toFloat() / logoBitmap.height.toFloat() 123 var logoWidth = logoMaxWidth 124 var logoHeight = (logoWidth / logoAspectRatio).toInt().coerceAtLeast(1) 125 var horizontalPadding = (logoHeight * 0.12f).toInt() 126 var verticalPadding = (logoHeight * 0.09f).toInt() 127 128 val maxOcclusionRatio = 0.11f 129 val currentOcclusionRatio = 130 ((logoWidth + horizontalPadding * 2f) * (logoHeight + verticalPadding * 2f)) / 131 (result.width.toFloat() * result.height.toFloat()) 132 if (currentOcclusionRatio > maxOcclusionRatio) { 133 val scale = kotlin.math.sqrt(maxOcclusionRatio / currentOcclusionRatio) 134 logoMaxWidth = (logoMaxWidth * scale).toInt().coerceAtLeast(1) 135 logoWidth = logoMaxWidth 136 logoHeight = (logoWidth / logoAspectRatio).toInt().coerceAtLeast(1) 137 horizontalPadding = (horizontalPadding * scale).toInt() 138 verticalPadding = (verticalPadding * scale).toInt() 139 } 140 141 val centerX = result.width / 2 142 val centerY = result.height / 2 143 144 if (drawBackground) { 145 val halfBackgroundWidth = (logoWidth / 2f) + horizontalPadding 146 val halfBackgroundHeight = (logoHeight / 2f) + verticalPadding 147 val backgroundRect = RectF( 148 centerX - halfBackgroundWidth, 149 centerY - halfBackgroundHeight, 150 centerX + halfBackgroundWidth, 151 centerY + halfBackgroundHeight, 152 ) 153 154 val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { 155 style = Paint.Style.FILL 156 color = logoBackgroundColor 157 } 158 val cornerRadius = 159 halfBackgroundHeight // * 0.8f taler has circle in logo, so it can be fine 160 canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint) 161 } 162 163 val destinationRect = Rect( 164 centerX - logoWidth / 2, 165 centerY - logoHeight / 2, 166 centerX + logoWidth / 2, 167 centerY + logoHeight / 2, 168 ) 169 canvas.drawBitmap(logoBitmap, null, destinationRect, Paint(Paint.ANTI_ALIAS_FLAG)) 170 return result 171 } 172 173 private fun drawableToBitmap(drawable: Drawable): Bitmap { 174 if (drawable is BitmapDrawable && drawable.bitmap != null) { 175 return drawable.bitmap 176 } 177 val width = drawable.intrinsicWidth.coerceAtLeast(1) 178 val height = drawable.intrinsicHeight.coerceAtLeast(1) 179 val bitmap = createBitmap(width, height) 180 val canvas = Canvas(bitmap) 181 drawable.setBounds(0, 0, canvas.width, canvas.height) 182 drawable.draw(canvas) 183 return bitmap 184 } 185 }