A pure Kotlin library for QR code reading/decoding. Port of Paul Miller's paulmillr/qr library.
No external dependencies.
- Decode QR codes from raw pixel data (Grayscale, RGB, or RGBA)
- Optimized for camera frames - pass Y plane directly, skip color conversion
- Supports QR versions 1-40
- All error correction levels (L, M, Q, H)
- Numeric, Alphanumeric, and Byte encoding modes
- ECI (Extended Channel Interpretation) - Supports 20+ character encodings (UTF-8, ISO-8859-*, Shift-JIS, Big5, GBK, etc.)
- Inverted QR code detection - Automatically detects and decodes white-on-black QR codes
- Automatic perspective correction
- Reed-Solomon error correction
Add JitPack repository to your settings.gradle.kts:
dependencyResolutionManagement {
repositories {
maven("https://jitpack.io")
}
}Then add the dependency:
dependencies {
implementation("com.github.limpbrains:qr:TAG")
}dependencies {
implementation 'com.github.limpbrains:qr:TAG'
}Replace TAG with the latest release tag (e.g., v0.0.2).
import qr.QRDecoder
import qr.Image
// From raw pixel data (auto-detects format based on size)
val width = 640
val height = 480
// Grayscale (1 byte per pixel) - fastest, ideal for camera Y plane
val grayscaleData: ByteArray = // ... width * height bytes
val decoded = QRDecoder.decode(width, height, grayscaleData)
// RGB (3 bytes per pixel)
val rgbData: ByteArray = // ... width * height * 3 bytes
val decoded2 = QRDecoder.decode(width, height, rgbData)
// RGBA (4 bytes per pixel)
val rgbaData: ByteArray = // ... width * height * 4 bytes
val decoded3 = QRDecoder.decode(width, height, rgbaData)For optimal performance with Android CameraX, extract the Y (luminance) plane directly. The Y plane is already grayscale - no color conversion needed:
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import qr.QRDecoder
import qr.QRDecodingException
class QRCodeAnalyzer(
private val onQRCodeDetected: (String) -> Unit
) : ImageAnalysis.Analyzer {
override fun analyze(image: ImageProxy) {
try {
val grayscaleData = extractYPlane(image)
val decoded = QRDecoder.decode(image.width, image.height, grayscaleData)
onQRCodeDetected(decoded)
} catch (e: QRDecodingException) {
// No QR code found - expected for most frames
} finally {
image.close()
}
}
private fun extractYPlane(image: ImageProxy): ByteArray {
val yPlane = image.planes[0]
val yBuffer = yPlane.buffer
val rowStride = yPlane.rowStride
val width = image.width
val height = image.height
val yBytes = ByteArray(width * height)
if (rowStride == width) {
// Fast path: no padding
yBuffer.rewind()
yBuffer.get(yBytes, 0, width * height)
} else {
// Handle row stride padding
yBuffer.rewind()
for (row in 0 until height) {
yBuffer.position(row * rowStride)
yBuffer.get(yBytes, row * width, width)
}
}
return yBytes
}
}This approach is 3-5x faster than converting YUV to RGB first, as it skips:
- U/V plane extraction
- YUV to RGB color conversion (6 float multiplications per pixel)
- RGB to grayscale conversion
import qr.*
try {
val result = QRDecoder.decode(image)
} catch (e: ImageTooSmallException) {
println("Image is too small for QR detection")
} catch (e: FinderNotFoundException) {
println("No QR code finder patterns detected")
} catch (e: InvalidFormatException) {
println("Invalid QR code format")
} catch (e: QRDecodingException) {
println("Failed to decode QR code: ${e.message}")
}- JDK 17 or higher
git submodule update --init --recursive
# Build
./gradlew build
# Run tests
./gradlew test
# Publish to local Maven repository
./gradlew publishToMavenLocalsrc/main/kotlin/qr/
├── QRDecoder.kt # Main public API (entry point)
├── Image.kt # Image data class
├── Types.kt # Point, Pattern, enums, exceptions
├── Bitmap.kt # 2D bitmap class
├── PatternDetector.kt # Finder/alignment pattern detection
├── Transform.kt # Perspective transformation
├── BitDecoder.kt # Data extraction
├── QRInfo.kt # QR constants/tables
├── GaloisField.kt # GF(256) math
├── ReedSolomon.kt # Error correction
└── Interleave.kt # Block interleaving
src/test/kotlin/qr/
└── *.kt # Unit and integration tests
test/vectors/ # Test vectors (git submodule)
This Kotlin library is a port of the JavaScript paulmillr/qr library with the following differences:
Before stopping early during finder pattern detection, the Kotlin implementation validates that the 3 found patterns form a valid right isosceles triangle (ratio of two shorter sides > 0.8). This prevents false positives when QR data accidentally contains patterns matching the 1:1:3:1:1 finder ratio.
Impact: +83 synthetic QR codes decoded correctly, -2 JPEG images.
JS (jpeg-js) and Kotlin (ImageIO) JPEG decoders produce slightly different pixel values (1-2 pixel brightness difference). To compensate, QRDecoder retries with threshold offsets [0, -5, 5].
To adjust, modify THRESHOLD_OFFSETS in QRDecoder.kt.
Same as the JS library - Kanji and ECI encoding modes are not supported.
Tested against paulmillr/qr-code-vectors:
| Test Suite | Pass Rate | Notes |
|---|---|---|
| Small vectors (synthetic ASCII art QR) | 9134/9281 (98.42%) | ✓ Matches JavaScript decoder baseline |
| JPEG images (boofcv-v3) | 113/118 (95.76%) | Real-world test images |
Note: The 98.42% baseline has been validated against the JavaScript decoder. The 147 failing tests also fail in JavaScript with identical errors, confirming this is the correct baseline for both implementations.
- Original JavaScript library: paulmillr/qr by Paul Miller
- Test vectors: paulmillr/qr-code-vectors
- For React Native, use react-native-camera-kit-no-google for high-performance camera scanning.
Copyright (c) 2025 limpbrains
Copyright (c) 2023 Paul Miller (paulmillr.com)
Copyright (c) 2019 ZXing authors
The library is dual-licensed under the Apache 2.0 OR MIT license. You can select a license of your choice.
The library is derived from ZXing, which is licensed under Apache 2.0.
QR Code is trademarked by DENSO WAVE INCORPORATED. The word "QR Code" used in this project is for identification purposes only.
The specification is not covered by any known patents: QR Code was patented, but Denso Wave chose not to exercise those rights.