ZXing, Android, JetPack Compose - Draw Bounding Box Around QR Code

228 views Asked by At

I am building a QR code scanner using Jetpack Compose in Android. The scanning part and reading the bit values in the QR code work great and as expected. However, as a part of my prototype, I want to be able to draw a red box around the QR code.

Currently, I am dealing with two issues. The height of the red box appears to be correct, but the width is too small and I am not sure why. The second problem is that as I move my phone, the bounding box appears to go all over the place and not locked on the actual QR code. I'm not sure how to fix this.

The bounding box doesn't seem to be the right size and it does not align with the QR code unless the angle is perfect.

Here is my CameraView composable function...

@Composable
fun CameraPreview() {
    val context = LocalContext.current
    val scanner = MultiFormatReader().apply {
        val hints: MutableMap<DecodeHintType, Any> = EnumMap(DecodeHintType::class.java)
        hints[DecodeHintType.POSSIBLE_FORMATS] = listOf(BarcodeFormat.QR_CODE)
        setHints(hints)
    }

    val qrCodeBounds = remember { mutableStateOf<Rect?>(null) }

    BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
        val screenWidth = maxWidth
        val scanBoxSize = screenWidth * 0.6f // adjust the size of the scanning area here

        AndroidView(
            factory = { ctx ->
                val previewView = PreviewView(ctx)
                val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
                cameraProviderFuture.addListener({
                    val cameraProvider = cameraProviderFuture.get()
                    val preview = Preview.Builder().build().also {
                        it.setSurfaceProvider(previewView.surfaceProvider)
                    }
                    val imageAnalysis = ImageAnalysis.Builder()
                        .build()
                        .also {
                            it.setAnalyzer(ContextCompat.getMainExecutor(ctx)) { imageProxy ->
                                val result = scanQRCode(imageProxy, scanner)
                                imageProxy.close()
                                if (result != null) {
                                    println("QR Code found: ${result.text}")
                                    println("Image Proxy Width: ${imageProxy.width}, Height: ${imageProxy.height}")
                                    qrCodeBounds.value = getBoundingBox(result.resultPoints, imageProxy.width, imageProxy.height, previewView.width, previewView.height)
                                } else {
                                    qrCodeBounds.value = null
                                }
                            }
                        }

                    try {
                        cameraProvider.unbindAll()
                        cameraProvider.bindToLifecycle(
                            context as ComponentActivity,
                            CameraSelector.DEFAULT_BACK_CAMERA,
                            preview,
                            imageAnalysis
                        )
                    } catch (exc: Exception) {
                        // Handle exceptions
                    }
                }, ContextCompat.getMainExecutor(ctx))
                previewView
            },
            modifier = Modifier.fillMaxSize()
        )

        Box(modifier = Modifier
            .matchParentSize()
            .background(Color.Black.copy(alpha = 0.8f))
        )

        // Scanning area box with a clear cutout
        Box(
            modifier = Modifier
                .size(scanBoxSize)
                .align(Alignment.Center)
                .drawBehind {
                    // Draw a rounded clear rectangle to create a cutout effect
                    drawRoundRect(
                        color = Color.Transparent,
                        topLeft = Offset(0f, 0f),
                        size = Size(size.width, size.height),
                        cornerRadius = CornerRadius(x = 12.dp.toPx(), y = 12.dp.toPx()),
                        blendMode = BlendMode.Clear
                    )
                }
                .border(2.dp, MaterialTheme.colorScheme.onPrimary, RoundedCornerShape(12.dp))
        )

        qrCodeBounds.value?.let { bounds ->
            Canvas(modifier = Modifier.fillMaxSize()) {
                drawRect(
                    color = Color.Red,
                    topLeft = Offset(bounds.left.toFloat(), bounds.top.toFloat()),
                    size = Size(bounds.width().toFloat(), bounds.height().toFloat()),
                    style = Stroke(width = 3.dp.toPx())
                )
            }
        }
    }
}

This is my getBoundingBox function

private fun getBoundingBox(resultPoints: Array<ResultPoint>?, imageWidth: Int, imageHeight: Int, previewWidth: Int, previewHeight: Int): Rect? {
    if (resultPoints == null || resultPoints.size != 4) {
        return null
    }

    // Calculate scale factors
    val scaleX = previewWidth.toFloat() / imageWidth
    val scaleY = previewHeight.toFloat() / imageHeight

    // Apply scale factors to the coordinates
    val left = (resultPoints[0].x * scaleX).toInt()
    val top = (resultPoints[0].y * scaleY).toInt()
    val right = (resultPoints[2].x * scaleX).toInt()
    val bottom = (resultPoints[2].y * scaleY).toInt()

    return Rect(left, top, right, bottom)
}

And this is my scanQRCode function

private fun scanQRCode(imageProxy: ImageProxy, scanner: MultiFormatReader): Result? {
    val data = imageProxy.planes[0].buffer.let { buffer ->
        val data = ByteArray(buffer.capacity())
        buffer.get(data)
        buffer.clear()
        data
    }
    val source = PlanarYUVLuminanceSource(
        data,
        imageProxy.width,
        imageProxy.height,
        0,
        0,
        imageProxy.width,
        imageProxy.height,
        false
    )
    val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
    return try {
        scanner.decodeWithState(binaryBitmap)
    } catch (e: Exception) {
        null // QR Code not found
    }
}
0

There are 0 answers