diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index a4562f4c..844b42e1 100644 --- a/Android/app/build.gradle.kts +++ b/Android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.archipelago.app" minSdk = 26 targetSdk = 35 - versionCode = 17 - versionName = "0.4.13" + versionCode = 18 + versionName = "0.4.14" vectorDrawables { useSupportLibrary = true diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/QrScannerOverlay.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/QrScannerOverlay.kt index 2c9eaacc..96b57e53 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/components/QrScannerOverlay.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/components/QrScannerOverlay.kt @@ -239,7 +239,12 @@ private fun CameraQrPreview(onDecoded: (String) -> Unit) { val preview = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } + // CameraX's analysis default is 640x480 — too few pixels per module + // to decode a modal-sized QR at arm's length. 1280x720 more than + // doubles the pixel density at negligible analysis cost. + @Suppress("DEPRECATION") val analysis = ImageAnalysis.Builder() + .setTargetResolution(android.util.Size(1280, 720)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { @@ -268,7 +273,14 @@ private fun CameraQrPreview(onDecoded: (String) -> Unit) { /** ZXing-based QR decoder over the camera's Y (luminance) plane. */ private class QrCodeAnalyzer(private val onDecoded: (String) -> Unit) : ImageAnalysis.Analyzer { private val reader = MultiFormatReader().apply { - setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE))) + setHints( + mapOf( + DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE), + // Screen-displayed QRs come with moiré, glare, and soft focus at + // close range — the exhaustive search is worth the milliseconds. + DecodeHintType.TRY_HARDER to true, + ) + ) } override fun analyze(image: ImageProxy) { @@ -284,7 +296,13 @@ private class QrCodeAnalyzer(private val onDecoded: (String) -> Unit) : ImageAna 0, 0, image.width, image.height, false, ) - val result = reader.decodeWithState(BinaryBitmap(HybridBinarizer(source))) + val result = try { + reader.decodeWithState(BinaryBitmap(HybridBinarizer(source))) + } catch (_: NotFoundException) { + // Dark-themed pages can render light-on-dark QRs — retry inverted. + reader.reset() + reader.decodeWithState(BinaryBitmap(HybridBinarizer(source.invert()))) + } onDecoded(result.text) } catch (_: NotFoundException) { // No QR in this frame — keep scanning. diff --git a/neode-ui/public/packages/archipelago-companion.apk b/neode-ui/public/packages/archipelago-companion.apk index 20a05312..92b4ab56 100644 Binary files a/neode-ui/public/packages/archipelago-companion.apk and b/neode-ui/public/packages/archipelago-companion.apk differ diff --git a/neode-ui/src/components/CompanionIntroOverlay.vue b/neode-ui/src/components/CompanionIntroOverlay.vue index b059c14a..da2e9337 100644 --- a/neode-ui/src/components/CompanionIntroOverlay.vue +++ b/neode-ui/src/components/CompanionIntroOverlay.vue @@ -93,7 +93,9 @@
-
+ +
{ watch(visible, async (isVisible) => { if (!isVisible) return + // Generate large and let CSS scale down — at 112px source a ~45-module QR + // is 2.5px/module, which camera decoders (the companion app included) + // routinely fail on. 512px keeps every module crisp. qrDataUrl.value = await QRCode.toDataURL(companionDownloadUrl, { - width: 112, - margin: 1, + width: 512, + margin: 2, errorCorrectionLevel: 'M', color: { dark: '#111111', @@ -261,9 +266,11 @@ async function showPairScreen() { step.value = 'pair' if (!pairQrDataUrl.value) { pairingUrl.value = await buildPairingUrl() + // Large source + a real quiet zone; this QR is scanned by the companion + // app's camera, so give it every advantage (see download QR note above). pairQrDataUrl.value = await QRCode.toDataURL(pairingUrl.value, { - width: 112, - margin: 1, + width: 512, + margin: 3, errorCorrectionLevel: 'M', color: { dark: '#111111',