diff --git a/android/build.gradle b/android/build.gradle index 7f520c0..1daa8d9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group = "com.rajada1_docscan_kit.doc_scan_kit" version = "1.0-SNAPSHOT" buildscript { - ext.kotlin_version = "1.7.10" + ext.kotlin_version = "2.1.0" repositories { google() mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.7.0") + classpath("com.android.tools.build:gradle:8.13.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") } } diff --git a/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/DocScanBarcodeScanner.java b/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/DocScanBarcodeScanner.java deleted file mode 100644 index f361831..0000000 --- a/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/DocScanBarcodeScanner.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.rajada1_docscan_kit.doc_scan_kit; - -import com.google.android.gms.tasks.OnFailureListener; -import com.google.android.gms.tasks.OnSuccessListener; -import com.google.android.gms.tasks.Task; -import com.google.mlkit.vision.barcode.BarcodeScanning; -import com.google.mlkit.vision.barcode.common.Barcode; -import com.google.mlkit.vision.common.InputImage; - -import androidx.annotation.NonNull; - -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -public class DocScanBarcodeScanner { - - private final com.google.mlkit.vision.barcode.BarcodeScanner scanner; - private final Executor executor; - - public interface BarcodeScannerCallback { - void onSuccess(String barcodeContent); - void onFailure(Exception e); - } - - public DocScanBarcodeScanner() { - scanner = BarcodeScanning.getClient(); - executor = Executors.newSingleThreadExecutor(); - } - - public void scanBarcodes(InputImage image, final BarcodeScannerCallback callback) { - Task> result = scanner.process(image) - .addOnSuccessListener(executor, new OnSuccessListener>() { - @Override - public void onSuccess(List barcodes) { - if (barcodes.isEmpty()) { - callback.onSuccess(""); // Nenhum código de barras encontrado - return; - } - - StringBuilder resultContent = new StringBuilder(); - for (Barcode barcode : barcodes) { - String rawValue = barcode.getRawValue(); - if (rawValue != null) { - if (resultContent.length() > 0) { - resultContent.append(", "); - } - resultContent.append(rawValue); - } - } - - callback.onSuccess(resultContent.toString()); - } - }) - .addOnFailureListener(executor, new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception e) { - callback.onFailure(e); - } - }); - } - - public void close() { - scanner.close(); - } -} diff --git a/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPlugin.java b/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPlugin.java deleted file mode 100644 index 8249d2b..0000000 --- a/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPlugin.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.rajada1_docscan_kit.doc_scan_kit; - -import androidx.annotation.NonNull; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.embedding.engine.plugins.activity.ActivityAware; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.MethodChannel; - -public class DocScanKitPlugin implements FlutterPlugin, ActivityAware { - private static final String channelName = "doc_scan_kit"; - private MethodChannel channel; - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - channel = new MethodChannel(binding.getBinaryMessenger(), channelName); - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - channel.setMethodCallHandler(null); - } - - @Override - public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { - channel.setMethodCallHandler(new DocumentScanner(binding)); - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - - } - - @Override - public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { - channel.setMethodCallHandler(new DocumentScanner(binding)); - } - - @Override - public void onDetachedFromActivity() { - - } -} diff --git a/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/DocumentScanner.java b/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/DocumentScanner.java deleted file mode 100644 index 293b551..0000000 --- a/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/DocumentScanner.java +++ /dev/null @@ -1,329 +0,0 @@ -package com.rajada1_docscan_kit.doc_scan_kit; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.IntentSender; -import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import io.flutter.Log; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; - -import com.google.android.gms.tasks.OnFailureListener; -import com.google.android.gms.tasks.OnSuccessListener; -import com.google.mlkit.vision.common.InputImage; -import com.google.mlkit.vision.documentscanner.GmsDocumentScanner; -import com.google.mlkit.vision.documentscanner.GmsDocumentScanning; -import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions; -import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.List; -import java.util.Objects; - - -public class DocumentScanner implements MethodChannel.MethodCallHandler, PluginRegistry.ActivityResultListener { - private static final String START = "scanKit#startDocumentScanner"; - private static final String CLOSE = "scanKit#closeDocumentScanner"; - private static final String RECOGNIZE_TEXT = "scanKit#recognizeText"; - private static final String SCAN_QR_CODE = "scanKit#scanQrCode"; - private static final String TAG = "DocumentScanner"; - private final Map instance = new HashMap<>(); - private final Map instancesBarCode = new HashMap<>(); - private final Map instancesTextRecognizer = new HashMap<>(); - private Map extractedOptions; - private final ActivityPluginBinding binding; - private MethodChannel.Result pendingResult = null; - - final private int START_DOCUMENT_ACTIVITY = 0x362738; - - public DocumentScanner(ActivityPluginBinding binding){ - this.binding = binding; - binding.addActivityResultListener(this); - - } - - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - String method = call.method; - - switch (method){ - case START: - startScanner(call, result); - break; - case CLOSE: - closeScanner(call); - break; - case RECOGNIZE_TEXT: - startRecognizeText(call, result); - break; - case SCAN_QR_CODE: - startScanQrCode(call, result); - break; - default: - result.notImplemented(); - break; - } - - } - - @Override - public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if(requestCode == START_DOCUMENT_ACTIVITY){ - if(resultCode == Activity.RESULT_OK){ - GmsDocumentScanningResult result = GmsDocumentScanningResult.fromActivityResultIntent(data); - if(result != null){ - handleScannerResult(result); - } - }else if(resultCode == Activity.RESULT_CANCELED){ - // Add null check before using pendingResult to prevent NullPointerException - if (pendingResult != null) { - pendingResult.error(TAG, "Operation canceled", null); - } else { - Log.e(TAG, "pendingResult is null when trying to handle cancellation"); - } - }else{ - pendingResult.error(TAG, "Unknown Error", null); - } - return true; - } - return false; - } - - - private void startScanner(MethodCall call, final MethodChannel.Result result){ - String id = call.argument("id"); - extractedOptions = call.argument("androidOptions"); - GmsDocumentScanner scanner = instance.get(id); - pendingResult = result; - - if (scanner == null) { - Map options = call.argument("androidOptions"); - if(options == null){ - result.error(TAG, "Invalid options", null); - return; - } - GmsDocumentScannerOptions scannerOptions =makeOptions(options); - scanner = GmsDocumentScanning.getClient(scannerOptions); - instance.put(id, scanner); - - } - - Activity activity = binding.getActivity(); - scanner.getStartScanIntent(activity).addOnSuccessListener(new OnSuccessListener() { - @Override - public void onSuccess(IntentSender intentSender) { - try { - activity.startIntentSenderForResult(intentSender, START_DOCUMENT_ACTIVITY, null, 0, 0, 0); - } catch (IntentSender.SendIntentException e) { - result.error(TAG, "Failed Start document Scanner", e); - } - } - }).addOnFailureListener(new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception e) { - result.error(TAG, "Failed to Start document Scanner", e); - } - }); - - } - - - private GmsDocumentScannerOptions makeOptions(Map options){ - boolean isGalleryImport = Boolean.TRUE.equals(options.get("isGalleryImport")); - Object pageLimitObject = options.get("pageLimit"); - Object scannerModeObject = options.get("scannerMode"); - int pageLimit = (pageLimitObject instanceof Number) ? ((Number) pageLimitObject).intValue() : 1; - String scannerModeValue = (scannerModeObject instanceof String) ? ((String) scannerModeObject) : "full"; - int scannerMode = GmsDocumentScannerOptions.SCANNER_MODE_FULL; - switch (scannerModeValue){ - case "base": - scannerMode = GmsDocumentScannerOptions.SCANNER_MODE_BASE; - break; - case "filter": - scannerMode = GmsDocumentScannerOptions.SCANNER_MODE_BASE_WITH_FILTER; - break; - case "full": - break; - - } - GmsDocumentScannerOptions.Builder builder = new GmsDocumentScannerOptions.Builder() - .setGalleryImportAllowed(isGalleryImport) - .setPageLimit(pageLimit) - .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG) - .setScannerMode(scannerMode); - return builder.build(); - - } - - private void closeScanner(MethodCall call ){ - String id = call.argument("id"); - GmsDocumentScanner scanner = instance.get(id); - TextRecognizer text = instancesTextRecognizer.get(id); - DocScanBarcodeScanner barcode = instancesBarCode.get(id); - if(scanner != null) instance.remove(id); - - if(text != null){ - text.closedTextRecognizer(); - instancesTextRecognizer.remove(id); - } - - if(barcode != null){ - barcode.close(); - instancesBarCode.remove(id); - } - } - - private void handleScannerResult(GmsDocumentScanningResult result) { - List> resultMap = new ArrayList<>(); - List pages = result.getPages(); - if(pages != null && !pages.isEmpty()){ - for (GmsDocumentScanningResult.Page page : pages){ - Map imageData = new HashMap<>(); - Uri imageUri = page.getImageUri(); - Context context = binding.getActivity().getApplicationContext(); - byte[] imageBytes = getBytesFromUri(context, imageUri); - imageData.put("bytes", imageBytes); - - boolean saveImage = true; - - if (extractedOptions != null) { - saveImage = Boolean.TRUE.equals(extractedOptions.get("saveImage")); - } - if(!saveImage){ - File file = new File(Objects.requireNonNull(imageUri.getPath())); - file.deleteOnExit(); - }else{ - imageData.put("path", imageUri.getPath()); - } - resultMap.add(imageData); - } - }else{ - resultMap.add(null); - } - - // Add null check before using pendingResult to prevent NullPointerException - if (pendingResult != null) { - pendingResult.success(resultMap); - pendingResult = null; - } else { - Log.e(TAG, "pendingResult is null when trying to handle scanner result"); - } - } - - - private byte[] getBytesFromUri(Context context, Uri uri) { - try (InputStream inputStream = context.getContentResolver().openInputStream(uri); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - byte[] buffer = new byte[4096]; - int bytesRead; - while (true) { - assert inputStream != null; - if ((bytesRead = inputStream.read(buffer)) == -1) break; - outputStream.write(buffer, 0, bytesRead); - } - return outputStream.toByteArray(); - } catch (IOException e) { - - Log.d("error GetImg Bytes", e.toString()); - return null; - } - } - - private void startRecognizeText(MethodCall call, final MethodChannel.Result result) { - String id = call.argument("id"); - TextRecognizer textRecognizerInstance = instancesTextRecognizer.get(id); - pendingResult = result; - if (textRecognizerInstance == null) { - textRecognizerInstance = new TextRecognizer(); - instancesTextRecognizer.put(id, textRecognizerInstance); - - } - - - - try { - byte[] imageBytes = call.argument("imageBytes"); - if (imageBytes == null) { - result.error(TAG, "Invalid image data", null); - return; - } - - String text = textRecognizerInstance.handleDetection2(getInputImageByByteArray(imageBytes)); - result.success(text); - } catch (Exception e) { - Log.e(TAG, "Error in text recognition", e); - result.error(TAG, "Failed to recognize text", e); - } - } - private void startScanQrCode(MethodCall call, final MethodChannel.Result result) { - try { - String id = call.argument("id"); - DocScanBarcodeScanner barcodeScanner = instancesBarCode.get(id); - pendingResult = result; - if (barcodeScanner == null) { - barcodeScanner = new DocScanBarcodeScanner(); - instancesBarCode.put(id, barcodeScanner); - - } - - byte[] imageBytes = call.argument("imageBytes"); - if (imageBytes == null) { - result.error(TAG, "Invalid image data", null); - return; - } - barcodeScanner.scanBarcodes(getInputImageByByteArray(imageBytes), new DocScanBarcodeScanner.BarcodeScannerCallback() { - @Override - public void onSuccess(String barcodeContent) { - result.success(barcodeContent); - } - - @Override - public void onFailure(Exception e) { - result.error(TAG, "Failed to scan barcode", e); - } - }); - } catch (Exception e) { - Log.e(TAG, "Error in QR code scanning", e); - result.error(TAG, "Failed to scan QR code", e); - } - } - - private InputImage getInputImageByByteArray(byte[] imageBytes) throws Exception { - File tempFile = File.createTempFile("temp_image", ".jpeg", binding.getActivity().getCacheDir()); - try { - - try (FileOutputStream fos = new FileOutputStream(tempFile)) { - fos.write(imageBytes); - fos.flush(); - } catch (IOException e) { - Log.e(TAG, "Error write bytes in temp file", e); - } - Context context = binding.getActivity().getApplicationContext(); - return InputImage.fromFilePath(context, Uri.fromFile(tempFile)); - }catch (Exception e) { - Log.e(TAG, "Error in text recognition", e); - throw new Exception("Failed to recognize text", e); - } finally { - boolean deleteSuccess = tempFile.delete(); - if (!deleteSuccess) { - Log.w(TAG, "Failed to delete temporary file"); - } - } - - } - -} diff --git a/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/TextRecognizer.java b/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/TextRecognizer.java deleted file mode 100644 index 32c819f..0000000 --- a/android/src/main/java/com/rajada1_docscan_kit/doc_scan_kit/TextRecognizer.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.rajada1_docscan_kit.doc_scan_kit; - -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; -import com.google.mlkit.vision.common.InputImage; -import com.google.mlkit.vision.text.Text; -import com.google.mlkit.vision.text.TextRecognition; -import com.google.mlkit.vision.text.latin.TextRecognizerOptions; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - - -public class TextRecognizer { - - private final Map instances = new HashMap<>(); - -private final ExecutorService executorService; - private final com.google.mlkit.vision.text.TextRecognizer textRecognizer; - - public TextRecognizer() { - - executorService = Executors.newSingleThreadExecutor(); - - textRecognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS); - - } - - public String handleDetection2(InputImage inputImage) { - - Future future = executorService.submit( - () -> { - Task result =textRecognizer.process(inputImage); - try { - Tasks.await(result); - - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - return result.getResult().getText(); - }); - try { - return future.get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - - } - - public void closedTextRecognizer(){ - if(textRecognizer != null){ - textRecognizer.close();} - if(executorService != null){ - executorService.shutdown(); - } - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanBarcodeScanner.kt b/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanBarcodeScanner.kt new file mode 100644 index 0000000..a25cde4 --- /dev/null +++ b/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanBarcodeScanner.kt @@ -0,0 +1,85 @@ +package com.rajada1_docscan_kit.doc_scan_kit + +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class DocScanBarcodeScanner { + private val scanner = BarcodeScanning.getClient() + private val scope = CoroutineScope(Dispatchers.IO + Job()) + private var scanJob: Job? = null + + interface BarcodeScannerCallback { + fun onSuccess(barcodeContent: String) + fun onFailure(exception: Exception) + } + + fun scanBarcodes(image: InputImage, callback: BarcodeScannerCallback) { + scanJob = scope.launch { + try { + val barcodes = withContext(Dispatchers.IO) { + scanner.process(image).await() + } + + if (barcodes.isEmpty()) { + callback.onSuccess("") + return@launch + } + + val resultContent = barcodes + .mapNotNull { it.rawValue } + .joinToString(", ") + + callback.onSuccess(resultContent) + } catch (e: Exception) { + callback.onFailure(e) + } + } + } + + suspend fun scanBarcodesSuspend(image: InputImage): String = withContext(Dispatchers.IO) { + suspendCancellableCoroutine { continuation -> + val callback = object : BarcodeScannerCallback { + override fun onSuccess(barcodeContent: String) { + continuation.resume(barcodeContent) + } + + override fun onFailure(exception: Exception) { + continuation.resumeWithException(exception) + } + } + + scanBarcodes(image, callback) + + continuation.invokeOnCancellation { + scanJob?.cancel() + } + } + } + + fun close() { + scanJob?.cancel() + scanner.close() + } +} + +// Extension function to convert Task to suspend function +private suspend fun com.google.android.gms.tasks.Task.await(): T { + return suspendCancellableCoroutine { continuation -> + addOnCompleteListener { task -> + if (task.isSuccessful) { + continuation.resume(task.result!!) + } else { + continuation.resumeWithException(task.exception!!) + } + } + } +} diff --git a/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPlugin.kt b/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPlugin.kt index 1caf318..500e46b 100644 --- a/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPlugin.kt +++ b/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPlugin.kt @@ -1,29 +1,41 @@ -//package com.rajada1_docscan_kit.doc_scan_kit -//import io.flutter.embedding.engine.plugins.FlutterPlugin -//import io.flutter.embedding.engine.plugins.activity.ActivityAware -//import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -//import io.flutter.plugin.common.MethodChannel -// -///** DocScanKitPlugin */ -//class DocScanKitPlugin: FlutterPlugin, ActivityAware { -// private lateinit var handler: DocumentScanKit -// private lateinit var channel : MethodChannel -// override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { -// handler = DocumentScanKit() -// channel = MethodChannel(flutterPluginBinding.binaryMessenger, "doc_scan_kit") -// channel.setMethodCallHandler(handler) -// } -// override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { -// channel.setMethodCallHandler(null) -// } -// -// override fun onAttachedToActivity(binding: ActivityPluginBinding) { -// handler.setActivityPluginBinding(binding) -// } -// -// override fun onDetachedFromActivityForConfigChanges() {} -// -// override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {} -// -// override fun onDetachedFromActivity() {} -//} +package com.rajada1_docscan_kit.doc_scan_kit + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodChannel + +class DocScanKitPlugin : FlutterPlugin, ActivityAware { + private val channelName = "doc_scan_kit" + private lateinit var channel: MethodChannel + private var documentScanner: DocumentScanner? = null + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(binding.binaryMessenger, channelName) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + documentScanner = DocumentScanner(binding).also { + channel.setMethodCallHandler(it) + } + } + + override fun onDetachedFromActivityForConfigChanges() { + // No special handling needed for config changes + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + documentScanner = DocumentScanner(binding).also { + channel.setMethodCallHandler(it) + } + } + + override fun onDetachedFromActivity() { + documentScanner = null + channel.setMethodCallHandler(null) + } +} diff --git a/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocumentScanKit.kt b/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocumentScanKit.kt deleted file mode 100644 index fc0f72d..0000000 --- a/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocumentScanKit.kt +++ /dev/null @@ -1,132 +0,0 @@ -//package com.rajada1_docscan_kit.doc_scan_kit -//import android.app.Activity -//import android.content.Intent -//import android.content.IntentSender -//import android.util.Log -//import io.flutter.plugin.common.MethodChannel -//import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -//import io.flutter.plugin.common.PluginRegistry -//import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions -//import com.google.mlkit.vision.documentscanner.GmsDocumentScanning -//import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult -//import io.flutter.plugin.common.MethodCall -//import java.io.File -// -// -//class DocumentScanKit : MethodChannel.MethodCallHandler, PluginRegistry.ActivityResultListener { -// -// private lateinit var binding : ActivityPluginBinding -// private val scanRequestCode = 1 -// private var pendingResult: MethodChannel.Result? = null -// private var optionsAndroid : Map<*, *> = emptyMap() -// -// fun setActivityPluginBinding(binding:ActivityPluginBinding){ -// this.binding = binding -// binding.addActivityResultListener(this) -// } -// -// private fun makeScanMode(mode : String) : Int { -// val scanMode = when (mode){ -// "base" -> GmsDocumentScannerOptions.SCANNER_MODE_BASE -// "filter" -> GmsDocumentScannerOptions.SCANNER_MODE_BASE_WITH_FILTER -// "full" -> GmsDocumentScannerOptions.SCANNER_MODE_FULL -// else -> { -// GmsDocumentScannerOptions.SCANNER_MODE_FULL -// } -// } -// return scanMode -// } -// private fun scanner(result:MethodChannel.Result, optionsAndroid : Map<*, *>){ -// this.pendingResult = result -// -// -// val options = GmsDocumentScannerOptions.Builder() -// .setGalleryImportAllowed(optionsAndroid["isGalleryImport"] as Boolean) -// .setPageLimit(optionsAndroid["pageLimit"] as Int) -// .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG) -// .setScannerMode(makeScanMode(optionsAndroid["scannerMode"] as String)) -// .build() -// -// val activity = binding.activity -// try { -// val scan = GmsDocumentScanning.getClient(options) -// scan.getStartScanIntent(activity).addOnSuccessListener { -// intentSender: IntentSender -> -// try { -// activity.startIntentSenderForResult( -// intentSender, scanRequestCode,null,0,0,0 -// -// ) -// }catch (e: IntentSender.SendIntentException){ -// e.printStackTrace() -// this.pendingResult?.error(ScanErrorTypes.starterror.toString(), "on start internet send for result error", e) -// throw e -// } -// } -// } catch (e: Exception){ -// Log.e("DocScanKit","Error $e") -// this.pendingResult?.error("Error", e.message,null) -// } -// -// } -// -// override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { -// if(requestCode == scanRequestCode){ -// val scanResult = GmsDocumentScanningResult.fromActivityResultIntent(data) -// if(scanResult != null){ -// val pages = scanResult.pages -// if(!pages.isNullOrEmpty()){ -// val imgArray: ArrayList> = arrayListOf() -// for (page in pages){ -// val inputStream = binding.activity.contentResolver.openInputStream(page.imageUri) -// if (inputStream != null) { -// val resultHashMap: HashMap = HashMap() -// resultHashMap["bytes"] = inputStream.readBytes() -// val saveImg = (optionsAndroid["saveImage"] as? Boolean) ?: false -// if(!saveImg){ -// page.imageUri.path?.let { -// path -> val file = File(path) -// file.delete() -// } -// }else{ -// resultHashMap["path"] = page.imageUri.path!! -// } -// inputStream.close() -// imgArray.add(resultHashMap) -// } else { -// Log.e("DocumentScannerKit", "Error opening input stream for page ${page.imageUri}") -// } -// } -// this.pendingResult?.success(imgArray.toList()) -// this.pendingResult = null -// return true -// }else{ -// this.pendingResult?.error(ScanErrorTypes.empty.toString(), "Page empty or null", null) -// } -// }else{ -// this.pendingResult?.error("Error", "Result null", resultCode) -// } -// } -// else if (resultCode == Activity.RESULT_CANCELED){ -// this.pendingResult?.success(null) -// this.pendingResult = null -// return true -// }else{ -// this.pendingResult?.error(ScanErrorTypes.unknown.toString(), "The RequestCode is invalid", resultCode) -// Log.i("DocumentScanKit", "Activity end $resultCode") -// } -// return false -// } -// -// override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { -// if(call.method == "scanner"){ -// val options = call.arguments as? Map<*, *> -// this.optionsAndroid = options!!["androidOptions"] as Map<*, *> -// scanner(result, optionsAndroid) -// }else{ -// result.notImplemented() -// } -// } -// -// -//} \ No newline at end of file diff --git a/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocumentScanner.kt b/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocumentScanner.kt new file mode 100644 index 0000000..163c369 --- /dev/null +++ b/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocumentScanner.kt @@ -0,0 +1,241 @@ +package com.rajada1_docscan_kit.doc_scan_kit + +import android.app.Activity +import android.net.Uri +import android.util.Log +import com.google.android.gms.tasks.Task +import com.google.mlkit.vision.common.InputImage +import com.google.mlkit.vision.documentscanner.GmsDocumentScanner +import com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions +import com.google.mlkit.vision.documentscanner.GmsDocumentScanning +import com.google.mlkit.vision.documentscanner.GmsDocumentScanningResult +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileOutputStream +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class DocumentScanner( + private val binding: ActivityPluginBinding +) : MethodChannel.MethodCallHandler { + + companion object { + private const val TAG = "DocumentScanner" + private const val START = "scanKit#startDocumentScanner" + private const val CLOSE = "scanKit#closeDocumentScanner" + private const val RECOGNIZE_TEXT = "scanKit#recognizeText" + private const val SCAN_QR_CODE = "scanKit#scanQrCode" + private const val START_DOCUMENT_ACTIVITY = 0x362738 + } + + private val instances = mutableMapOf() + private val instancesBarCode = mutableMapOf() + private val instancesTextRecognizer = mutableMapOf() + private var extractedOptions: Map? = null + private var pendingResult: MethodChannel.Result? = null + private val scope = CoroutineScope(Dispatchers.Main + Job()) + private var currentJob: Job? = null + + init { + binding.addActivityResultListener { requestCode, resultCode, data -> + if (requestCode == START_DOCUMENT_ACTIVITY) { + when (resultCode) { + Activity.RESULT_OK -> { + val result = GmsDocumentScanningResult.fromActivityResultIntent(data) + if (result != null) { + scope.launch { + handleScannerResult(result) + } + } + } + + Activity.RESULT_CANCELED -> { + pendingResult?.error(TAG, "Operation canceled", null) + } + + else -> { + pendingResult?.error(TAG, "Unknown Error", null) + } + } + true + } else { + false + } + } + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + currentJob?.cancel() + currentJob = scope.launch { + try { + when (call.method) { + START -> startScanner(call, result) + CLOSE -> closeScanner(call) + RECOGNIZE_TEXT -> startRecognizeText(call, result) + SCAN_QR_CODE -> startScanQrCode(call, result) + else -> result.notImplemented() + } + } catch (e: Exception) { + Log.e(TAG, "Error in ${call.method}", e) + result.error(TAG, "Error in ${call.method}: ${e.message}", null) + } + } + } + + private suspend fun startScanner(call: MethodCall, result: MethodChannel.Result) { + val id = call.argument("id") ?: run { + result.error(TAG, "Missing id parameter", null) + return + } + + extractedOptions = call.argument("options") ?: run { + result.error(TAG, "Missing options parameter", null) + return + } + + val scanner = instances.getOrPut(id) { + val options = makeOptions(extractedOptions!!) + GmsDocumentScanning.getClient(options) + } + + pendingResult = result + + try { + val intentSender = scanner.getStartScanIntent(binding.activity).await() + binding.activity.startIntentSenderForResult( + intentSender, + START_DOCUMENT_ACTIVITY, + null, 0, 0, 0 + ) + } catch (e: Exception) { + result.error(TAG, "Failed to start document scanner: ${e.message}", null) + pendingResult = null + } + } + + private fun makeOptions(options: Map): GmsDocumentScannerOptions { + val isGalleryImport = options["isGalleryImport"] as? Boolean ?: true + val pageLimit = (options["pageLimit"] as? Number)?.toInt() ?: 1 + val scannerMode = when (options["scannerMode"] as? String ?: "full") { + "base" -> GmsDocumentScannerOptions.SCANNER_MODE_BASE + "filter" -> GmsDocumentScannerOptions.SCANNER_MODE_BASE_WITH_FILTER + else -> GmsDocumentScannerOptions.SCANNER_MODE_FULL + } + val resultFormat = when (options["format"]) { + "document" -> GmsDocumentScannerOptions.RESULT_FORMAT_PDF + else -> GmsDocumentScannerOptions.RESULT_FORMAT_JPEG + } + + return GmsDocumentScannerOptions.Builder() + .setGalleryImportAllowed(isGalleryImport) + .setPageLimit(pageLimit) + .setResultFormats(resultFormat) + .setScannerMode(scannerMode) + .build() + } + + private fun closeScanner(call: MethodCall) { + val id = call.argument("id") ?: return + + instances.remove(id) // GmsDocumentScanner doesn't have a close method + + instancesTextRecognizer[id]?.let { + it.close() + instancesTextRecognizer.remove(id) + } + + instancesBarCode[id]?.let { + it.close() + instancesBarCode.remove(id) + } + } + + private suspend fun handleScannerResult(result: GmsDocumentScanningResult) { + val fileUris = when { + !result.pdf?.uri?.path.isNullOrEmpty() -> listOf( + mutableMapOf("type" to "pdf", "path" to result.pdf?.uri?.path) + ) + + !result.pages.isNullOrEmpty() -> result.pages?.map { + mutableMapOf( + "type" to "jpeg", + "path" to it.imageUri.path + ) + } + + else -> emptyList() + } + + pendingResult?.success(fileUris) + pendingResult = null + } + + private suspend fun startRecognizeText(call: MethodCall, result: MethodChannel.Result) { + try { + val id = call.argument("id") + ?: throw IllegalArgumentException("Missing id parameter") + val imageBytes = call.argument("imageBytes") + ?: throw IllegalArgumentException("Missing imageBytes parameter") + + val textRecognizer = instancesTextRecognizer.getOrPut(id) { TextRecognizer() } + val inputImage = getInputImageByByteArray(imageBytes) + val text = textRecognizer.handleDetection2(inputImage) + + result.success(text) + } catch (e: Exception) { + Log.e(TAG, "Error in text recognition", e) + result.error(TAG, "Failed to recognize text: ${e.message}", null) + } + } + + private suspend fun startScanQrCode(call: MethodCall, result: MethodChannel.Result) { + try { + val id = call.argument("id") + ?: throw IllegalArgumentException("Missing id parameter") + val imageBytes = call.argument("imageBytes") + ?: throw IllegalArgumentException("Missing imageBytes parameter") + + val barcodeScanner = instancesBarCode.getOrPut(id) { DocScanBarcodeScanner() } + val inputImage = getInputImageByByteArray(imageBytes) + + val barcodeContent = barcodeScanner.scanBarcodesSuspend(inputImage) + result.success(barcodeContent) + } catch (e: Exception) { + Log.e(TAG, "Error in QR code scanning", e) + result.error(TAG, "Failed to scan QR code: ${e.message}", null) + } + } + + @Throws(Exception::class) + private fun getInputImageByByteArray(imageBytes: ByteArray): InputImage { + val tempFile = File.createTempFile("temp_image", ".jpeg", binding.activity.cacheDir) + try { + FileOutputStream(tempFile).use { it.write(imageBytes) } + return InputImage.fromFilePath( + binding.activity.applicationContext, + Uri.fromFile(tempFile) + ) + } finally { + if (!tempFile.delete()) { + Log.w(TAG, "Failed to delete temporary file") + } + } + } + + private suspend fun Task.await(): T = suspendCoroutine { continuation -> + addOnCompleteListener { task -> + if (task.isSuccessful) { + continuation.resume(task.result!!) + } else { + continuation.resumeWithException(task.exception!!) + } + } + } +} diff --git a/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/TextRecognizer.kt b/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/TextRecognizer.kt new file mode 100644 index 0000000..3c8f0e7 --- /dev/null +++ b/android/src/main/kotlin/com/rajada1_docscan_kit/doc_scan_kit/TextRecognizer.kt @@ -0,0 +1,41 @@ +package com.rajada1_docscan_kit.doc_scan_kit + +import com.google.android.gms.tasks.Task +import com.google.mlkit.vision.common.InputImage +import com.google.mlkit.vision.text.TextRecognition +import com.google.mlkit.vision.text.latin.TextRecognizerOptions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class TextRecognizer { + private val textRecognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) + private val scope = CoroutineScope(Dispatchers.IO + Job()) + private var recognitionJob: Job? = null + + suspend fun handleDetection2(inputImage: InputImage): String = withContext(Dispatchers.IO) { + suspendCancellableCoroutine { continuation -> + textRecognizer.process(inputImage) + .addOnSuccessListener { visionText -> + continuation.resume(visionText.text) + } + .addOnFailureListener { e -> + continuation.resumeWithException(e) + } + + continuation.invokeOnCancellation { + recognitionJob?.cancel() + } + } + } + + fun close() { + recognitionJob?.cancel() + textRecognizer.close() + } +} diff --git a/android/src/test/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPluginTest.kt b/android/src/test/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPluginTest.kt deleted file mode 100644 index 4a85ee3..0000000 --- a/android/src/test/kotlin/com/rajada1_docscan_kit/doc_scan_kit/DocScanKitPluginTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.rajada1_docscan_kit.doc_scan_kit - -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import kotlin.test.Test -import org.mockito.Mockito - -/* - * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. - * - * Once you have built the plugin's example app, you can run these tests from the command - * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or - * you can run them directly from IDEs that support JUnit such as Android Studio. - */ - -internal class DocScanKitPluginTest { - @Test - fun onMethodCall_getPlatformVersion_returnsExpectedValue() { - val plugin = DocScanKitPlugin() - - val call = MethodCall("getPlatformVersion", null) - val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) - plugin.onMethodCall(call, mockResult) - - Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) - } -} diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 6b74f0f..e6045a9 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 27029b3..e46d6da 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.6.0" apply false - id "org.jetbrains.kotlin.android" version "1.8.10" apply false + id "com.android.application" version '8.13.0' apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/example/integration_test/plugin_integration_test.dart b/example/integration_test/plugin_integration_test.dart index fe35235..5fbc229 100644 --- a/example/integration_test/plugin_integration_test.dart +++ b/example/integration_test/plugin_integration_test.dart @@ -15,8 +15,8 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('scan test', (WidgetTester tester) async { - final DocScanKit plugin = DocScanKit(); - final List images = await plugin.scanner(); + const plugin = DocScanKit(); + final images = await plugin.scanner(); expect(images.isNotEmpty, List); }); } diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index d97f17e..3e44f9c 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 3f93381..06366e1 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -99,7 +99,6 @@ F00B2CCD78D82ADCB8FF4A55 /* Pods-RunnerTests.release.xcconfig */, 80F687E6B7C3BCE766D82BE0 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -244,7 +243,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -286,10 +285,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -463,7 +466,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -486,7 +489,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.rajada1docscankit.docScanKitExample; + PRODUCT_BUNDLE_IDENTIFIER = com.rajada1docscankit.docScanKitExample2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -593,7 +596,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -644,7 +647,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -669,7 +672,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.rajada1docscankit.docScanKitExample; + PRODUCT_BUNDLE_IDENTIFIER = com.rajada1docscankit.docScanKitExample2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -692,7 +695,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.rajada1docscankit.docScanKitExample; + PRODUCT_BUNDLE_IDENTIFIER = com.rajada1docscankit.docScanKitExample2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -736,7 +739,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index d795332..c3fedb2 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -44,6 +44,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,10 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSCameraUsageDescription + The app needs access to the camera to capture document images. + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,11 +47,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - NSCameraUsageDescription - The app needs access to the camera to capture document images. diff --git a/example/lib/main.dart b/example/lib/main.dart index d1e315f..912ee26 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,6 @@ -import 'dart:io'; +import 'package:doc_scan_kit/doc_scan_kit.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:doc_scan_kit/doc_scan_kit.dart'; import 'package:image_picker/image_picker.dart'; class CustomScanResult { @@ -53,35 +52,35 @@ class DocumentScannerScreen extends StatefulWidget { class _DocumentScannerScreenState extends State { // iOS configuration options + DocScanKitFormat format = DocScanKitFormat.images; double compressionQuality = 0.2; - bool saveImage = true; - bool useQrCodeScanner = true; - bool useTextRecognizer = true; + bool useQrCodeScanner = false; + bool useTextRecognizer = false; Color color = Colors.orange; - ModalPresentationStyle modalPresentationStyle = - ModalPresentationStyle.overFullScreen; + ModalPresentationStyleIOS modalPresentationStyle = + ModalPresentationStyleIOS.overFullScreen; // Android configuration options + DocScanKitFormat formatAndroid = DocScanKitFormat.images; int pageLimit = 3; bool recognizerTextAndroid = false; - bool saveImageAndroid = true; - bool isGalleryImport = true; + bool isGalleryImport = false; ScannerModeAndroid scannerMode = ScannerModeAndroid.full; List imageData = []; bool isLoading = false; Future scan() async { - DocScanKit instance = DocScanKit( - iosOptions: DocumentScanKitOptionsiOS( + final instance = DocScanKit( + iosOptions: DocScanKitOptionsIOS( compressionQuality: compressionQuality, - saveImage: saveImage, + format: format, color: color, modalPresentationStyle: modalPresentationStyle, ), - androidOptions: DocumentScanKitOptionsAndroid( + androidOptions: DocScanKitOptionsAndroid( pageLimit: pageLimit, - saveImage: saveImageAndroid, + format: formatAndroid, isGalleryImport: isGalleryImport, scannerMode: scannerMode, ), @@ -89,35 +88,35 @@ class _DocumentScannerScreenState extends State { try { setState(() => isLoading = true); - final List images = await instance.scanner(); - List results = []; - - for (var image in images) { - CustomScanResult customResult = CustomScanResult( - imagesBytes: image.imagesBytes, - imagePath: image.imagePath, - ); - - // Processing text recognition if enabled - if ((recognizerTextAndroid && Platform.isAndroid) || - (useTextRecognizer && Platform.isIOS)) { - try { - customResult.text = await instance.recognizeText(image.imagesBytes); - } catch (e) { - debugPrint('Text recognition failed: $e'); - } - } - - // Processing QR code if enabled - if (useQrCodeScanner) { - try { - customResult.qrCode = await instance.scanQrCode(image.imagesBytes); - } catch (e) { - debugPrint('QR Code scanning failed: $e'); - } - } - - results.add(customResult); + final List files = await instance.scanner(); + final results = []; + + for (final file in files) { + // CustomScanResult customResult = CustomScanResult( + // imagesBytes: file.imagesBytes, + // imagePath: file.imagePath, + // ); + // + // // Processing text recognition if enabled + // if ((recognizerTextAndroid && Platform.isAndroid) || + // (useTextRecognizer && Platform.isIOS)) { + // try { + // customResult.text = await instance.recognizeText(file.imagesBytes); + // } catch (e) { + // debugPrint('Text recognition failed: $e'); + // } + // } + // + // // Processing QR code if enabled + // if (useQrCodeScanner) { + // try { + // customResult.qrCode = await instance.scanQrCode(file.imagesBytes); + // } catch (e) { + // debugPrint('QR Code scanning failed: $e'); + // } + // } + // + // results.add(customResult); } setState(() => imageData = results); @@ -142,15 +141,15 @@ class _DocumentScannerScreenState extends State { } DocScanKit instance = DocScanKit( - iosOptions: DocumentScanKitOptionsiOS( + iosOptions: DocScanKitOptionsIOS( compressionQuality: compressionQuality, - saveImage: saveImage, + format: format, color: color, modalPresentationStyle: modalPresentationStyle, ), - androidOptions: DocumentScanKitOptionsAndroid( + androidOptions: DocScanKitOptionsAndroid( pageLimit: pageLimit, - saveImage: saveImageAndroid, + format: formatAndroid, isGalleryImport: isGalleryImport, scannerMode: scannerMode, ), @@ -204,7 +203,7 @@ class _DocumentScannerScreenState extends State { builder: (context) => ConfigurationScreen( // iOS options compressionQuality: compressionQuality, - saveImage: saveImage, + format: format, useQrCodeScanner: useQrCodeScanner, useTextRecognizer: useTextRecognizer, color: color, @@ -212,31 +211,38 @@ class _DocumentScannerScreenState extends State { // Android options pageLimit: pageLimit, recognizerTextAndroid: recognizerTextAndroid, - saveImageAndroid: saveImageAndroid, + formatAndroid: formatAndroid, isGalleryImport: isGalleryImport, scannerMode: scannerMode, // Callbacks for updating options - onIOSOptionsChanged: (newCompressionQuality, - newSaveImage, - newUseQrCodeScanner, - newUseTextRecognizer, - newColor, - newModalStyle) { + onIOSOptionsChanged: ( + newCompressionQuality, + newFormat, + newUseQrCodeScanner, + newUseTextRecognizer, + newColor, + newModalStyle, + ) { setState(() { compressionQuality = newCompressionQuality; - saveImage = newSaveImage; + format = newFormat; useQrCodeScanner = newUseQrCodeScanner; useTextRecognizer = newUseTextRecognizer; color = newColor; modalPresentationStyle = newModalStyle; }); }, - onAndroidOptionsChanged: (newPageLimit, newRecognizerText, - newSaveImage, newIsGalleryImport, newScannerMode) { + onAndroidOptionsChanged: ( + newPageLimit, + newRecognizerText, + newFormat, + newIsGalleryImport, + newScannerMode, + ) { setState(() { pageLimit = newPageLimit; recognizerTextAndroid = newRecognizerText; - saveImageAndroid = newSaveImage; + formatAndroid = newFormat; isGalleryImport = newIsGalleryImport; scannerMode = newScannerMode; }); @@ -370,36 +376,48 @@ class ScanResultsList extends StatelessWidget { class ConfigurationScreen extends StatefulWidget { // iOS options final double compressionQuality; - final bool saveImage; + final DocScanKitFormat format; final bool useQrCodeScanner; final bool useTextRecognizer; final Color color; - final ModalPresentationStyle modalPresentationStyle; + final ModalPresentationStyleIOS modalPresentationStyle; // Android options final int pageLimit; final bool recognizerTextAndroid; - final bool saveImageAndroid; + final DocScanKitFormat formatAndroid; final bool isGalleryImport; final ScannerModeAndroid scannerMode; // Callbacks - final Function(double, bool, bool, bool, Color, ModalPresentationStyle) - onIOSOptionsChanged; - final Function(int, bool, bool, bool, ScannerModeAndroid) - onAndroidOptionsChanged; + final Function( + double compressionQuality, + DocScanKitFormat format, + bool useQrCodeScanner, + bool useTextRecognizer, + Color color, + ModalPresentationStyleIOS modalPresentationStyle, + ) onIOSOptionsChanged; + + final Function( + int pageLimit, + bool recognizerTextAndroid, + DocScanKitFormat formatAndroid, + bool isGalleryImport, + ScannerModeAndroid scannerMode, + ) onAndroidOptionsChanged; const ConfigurationScreen({ super.key, required this.compressionQuality, - required this.saveImage, + required this.format, required this.useQrCodeScanner, required this.useTextRecognizer, required this.color, required this.modalPresentationStyle, required this.pageLimit, required this.recognizerTextAndroid, - required this.saveImageAndroid, + required this.formatAndroid, required this.isGalleryImport, required this.scannerMode, required this.onIOSOptionsChanged, @@ -416,15 +434,15 @@ class _ConfigurationScreenState extends State // Local state variables late double _compressionQuality; - late bool _saveImage; + late DocScanKitFormat _format; late bool _useQrCodeScanner; late bool _useTextRecognizer; late Color _color; - late ModalPresentationStyle _modalPresentationStyle; + late ModalPresentationStyleIOS _modalPresentationStyle; late int _pageLimit; late bool _recognizerTextAndroid; - late bool _saveImageAndroid; + late DocScanKitFormat _formatAndroid; late bool _isGalleryImport; late ScannerModeAndroid _scannerMode; @@ -435,7 +453,7 @@ class _ConfigurationScreenState extends State // Initialize with widget values _compressionQuality = widget.compressionQuality; - _saveImage = widget.saveImage; + _format = widget.format; _useQrCodeScanner = widget.useQrCodeScanner; _useTextRecognizer = widget.useTextRecognizer; _color = widget.color; @@ -443,7 +461,7 @@ class _ConfigurationScreenState extends State _pageLimit = widget.pageLimit; _recognizerTextAndroid = widget.recognizerTextAndroid; - _saveImageAndroid = widget.saveImageAndroid; + _formatAndroid = widget.formatAndroid; _isGalleryImport = widget.isGalleryImport; _scannerMode = widget.scannerMode; } @@ -478,7 +496,7 @@ class _ConfigurationScreenState extends State onPressed: () { widget.onIOSOptionsChanged( _compressionQuality, - _saveImage, + _format, _useQrCodeScanner, _useTextRecognizer, _color, @@ -487,7 +505,7 @@ class _ConfigurationScreenState extends State widget.onAndroidOptionsChanged( _pageLimit, _recognizerTextAndroid, - _saveImageAndroid, + _formatAndroid, _isGalleryImport, _scannerMode, ); @@ -506,15 +524,32 @@ class _ConfigurationScreenState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Scanner Options', - style: - TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const Text( + 'Scanner Options', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), const SizedBox(height: 8), - SwitchListTile( - title: const Text('Save Image'), - subtitle: const Text('Save scanned image to gallery'), - value: _saveImage, - onChanged: (value) => setState(() => _saveImage = value), + const Text( + 'Result Format', + style: TextStyle(fontWeight: FontWeight.bold), + ), + DropdownButtonFormField( + initialValue: _format, + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12), + ), + onChanged: (value) { + if (value != null) { + setState(() => _format = value); + } + }, + items: DocScanKitFormat.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.name), + )) + .toList(), ), SwitchListTile( title: const Text('Use QR Code Scanner'), @@ -557,8 +592,8 @@ class _ConfigurationScreenState extends State const Divider(), const Text('Modal Presentation Style', style: TextStyle(fontWeight: FontWeight.bold)), - DropdownButtonFormField( - value: _modalPresentationStyle, + DropdownButtonFormField( + initialValue: _modalPresentationStyle, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12), @@ -568,7 +603,7 @@ class _ConfigurationScreenState extends State setState(() => _modalPresentationStyle = value); } }, - items: ModalPresentationStyle.values + items: ModalPresentationStyleIOS.values .map((style) => DropdownMenuItem( value: style, child: Text(style.toString().split('.').last), @@ -585,9 +620,10 @@ class _ConfigurationScreenState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Scanner Options', - style: - TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const Text( + 'Scanner Options', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), const SizedBox(height: 8), ListTile( title: const Text('Page Limit'), @@ -625,12 +661,25 @@ class _ConfigurationScreenState extends State onChanged: (value) => setState(() => _useQrCodeScanner = value), ), - SwitchListTile( - title: const Text('Save Image'), - subtitle: const Text('Save scanned image to gallery'), - value: _saveImageAndroid, - onChanged: (value) => - setState(() => _saveImageAndroid = value), + const Text('Result Format', + style: TextStyle(fontWeight: FontWeight.bold)), + DropdownButtonFormField( + initialValue: _formatAndroid, + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12), + ), + onChanged: (value) { + if (value != null) { + setState(() => _formatAndroid = value); + } + }, + items: DocScanKitFormat.values + .map((mode) => DropdownMenuItem( + value: mode, + child: Text(mode.name), + )) + .toList(), ), SwitchListTile( title: const Text('Gallery Import'), @@ -643,7 +692,7 @@ class _ConfigurationScreenState extends State const Text('Scanner Mode', style: TextStyle(fontWeight: FontWeight.bold)), DropdownButtonFormField( - value: _scannerMode, + initialValue: _scannerMode, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12), diff --git a/ios/doc_scan_kit/Sources/doc_scan_kit/DocScanKitPlugin.swift b/ios/doc_scan_kit/Sources/doc_scan_kit/DocScanKitPlugin.swift index 83acf6e..33a87ef 100644 --- a/ios/doc_scan_kit/Sources/doc_scan_kit/DocScanKitPlugin.swift +++ b/ios/doc_scan_kit/Sources/doc_scan_kit/DocScanKitPlugin.swift @@ -1,6 +1,11 @@ import Flutter import UIKit +enum DocScanKitFormat: String, CaseIterable { + case images = "images" + case document = "document" +} + @available(iOS 13.0, *) public class DocScanKitPlugin: NSObject, FlutterPlugin { @@ -11,7 +16,6 @@ public class DocScanKitPlugin: NSObject, FlutterPlugin { registrar.addMethodCallDelegate(instance, channel: channel) } - func mapPresentationStyle(from string: String) -> UIModalPresentationStyle { switch string { case "automatic": @@ -26,6 +30,7 @@ public class DocScanKitPlugin: NSObject, FlutterPlugin { return .overFullScreen } } + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "scanKit#startDocumentScanner": @@ -35,18 +40,18 @@ public class DocScanKitPlugin: NSObject, FlutterPlugin { details: "the NSCameraUsageDescription parameter needs to be configured in the Info.plist")) } guard let args = call.arguments as? [String: Any], - let iosOptions = args["iosOptions"] as? [String: Any] else { + let options = args["options"] as? [String: Any] else { return result(FlutterError(code: "invalid_arguments", message: "Invalid or missing arguments", details: "Expected iOS options and scanner parameters.")) } - let presentationStyleString = iosOptions["modalPresentationStyle"] as? String ?? "overFullScreen" + let presentationStyleString = options["modalPresentationStyle"] as? String ?? "overFullScreen" let presentationStyle = mapPresentationStyle(from: presentationStyleString) - let compressionQuality = iosOptions["compressionQuality"] as? CGFloat ?? 1.0 - let saveImage = iosOptions["saveImage"] as? Bool ?? true - let colorList = iosOptions["color"] as? [NSNumber] ?? [] + let compressionQuality = options["compressionQuality"] as? CGFloat ?? 1.0 + let format = DocScanKitFormat(rawValue: options["format"] as? String ?? "images") ?? .images + let colorList = options["color"] as? [NSNumber] ?? [] if let viewController = UIApplication.shared.delegate?.window??.rootViewController as? FlutterViewController { - let scanController = ScanDocKitController(result: result, compressionQuality: compressionQuality, saveImage:saveImage,colorList: colorList) + let scanController = ScanDocKitController(result: result, compressionQuality: compressionQuality, format:format,colorList: colorList) scanController.isModalInPresentation = true scanController.modalPresentationStyle = presentationStyle viewController.present(scanController, animated: true) @@ -55,6 +60,8 @@ public class DocScanKitPlugin: NSObject, FlutterPlugin { message: "Unable to retrieve the root view controller.", details: nil)) } + case "scanKit#closeDocumentScanner": + result(nil) case "scanKit#recognizeText": guard let args = call.arguments as? [String: Any], let imageBytes = args["imageBytes"] as? FlutterStandardTypedData else { diff --git a/ios/doc_scan_kit/Sources/doc_scan_kit/ScanDocKitController.swift b/ios/doc_scan_kit/Sources/doc_scan_kit/ScanDocKitController.swift index 136ecb7..c2eff5f 100644 --- a/ios/doc_scan_kit/Sources/doc_scan_kit/ScanDocKitController.swift +++ b/ios/doc_scan_kit/Sources/doc_scan_kit/ScanDocKitController.swift @@ -2,21 +2,22 @@ import Foundation import VisionKit import Vision import Flutter +import PDFKit @available(iOS 13.0, *) class ScanDocKitController: UIViewController, VNDocumentCameraViewControllerDelegate { let result: FlutterResult let compressionQuality: CGFloat - let saveImage: Bool + let format: DocScanKitFormat let colorList : [NSNumber] var activityIndicator: UIActivityIndicatorView! - init(result: @escaping FlutterResult, compressionQuality: CGFloat, saveImage: Bool, colorList:[NSNumber] ) { + init(result: @escaping FlutterResult, compressionQuality: CGFloat, format: DocScanKitFormat, colorList:[NSNumber] ) { self.result = result self.compressionQuality = compressionQuality - self.saveImage = saveImage + self.format = format self.colorList = colorList super.init(nibName: nil, bundle: nil) } @@ -46,8 +47,6 @@ class ScanDocKitController: UIViewController, VNDocumentCameraViewControllerDele } } - - func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) { result(nil) controller.dismiss(animated: true) @@ -61,23 +60,39 @@ class ScanDocKitController: UIViewController, VNDocumentCameraViewControllerDele } func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) { - - controller.dismiss(animated: true){ + controller.dismiss(animated: true) { var resultArray: [[String: Any?]] = [] - for i in (0 ..< scan.pageCount) { - let image = scan.imageOfPage(at: i) - if let imageData = image.jpegData(compressionQuality: self.compressionQuality) { - var dict = ["path": "", "bytes": FlutterStandardTypedData(bytes: imageData)] as [String: Any?] - - if self.saveImage { - dict["path"] = self.saveImg(image: imageData) - } - - resultArray.append(dict) + + switch self.format { + case .document: + // Save as PDF with each image on separate page + if let pdfPath = self.createPDF(from: scan) { + resultArray.append([ + "type": "pdf", + "path": pdfPath + ]) } else { - print("Erro converter imagem para jpeg") + print("Error creating PDF") + } + case .images: + // Save as individual images + for i in (0 ..< scan.pageCount) { + autoreleasepool { + let image = scan.imageOfPage(at: i) + if let imageData = image.jpegData(compressionQuality: self.compressionQuality) { + let filePath = self.saveImg(image: imageData) + + resultArray.append([ + "type": "jpeg", + "path": filePath + ]) + } else { + print("Error converting image to jpeg") + } + } } } + self.activityIndicator.stopAnimating() self.result(resultArray) @@ -89,9 +104,9 @@ class ScanDocKitController: UIViewController, VNDocumentCameraViewControllerDele guard let directory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) as NSURL else { return nil } - let fileName = UUID().uuidString + let fileName = UUID().uuidString + ".jpeg" - guard let filePath = directory.appendingPathComponent(fileName + ".jpeg") else { + guard let filePath = directory.appendingPathComponent(fileName) else { return nil } do { @@ -103,6 +118,36 @@ class ScanDocKitController: UIViewController, VNDocumentCameraViewControllerDele } } + func createPDF(from scan: VNDocumentCameraScan) -> String? { + guard let directory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else { + return nil + } + + let fileName = UUID().uuidString + ".pdf" + let pdfURL = directory.appendingPathComponent(fileName) + + let pdfDocument = PDFDocument() + + for i in 0..= 4 { let red = CGFloat(truncating: colorList[0]) diff --git a/lib/doc_scan_kit.dart b/lib/doc_scan_kit.dart index a4c7290..2547dc8 100644 --- a/lib/doc_scan_kit.dart +++ b/lib/doc_scan_kit.dart @@ -1,4 +1,9 @@ -export 'src/doc_scan.dart'; -export 'src/options/android_options.dart'; -export 'src/options/ios_options.dart'; -export 'src/options/scan_result.dart'; +export 'package:doc_scan_kit/src/doc_scan_kit.dart'; +export 'package:doc_scan_kit/src/doc_scan_kit_platform_android.dart'; +export 'package:doc_scan_kit/src/doc_scan_kit_platform_ios.dart'; +export 'package:doc_scan_kit/src/models/doc_scan_kit_format.dart'; +export 'package:doc_scan_kit/src/models/doc_scan_kit_options_android.dart'; +export 'package:doc_scan_kit/src/models/doc_scan_kit_options_ios.dart'; +export 'package:doc_scan_kit/src/models/doc_scan_kit_result.dart'; +export 'package:doc_scan_kit/src/models/modal_presentation_style_ios.dart'; +export 'package:doc_scan_kit/src/models/scanner_mode_android.dart'; diff --git a/lib/src/doc_scan.dart b/lib/src/doc_scan.dart deleted file mode 100644 index a938cdc..0000000 --- a/lib/src/doc_scan.dart +++ /dev/null @@ -1,34 +0,0 @@ -import '../doc_scan_kit.dart'; -import 'doc_scan_kit_platform_interface.dart'; - -class DocScanKit { - final DocumentScanKitOptionsAndroid? androidOptions; - final DocumentScanKitOptionsiOS? iosOptions; - - DocScanKit({ - this.androidOptions, - this.iosOptions, - }); - - Future> scanner() { - return DocScanKitPlatform.instance.scanner( - androidOptions ?? DocumentScanKitOptionsAndroid(), - iosOptions ?? DocumentScanKitOptionsiOS()); - } - - /// Recognizes text from the provided image bytes - /// - /// Returns the recognized text as a string - Future recognizeText(List imageBytes) { - return DocScanKitPlatform.instance.recognizeText(imageBytes); - } - - /// Scans for QR codes in the provided image bytes - /// - /// Returns the detected QR code content as a string - Future scanQrCode(List imageBytes) { - return DocScanKitPlatform.instance.scanQrCode(imageBytes); - } - - Future close() => DocScanKitPlatform.instance.close(); -} diff --git a/lib/src/doc_scan_kit.dart b/lib/src/doc_scan_kit.dart new file mode 100644 index 0000000..1f029ee --- /dev/null +++ b/lib/src/doc_scan_kit.dart @@ -0,0 +1,201 @@ +import 'dart:io'; + +import 'package:doc_scan_kit/src/doc_scan_kit_platform.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_options.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_options_android.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_options_ios.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_result.dart'; + +/// DocScanKit is the main class that provides a platform-agnostic interface for +/// scanning documents, recognizing text, and scanning QR codes. +/// +/// This class automatically handles platform-specific implementations and +/// provides a unified API for both Android and iOS platforms. +/// +/// Example usage: +/// ```dart +/// final docScanKit = DocScanKit( +/// androidOptions: DocScanKitOptionsAndroid( +/// pageLimit: 5, +/// scannerMode: ScannerModeAndroid.full, +/// ), +/// iosOptions: DocScanKitOptionsIOS( +/// compressionQuality: 0.8, +/// modalPresentationStyle: ModalPresentationStyleIOS.overFullScreen, +/// ), +/// ); +/// +/// final results = await docScanKit.scanner(); +/// ``` +class DocScanKit { + /// Creates a new instance of [DocScanKit] with platform-specific options. + /// + /// [androidOptions] - Configuration options for Android platform. + /// Defaults to [DocScanKitOptionsAndroid()] if not specified. + /// [iosOptions] - Configuration options for iOS platform. + /// Defaults to [DocScanKitOptionsIOS()] if not specified. + /// + /// The provided options serve as defaults but can be overridden + /// on a per-call basis in the [scanner] method. + const DocScanKit({ + this.androidOptions = const DocScanKitOptionsAndroid(), + this.iosOptions = const DocScanKitOptionsIOS(), + }); + + /// Default platform-specific options for Android devices. + /// + /// These options control Android-specific behavior such as page limits, + /// scanner modes, and gallery import capabilities. + /// + /// These serve as default values that can be overridden in individual + /// [scanner] method calls. + final DocScanKitOptionsAndroid androidOptions; + + /// Default platform-specific options for iOS devices. + /// + /// These options control iOS-specific behavior such as modal presentation style, + /// image compression quality, and UI tint colors. + /// + /// These serve as default values that can be overridden in individual + /// [scanner] method calls. + final DocScanKitOptionsIOS iosOptions; + + /// Launches the native document scanner interface and returns scanned results. + /// + /// This method opens the platform-specific document scanner (Camera on iOS, + /// ML Kit Document Scanner on Android) and allows users to scan one or more + /// document pages. + /// + /// [androidOptions] - Optional Android-specific options for this scan session. + /// If not provided, uses the instance's default [androidOptions]. + /// [iosOptions] - Optional iOS-specific options for this scan session. + /// If not provided, uses the instance's default [iosOptions]. + /// + /// The scanner behavior is controlled by the platform-specific options: + /// - On Android: Uses provided [androidOptions] or instance defaults + /// - On iOS: Uses provided [iosOptions] or instance defaults + /// - Throws [UnsupportedError] on unsupported platforms + /// + /// Returns a [Future] that completes with a list of [DocScanKitResult] objects, + /// each containing the file path and type (JPEG or PDF) of the scanned content. + /// + /// The format of results depends on the [DocScanKitFormat] setting: + /// - [DocScanKitFormat.images]: Returns individual JPEG image files + /// - [DocScanKitFormat.document]: Returns a single PDF file with all pages + /// + /// Throws [PlatformException] if the scanner fails to initialize or + /// if camera permissions are not granted. + /// Throws [UnsupportedError] if called on an unsupported platform. + /// + /// Example: + /// ```dart + /// try { + /// // Use default options + /// final results = await docScanKit.scanner(); + /// + /// // Or override options for this call + /// final customResults = await docScanKit.scanner( + /// androidOptions: DocScanKitOptionsAndroid(pageLimit: 5), + /// iosOptions: DocScanKitOptionsIOS(compressionQuality: 0.5), + /// ); + /// + /// for (final result in results) { + /// print('Scanned ${result.type.name}: ${result.path}'); + /// } + /// } catch (e) { + /// print('Scanning failed: $e'); + /// } + /// ``` + Future> scanner({ + DocScanKitOptionsAndroid? androidOptions, + DocScanKitOptionsIOS? iosOptions, + }) { + final DocScanKitOptions options; + + if (Platform.isAndroid) { + options = androidOptions ?? this.androidOptions; + } else if (Platform.isIOS) { + options = iosOptions ?? this.iosOptions; + } else { + throw UnsupportedError('Unsupported platform'); + } + + return DocScanKitPlatform.instance.scanner(options); + } + + /// Performs Optical Character Recognition (OCR) on the provided image bytes. + /// + /// This method uses platform-specific text recognition capabilities to extract + /// text content from image data. The image should contain readable text for + /// optimal results. + /// + /// [imageBytes] - The raw bytes of the image file (JPEG, PNG, etc.) + /// + /// Returns a [Future] that completes with the recognized text as a [String]. + /// If no text is found or recognition fails, returns an empty string. + /// + /// The accuracy of text recognition depends on: + /// - Image quality and resolution + /// - Text clarity and font size + /// - Lighting conditions when the image was captured + /// - Language of the text (platform-dependent support) + /// + /// Example: + /// ```dart + /// final imageBytes = await File('document.jpg').readAsBytes(); + /// final recognizedText = await docScanKit.recognizeText(imageBytes); + /// print('Extracted text: $recognizedText'); + /// ``` + Future recognizeText(final List imageBytes) => + DocScanKitPlatform.instance.recognizeText(imageBytes); + + /// Scans and decodes QR codes from the provided image bytes. + /// + /// This method analyzes the image data to detect and decode any QR codes + /// present in the image. It can handle various QR code formats and sizes. + /// + /// [imageBytes] - The raw bytes of the image file containing QR code(s) + /// + /// Returns a [Future] that completes with the decoded QR code content as a [String]. + /// If no QR code is found or decoding fails, returns an empty string. + /// + /// For optimal QR code detection: + /// - Ensure the QR code is clearly visible and not distorted + /// - Provide adequate contrast between the code and background + /// - Avoid excessive blur or low resolution images + /// + /// Example: + /// ```dart + /// final imageBytes = await File('qr_code.jpg').readAsBytes(); + /// final qrContent = await docScanKit.scanQrCode(imageBytes); + /// if (qrContent.isNotEmpty) { + /// print('QR Code content: $qrContent'); + /// } else { + /// print('No QR code found'); + /// } + /// ``` + Future scanQrCode(final List imageBytes) => + DocScanKitPlatform.instance.scanQrCode(imageBytes); + + /// Releases any resources and closes active scanner sessions. + /// + /// This method should be called when the DocScanKit instance is no longer needed, + /// particularly on Android where it helps free up ML Kit resources and prevents + /// memory leaks. + /// + /// On iOS, this method completes immediately as resources are managed automatically. + /// On Android, it properly disposes of the ML Kit document scanner and related resources. + /// + /// It's recommended to call this method in the dispose() method of your widget + /// or when your app is shutting down. + /// + /// Example: + /// ```dart + /// @override + /// void dispose() { + /// docScanKit.close(); + /// super.dispose(); + /// } + /// ``` + Future close() => DocScanKitPlatform.instance.close(); +} diff --git a/lib/src/doc_scan_kit_method_channel.dart b/lib/src/doc_scan_kit_method_channel.dart index 966b053..649886e 100644 --- a/lib/src/doc_scan_kit_method_channel.dart +++ b/lib/src/doc_scan_kit_method_channel.dart @@ -1,71 +1,143 @@ -import 'package:doc_scan_kit/src/options/android_options.dart'; -import 'package:doc_scan_kit/src/options/ios_options.dart'; -import 'package:doc_scan_kit/src/options/scan_result.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_method.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_options.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_result.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_result_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'doc_scan_kit_platform_interface.dart'; -/// An implementation of [DocScanKitPlatform] that uses method channels. -class MethodChannelDocScanKit extends DocScanKitPlatform { - /// The method channel used to interact with the native platform. +import 'doc_scan_kit_platform.dart'; + +/// Abstract base class for method channel implementation of [DocScanKitPlatform]. +/// +/// This class provides the common implementation for communicating with +/// native platform code through Flutter's method channel system. It handles +/// serialization of method calls and deserialization of results. +/// +/// Platform-specific implementations ([DocScanKitPlatformAndroid] and +/// [DocScanKitPlatformIOS]) extend this class to provide the actual +/// registration and platform-specific behavior. +/// +/// This class is abstract and cannot be instantiated directly. Use the +/// platform-specific implementations instead. +abstract class DocScanKitMethodChannel extends DocScanKitPlatform { + /// The method channel used to communicate with native platform implementations. + /// + /// This channel is used for all method calls to native code including + /// document scanning, text recognition, and QR code scanning operations. + /// + /// The channel name 'doc_scan_kit' must match the channel name registered + /// in the native platform implementations. @visibleForTesting final methodChannel = const MethodChannel('doc_scan_kit'); - /// Instance id. + /// Unique identifier for this plugin instance. + /// + /// This ID is generated based on the current timestamp and is used to + /// identify this specific instance when communicating with native code. + /// It helps distinguish between multiple plugin instances and manage + /// platform-specific resources properly. final id = DateTime.now().microsecondsSinceEpoch.toString(); + + /// Invokes the native document scanner through method channel communication. + /// + /// This method serializes the provided [options] and sends them to the native + /// platform implementation via the 'scanKit#startDocumentScanner' method call. + /// + /// [options] - Platform-specific configuration options that control scanner behavior. + /// + /// The native implementation will: + /// 1. Launch the appropriate document scanner UI + /// 2. Handle user interactions and document capture + /// 3. Process scanned images according to the format setting + /// 4. Return results as a list of file paths and types + /// + /// Returns a [Future] that completes with a list of [DocScanKitResult] objects. + /// Each result contains the file path and type (JPEG or PDF) of scanned content. + /// + /// Throws [PlatformException] if the native scanner encounters an error. @override - Future> scanner( - final DocumentScanKitOptionsAndroid androidOptions, - final DocumentScanKitOptionsiOS iosOptions, - ) async { - return (await methodChannel.invokeMethod>( - 'scanKit#startDocumentScanner', { - 'androidOptions': androidOptions.toJson(), - 'id': id, - 'iosOptions': iosOptions.toJson() - })) - ?.map( - (e) { - e as Map; - return ScanResult(imagePath: e['path'], imagesBytes: e['bytes']); - }, - ).toList() ?? + Future> scanner(final DocScanKitOptions options) async { + final result = await methodChannel.invokeMethod>( + DocScanKitMethod.startDocumentScanner.platformName, + {'id': id, 'options': options.toJson()}, + ); + + return result + ?.cast() + .map((e) => DocScanKitResult( + type: DocScanKitResultType.values.byName(e['type']), + path: e['path'] as String, + )) + .toList() ?? []; } + /// Closes the scanner and releases native resources through method channel. + /// + /// This method notifies the native platform implementation to clean up + /// any resources associated with this plugin instance via the + /// 'scanKit#closeDocumentScanner' method call. + /// + /// The native implementation will: + /// 1. Dispose of any active scanner sessions + /// 2. Release ML Kit or other native resources + /// 3. Clean up temporary files if any + /// + /// This is particularly important on Android where ML Kit resources + /// need explicit cleanup to prevent memory leaks. + /// + /// Returns a [Future] that completes when the cleanup is finished. @override - - /// Recognizes text from image bytes - Future recognizeText(List imageBytes) async { - final result = await methodChannel.invokeMethod( - 'scanKit#recognizeText', - { - 'imageBytes': imageBytes, - 'id': id, - }, + Future close() { + return methodChannel.invokeMethod( + DocScanKitMethod.closeDocumentScanner.platformName, + {'id': id}, ); - return result ?? ''; } + /// Invokes native text recognition through method channel communication. + /// + /// This method sends the image bytes to the native platform implementation + /// via the 'scanKit#recognizeText' method call for OCR processing. + /// + /// [imageBytes] - Raw bytes of the image file to process for text recognition. + /// + /// The native implementation will: + /// 1. Convert the byte array to a platform-specific image format + /// 2. Apply OCR using platform-specific text recognition APIs + /// 3. Return the extracted text as a string + /// + /// Returns a [Future] that completes with the recognized text as a [String]. + /// Returns an empty string if no text is found or recognition fails. @override - - /// Scans for QR codes in image bytes - Future scanQrCode(List imageBytes) async { + Future recognizeText(final List imageBytes) async { final result = await methodChannel.invokeMethod( - 'scanKit#scanQrCode', - { - 'imageBytes': imageBytes, - 'id': id, - }, + DocScanKitMethod.recognizeText.platformName, + {'id': id, 'imageBytes': imageBytes}, ); return result ?? ''; } + /// Invokes native QR code scanning through method channel communication. + /// + /// This method sends the image bytes to the native platform implementation + /// via the 'scanKit#scanQrCode' method call for barcode detection and decoding. + /// + /// [imageBytes] - Raw bytes of the image file containing QR code(s) to scan. + /// + /// The native implementation will: + /// 1. Convert the byte array to a platform-specific image format + /// 2. Detect and decode QR codes using platform-specific barcode APIs + /// 3. Return the decoded content as a string + /// + /// Returns a [Future] that completes with the QR code content as a [String]. + /// Returns an empty string if no QR code is found or decoding fails. @override - - /// Close the detector and release resources. - Future close() { - return methodChannel - .invokeMethod("scanKit#closeDocumentScanner", {'id': id}); + Future scanQrCode(final List imageBytes) async { + final result = await methodChannel.invokeMethod( + DocScanKitMethod.scanQrCode.platformName, + {'id': id, 'imageBytes': imageBytes}, + ); + return result ?? ''; } } diff --git a/lib/src/doc_scan_kit_platform.dart b/lib/src/doc_scan_kit_platform.dart new file mode 100644 index 0000000..f7c2f4b --- /dev/null +++ b/lib/src/doc_scan_kit_platform.dart @@ -0,0 +1,96 @@ +import 'package:doc_scan_kit/src/doc_scan_kit_method_channel.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_options.dart'; +import 'package:doc_scan_kit/src/models/doc_scan_kit_result.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// Abstract base class for platform-specific implementations of DocScanKit. +/// +/// This class defines the interface that platform-specific implementations +/// must implement. It uses the plugin_platform_interface pattern to ensure +/// that platform implementations are properly verified and registered. +/// +/// Platform implementations should extend this class and implement all +/// abstract methods to provide native functionality for document scanning, +/// text recognition, and QR code scanning. +abstract class DocScanKitPlatform extends PlatformInterface { + /// Constructs a [DocScanKitPlatform] with the required verification token. + /// + /// This constructor is used by platform implementations to ensure proper + /// registration with the plugin_platform_interface system. + DocScanKitPlatform() : super(token: _token); + + /// Verification token used to ensure platform implementations are properly registered. + static final _token = Object(); + + /// The current platform implementation instance. + /// + /// Defaults to [DocScanKitMethodChannel] which provides method channel + /// communication with native platform code. + static late DocScanKitPlatform _instance; + + /// The default instance of [DocScanKitPlatform] to use for all operations. + /// + /// This getter returns the currently registered platform implementation. + /// By default, it returns [DocScanKitMethodChannel], but can be overridden + /// by platform-specific plugins or for testing purposes. + static DocScanKitPlatform get instance => _instance; + + /// Sets the platform implementation instance. + /// + /// Platform-specific implementations should call this setter with their own + /// platform-specific class that extends [DocScanKitPlatform] when they + /// register themselves. + /// + /// The [instance] must be a valid implementation that passes token verification. + /// + /// Throws [AssertionError] if the instance doesn't have the correct verification token. + static set instance(DocScanKitPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Launches the native document scanner and returns scan results. + /// + /// This method must be implemented by platform-specific classes to provide + /// native document scanning functionality. The behavior should match the + /// platform's native document scanning capabilities. + /// + /// [options] - Platform-specific configuration options that control scanner behavior. + /// + /// Returns a [Future] that completes with a list of [DocScanKitResult] objects + /// representing the scanned documents. + /// + /// Should throw [PlatformException] if scanning fails or permissions are denied. + Future> scanner(final DocScanKitOptions options); + + /// Performs text recognition on the provided image bytes. + /// + /// Platform implementations should use native OCR capabilities to extract + /// text from the image data. + /// + /// [imageBytes] - Raw bytes of the image file to process. + /// + /// Returns a [Future] that completes with the recognized text as a [String]. + /// Should return an empty string if no text is found or recognition fails. + Future recognizeText(final List imageBytes); + + /// Scans for QR codes in the provided image bytes. + /// + /// Platform implementations should use native barcode/QR code detection + /// capabilities to decode QR codes from the image data. + /// + /// [imageBytes] - Raw bytes of the image file containing QR code(s). + /// + /// Returns a [Future] that completes with the decoded QR code content as a [String]. + /// Should return an empty string if no QR code is found or decoding fails. + Future scanQrCode(final List imageBytes); + + /// Releases platform-specific resources and closes active sessions. + /// + /// Platform implementations should override this method to properly dispose + /// of any native resources, particularly on Android where ML Kit resources + /// need explicit cleanup. + /// + /// Returns a [Future] that completes when cleanup is finished. + Future close(); +} diff --git a/lib/src/doc_scan_kit_platform_android.dart b/lib/src/doc_scan_kit_platform_android.dart new file mode 100644 index 0000000..4821589 --- /dev/null +++ b/lib/src/doc_scan_kit_platform_android.dart @@ -0,0 +1,30 @@ +import 'package:doc_scan_kit/src/doc_scan_kit_method_channel.dart'; + +import 'doc_scan_kit_platform.dart'; + +/// Android-specific implementation of [DocScanKitPlatform]. +/// +/// This class provides the Android implementation for document scanning, +/// text recognition, and QR code scanning using Google's ML Kit APIs +/// through method channel communication. +/// +/// The implementation extends [DocScanKitMethodChannel] to inherit the +/// common method channel communication logic while providing Android-specific +/// registration and initialization. +/// +/// This class is automatically registered when the plugin is initialized +/// on Android devices through the Flutter plugin system. +class DocScanKitPlatformAndroid extends DocScanKitMethodChannel { + /// Registers this Android implementation as the default platform implementation. + /// + /// This method is called automatically by the Flutter plugin system + /// when the app runs on Android devices. It sets this instance as the + /// active platform implementation for all DocScanKit operations. + /// + /// The registration ensures that all document scanning, text recognition, + /// and QR code scanning operations will use Android-specific native code + /// through the ML Kit APIs. + static void registerWith() { + DocScanKitPlatform.instance = DocScanKitPlatformAndroid(); + } +} diff --git a/lib/src/doc_scan_kit_platform_interface.dart b/lib/src/doc_scan_kit_platform_interface.dart deleted file mode 100644 index 27cb9ad..0000000 --- a/lib/src/doc_scan_kit_platform_interface.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:doc_scan_kit/src/options/android_options.dart'; -import 'package:doc_scan_kit/src/options/ios_options.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'doc_scan_kit_method_channel.dart'; -import 'options/scan_result.dart'; - -abstract class DocScanKitPlatform extends PlatformInterface { - /// Constructs a DocScanKitPlatform. - DocScanKitPlatform() : super(token: _token); - - static final Object _token = Object(); - - static DocScanKitPlatform _instance = MethodChannelDocScanKit(); - - /// The default instance of [DocScanKitPlatform] to use. - /// - /// Defaults to [MethodChannelDocScanKit]. - static DocScanKitPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [DocScanKitPlatform] when - /// they register themselves. - static set instance(DocScanKitPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - Future> scanner( - final DocumentScanKitOptionsAndroid androidOptions, - final DocumentScanKitOptionsiOS iosOptions); - - /// Recognizes text from image bytes - Future recognizeText(List imageBytes); - - /// Scans for QR codes in image bytes - Future scanQrCode(List imageBytes); - - /// Disposes of any resources used by the plugin on Android. - Future close(); -} diff --git a/lib/src/doc_scan_kit_platform_ios.dart b/lib/src/doc_scan_kit_platform_ios.dart new file mode 100644 index 0000000..5c961c0 --- /dev/null +++ b/lib/src/doc_scan_kit_platform_ios.dart @@ -0,0 +1,31 @@ +import 'package:doc_scan_kit/src/doc_scan_kit_method_channel.dart'; + +import 'doc_scan_kit_platform.dart'; + +/// iOS-specific implementation of [DocScanKitPlatform]. +/// +/// This class provides the iOS implementation for document scanning, +/// text recognition, and QR code scanning using Apple's native APIs +/// including VNDocumentCameraViewController and Vision framework +/// through method channel communication. +/// +/// The implementation extends [DocScanKitMethodChannel] to inherit the +/// common method channel communication logic while providing iOS-specific +/// registration and initialization. +/// +/// This class is automatically registered when the plugin is initialized +/// on iOS devices through the Flutter plugin system. +class DocScanKitPlatformIOS extends DocScanKitMethodChannel { + /// Registers this iOS implementation as the default platform implementation. + /// + /// This method is called automatically by the Flutter plugin system + /// when the app runs on iOS devices. It sets this instance as the + /// active platform implementation for all DocScanKit operations. + /// + /// The registration ensures that all document scanning, text recognition, + /// and QR code scanning operations will use iOS-specific native code + /// through Apple's Vision and Document Camera frameworks. + static void registerWith() { + DocScanKitPlatform.instance = DocScanKitPlatformIOS(); + } +} diff --git a/lib/src/models/doc_scan_kit_format.dart b/lib/src/models/doc_scan_kit_format.dart new file mode 100644 index 0000000..3ee9b14 --- /dev/null +++ b/lib/src/models/doc_scan_kit_format.dart @@ -0,0 +1,47 @@ +/// Defines the output format for scanned documents. +/// +/// This enum determines how the document scanner processes and returns +/// scanned content. The choice affects both the file format and the +/// number of files returned from a scanning session. +/// +/// Example usage: +/// ```dart +/// final options = DocScanKitOptionsAndroid( +/// format: DocScanKitFormat.document, // Creates a single PDF +/// ); +/// +/// final results = await docScanKit.scanner(); +/// // With 'document' format: results contains 1 PDF file +/// // With 'images' format: results contains multiple JPEG files +/// ``` +enum DocScanKitFormat { + /// Returns individual JPEG image files for each scanned page. + /// + /// When this format is selected: + /// - Each scanned page becomes a separate JPEG file + /// - Multiple files are returned for multi-page documents + /// - Files are stored in the app's documents directory + /// - Each file has a unique filename with .jpeg extension + /// + /// This format is ideal when you need to: + /// - Process individual pages separately + /// - Display pages in a custom gallery interface + /// - Apply different processing to different pages + /// - Maintain maximum image quality without PDF compression + images, + + /// Returns a single PDF document containing all scanned pages. + /// + /// When this format is selected: + /// - All scanned pages are combined into one PDF file + /// - Only one file is returned regardless of page count + /// - Each page becomes a separate page within the PDF + /// - The PDF is stored in the app's documents directory + /// + /// This format is ideal when you need to: + /// - Create a complete document from multiple pages + /// - Share or store documents in a standard format + /// - Maintain page order and document structure + /// - Reduce the number of files to manage + document; +} diff --git a/lib/src/models/doc_scan_kit_method.dart b/lib/src/models/doc_scan_kit_method.dart new file mode 100644 index 0000000..953cc46 --- /dev/null +++ b/lib/src/models/doc_scan_kit_method.dart @@ -0,0 +1,70 @@ +/// Enumeration of available DocScanKit method calls for platform communication. +/// +/// This enum defines all the method calls that can be made to the native +/// platform implementations through method channels. Each enum value +/// corresponds to a specific functionality provided by the plugin. +/// +/// The enum provides a type-safe way to reference method names and +/// automatically generates the correct platform method names using +/// the 'scanKit#' prefix convention. +/// +/// Example usage: +/// ```dart +/// final methodName = DocScanKitMethod.startDocumentScanner.platformName; +/// // Returns: 'scanKit#startDocumentScanner' +/// ``` +enum DocScanKitMethod { + /// Initiates the native document scanner interface. + /// + /// This method launches the platform-specific document scanner: + /// - Android: Google ML Kit Document Scanner + /// - iOS: Apple VNDocumentCameraViewController + /// + /// The scanner allows users to capture one or more document pages + /// with automatic edge detection and image enhancement. + startDocumentScanner, + + /// Closes the document scanner and releases associated resources. + /// + /// This method properly disposes of scanner resources and cleans up: + /// - Active scanner sessions + /// - ML Kit resources (Android) + /// - Temporary files and memory allocations + /// + /// Important for preventing memory leaks, especially on Android. + closeDocumentScanner, + + /// Performs Optical Character Recognition (OCR) on image data. + /// + /// This method processes image bytes to extract readable text using: + /// - Android: Google ML Kit Text Recognition API + /// - iOS: Apple Vision Text Recognition framework + /// + /// Returns the recognized text as a string or empty string if no text found. + recognizeText, + + /// Scans and decodes QR codes from image data. + /// + /// This method analyzes image bytes to detect and decode QR codes using: + /// - Android: Google ML Kit Barcode Scanning API + /// - iOS: Apple Vision Barcode Detection framework + /// + /// Returns the decoded QR code content or empty string if no QR code found. + scanQrCode; + + /// Generates the platform-specific method name for native communication. + /// + /// This getter converts the enum value to the method name format expected + /// by the native platform implementations. All method names follow the + /// 'scanKit#methodName' convention. + /// + /// Returns a [String] in the format 'scanKit#enumName' for use in + /// method channel communication. + /// + /// Example: + /// ```dart + /// DocScanKitMethod.startDocumentScanner.platformName + /// // Returns: 'scanKit#startDocumentScanner' + /// ``` + String get platformName => 'scanKit#$name'; +} diff --git a/lib/src/models/doc_scan_kit_options.dart b/lib/src/models/doc_scan_kit_options.dart new file mode 100644 index 0000000..36df319 --- /dev/null +++ b/lib/src/models/doc_scan_kit_options.dart @@ -0,0 +1,35 @@ +import 'package:doc_scan_kit/src/models/doc_scan_kit_format.dart'; + +/// Abstract base class for platform-specific document scanner configuration options. +/// +/// This class defines the common interface and shared properties for configuring +/// the document scanner behavior across different platforms. Platform-specific +/// implementations should extend this class and provide their own additional options. +/// +/// The main shared property is [format], which determines whether the scanner +/// should return individual image files or a combined PDF document. +abstract class DocScanKitOptions { + /// Creates a new [DocScanKitOptions] instance with the specified format. + /// + /// [format] - Determines the output format of scanned documents. + /// Defaults to [DocScanKitFormat.images] if not specified. + const DocScanKitOptions({this.format = DocScanKitFormat.images}); + + /// The output format for scanned documents. + /// + /// This property controls how the scanner processes and returns scanned content: + /// - [DocScanKitFormat.images]: Returns individual JPEG image files for each scanned page + /// - [DocScanKitFormat.document]: Returns a single PDF file containing all scanned pages + /// + /// The default value is [DocScanKitFormat.images]. + final DocScanKitFormat format; + + /// Converts this options object to a JSON map for platform communication. + /// + /// Platform-specific implementations must override this method to serialize + /// their configuration options into a format that can be sent to native code + /// through method channels. + /// + /// Returns a [Map] containing the serialized options. + Map toJson(); +} diff --git a/lib/src/models/doc_scan_kit_options_android.dart b/lib/src/models/doc_scan_kit_options_android.dart new file mode 100644 index 0000000..78d4534 --- /dev/null +++ b/lib/src/models/doc_scan_kit_options_android.dart @@ -0,0 +1,89 @@ +import 'package:doc_scan_kit/src/models/doc_scan_kit_options.dart'; +import 'package:doc_scan_kit/src/models/scanner_mode_android.dart'; + +/// Android-specific configuration options for the document scanner. +/// +/// This class extends [DocScanKitOptions] to provide Android-specific settings +/// that control the behavior of Google's ML Kit Document Scanner API. +/// +/// These options allow fine-tuning of the scanner experience on Android devices, +/// including page limits, scanner modes, and gallery import capabilities. +/// +/// Example usage: +/// ```dart +/// final androidOptions = DocScanKitOptionsAndroid( +/// format: DocScanKitFormat.document, +/// pageLimit: 10, +/// scannerMode: ScannerModeAndroid.full, +/// isGalleryImport: true, +/// ); +/// ``` +class DocScanKitOptionsAndroid extends DocScanKitOptions { + /// Creates Android-specific scanner options with the given configuration. + /// + /// [format] - Output format for scanned documents (inherited from parent). + /// [pageLimit] - Maximum number of pages that can be scanned in one session. + /// [scannerMode] - Scanner mode determining available features and capabilities. + /// [isGalleryImport] - Whether users can import images from the photo gallery. + const DocScanKitOptionsAndroid({ + super.format, + this.pageLimit = 1, + this.scannerMode = ScannerModeAndroid.full, + this.isGalleryImport = true, + }); + + /// Maximum number of pages that can be scanned in a single scanning session. + /// + /// This property limits how many document pages a user can capture before + /// the scanner automatically completes the session. Setting a higher limit + /// allows for multi-page document scanning. + /// + /// Valid range: 1 to 100 pages + /// Default value: 1 + /// + /// Note: The actual limit may be constrained by device memory and performance. + final int pageLimit; + + /// Scanner mode that determines which features and capabilities are enabled. + /// + /// This property controls the level of functionality available in the scanner: + /// - [ScannerModeAndroid.base]: Basic editing capabilities (crop, rotate, reorder) + /// - [ScannerModeAndroid.filter]: Adds image filters to base mode + /// - [ScannerModeAndroid.full]: Adds ML-powered cleaning and future features + /// + /// Default value: [ScannerModeAndroid.full] + /// + /// Higher modes provide more features but may require more processing power + /// and potentially longer processing times. + final ScannerModeAndroid scannerMode; + + /// Whether users can import existing images from the device's photo gallery. + /// + /// When enabled, the scanner interface will include an option for users to + /// select existing photos from their gallery instead of only capturing new ones. + /// This is useful for processing documents that were previously photographed. + /// + /// Default value: true + /// + /// Note: Gallery import requires appropriate storage permissions on the device. + final bool isGalleryImport; + + /// Serializes these Android options to a JSON map for native communication. + /// + /// This method converts all Android-specific configuration options into a + /// map format that can be sent to the native Android implementation through + /// method channels. + /// + /// Returns a [Map] containing: + /// - 'format': The output format as a string name + /// - 'pageLimit': The maximum number of pages as an integer + /// - 'scannerMode': The scanner mode as a string name + /// - 'isGalleryImport': The gallery import setting as a boolean + @override + Map toJson() => { + 'format': format.name, + 'pageLimit': pageLimit, + 'scannerMode': scannerMode.name, + 'isGalleryImport': isGalleryImport, + }; +} diff --git a/lib/src/models/doc_scan_kit_options_ios.dart b/lib/src/models/doc_scan_kit_options_ios.dart new file mode 100644 index 0000000..b77d850 --- /dev/null +++ b/lib/src/models/doc_scan_kit_options_ios.dart @@ -0,0 +1,100 @@ +import 'dart:ui'; + +import 'package:doc_scan_kit/src/models/doc_scan_kit_options.dart'; +import 'package:doc_scan_kit/src/models/modal_presentation_style_ios.dart'; + +/// iOS-specific configuration options for the document scanner. +/// +/// This class extends [DocScanKitOptions] to provide iOS-specific settings +/// that control the behavior of Apple's VNDocumentCameraViewController. +/// +/// These options allow customization of the scanner presentation style, +/// image compression quality, and UI appearance on iOS devices. +/// +/// Example usage: +/// ```dart +/// final iosOptions = DocScanKitOptionsIOS( +/// format: DocScanKitFormat.document, +/// modalPresentationStyle: ModalPresentationStyleIOS.overFullScreen, +/// compressionQuality: 0.8, +/// color: Colors.blue, +/// ); +/// ``` +class DocScanKitOptionsIOS extends DocScanKitOptions { + /// Creates iOS-specific scanner options with the given configuration. + /// + /// [format] - Output format for scanned documents (inherited from parent). + /// [modalPresentationStyle] - How the scanner view controller is presented. + /// [compressionQuality] - JPEG compression quality for scanned images. + /// [color] - Tint color for scanner UI elements. + /// + /// Throws [AssertionError] if [compressionQuality] is not between 0.0 and 1.0. + const DocScanKitOptionsIOS({ + super.format, + this.modalPresentationStyle = ModalPresentationStyleIOS.overFullScreen, + this.compressionQuality = 1.0, + this.color, + }) : assert( + !(compressionQuality > 1.0 || compressionQuality < 0.0), + 'The compression quality value must be between 0.0 and 1.0', + ); + + /// The modal presentation style for the document scanner view controller. + /// + /// This property determines how the scanner interface is presented to the user: + /// - [ModalPresentationStyleIOS.automatic]: System chooses the best style + /// - [ModalPresentationStyleIOS.overFullScreen]: Covers the entire screen + /// - [ModalPresentationStyleIOS.currentContext]: Presented over current context + /// - [ModalPresentationStyleIOS.popover]: Presented in a popover (iPad) + /// + /// Default value: [ModalPresentationStyleIOS.overFullScreen] + /// + /// The presentation style affects the visual transition and how the scanner + /// appears relative to the presenting view controller. + final ModalPresentationStyleIOS modalPresentationStyle; + + /// JPEG compression quality for scanned images. + /// + /// This property controls the quality vs. file size trade-off for scanned images: + /// - 0.0: Maximum compression, smallest file size, lowest quality + /// - 1.0: No compression, largest file size, highest quality + /// + /// Valid range: 0.0 to 1.0 (inclusive) + /// Default value: 1.0 (no compression) + /// + /// Lower values result in smaller file sizes but may reduce image quality. + /// This setting applies to both individual image files and images within PDF documents. + final double compressionQuality; + + /// Tint color applied to scanner UI elements and document detection overlay. + /// + /// This property customizes the appearance of interactive elements in the scanner: + /// - Navigation bar buttons and controls + /// - Document detection overlay borders + /// - Action buttons and indicators + /// + /// If null, the system default tint color is used. + /// + /// **Important:** Setting the alpha channel to 0 will make buttons fully transparent, + /// which may render them unusable. Ensure adequate visibility for user interaction. + final Color? color; + + /// Serializes these iOS options to a JSON map for native communication. + /// + /// This method converts all iOS-specific configuration options into a + /// map format that can be sent to the native iOS implementation through + /// method channels. + /// + /// Returns a [Map] containing: + /// - 'format': The output format as a string name + /// - 'modalPresentationStyle': The presentation style as a string name + /// - 'compressionQuality': The compression quality as a double (0.0-1.0) + /// - 'color': The tint color as an array of RGBA values, or empty array if null + @override + Map toJson() => { + 'format': format.name, + 'modalPresentationStyle': modalPresentationStyle.name, + 'compressionQuality': compressionQuality, + 'color': color != null ? [color?.r, color?.g, color?.b, color?.a] : [], + }; +} diff --git a/lib/src/models/doc_scan_kit_result.dart b/lib/src/models/doc_scan_kit_result.dart new file mode 100644 index 0000000..6836f82 --- /dev/null +++ b/lib/src/models/doc_scan_kit_result.dart @@ -0,0 +1,52 @@ +import 'package:doc_scan_kit/src/models/doc_scan_kit_result_type.dart'; + +/// Represents the result of a document scanning operation. +/// +/// This class encapsulates the information about a single scanned document, +/// including its file type and the path where it's stored on the device. +/// +/// Each [DocScanKitResult] corresponds to either: +/// - A single JPEG image file (when format is [DocScanKitFormat.images]) +/// - A PDF document containing multiple pages (when format is [DocScanKitFormat.document]) +/// +/// Example usage: +/// ```dart +/// final results = await docScanKit.scanner(); +/// for (final result in results) { +/// print('Scanned ${result.type.name}: ${result.path}'); +/// final file = File(result.path); +/// // Process the scanned file... +/// if (await file.exists()) { +/// // File operations... +/// } +/// } +/// ``` +class DocScanKitResult { + /// Creates a new [DocScanKitResult] with the specified type and file path. + /// + /// [type] - The format/type of the scanned document file. + /// [path] - The file system path where the scanned document is stored. + const DocScanKitResult({required this.type, required this.path}); + + /// The type/format of the scanned document file. + /// + /// This property indicates what kind of file was created during the scan: + /// - [DocScanKitResultType.jpeg]: Individual JPEG image file + /// - [DocScanKitResultType.pdf]: PDF document file + /// + /// The type corresponds to the [DocScanKitFormat] setting used during scanning. + final DocScanKitResultType type; + + /// The file system path where the scanned document is stored. + /// + /// This path points to the location on the device where the scanned file + /// has been saved. The file can be accessed using standard file I/O operations. + /// + /// The path is always provided and points to a valid file location in the + /// app's documents directory. It includes the full filename with the + /// appropriate extension (.jpg for JPEG files, .pdf for PDF documents). + /// + /// The file is guaranteed to exist at this path when the result is returned, + /// though it may be moved or deleted by the system or app later. + final String path; +} diff --git a/lib/src/models/doc_scan_kit_result_type.dart b/lib/src/models/doc_scan_kit_result_type.dart new file mode 100644 index 0000000..aca21f1 --- /dev/null +++ b/lib/src/models/doc_scan_kit_result_type.dart @@ -0,0 +1,43 @@ +/// Represents the file type of a scanned document result. +/// +/// This enum indicates the actual file format of the scanned content +/// returned by the document scanner. The type corresponds to the +/// [DocScanKitFormat] setting used during scanning. +/// +/// Example usage: +/// ```dart +/// final results = await docScanKit.scanner(); +/// for (final result in results) { +/// switch (result.type) { +/// case DocScanKitResultType.jpeg: +/// print('Image file: ${result.path}'); +/// break; +/// case DocScanKitResultType.pdf: +/// print('PDF document: ${result.path}'); +/// break; +/// } +/// } +/// ``` +enum DocScanKitResultType { + /// Indicates the result is a JPEG image file. + /// + /// This type is returned when: + /// - [DocScanKitFormat.images] was used during scanning + /// - Each scanned page is saved as an individual JPEG file + /// - The file has a .jpeg or .jpg extension + /// + /// JPEG files can be displayed directly in image widgets + /// and processed using standard image manipulation libraries. + jpeg, + + /// Indicates the result is a PDF document file. + /// + /// This type is returned when: + /// - [DocScanKitFormat.document] was used during scanning + /// - All scanned pages are combined into a single PDF file + /// - The file has a .pdf extension + /// + /// PDF files can be viewed using PDF viewers, shared as documents, + /// or processed using PDF manipulation libraries. + pdf; +} diff --git a/lib/src/models/modal_presentation_style_ios.dart b/lib/src/models/modal_presentation_style_ios.dart new file mode 100644 index 0000000..c9f6ecb --- /dev/null +++ b/lib/src/models/modal_presentation_style_ios.dart @@ -0,0 +1,64 @@ +/// iOS modal presentation styles for the document scanner view controller. +/// +/// This enum defines how the document scanner interface is presented +/// to the user on iOS devices. Each style affects the visual transition +/// and layout behavior of the scanner view controller. +/// +/// These styles correspond to UIKit's UIModalPresentationStyle options +/// and provide different user experience patterns. +/// +/// Example usage: +/// ```dart +/// final iosOptions = DocScanKitOptionsIOS( +/// modalPresentationStyle: ModalPresentationStyleIOS.overFullScreen, +/// ); +/// ``` +enum ModalPresentationStyleIOS { + /// The system automatically chooses the most appropriate presentation style. + /// + /// This style allows iOS to select the best presentation method based on: + /// - Device type (iPhone vs iPad) + /// - Screen size and orientation + /// - Current interface idiom + /// - System preferences and accessibility settings + /// + /// This is often the safest choice for apps that need to work well + /// across different iOS devices and configurations. + automatic, + + /// Presents the scanner over the current view controller's content. + /// + /// In this style: + /// - The scanner is presented within the bounds of the current context + /// - The underlying content may remain partially visible + /// - Useful for maintaining visual context with the presenting view + /// - May not cover the entire screen on larger devices + /// + /// This style works well when you want to maintain some connection + /// to the presenting interface. + currentContext, + + /// Presents the scanner covering the entire screen. + /// + /// In this style: + /// - The scanner completely covers the screen content + /// - Provides maximum screen real estate for scanning + /// - Creates a focused, distraction-free scanning experience + /// - Works consistently across all device sizes + /// + /// This is the recommended style for most scanning scenarios as it + /// provides the best user experience for document capture. + overFullScreen, + + /// Presents the scanner in a popover view (primarily for iPad). + /// + /// In this style: + /// - The scanner appears in a floating popover window + /// - Particularly useful on iPad with larger screen sizes + /// - Allows users to see and interact with underlying content + /// - May automatically adapt to other styles on smaller devices + /// + /// This style is ideal for iPad apps where the scanner is part of + /// a larger workflow and context preservation is important. + popover, +} diff --git a/lib/src/models/scanner_mode_android.dart b/lib/src/models/scanner_mode_android.dart new file mode 100644 index 0000000..a55de8f --- /dev/null +++ b/lib/src/models/scanner_mode_android.dart @@ -0,0 +1,70 @@ +/// Android scanner modes that determine available features and capabilities. +/// +/// This enum defines the different levels of functionality available in +/// Google's ML Kit Document Scanner on Android devices. Each mode builds +/// upon the previous one, adding more advanced features and capabilities. +/// +/// The choice of scanner mode affects: +/// - Available editing and enhancement features +/// - Processing time and resource usage +/// - Future feature compatibility +/// - User interface options +/// +/// Example usage: +/// ```dart +/// final androidOptions = DocScanKitOptionsAndroid( +/// scannerMode: ScannerModeAndroid.full, // Maximum features +/// ); +/// ``` +enum ScannerModeAndroid { + /// Basic document scanning with essential editing capabilities. + /// + /// This mode provides fundamental document scanning features: + /// - Document detection and cropping + /// - Manual crop adjustment + /// - Page rotation (90-degree increments) + /// - Page reordering for multi-page documents + /// - Basic image quality adjustments + /// + /// Use this mode when you need: + /// - Minimal processing overhead + /// - Fast scanning performance + /// - Basic document capture without advanced features + /// - Compatibility with older or lower-end devices + base, + + /// Enhanced scanning with image filters and automatic improvements. + /// + /// This mode includes all [base] features plus: + /// - Grayscale conversion options + /// - Automatic image enhancement algorithms + /// - Contrast and brightness optimization + /// - Shadow removal and lighting correction + /// - Color adjustment and saturation controls + /// + /// Use this mode when you need: + /// - Better image quality for scanned documents + /// - Automatic enhancement of poor lighting conditions + /// - Professional-looking document output + /// - Balance between features and performance + filter, + + /// Full-featured scanning with ML-powered cleaning and future updates. + /// + /// This mode includes all [filter] features plus: + /// - ML-enabled stain and mark removal + /// - Automatic finger and hand detection/removal + /// - Advanced noise reduction algorithms + /// - Smart content enhancement + /// - Automatic future feature additions via Google Play Services + /// + /// Use this mode when you need: + /// - Maximum document quality and cleanliness + /// - Advanced ML-powered image processing + /// - Automatic access to new features as they're released + /// - Best possible scanning results regardless of source quality + /// + /// Note: This mode requires more processing power and may take longer + /// to process documents, especially on older devices. + full; +} diff --git a/lib/src/options/android_options.dart b/lib/src/options/android_options.dart deleted file mode 100644 index a556c09..0000000 --- a/lib/src/options/android_options.dart +++ /dev/null @@ -1,36 +0,0 @@ -import './../options/scan_result.dart'; - -class DocumentScanKitOptionsAndroid { - DocumentScanKitOptionsAndroid( - {this.pageLimit = 1, - this.scannerMode = ScannerModeAndroid.full, - this.isGalleryImport = true, - this.saveImage = true}); - - /// Sets a page limit for the maximum number of pages that can be scanned in a single scanning session. default = 1. - final int pageLimit; - - /// Sets the scanner mode which determines what features are enabled. default = ScannerModel.full. - final ScannerModeAndroid scannerMode; - - /// Enable or disable the capability to import from the photo gallery. default = true. - final bool isGalleryImport; - - /// Enable save image and return image path in [ScanResult.imagePath] - /// if you set false, the image is not saved in device and return empty, - /// Default is true - final bool saveImage; - - Map toJson() => { - 'pageLimit': pageLimit, - 'scannerMode': scannerMode.name, - 'isGalleryImport': isGalleryImport, - 'saveImage': saveImage - }; -} - -enum ScannerModeAndroid { - base, - filter, - full, -} diff --git a/lib/src/options/ios_options.dart b/lib/src/options/ios_options.dart deleted file mode 100644 index 5849a1a..0000000 --- a/lib/src/options/ios_options.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:ui'; - -import './scan_result.dart'; - -class DocumentScanKitOptionsiOS { - DocumentScanKitOptionsiOS( - {this.modalPresentationStyle = ModalPresentationStyle.overFullScreen, - this.compressionQuality = 1.0, - this.saveImage = true, - this.color}) - : assert(!(compressionQuality > 1.0 || compressionQuality < 0.0), - 'The comprehension value must be between 0 and 1'); - - ///Modal presentation styles available when presenting view controllers - ///default: overFullScreen. - final ModalPresentationStyle modalPresentationStyle; - - ///return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least) - ///default = 1 - final double compressionQuality; - - /// Set to true to enable saving the image - /// and returning the image path in [ScanResult.imagePath]. - /// The default is true. - /// In Android imagePath is always returned - final bool saveImage; - - /// Defines the color applied to buttons and the document detection overlay. - /// - /// **Note:** Setting `alpha = 0` makes the buttons fully transparent. - final Color? color; - - Map toJson() => { - 'modalPresentationStyle': modalPresentationStyle.name, - 'compressionQuality': compressionQuality, - 'saveImage': saveImage, - 'color': color != null ? [color?.r, color?.g, color?.b, color?.a] : [], - }; -} - -enum ModalPresentationStyle { - /// The default presentation style chosen by the system. - automatic, - - /// A presentation style where the content is displayed over another view controller’s content. - currentContext, - - ///A view presentation style in which the presented view covers the screen. - overFullScreen, - - /// A presentation style where the content is displayed in a popover view. - popover, -} diff --git a/lib/src/options/scan_result.dart b/lib/src/options/scan_result.dart deleted file mode 100644 index cfa1965..0000000 --- a/lib/src/options/scan_result.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'dart:typed_data'; - -class ScanResult { - String? imagePath; - Uint8List imagesBytes; - ScanResult({ - required this.imagePath, - required this.imagesBytes, - }); -} diff --git a/pubspec.yaml b/pubspec.yaml index b767042..a03fbdf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,5 +23,7 @@ flutter: android: package: com.rajada1_docscan_kit.doc_scan_kit pluginClass: DocScanKitPlugin + dartPluginClass: DocScanKitPlatformAndroid ios: pluginClass: DocScanKitPlugin + dartPluginClass: DocScanKitPlatformIOS diff --git a/test/doc_scan_kit_method_channel_test.dart b/test/doc_scan_kit_method_channel_test.dart deleted file mode 100644 index ef7c06a..0000000 --- a/test/doc_scan_kit_method_channel_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:doc_scan_kit/doc_scan_kit.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:doc_scan_kit/src/doc_scan_kit_method_channel.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - MethodChannelDocScanKit platform = MethodChannelDocScanKit(); - const MethodChannel channel = MethodChannel('doc_scan_kit'); - - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - channel, - (MethodCall methodCall) async { - return []; - }, - ); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null); - }); - DocumentScanKitOptionsAndroid optionsAndroid = - DocumentScanKitOptionsAndroid(); - DocumentScanKitOptionsiOS optionsIos = DocumentScanKitOptionsiOS(); - - test('scanner', () async { - expect( - (await platform.scanner(optionsAndroid, optionsIos)), []); - }); -} diff --git a/test/doc_scan_kit_test.dart b/test/doc_scan_kit_test.dart deleted file mode 100644 index dff9a02..0000000 --- a/test/doc_scan_kit_test.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:typed_data'; -import 'package:doc_scan_kit/doc_scan_kit.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:doc_scan_kit/src/doc_scan_kit_platform_interface.dart'; -import 'package:doc_scan_kit/src/doc_scan_kit_method_channel.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockDocScanKitPlatform - with MockPlatformInterfaceMixin - implements DocScanKitPlatform { - bool closeCalled = false; - int scannerCallCount = 0; - - @override - Future recognizeText(List imageBytes) async { - return Future.value("Mocked recognized text"); - } - - @override - Future scanQrCode(List imageBytes) async { - return Future.value("Mocked QR code result"); - } - - @override - Future> scanner( - final DocumentScanKitOptionsAndroid optionsAndroid, - final DocumentScanKitOptionsiOS optionsIos, - ) async { - scannerCallCount++; - final list = await Future.value([ - ScanResult( - imagePath: 'test/path', - imagesBytes: Uint8List(0), - ) - ]); - return list; - } - - @override - Future close() async { - closeCalled = true; - } -} - -void main() { - final fakePlatform = MockDocScanKitPlatform(); - - test('$MethodChannelDocScanKit is the default instance', () { - expect( - DocScanKitPlatform.instance, isInstanceOf()); - }); - - group('DocScanKit tests with mock', () { - late DocScanKit docScanKitPlugin; - - setUp(() { - DocScanKitPlatform.instance = fakePlatform; - docScanKitPlugin = DocScanKit(); - }); - - tearDown(() { - // Restaura a instância padrão após cada teste - DocScanKitPlatform.instance = MethodChannelDocScanKit(); - }); - - test('scanner returns scan results', () async { - final result = await docScanKitPlugin.scanner(); - - expect(fakePlatform.scannerCallCount, 1); - expect(result.length, 1); - expect(result.first, isA()); - expect(result.first.imagePath, 'test/path'); - }); - - test('close should call platform implementation', () async { - await docScanKitPlugin.close(); - - expect(fakePlatform.closeCalled, true); - }); - }); -}