g0dkar / qrcode-kotlin

QRCode Generator implemented in pure Kotlin
https://qrcodekotlin.com/
MIT License
166 stars 14 forks source link

[Android] Blank QR code squares with compose #88

Open LossyDragon opened 8 months ago

LossyDragon commented 8 months ago

Describe the bug 4.0.2-3

I'm updating a hobby project from v3.3.0 to 4.0.+ and it seems I am unable to render a QR code through Jetpack compose.

I am using landscapist as my image loading library and QR codes were working fine throughout v3, but now they are just blank solid colored squares moving to v4

I have tried using render(), render("bmp"), and renderToGraphics().nativeImage() to no avail. nativeImage() was my working way last major version.

render() seems to actually produce an inner square but, and rounded only shows rounded corners along the right side.

Correction: Top left corner is square while the other corners show a rounded corner

To Reproduce Steps to reproduce the behavior. For example:

  1. Create a android sample project with compose
  2. Implement landscapist and qrcode-kotlin
  3. Create a sample screen with some test cases
  4. See error

Expected behavior Rendered QR codes using Jetpack compose. Maybe a quick sample showing it off would help newcomers too!

Screenshots or other QRCodes rendered with other tools

qr1 qr2

Additional context

Quick sample code

                  LazyColumn(
                        modifier = Modifier.fillMaxSize(),
                        verticalArrangement = Arrangement.Top,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        item {
                            Text(text = "QR Examples!")
                            val qr1 = QRCode.ofSquares()
                                .withColor(Color.BLUE)
                                .withBackgroundColor(Color.RED)
                                .build("Hello QR 1!")
                            CoilImage(imageModel = { qr1.render() })
                            Spacer(modifier = Modifier.height(6.dp))
                        }

                        item {
                            val qr2 = QRCode.ofCircles()
                                .withColor(Color.BLUE)
                                .withBackgroundColor(Color.RED)
                                .build("Hello QR 2!")
                            CoilImage(imageModel = { qr2.render("bmp") })
                            Spacer(modifier = Modifier.height(6.dp))
                        }

                        item {
                            val qr3 = QRCode.ofCircles()
                                .withColor(Color.BLUE)
                                .withBackgroundColor(Color.RED)
                                .build("Hello QR 3!")
                            CoilImage(imageModel = { qr3.renderToGraphics().nativeImage() })
                            Spacer(modifier = Modifier.height(6.dp))
                        }
                    }
rafaellins-swile commented 8 months ago

Heya! Thanks for bringing up this issue! I'll give it a try later today and I'll get back to you 😄

Might be something due to the optimization implemented.

Also, I'm working on implementing examples with all platforms of the library, but I'll definitely add a Jetpack Compose one! Thanks for suggesting it ^^

barry-irvine commented 8 months ago

I think this is also related to our issue. We had:

  Image(
            modifier = Modifier.padding(8.dp),
            bitmap = (QRCode(barcode).render().nativeImage() as Bitmap).asImageBitmap(),
            alpha = if (expired) 0.38f else 1.0f,
            contentDescription = stringResource(R.string.alt_qr_code)
        )

And this worked fine in v3.x.x but with v4.0.2 and the QRCodeProcessor instead of QRCode we now just get a black square with no white at all.

g0dkar commented 8 months ago

I see! That really seems like it is related, I'll try this out right now

g0dkar commented 8 months ago

Heya, I pinpointed the issue. I'm not 100% sure of what caused it, but it is because the Ints of the colors are kind of being compiled wrong on Android. Color.WHITE for example goes from 0xFFFFFFFF to -1 which makes it black... and basically the same for all other colors 🤔

Still figuring out how to fix it x_x

g0dkar commented 8 months ago

Might not be that, btw... Anyway, I'm still trying to figure out what is going on O_o

g0dkar commented 8 months ago

🤦‍♂️ found the issue, preparing the next version with the fix

In short, either I'm dumb or Android API is. Thing is, rectangles are described via:

(x1, y1) of the top-left corner and (x2, y2) of the bottom right corner. I assumed, as all other platforms works like this, that they were x, y, width, height...

This is what causes this weird "everything is black" state.

Edit: Now that the library draws all squares directly into the Canvas, this was what caused the issue: any black rectangles would become something like "draw from 1000,1000 to -20,-20"

g0dkar commented 8 months ago

Also, I'll make a few adjustments to the compatibility API for the use-case @barry-irvine shared :)

Right now, since the innerSpace is being defaulted to 1, the QRCodes might end up looking like this:

image

g0dkar commented 8 months ago

This should fix both reported issues. I'll keep this issue open since I didn't implement Jetpack Compose examples yet 😅

barry-irvine commented 8 months ago

@g0dkar I can confirm that the QR code now looks good in Compose (for my scenario at least)

drbunbury commented 3 months ago
Screenshot 2024-04-04 at 12 45 07

I am not sure if this is a related issue (or even a feature!) either way there is no timing pattern on the generated QR codes- is this a feature of a bug?! Trying to optimise readability on larger & animated codes. Thank you. Great library.

g0dkar commented 3 months ago

Heya! I was having little time to work on the lib lately, but I'm coming back to it this month :)

I'll check the timing pattern and some other reported bugs today and the following days.

And animated QRCodes sound like an awesome idea hahaha, I'll look into it 😬

drbunbury commented 3 months ago

Thank you very much! It turns out the reason for the poor reading rate was the size of the QR code- when I limited the number of alphanumeric characters to about 55 the smaller QR code resulted in a better overall outcome. The larger ones overwhelmed the ability to hold & process multiple images so kept dropping them, requiring the loop of QR codes to cycle round again until the missed QRs were read.

So the timing pattern did not seem to have any impact on the readability of the QR code.

Code to display 'animated' QR code below:

@Composable
fun QRCodeSlideshow(
    modifier: Modifier = Modifier,
    qrContents: String,
) {
    var currentBitmap by remember { mutableStateOf<Bitmap?>(null) }
    var currentIndex by remember { mutableStateOf(0) }
    val qrParts = divideStringIntoEqualParts(qrContents, 50) 
    // Divide the string into parts: 50 empirically optimal with a 100ms display duration

    LaunchedEffect(key1 = "QRSlideshow") {
        if (qrParts.isNotEmpty()) { // Only proceed if qrParts is not empty
            while (true) {
                val content = qrParts[currentIndex]
                // Generate the QR code bitmap. 
                currentBitmap = QRCodeGenerator.generateQRCodeBitmap("ur:bytes/${currentIndex+1}-5/$content")

                // Show the QR code for at least 100ms
                delay(100)

                // Move to the next QR code content
                currentIndex = (currentIndex + 1) % qrParts.size
            }
        }
    }

    Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
        if (qrParts.isEmpty()) {
            Text(
                text = "No data to transfer",
                modifier = Modifier.fillMaxWidth(),
                textAlign = TextAlign.Center
            )
        } else {
            currentBitmap?.let {
                Image(bitmap = it.asImageBitmap(), contentDescription = "QR Code")
            }
            Text(
                text = "${currentIndex + 1} / ${qrParts.size}",
                modifier = Modifier.fillMaxWidth(),
                textAlign = TextAlign.Center
            )
        }
    }
}

fun divideStringIntoEqualParts(inputString: String, maxLength: Int): List<String> {
    // Check if the maxLength is less than or equal to zero to avoid division by zero or infinite loops
    if (maxLength <= 0) throw IllegalArgumentException("Maximum length must be greater than zero")

    val result = mutableListOf<String>()

    // Iterate over the string, taking substrings of length `maxLength` at a time
    var start = 0
    while (start < inputString.length) {
        // Calculate end index. Ensure it does not exceed the string's length
        val end = minOf(start + maxLength, inputString.length)
        // Add the substring to the result list
        result.add(inputString.substring(start, end))
        // Move the start index forward
        start += maxLength
    }

    return result
}

object QRCodeGenerator {

    fun generateQRCodeBitmap(content: String): Bitmap {

        val QRtoReturn = QRCode.ofSquares()
            .withColor(Colors.BLACK)
            .withSize(25).build(content).render()

        return QRtoReturn.nativeImage() as Bitmap
    }
}