From 37c364d66a2d865a949f248de8d034b71c0f2a80 Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Wed, 15 Jul 2026 01:10:48 +0200 Subject: [PATCH] fix(shared): keep dense transfer QR codes scannable --- .../app/feature/send/TransferDetails.kt | 12 +++++--- .../app/feature/send/TransferQrCode.kt | 29 +++++++++++++++++++ .../app/feature/send/TransferQrCodeTest.kt | 22 ++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferQrCode.kt create mode 100644 shared/src/commonTest/kotlin/com/vnidrop/app/feature/send/TransferQrCodeTest.kt diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt index 4468fc1..1cbaeee 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt @@ -37,8 +37,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -59,7 +59,6 @@ import com.vnidrop.app.ui.state.progressForReceiver import com.vnidrop.app.ui.theme.LocalVniDropColors import org.jetbrains.compose.resources.decodeToImageBitmap import org.jetbrains.compose.resources.stringResource -import qrcode.QRCode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import vnidrop.shared.generated.resources.* @@ -247,7 +246,7 @@ internal fun TransferSharePanel( val renderedBitmap by produceState(qrBitmap, ticket, qrBitmap) { if (value == null) { value = withContext(Dispatchers.Default) { - runCatching { QRCode.ofSquares().withSize(8).build(ticket).renderToBytes().decodeToImageBitmap() }.getOrNull() + runCatching { buildTransferQrCode(ticket).renderToBytes().decodeToImageBitmap() }.getOrNull() } value?.let { onQrRendered(ticket, it) } } @@ -259,7 +258,12 @@ internal fun TransferSharePanel( color = Color.White, ) { if (renderedQr != null) { - Image(renderedQr, null, Modifier.padding(14.dp).fillMaxSize().clip(RoundedCornerShape(8.dp))) + Image( + bitmap = renderedQr, + contentDescription = null, + modifier = Modifier.padding(14.dp).fillMaxSize(), + filterQuality = FilterQuality.None, + ) } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferQrCode.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferQrCode.kt new file mode 100644 index 0000000..e5466b0 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferQrCode.kt @@ -0,0 +1,29 @@ +package com.vnidrop.app.feature.send + +import qrcode.QRCode +import qrcode.raw.ErrorCorrectionLevel + +private const val QrCellSize = 8 +private const val QrQuietZoneModules = 4 +// qrcode-kotlin 4.5.0 falls back to version 40 above version 20's byte capacity. +private val QrLowErrorCorrectionByteCapacities = intArrayOf( + 17, 32, 53, 78, 106, 134, 154, 192, 230, 271, + 321, 367, 425, 458, 520, 586, 644, 718, 792, 858, + 929, 1003, 1091, 1171, 1273, 1367, 1465, 1528, 1628, 1732, + 1840, 1952, 2068, 2188, 2303, 2431, 2563, 2699, 2809, 2953, +) + +internal fun buildTransferQrCode(ticket: String): QRCode = + QRCode.ofSquares() + .withSize(QrCellSize) + .withInnerSpacing(0) + .withMargin(QrQuietZoneModules * QrCellSize) + .withErrorCorrectionLevel(ErrorCorrectionLevel.LOW) + .withInformationDensity(transferQrInformationDensity(ticket)) + .build(ticket) + +internal fun transferQrInformationDensity(ticket: String): Int { + val byteCount = ticket.encodeToByteArray().size + val index = QrLowErrorCorrectionByteCapacities.indexOfFirst { byteCount <= it } + return if (index >= 0) index + 1 else throw IllegalArgumentException("The invitation is too large for a QR code") +} diff --git a/shared/src/commonTest/kotlin/com/vnidrop/app/feature/send/TransferQrCodeTest.kt b/shared/src/commonTest/kotlin/com/vnidrop/app/feature/send/TransferQrCodeTest.kt new file mode 100644 index 0000000..d90620a --- /dev/null +++ b/shared/src/commonTest/kotlin/com/vnidrop/app/feature/send/TransferQrCodeTest.kt @@ -0,0 +1,22 @@ +package com.vnidrop.app.feature.send + +import kotlin.test.Test +import kotlin.test.assertEquals + +class TransferQrCodeTest { + @Test + fun ticketPastVersionTwentyCapacityUsesNextAvailableVersion() { + assertEquals(20, transferQrInformationDensity("a".repeat(858))) + assertEquals(21, transferQrInformationDensity("a".repeat(859))) + assertEquals(21, transferQrInformationDensity("a".repeat(876))) + } + + @Test + fun renderedQrUsesExpectedVersionSizeAndQuietZone() { + val qrCode = buildTransferQrCode("a".repeat(876)) + + assertEquals(21, qrCode.informationDensity) + assertEquals(101, qrCode.rawData.size) + assertEquals(872, qrCode.canvasSize) + } +}