mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
fix(shared): keep dense transfer QR codes scannable
This commit is contained in:
@@ -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() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user