Skip to content

waqas028/Document_Scanner_CMP

Repository files navigation

Document Scanner (CMP Kotlin Multiplatform)

A powerful, cross-platform Document Scanner built with Compose Multiplatform (CMP) to share UI and core logic between Android and iOS. This open-source project allows users to effortlessly scan documents using their device's camera, providing a seamless and high-quality experience on both platforms.

Provides fast, accurate, and offline-capable document capture while leveraging the best native scanning technologies on each platform.


Features ✨

  • Cross-Platform UI: A single codebase for a consistent, modern UI on both Android and iOS, powered by Compose Multiplatform.

  • High-Quality Scanning: Leverages native platform APIs for sharp, high-quality document capture.

  • Automatic Edge Detection & Cropping: Intelligent algorithms automatically detect the document's boundaries for a perfect crop every time.

  • Offline-First: All scanning and processing are done on-device, ensuring the app works perfectly without an internet connection.


Android

android_doc_cmp.mp4

IOS

ScreenRecording_08-19-2025.14-45-29_1.mp4

Expect Fun in Comman Module

@Composable
expect fun ScanDocument(onResult: (Result<ImageBitmap>) -> Unit)

android moduel

@Composable
fun ScanAndroidDocument(onResult: (Uri) -> Unit){
    val context = LocalContext.current
    val activity = context.getActivity()
    val options = GmsDocumentScannerOptions.Builder()
        .setGalleryImportAllowed(false)
        .setPageLimit(1)
        .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
        .setScannerMode(GmsDocumentScannerOptions.SCANNER_MODE_FULL)
        .build()
    val scanner = GmsDocumentScanning.getClient(options)
    val scannerLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartIntentSenderForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            val scanningResult = GmsDocumentScanningResult.fromActivityResultIntent(result.data)
            scanningResult?.pages?.forEach {
                onResult(it.imageUri)
                Log.i("TAG", "BeneficiaryDetailsFormInfo: ML Kit Uri:${it.imageUri}")
            }
        }
    }
    if (activity != null) {
        scanner.getStartScanIntent(activity)
            .addOnSuccessListener { intentSender ->
                scannerLauncher.launch(IntentSenderRequest.Builder(intentSender).build())
            }
            .addOnFailureListener {
                context.showToast(it.message ?: "Ml Kit Error")
            }
    }
}

ios main module

@Composable
fun IosDocumentScanner(onResult: (Result<ImageBitmap>) -> Unit){
    var delegate by remember { mutableStateOf<DocumentScannerDelegate?>(null) }
    val controller = VNDocumentCameraViewController()
    val localViewController = LocalUIViewController.current
    controller.setDelegate(
        DocumentScannerDelegate(
            onError = {
                controller.dismissViewControllerAnimated(true) {}
                val exception = when (it.code) {
                    AVErrorApplicationIsNotAuthorizedToUseDevice -> DocumentScannerException.NotAuthorized(
                        it.localizedDescription
                    )

                    else -> DocumentScannerException.Unknown(it.localizedDescription)
                }
                onResult(Result.failure(exception))
            },
            onCancel = {
                println("onCancel")
                controller.dismissViewControllerAnimated(true) {}
            },
            onResult = { result ->
                println("success")
                controller.dismissViewControllerAnimated(true) {}
                val documents = (0..<result.pageCount.toInt()).map {
                    val image = result.imageOfPageAtIndex(it.toULong())
                    val bitmap = image.toImageBitmap()
                    bitmap
                }
                println(documents.first())
                if (documents.isNotEmpty()) {
                    val firstImage = documents.first()
                    onResult(Result.success(firstImage))
                } else {
                    onResult(Result.failure(DocumentScannerException.Unknown("No documents were scanned or converted.")))
                }
            }
        ).also {
            delegate = it // need to remember delegate else it is dereferenced
        }
    )
    localViewController.presentViewController(controller, animated = true){}
}

@OptIn(ExperimentalForeignApi::class)
fun NSData.toByteArray(): ByteArray {
    val length = this.length.toInt()
    if (length == 0) return ByteArray(0)
    val bytes = this.bytes ?: return ByteArray(0) // Pointer to the data

    return ByteArray(length).apply {
        usePinned { pinned ->
            memcpy(pinned.addressOf(0), bytes, this@toByteArray.length)
        }
    }
}

@OptIn(ExperimentalForeignApi::class)
fun UIImage.toImageBitmap(): ImageBitmap {
    val imageData = UIImagePNGRepresentation(this) ?: error("Could not get PNG representation of UIImage")
    val byteArray = imageData.toByteArray() // Use the new extension function
    val skiaImage = makeFromEncoded(byteArray)
    return skiaImage.toComposeImageBitmap()
}

Technology Stack 🛠️

This project showcases the power of Kotlin Multiplatform by sharing UI and logic, while using platform-specific integrations for the core document scanning functionality to achieve optimal performance.

Shared

  • Kotlin Multiplatform (CMP) – Shared business logic, data models, and utilities.

Android

iOS

  • Language: Swift (with Kotlin/Native integration)
  • Scanning API: VisionKit
  • Minimum iOS: 13+

Contributing

Contributions are welcome! Please follow these steps:

  • Fork the repository.
  • Create a new branch (git checkout -b feature-branch).
  • Commit your changes (git commit -m 'Add some feature').
  • Push to the branch (git push origin feature-branch).
  • Open a pull request.

👤 Developed By

Muhammad Waqas

Email
LinkedIn
GitHub
Twitter
Skype
WhatsApp
Google Developer

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors