fix(shared): keep dense transfer QR codes scannable

This commit is contained in:
2026-07-15 01:10:48 +02:00
parent b602a3acb6
commit 37c364d66a
3 changed files with 59 additions and 4 deletions

View File

@@ -37,8 +37,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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 com.vnidrop.app.ui.theme.LocalVniDropColors
import org.jetbrains.compose.resources.decodeToImageBitmap import org.jetbrains.compose.resources.decodeToImageBitmap
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import qrcode.QRCode
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import vnidrop.shared.generated.resources.* import vnidrop.shared.generated.resources.*
@@ -247,7 +246,7 @@ internal fun TransferSharePanel(
val renderedBitmap by produceState(qrBitmap, ticket, qrBitmap) { val renderedBitmap by produceState(qrBitmap, ticket, qrBitmap) {
if (value == null) { if (value == null) {
value = withContext(Dispatchers.Default) { 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) } value?.let { onQrRendered(ticket, it) }
} }
@@ -259,7 +258,12 @@ internal fun TransferSharePanel(
color = Color.White, color = Color.White,
) { ) {
if (renderedQr != null) { 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 { } else {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
} }

View File

@@ -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")
}

View File

@@ -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)
}
}