taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

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 }