diff --git a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/CunningDocumentScannerPlugin.kt b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/CunningDocumentScannerPlugin.kt index e624c52..c0a4406 100644 --- a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/CunningDocumentScannerPlugin.kt +++ b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/CunningDocumentScannerPlugin.kt @@ -28,6 +28,7 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA private var delegate: PluginRegistry.ActivityResultListener? = null private var binding: ActivityPluginBinding? = null private var pendingResult: Result? = null + private var singleDocumentMode: Boolean = false private lateinit var activity: Activity private val START_DOCUMENT_ACTIVITY: Int = 0x362738 private val START_DOCUMENT_FB_ACTIVITY: Int = 0x362737 @@ -48,8 +49,10 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA if (call.method == "getPictures") { val noOfPages = call.argument("noOfPages") ?: 50; val isGalleryImportAllowed = call.argument("isGalleryImportAllowed") ?: false; + singleDocumentMode = call.argument("singleDocumentMode") ?: false; + val frameColor = call.argument("frameColor"); this.pendingResult = result - startScan(noOfPages, isGalleryImportAllowed) + startScan(noOfPages, isGalleryImportAllowed, singleDocumentMode, frameColor) } else { result.notImplemented() } @@ -87,9 +90,15 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA data?.extras?.getParcelable("extra_scanning_result") ?: return@ActivityResultListener false - val successResponse = scanningResult.pages?.map { + var successResponse = scanningResult.pages?.map { it.imageUri.toString().removePrefix("file://") - }?.toList() + }?.toList() ?: emptyList() + + // If single document mode is enabled, return only the first page + if (singleDocumentMode && successResponse.isNotEmpty()) { + successResponse = listOf(successResponse[0]) + } + // trigger the success event handler with an array of cropped images pendingResult?.success(successResponse) } @@ -120,9 +129,15 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA // return a list of file paths // removing file uri prefix as Flutter file will have problems with it - val successResponse = croppedImageResults.map { + var successResponse = croppedImageResults.map { it.removePrefix("file://") }.toList() + + // If single document mode is enabled, return only the first page + if (singleDocumentMode && successResponse.isNotEmpty()) { + successResponse = listOf(successResponse[0]) + } + // trigger the success event handler with an array of cropped images pendingResult?.success(successResponse) } @@ -140,6 +155,7 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA if (handled) { // Clear the pending result to avoid reuse pendingResult = null + singleDocumentMode = false } return@ActivityResultListener handled } @@ -154,13 +170,23 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA /** * create intent to launch document scanner and set custom options */ - private fun createDocumentScanIntent(noOfPages: Int): Intent { + private fun createDocumentScanIntent(noOfPages: Int, singleDocumentMode: Boolean, frameColor: String?): Intent { val documentScanIntent = Intent(activity, DocumentScannerActivity::class.java) documentScanIntent.putExtra( DocumentScannerExtra.EXTRA_MAX_NUM_DOCUMENTS, noOfPages ) + documentScanIntent.putExtra( + DocumentScannerExtra.EXTRA_SINGLE_DOCUMENT_MODE, + singleDocumentMode + ) + if (frameColor != null) { + documentScanIntent.putExtra( + DocumentScannerExtra.EXTRA_FRAME_COLOR, + frameColor + ) + } return documentScanIntent } @@ -169,10 +195,13 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA /** * add document scanner result handler and launch the document scanner */ - private fun startScan(noOfPages: Int, isGalleryImportAllowed: Boolean) { + private fun startScan(noOfPages: Int, isGalleryImportAllowed: Boolean, singleDocumentMode: Boolean, frameColor: String?) { + // If single document mode is enabled, limit pages to 1 + val pageLimit = if (singleDocumentMode) 1 else noOfPages + val options = GmsDocumentScannerOptions.Builder() .setGalleryImportAllowed(isGalleryImportAllowed) - .setPageLimit(noOfPages) + .setPageLimit(pageLimit) .setResultFormats(RESULT_FORMAT_JPEG) .setScannerMode(SCANNER_MODE_FULL) .build() @@ -187,7 +216,7 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA } }.addOnFailureListener { if (it is MlKitException) { - val intent = createDocumentScanIntent(noOfPages) + val intent = createDocumentScanIntent(noOfPages, singleDocumentMode, frameColor) try { ActivityCompat.startActivityForResult( this.activity, diff --git a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/DocumentScannerActivity.kt b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/DocumentScannerActivity.kt index fdad53a..0181b92 100644 --- a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/DocumentScannerActivity.kt +++ b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/DocumentScannerActivity.kt @@ -43,6 +43,16 @@ class DocumentScannerActivity : AppCompatActivity() { */ private var croppedImageQuality = DefaultSetting.CROPPED_IMAGE_QUALITY + /** + * @property singleDocumentMode when true, only one document can be scanned + */ + private var singleDocumentMode = false + + /** + * @property frameColor the color of the document detection frame + */ + private var frameColor: String? = null + /** * @property cropperOffsetWhenCornersNotFound if we can't find document corners, we set * corners to image size with a slight margin @@ -71,13 +81,15 @@ class DocumentScannerActivity : AppCompatActivity() { // user takes photo originalPhotoPath -> - // if maxNumDocuments is 3 and this is the 3rd photo, hide the new photo button since - // we reach the allowed limit + // Hide new photo button only if we're at max documents limit + // For singleDocumentMode, we'll try auto-return but allow manual continuation if needed if (documents.size == maxNumDocuments - 1) { val newPhotoButton: ImageButton = findViewById(R.id.new_photo_button) newPhotoButton.isClickable = false - newPhotoButton.visibility = View.INVISIBLE + newPhotoButton.visibility = View.GONE } + // Note: For singleDocumentMode, we don't hide the button immediately + // This allows fallback to manual scanning if auto-return doesn't work // get bitmap from photo file path val photo: Bitmap? = try { @@ -124,8 +136,37 @@ class DocumentScannerActivity : AppCompatActivity() { imageView.imagePreviewBounds.height() / photo.height ) + // Apply frame color before setting cropper (so it's used when drawing) + if (frameColor != null) { + imageView.setFrameColor(frameColor) + } + // display cropper, and allow user to move corners imageView.setCropper(cornersInImagePreviewCoordinates) + + // If singleDocumentMode is enabled and this is the first document, try auto-return + // If auto-return fails for any reason, fall back to manual scanning (default behavior) + if (singleDocumentMode && documents.size == 0) { + // Use a post with a small delay to attempt auto-return + // If user interacts before this completes, they can continue manually + imageView.postDelayed({ + try { + // Only auto-return if still on first document (user didn't continue manually) + if (documents.size == 0 && document != null) { + // Add the document to the list with the detected corners + addSelectedCornersAndOriginalPhotoPathToDocuments() + // Automatically finish and return to app immediately + cropDocumentAndFinishIntent() + } + // If documents.size > 0, user already continued manually, so don't auto-return + } catch (e: Exception) { + // If auto-return fails, allow manual scanning (default behavior) + // User can continue scanning normally + } + }, 100) // Small delay to allow for potential user interaction + // Don't return here - allow the UI to show so user can see the document + // If auto-return succeeds, it will finish. If not, user can continue manually. + } } catch (exception: Exception) { finishIntentWithError( "unable get image preview ready: ${exception.message}" @@ -161,6 +202,11 @@ class DocumentScannerActivity : AppCompatActivity() { // doesn't see this until they finish taking a photo setContentView(R.layout.activity_image_crop) imageView = findViewById(R.id.image_view) + + // Apply frame color early if specified (before any images are set) + if (frameColor != null) { + imageView.setFrameColor(frameColor) + } try { // validate maxNumDocuments option, and update default if user sets it @@ -185,6 +231,20 @@ class DocumentScannerActivity : AppCompatActivity() { } croppedImageQuality = it } + + // read singleDocumentMode option + intent.extras?.get(DocumentScannerExtra.EXTRA_SINGLE_DOCUMENT_MODE)?.let { + if (it is Boolean) { + singleDocumentMode = it + } + } + + // read frameColor option + intent.extras?.get(DocumentScannerExtra.EXTRA_FRAME_COLOR)?.let { + if (it is String) { + frameColor = it + } + } } catch (exception: Exception) { finishIntentWithError( "invalid extra: ${exception.message}" @@ -192,6 +252,11 @@ class DocumentScannerActivity : AppCompatActivity() { return } + // Apply frame color to imageView after it's initialized + if (frameColor != null) { + imageView.setFrameColor(frameColor) + } + // set click event handlers for new document button, accept and crop document button, // and retake document photo button val newPhotoButton: ImageButton = findViewById(R.id.new_photo_button) diff --git a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/constants/DocumentScannerExtra.kt b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/constants/DocumentScannerExtra.kt index ac570d4..28fcdea 100644 --- a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/constants/DocumentScannerExtra.kt +++ b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/constants/DocumentScannerExtra.kt @@ -7,5 +7,7 @@ class DocumentScannerExtra { companion object { const val EXTRA_CROPPED_IMAGE_QUALITY = "croppedImageQuality" const val EXTRA_MAX_NUM_DOCUMENTS = "maxNumDocuments" + const val EXTRA_SINGLE_DOCUMENT_MODE = "singleDocumentMode" + const val EXTRA_FRAME_COLOR = "frameColor" } } \ No newline at end of file diff --git a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/ui/ImageCropView.kt b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/ui/ImageCropView.kt index b79c10d..ea34691 100644 --- a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/ui/ImageCropView.kt +++ b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/fallback/ui/ImageCropView.kt @@ -155,6 +155,58 @@ class ImageCropView(context: Context, attrs: AttributeSet) : AppCompatImageView( quad = cropperCorners } + /** + * Sets the frame color for the document detection overlay. + * Supports hex colors (e.g., "#FF0000" or "FF0000") or named colors (e.g., "red", "blue"). + * + * @param colorString the color as a string (hex or named color) + */ + fun setFrameColor(colorString: String?) { + val color = parseColor(colorString) + cropperLinesAndCornersStyles.color = color + invalidate() + } + + /** + * Parses a color string and returns an Android Color integer. + * Supports hex colors (e.g., "#FF0000", "#F00", "FF0000") or named colors (e.g., "red", "blue"). + * + * @param colorString the color as a string (hex or named color) + * @return the parsed color as an integer, or Color.WHITE if invalid or null + */ + private fun parseColor(colorString: String?): Int { + if (colorString == null || colorString.isEmpty()) { + return Color.WHITE + } + + // Try hex color first + try { + if (colorString.startsWith("#")) { + return Color.parseColor(colorString) + } else if (colorString.length == 6 || colorString.length == 3) { + return Color.parseColor("#$colorString") + } + } catch (e: IllegalArgumentException) { + // Not a valid hex color, try named color + } + + // Try named colors + return when (colorString.lowercase()) { + "red" -> Color.RED + "blue" -> Color.BLUE + "green" -> Color.GREEN + "white" -> Color.WHITE + "black" -> Color.BLACK + "yellow" -> Color.YELLOW + "cyan" -> Color.CYAN + "magenta" -> Color.MAGENTA + "gray", "grey" -> Color.GRAY + "darkgray", "darkgrey" -> Color.DKGRAY + "lightgray", "lightgrey" -> Color.LTGRAY + else -> Color.WHITE // Default + } + } + /** * @property imagePreviewBounds image coordinates - if the image ratio is different than * the image container ratio then there's blank space either at the top and bottom of the diff --git a/example/lib/main.dart b/example/lib/main.dart index be717f8..5a85bcf 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,7 +9,7 @@ void main() { } class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override State createState() => _MyAppState(); diff --git a/example/pubspec.lock b/example/pubspec.lock index 0e5ddab..8749c85 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -47,7 +47,7 @@ packages: path: ".." relative: true source: path - version: "2.0.0" + version: "2.0.1" cupertino_icons: dependency: "direct main" description: @@ -147,10 +147,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" path: dependency: transitive description: @@ -320,10 +320,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" vector_math: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9cce558..403ae95 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,7 +6,7 @@ description: Demonstrates how to use the cunning_document_scanner plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: ">=2.15.1 <4.0.0" + sdk: ">=2.17.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index 460a8db..c70bca6 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -8,7 +8,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:cunning_document_scanner_example/main.dart'; +import '../lib/main.dart'; + void main() { testWidgets('View is created', (WidgetTester tester) async { diff --git a/ios/Classes/CunningScannerOptions.swift b/ios/Classes/CunningScannerOptions.swift index 3c4b708..9c4c0ee 100644 --- a/ios/Classes/CunningScannerOptions.swift +++ b/ios/Classes/CunningScannerOptions.swift @@ -15,20 +15,35 @@ enum CunningScannerImageFormat: String { struct CunningScannerOptions { let imageFormat: CunningScannerImageFormat let jpgCompressionQuality: Double + let singleDocumentMode: Bool + let frameColor: String? init() { self.imageFormat = CunningScannerImageFormat.png self.jpgCompressionQuality = 1.0 + self.singleDocumentMode = false + self.frameColor = nil } init(imageFormat: CunningScannerImageFormat) { self.imageFormat = imageFormat self.jpgCompressionQuality = 1.0 + self.singleDocumentMode = false + self.frameColor = nil } init(imageFormat: CunningScannerImageFormat, jpgCompressionQuality: Double) { self.imageFormat = imageFormat self.jpgCompressionQuality = jpgCompressionQuality + self.singleDocumentMode = false + self.frameColor = nil + } + + init(imageFormat: CunningScannerImageFormat, jpgCompressionQuality: Double, singleDocumentMode: Bool, frameColor: String?) { + self.imageFormat = imageFormat + self.jpgCompressionQuality = jpgCompressionQuality + self.singleDocumentMode = singleDocumentMode + self.frameColor = frameColor } static func fromArguments(args: Any?) -> CunningScannerOptions { @@ -38,14 +53,33 @@ struct CunningScannerOptions { let arguments = args as? Dictionary + // Check for top-level singleDocumentMode first (for Android compatibility) + let topLevelSingleDocumentMode: Bool = (arguments?["singleDocumentMode"] as? Bool) ?? false + let topLevelFrameColor: String? = arguments?["frameColor"] as? String + if arguments == nil || arguments!.keys.contains("iosScannerOptions") == false { - return CunningScannerOptions() + // If no iosScannerOptions, use top-level values or defaults + return CunningScannerOptions( + imageFormat: CunningScannerImageFormat.png, + jpgCompressionQuality: 1.0, + singleDocumentMode: topLevelSingleDocumentMode, + frameColor: topLevelFrameColor + ) } let scannerOptionsDict = arguments!["iosScannerOptions"] as! Dictionary let imageFormat: String = (scannerOptionsDict["imageFormat"] as? String) ?? "png" let jpgCompressionQuality: Double = (scannerOptionsDict["jpgCompressionQuality"] as? Double) ?? 1.0 + // Use singleDocumentMode from iosScannerOptions if provided, otherwise use top-level + let singleDocumentMode: Bool = (scannerOptionsDict["singleDocumentMode"] as? Bool) ?? topLevelSingleDocumentMode + // Use frameColor from iosScannerOptions if provided, otherwise use top-level + let frameColor: String? = (scannerOptionsDict["frameColor"] as? String) ?? topLevelFrameColor - return CunningScannerOptions(imageFormat: CunningScannerImageFormat(rawValue: imageFormat) ?? CunningScannerImageFormat.png, jpgCompressionQuality: jpgCompressionQuality) + return CunningScannerOptions( + imageFormat: CunningScannerImageFormat(rawValue: imageFormat) ?? CunningScannerImageFormat.png, + jpgCompressionQuality: jpgCompressionQuality, + singleDocumentMode: singleDocumentMode, + frameColor: frameColor + ) } } diff --git a/ios/Classes/SwiftCunningDocumentScannerPlugin.swift b/ios/Classes/SwiftCunningDocumentScannerPlugin.swift index 8b5c53f..72d28a8 100644 --- a/ios/Classes/SwiftCunningDocumentScannerPlugin.swift +++ b/ios/Classes/SwiftCunningDocumentScannerPlugin.swift @@ -41,28 +41,68 @@ public class SwiftCunningDocumentScannerPlugin: NSObject, FlutterPlugin, VNDocum } public func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) { - let tempDirPath = self.getDocumentsDirectory() - let currentDateTime = Date() - let df = DateFormatter() - df.dateFormat = "yyyyMMdd-HHmmss" - let formattedDate = df.string(from: currentDateTime) - var filenames: [String] = [] - for i in 0 ..< scan.pageCount { - let page = scan.imageOfPage(at: i) - let url = tempDirPath.appendingPathComponent(formattedDate + "-\(i).\(scannerOptions.imageFormat.rawValue)") - switch scannerOptions.imageFormat { - case CunningScannerImageFormat.jpg: - try? page.jpegData(compressionQuality: scannerOptions.jpgCompressionQuality)?.write(to: url) - break - case CunningScannerImageFormat.png: - try? page.pngData()?.write(to: url) - break + // Dismiss the scanner immediately to return to app as fast as possible + presentingController?.dismiss(animated: true, completion: nil) + + // Process the scan in the background to return quickly + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self = self else { return } + + let tempDirPath = self.getDocumentsDirectory() + let currentDateTime = Date() + let df = DateFormatter() + df.dateFormat = "yyyyMMdd-HHmmss" + let formattedDate = df.string(from: currentDateTime) + var filenames: [String] = [] + + // If singleDocumentMode is enabled, try to process only the first page + // If user scanned multiple pages, process all of them as fallback (manual mode) + let maxPages = self.scannerOptions.singleDocumentMode ? min(1, scan.pageCount) : scan.pageCount + + // Process pages based on singleDocumentMode setting + // If singleDocumentMode is true and only one page was scanned, process just that + // If multiple pages were scanned despite singleDocumentMode, process all (fallback to manual) + if self.scannerOptions.singleDocumentMode && scan.pageCount == 1 { + // Only one page scanned - process it and return + let page = scan.imageOfPage(at: 0) + let url = tempDirPath.appendingPathComponent(formattedDate + "-0.\(self.scannerOptions.imageFormat.rawValue)") + + switch self.scannerOptions.imageFormat { + case CunningScannerImageFormat.jpg: + try? page.jpegData(compressionQuality: self.scannerOptions.jpgCompressionQuality)?.write(to: url) + break + case CunningScannerImageFormat.png: + try? page.pngData()?.write(to: url) + break + } + + filenames.append(url.path) + } else { + // Process all pages (either singleDocumentMode is false, or multiple pages were scanned) + // This allows fallback to manual mode if user scanned multiple pages + let pagesToProcess = self.scannerOptions.singleDocumentMode ? 1 : scan.pageCount + for i in 0 ..< pagesToProcess { + let page = scan.imageOfPage(at: i) + let url = tempDirPath.appendingPathComponent(formattedDate + "-\(i).\(self.scannerOptions.imageFormat.rawValue)") + switch self.scannerOptions.imageFormat { + case CunningScannerImageFormat.jpg: + try? page.jpegData(compressionQuality: self.scannerOptions.jpgCompressionQuality)?.write(to: url) + break + case CunningScannerImageFormat.png: + try? page.pngData()?.write(to: url) + break + } + + filenames.append(url.path) + } } - filenames.append(url.path) + // Return result on main thread + DispatchQueue.main.async { + self.resultChannel?(filenames) + self.resultChannel = nil + } } - resultChannel?(filenames) - presentingController?.dismiss(animated: true) } public func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) { diff --git a/lib/src/cunning_document_scanner.dart b/lib/src/cunning_document_scanner.dart index 31ea74b..d889b6c 100644 --- a/lib/src/cunning_document_scanner.dart +++ b/lib/src/cunning_document_scanner.dart @@ -18,12 +18,18 @@ class CunningDocumentScanner { /// /// [noOfPages] is the maximum number of pages that can be scanned. /// [isGalleryImportAllowed] is a flag that allows the user to import images from the gallery. + /// [singleDocumentMode] when true, ensures only one document is scanned and the scanner + /// returns immediately after the first document is captured. + /// [frameColor] allows customization of the document detection frame color on Android. + /// Supports hex colors (e.g., "#FF0000" or "FF0000") or named colors (e.g., "red", "blue"). /// [iosScannerOptions] is a set of options for the iOS scanner. /// /// Returns a list of paths to the scanned images, or null if the user cancels the operation. static Future?> getPictures({ int noOfPages = 100, bool isGalleryImportAllowed = false, + bool singleDocumentMode = false, + String? frameColor, IosScannerOptions? iosScannerOptions, }) async { Map statuses = await [ @@ -38,10 +44,15 @@ class CunningDocumentScanner { final List? pictures = await _channel.invokeMethod('getPictures', { 'noOfPages': noOfPages, 'isGalleryImportAllowed': isGalleryImportAllowed, + 'singleDocumentMode': singleDocumentMode, + if (frameColor != null) 'frameColor': frameColor, if (iosScannerOptions != null) 'iosScannerOptions': { 'imageFormat': iosScannerOptions.imageFormat.name, 'jpgCompressionQuality': iosScannerOptions.jpgCompressionQuality, + 'singleDocumentMode': iosScannerOptions.singleDocumentMode, + if (iosScannerOptions.frameColor != null) + 'frameColor': iosScannerOptions.frameColor, } }); return pictures?.map((e) => e as String).toList(); diff --git a/lib/src/ios_scanner_options.dart b/lib/src/ios_scanner_options.dart index ecce6fe..c6ea6c0 100644 --- a/lib/src/ios_scanner_options.dart +++ b/lib/src/ios_scanner_options.dart @@ -10,11 +10,21 @@ import 'ios_image_format.dart'; /// can be used to control the quality of the resulting JPEG image. The value /// 0.0 represents the maximum compression (or lowest quality) while the value /// 1.0 represents the least compression (or best quality). Default value is 1.0. +/// +/// The [singleDocumentMode] when set to true, ensures only the first document/page +/// is processed and returned, even if the user scans multiple pages. +/// +/// The [frameColor] allows customization of the document detection frame color. +/// Note: This feature is not available on iOS as VNDocumentCameraViewController +/// uses system UI that cannot be customized. This parameter is included for API +/// consistency but will be ignored on iOS. final class IosScannerOptions { /// Creates a [IosScannerOptions]. const IosScannerOptions({ this.imageFormat = IosImageFormat.png, this.jpgCompressionQuality = 1.0, + this.singleDocumentMode = false, + this.frameColor, }); final IosImageFormat imageFormat; @@ -27,4 +37,13 @@ final class IosScannerOptions { /// [jpgCompressionQuality] only has an effect if the [imageFormat] is set to /// [IosImageFormat.jpeg] and is ignored otherwise. final double jpgCompressionQuality; + + /// When true, only the first scanned document/page will be processed and returned. + /// Default value is false. + final bool singleDocumentMode; + + /// The color of the document detection frame/overlay. + /// Supports hex colors (e.g., "#FF0000" or "FF0000") or named colors (e.g., "red", "blue"). + /// Note: This feature is not available on iOS and will be ignored. + final String? frameColor; } diff --git a/pubspec.yaml b/pubspec.yaml index 2263c84..60438ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: cunning_document_scanner description: A document scanner plugin for flutter. Scan and crop automatically on iOS and Android. -version: 2.0.0 +version: 2.0.1 homepage: https://cunning.biz repository: https://github.com/jachzen/cunning_document_scanner environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=2.17.0 <4.0.0' flutter: ">=2.5.0" dependencies: