Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,21 @@ The scanner is included by calling a single composable function ```Scanner()```
```kotlin
// basic permission handling included:
ScannerWithPermissions(
onScanned = { println(it); true }, // return true to disable the scanner, false to continue scanning
onScanned = { code, type ->
println("Scanned $type: $code")
true
}, // return true to disable the scanner, false to continue scanning
types = listOf(CodeType.QR),
cameraPosition = CameraPosition.BACK,
enableTorch = false // toggle this to enable/disable the flashlight
)

// or, if you handle permissions yourself:
Scanner(onScanned = { println(it); true }, types = listOf(CodeType.QR))
Scanner(onScanned = { code, type -> println("Scanned $type: $code"); true }, types = listOf(CodeType.QR))
```

Check out the [sample app](./sample-app) included in the repository.

# Code Types
Code types supported are:
Codabar, Code39, Code93, Code128, EAN8, EAN13, ITF, UPCE, Aztec, DataMatrix, PDF417, QR
Codabar, Code39, Code93, Code128, EAN8, EAN13, ITF, UPCE, Aztec, DataMatrix, PDF417, QR
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,19 @@ class MavenCentralPublishConventionPlugin : Plugin<Project> {
}

extensions.configure<SigningExtension> {
useInMemoryPgpKeys(
getLocalProperty("SIGNING_KEY_ID") ?: System.getenv("SIGNING_KEY_ID"),
getLocalProperty("SIGNING_KEY") ?: System.getenv("SIGNING_KEY"),
getLocalProperty("SIGNING_KEY_PASSWORD") ?: System.getenv("SIGNING_KEY_PASSWORD"),
)
val publishing = extensions.getByType<PublishingExtension>()
sign(publishing.publications)
val signingKeyId = getLocalProperty("SIGNING_KEY_ID") ?: System.getenv("SIGNING_KEY_ID")
val signingKey = getLocalProperty("SIGNING_KEY") ?: System.getenv("SIGNING_KEY")
val signingPassword = getLocalProperty("SIGNING_KEY_PASSWORD") ?: System.getenv("SIGNING_KEY_PASSWORD")

if (!signingKey.isNullOrBlank()) {
useInMemoryPgpKeys(
signingKeyId,
signingKey,
signingPassword,
)
val publishing = extensions.getByType<PublishingExtension>()
sign(publishing.publications)
}
}


Expand All @@ -94,4 +100,4 @@ class MavenCentralPublishConventionPlugin : Plugin<Project> {
//endregion
}
}
}
}
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ plugins {
}

subprojects {
group = "io.github.kalinjul.easyqrscan"
group = "momap.driver.easyqrscan"
version = "0.7.0-local"
}

nexusPublishing {
Expand Down
4 changes: 2 additions & 2 deletions local.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Mon Sep 29 13:54:55 CEST 2025
sdk.dir=/Users/KalinJul/Library/Android/sdk
#Thu Apr 09 10:24:21 PKT 2026
sdk.dir=/Users/sohailmac/Library/Android/sdk
12 changes: 6 additions & 6 deletions sample-app/shared/src/commonMain/kotlin/MainView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,20 @@ fun MainView() {
val scope = rememberCoroutineScope()
ScannerWithPermissions(
modifier = Modifier.padding(16.dp),
onScanned = {
if (lastCode == it) { return@ScannerWithPermissions false }
onScanned = { code, _ ->
if (lastCode == code) { return@ScannerWithPermissions false }
if (TimeSource.Monotonic.markNow().minus(lastSnackbar) < 1.seconds) {
return@ScannerWithPermissions false
}
snackbarJob?.cancel()
lastSnackbar = TimeSource.Monotonic.markNow()
snackbarJob = scope.launch {
snackbarHostState.showSnackbar(it, duration = SnackbarDuration.Short)
if (lastCode == it) {
snackbarHostState.showSnackbar(code, duration = SnackbarDuration.Short)
if (lastCode == code) {
lastCode = null
}
}
lastCode = it
lastCode = code
false // continue scanning
},
types = listOf(CodeType.QR),
Expand All @@ -104,4 +104,4 @@ fun MainView() {
}
}
}
}
}
12 changes: 8 additions & 4 deletions scanner/src/androidMain/kotlin/BarcodeAnalyzer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.google.mlkit.vision.common.InputImage

class BarcodeAnalyzer(
formats: Int = Barcode.FORMAT_QR_CODE,
private val onScanned: (String) -> Boolean
private val onScanned: (String, CodeType) -> Boolean
) : ImageAnalysis.Analyzer {

private val options = BarcodeScannerOptions.Builder()
Expand All @@ -28,9 +28,13 @@ class BarcodeAnalyzer(
)
).addOnSuccessListener { barcode ->
barcode?.takeIf { it.isNotEmpty() }
?.mapNotNull { it.rawValue }
?.mapNotNull { scannedCode ->
val rawValue = scannedCode.rawValue ?: return@mapNotNull null
val codeType = scannedCode.format.toCodeType() ?: return@mapNotNull null
rawValue to codeType
}
?.forEach {
if (onScanned(it)) {
if (onScanned(it.first, it.second)) {
scanner.close()
}
}
Expand All @@ -39,4 +43,4 @@ class BarcodeAnalyzer(
}
}
}
}
}
18 changes: 17 additions & 1 deletion scanner/src/androidMain/kotlin/Codetype+toFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,20 @@ fun List<CodeType>.toFormat(): Int = map {
}
}.fold(0) { acc, next ->
acc + next
}
}

fun Int.toCodeType(): CodeType? = when (this) {
Barcode.FORMAT_CODABAR -> CodeType.Codabar
Barcode.FORMAT_CODE_39 -> CodeType.Code39
Barcode.FORMAT_CODE_93 -> CodeType.Code93
Barcode.FORMAT_CODE_128 -> CodeType.Code128
Barcode.FORMAT_EAN_8 -> CodeType.EAN8
Barcode.FORMAT_EAN_13 -> CodeType.EAN13
Barcode.FORMAT_ITF -> CodeType.ITF
Barcode.FORMAT_UPC_E -> CodeType.UPCE
Barcode.FORMAT_AZTEC -> CodeType.Aztec
Barcode.FORMAT_DATA_MATRIX -> CodeType.DataMatrix
Barcode.FORMAT_PDF417 -> CodeType.PDF417
Barcode.FORMAT_QR_CODE -> CodeType.QR
else -> null
}
2 changes: 1 addition & 1 deletion scanner/src/androidMain/kotlin/Scanner.android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import com.google.accompanist.permissions.rememberPermissionState
@Composable
actual fun Scanner(
modifier: Modifier,
onScanned: (String) -> Boolean,
onScanned: (String, CodeType) -> Boolean,
types: List<CodeType>,
cameraPosition: CameraPosition,
enableTorch: Boolean,
Expand Down
26 changes: 13 additions & 13 deletions scanner/src/commonMain/kotlin/Scanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import androidx.compose.ui.unit.dp
* Code Scanner
*
* @param types Code types to scan.
* @param onScanned Called when a code was scanned. The given lambda should return true
* if scanning was successful and scanning should be aborted.
* Return false if scanning should continue.
* @param onScanned Called when a code was scanned together with its detected code type.
* The given lambda should return true if scanning was successful and
* scanning should be aborted. Return false if scanning should continue.
*/
@Composable
expect fun Scanner(
modifier: Modifier = Modifier,
onScanned: (String) -> Boolean,
onScanned: (String, CodeType) -> Boolean,
types: List<CodeType>,
cameraPosition: CameraPosition = CameraPosition.BACK,
enableTorch: Boolean,
Expand All @@ -32,16 +32,16 @@ expect fun Scanner(
* Code Scanner with permission handling.
*
* @param types Code types to scan.
* @param onScanned Called when a code was scanned. The given lambda should return true
* if scanning was successful and scanning should be aborted.
* Return false if scanning should continue.
* @param onScanned Called when a code was scanned together with its detected code type.
* The given lambda should return true if scanning was successful and
* scanning should be aborted. Return false if scanning should continue.
* @param permissionText Text to show if permission was denied.
* @param openSettingsLabel Label to show on the "Go to settings" Button
*/
@Composable
fun ScannerWithPermissions(
modifier: Modifier = Modifier,
onScanned: (String) -> Boolean,
onScanned: (String, CodeType) -> Boolean,
types: List<CodeType>,
cameraPosition: CameraPosition = CameraPosition.BACK,
enableTorch: Boolean,
Expand Down Expand Up @@ -72,15 +72,15 @@ fun ScannerWithPermissions(
* Code Scanner with permission handling.
*
* @param types Code types to scan.
* @param onScanned Called when a code was scanned. The given lambda should return true
* if scanning was successful and scanning should be aborted.
* Return false if scanning should continue.
* @param onScanned Called when a code was scanned together with its detected code type.
* The given lambda should return true if scanning was successful and
* scanning should be aborted. Return false if scanning should continue.
* @param permissionDeniedContent Content to show if permission was denied.
*/
@Composable
fun ScannerWithPermissions(
modifier: Modifier = Modifier,
onScanned: (String) -> Boolean,
onScanned: (String, CodeType) -> Boolean,
types: List<CodeType>,
cameraPosition: CameraPosition,
enableTorch: Boolean,
Expand All @@ -99,4 +99,4 @@ fun ScannerWithPermissions(
} else {
permissionDeniedContent(permissionState)
}
}
}
21 changes: 19 additions & 2 deletions scanner/src/iosMain/kotlin/Codetype+toFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import platform.AVFoundation.AVMetadataObjectTypeDataMatrixCode
import platform.AVFoundation.AVMetadataObjectTypeEAN13Code
import platform.AVFoundation.AVMetadataObjectTypeEAN8Code
import platform.AVFoundation.AVMetadataObjectTypeITF14Code
import platform.AVFoundation.AVMetadataObjectType
import platform.AVFoundation.AVMetadataObjectTypePDF417Code
import platform.AVFoundation.AVMetadataObjectTypeQRCode
import platform.AVFoundation.AVMetadataObjectTypeUPCECode

fun List<CodeType>.toFormat(): List<platform.AVFoundation.AVMetadataObjectType> = map {
fun List<CodeType>.toFormat(): List<AVMetadataObjectType> = map {
when(it) {
CodeType.Codabar -> if (iosVersionIsMin(15,4)) { AVMetadataObjectTypeCodabarCode } else error("AVMetadataObjectTypeCodabarCode not available on iOS ${iosVersion()}")
CodeType.Code39 -> AVMetadataObjectTypeCode39Code
Expand All @@ -28,4 +29,20 @@ fun List<CodeType>.toFormat(): List<platform.AVFoundation.AVMetadataObjectType>
CodeType.PDF417 -> AVMetadataObjectTypePDF417Code
CodeType.QR -> AVMetadataObjectTypeQRCode
}
}
}

fun AVMetadataObjectType.toCodeType(): CodeType? = when (this) {
AVMetadataObjectTypeCodabarCode -> CodeType.Codabar
AVMetadataObjectTypeCode39Code -> CodeType.Code39
AVMetadataObjectTypeCode93Code -> CodeType.Code93
AVMetadataObjectTypeCode128Code -> CodeType.Code128
AVMetadataObjectTypeEAN8Code -> CodeType.EAN8
AVMetadataObjectTypeEAN13Code -> CodeType.EAN13
AVMetadataObjectTypeITF14Code -> CodeType.ITF
AVMetadataObjectTypeUPCECode -> CodeType.UPCE
AVMetadataObjectTypeAztecCode -> CodeType.Aztec
AVMetadataObjectTypeDataMatrixCode -> CodeType.DataMatrix
AVMetadataObjectTypePDF417Code -> CodeType.PDF417
AVMetadataObjectTypeQRCode -> CodeType.QR
else -> null
}
8 changes: 3 additions & 5 deletions scanner/src/iosMain/kotlin/Scanner.ios.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import platform.UIKit.UIApplicationOpenSettingsURLString
@Composable
actual fun Scanner(
modifier: Modifier,
onScanned: (String) -> Boolean, // return true to abort scanning
onScanned: (String, CodeType) -> Boolean, // return true to abort scanning
types: List<CodeType>,
cameraPosition: CameraPosition,
enableTorch: Boolean,
Expand All @@ -39,9 +39,7 @@ actual fun Scanner(
}
UiScannerView(
modifier = modifier,
onScanned = {
onScanned(it)
},
onScanned = onScanned,
allowedMetadataTypes = types.toFormat(),
cameraPosition = cameraPosition,
onStarted = {
Expand Down Expand Up @@ -81,4 +79,4 @@ class IosMutableCameraPermissionState: MutableCameraPermissionState() {
fun getCameraPermissionStatus(): CameraPermissionStatus {
val authorizationStatus = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
return if (authorizationStatus == AVAuthorizationStatusAuthorized) CameraPermissionStatus.Granted else CameraPermissionStatus.Denied
}
}
12 changes: 7 additions & 5 deletions scanner/src/iosMain/kotlin/ScannerView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fun UiScannerView(
// https://developer.apple.com/documentation/avfoundation/avmetadataobjecttype?language=objc
allowedMetadataTypes: List<AVMetadataObjectType>,
cameraPosition: CameraPosition,
onScanned: (String) -> Boolean,
onScanned: (String, CodeType) -> Boolean,
onStarted: () -> Unit,
) {
val coordinator = remember {
Expand Down Expand Up @@ -105,7 +105,7 @@ class ScannerPreviewView(private val coordinator: ScannerCameraCoordinator): UIV

@OptIn(ExperimentalForeignApi::class)
class ScannerCameraCoordinator(
val onScanned: (String) -> Boolean,
val onScanned: (String, CodeType) -> Boolean,
val onStarted: () -> Unit,
val cameraPosition: CameraPosition
): AVCaptureMetadataOutputObjectsDelegateProtocol, NSObject() {
Expand Down Expand Up @@ -181,11 +181,13 @@ class ScannerCameraCoordinator(

override fun captureOutput(output: platform.AVFoundation.AVCaptureOutput, didOutputMetadataObjects: List<*>, fromConnection: platform.AVFoundation.AVCaptureConnection) {
val metadataObject = didOutputMetadataObjects.firstOrNull() as? AVMetadataMachineReadableCodeObject
metadataObject?.stringValue?.let { onFound(it) }
val code = metadataObject?.stringValue ?: return
val codeType = metadataObject.type.toCodeType() ?: return
onFound(code, codeType)
}

fun onFound(code: String) {
val stopScanning = onScanned(code)
fun onFound(code: String, codeType: CodeType) {
val stopScanning = onScanned(code, codeType)
if (stopScanning) {
captureSession.stopRunning()
}
Expand Down
4 changes: 2 additions & 2 deletions scanner/src/jvmMain/kotlin/Scanner.jvm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import androidx.compose.ui.Modifier
@Composable
actual fun Scanner(
modifier: Modifier,
onScanned: (String) -> Boolean,
onScanned: (String, CodeType) -> Boolean,
types: List<CodeType>,
cameraPosition: CameraPosition,
enableTorch: Boolean,
) {
Text("Scanner not implemented for JVM")
}
}